mirror of
https://github.com/antebudimir/feishin.git
synced 2026-03-01 03:37:26 +00:00
restructure files onto electron-vite boilerplate
This commit is contained in:
parent
91ce2cd8a1
commit
1cf587bc8f
457 changed files with 9927 additions and 11705 deletions
|
|
@ -1,12 +1,9 @@
|
||||||
root = true
|
root = true
|
||||||
|
|
||||||
[*]
|
[*]
|
||||||
indent_style = space
|
|
||||||
indent_size = 4
|
|
||||||
end_of_line = lf
|
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
trim_trailing_whitespace = true
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
[*.md]
|
|
||||||
trim_trailing_whitespace = false
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
{
|
|
||||||
"rules": {
|
|
||||||
"no-console": "off",
|
|
||||||
"global-require": "off",
|
|
||||||
"import/no-dynamic-require": "off"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
/**
|
|
||||||
* Base webpack config used across other specific configs
|
|
||||||
*/
|
|
||||||
|
|
||||||
import webpack from 'webpack';
|
|
||||||
import { dependencies as externals } from '../../release/app/package.json';
|
|
||||||
import webpackPaths from './webpack.paths';
|
|
||||||
import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin';
|
|
||||||
|
|
||||||
const createStyledComponentsTransformer = require('typescript-plugin-styled-components').default;
|
|
||||||
|
|
||||||
const styledComponentsTransformer = createStyledComponentsTransformer();
|
|
||||||
|
|
||||||
const configuration: webpack.Configuration = {
|
|
||||||
externals: [...Object.keys(externals || {})],
|
|
||||||
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
exclude: /node_modules/,
|
|
||||||
test: /\.[jt]sx?$/,
|
|
||||||
use: {
|
|
||||||
loader: 'ts-loader',
|
|
||||||
options: {
|
|
||||||
// Remove this line to enable type checking in webpack builds
|
|
||||||
transpileOnly: true,
|
|
||||||
getCustomTransformers: () => ({ before: [styledComponentsTransformer] }),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
output: {
|
|
||||||
// https://github.com/webpack/webpack/issues/1114
|
|
||||||
library: {
|
|
||||||
type: 'commonjs2',
|
|
||||||
},
|
|
||||||
|
|
||||||
path: webpackPaths.srcPath,
|
|
||||||
},
|
|
||||||
|
|
||||||
plugins: [
|
|
||||||
new webpack.EnvironmentPlugin({
|
|
||||||
NODE_ENV: 'production',
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine the array of extensions that should be used to resolve modules.
|
|
||||||
*/
|
|
||||||
resolve: {
|
|
||||||
extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'],
|
|
||||||
fallback: {
|
|
||||||
child_process: false,
|
|
||||||
},
|
|
||||||
plugins: [new TsconfigPathsPlugin({ baseUrl: webpackPaths.srcPath })],
|
|
||||||
modules: [webpackPaths.srcPath, 'node_modules'],
|
|
||||||
},
|
|
||||||
|
|
||||||
stats: 'errors-only',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default configuration;
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
/* eslint import/no-unresolved: off, import/no-self-import: off */
|
|
||||||
|
|
||||||
module.exports = require('./webpack.config.renderer.dev').default;
|
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
/**
|
|
||||||
* Webpack config for production electron main process
|
|
||||||
*/
|
|
||||||
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
import TerserPlugin from 'terser-webpack-plugin';
|
|
||||||
import webpack from 'webpack';
|
|
||||||
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
|
|
||||||
import { merge } from 'webpack-merge';
|
|
||||||
|
|
||||||
import checkNodeEnv from '../scripts/check-node-env';
|
|
||||||
import deleteSourceMaps from '../scripts/delete-source-maps';
|
|
||||||
import baseConfig from './webpack.config.base';
|
|
||||||
import webpackPaths from './webpack.paths';
|
|
||||||
|
|
||||||
checkNodeEnv('production');
|
|
||||||
deleteSourceMaps();
|
|
||||||
|
|
||||||
const devtoolsConfig =
|
|
||||||
process.env.DEBUG_PROD === 'true'
|
|
||||||
? {
|
|
||||||
devtool: 'source-map',
|
|
||||||
}
|
|
||||||
: {};
|
|
||||||
|
|
||||||
const configuration: webpack.Configuration = {
|
|
||||||
...devtoolsConfig,
|
|
||||||
|
|
||||||
mode: 'production',
|
|
||||||
|
|
||||||
target: 'electron-main',
|
|
||||||
|
|
||||||
entry: {
|
|
||||||
main: path.join(webpackPaths.srcMainPath, 'main.ts'),
|
|
||||||
preload: path.join(webpackPaths.srcMainPath, 'preload.ts'),
|
|
||||||
},
|
|
||||||
|
|
||||||
output: {
|
|
||||||
path: webpackPaths.distMainPath,
|
|
||||||
filename: '[name].js',
|
|
||||||
},
|
|
||||||
|
|
||||||
optimization: {
|
|
||||||
minimizer: [
|
|
||||||
new TerserPlugin({
|
|
||||||
parallel: true,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
plugins: [
|
|
||||||
new BundleAnalyzerPlugin({
|
|
||||||
analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled',
|
|
||||||
}),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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,
|
|
||||||
START_MINIMIZED: false,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Disables webpack processing of __dirname and __filename.
|
|
||||||
* If you run the bundle in node.js it falls back to these values of node.js.
|
|
||||||
* https://github.com/webpack/webpack/issues/2010
|
|
||||||
*/
|
|
||||||
node: {
|
|
||||||
__dirname: false,
|
|
||||||
__filename: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default merge(baseConfig, configuration);
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
import webpack from 'webpack';
|
|
||||||
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
|
|
||||||
import { merge } from 'webpack-merge';
|
|
||||||
|
|
||||||
import checkNodeEnv from '../scripts/check-node-env';
|
|
||||||
import baseConfig from './webpack.config.base';
|
|
||||||
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
|
|
||||||
// at the dev webpack config is not accidentally run in a production environment
|
|
||||||
if (process.env.NODE_ENV === 'production') {
|
|
||||||
checkNodeEnv('development');
|
|
||||||
}
|
|
||||||
|
|
||||||
const configuration: webpack.Configuration = {
|
|
||||||
devtool: 'inline-source-map',
|
|
||||||
|
|
||||||
mode: 'development',
|
|
||||||
|
|
||||||
target: 'electron-preload',
|
|
||||||
|
|
||||||
entry: path.join(webpackPaths.srcMainPath, 'preload.ts'),
|
|
||||||
|
|
||||||
output: {
|
|
||||||
path: webpackPaths.dllPath,
|
|
||||||
filename: 'preload.js',
|
|
||||||
},
|
|
||||||
|
|
||||||
plugins: [
|
|
||||||
new BundleAnalyzerPlugin({
|
|
||||||
analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled',
|
|
||||||
}),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Disables webpack processing of __dirname and __filename.
|
|
||||||
* If you run the bundle in node.js it falls back to these values of node.js.
|
|
||||||
* https://github.com/webpack/webpack/issues/2010
|
|
||||||
*/
|
|
||||||
node: {
|
|
||||||
__dirname: false,
|
|
||||||
__filename: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default merge(baseConfig, configuration);
|
|
||||||
|
|
@ -1,127 +0,0 @@
|
||||||
import 'webpack-dev-server';
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
import HtmlWebpackPlugin from 'html-webpack-plugin';
|
|
||||||
import webpack from 'webpack';
|
|
||||||
import { merge } from 'webpack-merge';
|
|
||||||
|
|
||||||
import checkNodeEnv from '../scripts/check-node-env';
|
|
||||||
import baseConfig from './webpack.config.base';
|
|
||||||
import webpackPaths from './webpack.paths';
|
|
||||||
|
|
||||||
const { version } = require('../../package.json');
|
|
||||||
|
|
||||||
// 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
|
|
||||||
if (process.env.NODE_ENV === 'production') {
|
|
||||||
checkNodeEnv('development');
|
|
||||||
}
|
|
||||||
|
|
||||||
const configuration: webpack.Configuration = {
|
|
||||||
devtool: 'inline-source-map',
|
|
||||||
|
|
||||||
mode: 'development',
|
|
||||||
|
|
||||||
target: ['web'],
|
|
||||||
|
|
||||||
entry: {
|
|
||||||
remote: path.join(webpackPaths.srcRemotePath, 'index.tsx'),
|
|
||||||
worker: path.join(webpackPaths.srcRemotePath, 'service-worker.ts'),
|
|
||||||
},
|
|
||||||
|
|
||||||
output: {
|
|
||||||
path: webpackPaths.dllPath,
|
|
||||||
publicPath: '/',
|
|
||||||
filename: '[name].js',
|
|
||||||
library: {
|
|
||||||
type: 'umd',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.s?css$/,
|
|
||||||
use: [
|
|
||||||
'style-loader',
|
|
||||||
{
|
|
||||||
loader: 'css-loader',
|
|
||||||
options: {
|
|
||||||
modules: true,
|
|
||||||
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: [
|
|
||||||
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 HtmlWebpackPlugin({
|
|
||||||
filename: path.join('index.html'),
|
|
||||||
template: path.join(webpackPaths.srcRemotePath, 'index.ejs'),
|
|
||||||
favicon: path.join(webpackPaths.assetsPath, 'icons', 'favicon.ico'),
|
|
||||||
minify: {
|
|
||||||
collapseWhitespace: true,
|
|
||||||
removeAttributeQuotes: true,
|
|
||||||
removeComments: true,
|
|
||||||
},
|
|
||||||
isBrowser: true,
|
|
||||||
env: process.env.NODE_ENV,
|
|
||||||
isDevelopment: process.env.NODE_ENV !== 'production',
|
|
||||||
nodeModules: webpackPaths.appNodeModulesPath,
|
|
||||||
templateParameters: {
|
|
||||||
version,
|
|
||||||
prod: false,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
|
|
||||||
node: {
|
|
||||||
__dirname: false,
|
|
||||||
__filename: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default merge(baseConfig, configuration);
|
|
||||||
|
|
@ -1,142 +0,0 @@
|
||||||
/**
|
|
||||||
* Build config for electron renderer process
|
|
||||||
*/
|
|
||||||
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
import CssMinimizerPlugin from 'css-minimizer-webpack-plugin';
|
|
||||||
import HtmlWebpackPlugin from 'html-webpack-plugin';
|
|
||||||
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
|
|
||||||
import TerserPlugin from 'terser-webpack-plugin';
|
|
||||||
import webpack from 'webpack';
|
|
||||||
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
|
|
||||||
import { merge } from 'webpack-merge';
|
|
||||||
|
|
||||||
import checkNodeEnv from '../scripts/check-node-env';
|
|
||||||
import deleteSourceMaps from '../scripts/delete-source-maps';
|
|
||||||
import baseConfig from './webpack.config.base';
|
|
||||||
import webpackPaths from './webpack.paths';
|
|
||||||
|
|
||||||
const { version } = require('../../package.json');
|
|
||||||
|
|
||||||
checkNodeEnv('production');
|
|
||||||
deleteSourceMaps();
|
|
||||||
|
|
||||||
const devtoolsConfig =
|
|
||||||
process.env.DEBUG_PROD === 'true'
|
|
||||||
? {
|
|
||||||
devtool: 'source-map',
|
|
||||||
}
|
|
||||||
: {};
|
|
||||||
|
|
||||||
const configuration: webpack.Configuration = {
|
|
||||||
...devtoolsConfig,
|
|
||||||
|
|
||||||
mode: 'production',
|
|
||||||
|
|
||||||
target: ['web'],
|
|
||||||
|
|
||||||
entry: {
|
|
||||||
remote: path.join(webpackPaths.srcRemotePath, 'index.tsx'),
|
|
||||||
worker: path.join(webpackPaths.srcRemotePath, 'service-worker.ts'),
|
|
||||||
},
|
|
||||||
|
|
||||||
output: {
|
|
||||||
path: webpackPaths.distRemotePath,
|
|
||||||
publicPath: './',
|
|
||||||
filename: '[name].js',
|
|
||||||
library: {
|
|
||||||
type: 'umd',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.s?(a|c)ss$/,
|
|
||||||
use: [
|
|
||||||
MiniCssExtractPlugin.loader,
|
|
||||||
{
|
|
||||||
loader: 'css-loader',
|
|
||||||
options: {
|
|
||||||
modules: true,
|
|
||||||
sourceMap: true,
|
|
||||||
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',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
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: 'remote.css',
|
|
||||||
}),
|
|
||||||
|
|
||||||
new BundleAnalyzerPlugin({
|
|
||||||
analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled',
|
|
||||||
}),
|
|
||||||
|
|
||||||
new HtmlWebpackPlugin({
|
|
||||||
filename: 'index.html',
|
|
||||||
template: path.join(webpackPaths.srcRemotePath, 'index.ejs'),
|
|
||||||
favicon: path.join(webpackPaths.assetsPath, 'icons', 'favicon.ico'),
|
|
||||||
minify: {
|
|
||||||
collapseWhitespace: true,
|
|
||||||
removeAttributeQuotes: true,
|
|
||||||
removeComments: true,
|
|
||||||
},
|
|
||||||
isBrowser: true,
|
|
||||||
env: process.env.NODE_ENV,
|
|
||||||
isDevelopment: process.env.NODE_ENV !== 'production',
|
|
||||||
templateParameters: {
|
|
||||||
version,
|
|
||||||
prod: true,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default merge(baseConfig, configuration);
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
/**
|
|
||||||
* Builds the DLL for development electron renderer process
|
|
||||||
*/
|
|
||||||
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
import webpack from 'webpack';
|
|
||||||
import { merge } from 'webpack-merge';
|
|
||||||
|
|
||||||
import { dependencies } from '../../package.json';
|
|
||||||
import checkNodeEnv from '../scripts/check-node-env';
|
|
||||||
import baseConfig from './webpack.config.base';
|
|
||||||
import webpackPaths from './webpack.paths';
|
|
||||||
|
|
||||||
checkNodeEnv('development');
|
|
||||||
|
|
||||||
const dist = webpackPaths.dllPath;
|
|
||||||
|
|
||||||
const configuration: webpack.Configuration = {
|
|
||||||
context: webpackPaths.rootPath,
|
|
||||||
|
|
||||||
devtool: 'eval',
|
|
||||||
|
|
||||||
mode: 'development',
|
|
||||||
|
|
||||||
target: 'electron-renderer',
|
|
||||||
|
|
||||||
externals: ['fsevents', 'crypto-browserify'],
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use `module` from `webpack.config.renderer.dev.js`
|
|
||||||
*/
|
|
||||||
module: require('./webpack.config.renderer.dev').default.module,
|
|
||||||
|
|
||||||
entry: {
|
|
||||||
renderer: Object.keys(dependencies || {}),
|
|
||||||
},
|
|
||||||
|
|
||||||
output: {
|
|
||||||
path: dist,
|
|
||||||
filename: '[name].dev.dll.js',
|
|
||||||
library: {
|
|
||||||
name: 'renderer',
|
|
||||||
type: 'var',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
plugins: [
|
|
||||||
new webpack.DllPlugin({
|
|
||||||
path: path.join(dist, '[name].json'),
|
|
||||||
name: '[name]',
|
|
||||||
}),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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: 'development',
|
|
||||||
}),
|
|
||||||
|
|
||||||
new webpack.LoaderOptionsPlugin({
|
|
||||||
debug: true,
|
|
||||||
options: {
|
|
||||||
context: webpackPaths.srcPath,
|
|
||||||
output: {
|
|
||||||
path: webpackPaths.dllPath,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default merge(baseConfig, configuration);
|
|
||||||
|
|
@ -1,201 +0,0 @@
|
||||||
import 'webpack-dev-server';
|
|
||||||
import { execSync, spawn } from 'child_process';
|
|
||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin';
|
|
||||||
import chalk from 'chalk';
|
|
||||||
import HtmlWebpackPlugin from 'html-webpack-plugin';
|
|
||||||
import webpack from 'webpack';
|
|
||||||
import { merge } from 'webpack-merge';
|
|
||||||
|
|
||||||
import checkNodeEnv from '../scripts/check-node-env';
|
|
||||||
import baseConfig from './webpack.config.base';
|
|
||||||
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
|
|
||||||
// at the dev webpack config is not accidentally run in a production environment
|
|
||||||
if (process.env.NODE_ENV === 'production') {
|
|
||||||
checkNodeEnv('development');
|
|
||||||
}
|
|
||||||
|
|
||||||
const port = process.env.PORT || 4343;
|
|
||||||
const manifest = path.resolve(webpackPaths.dllPath, 'renderer.json');
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
const requiredByDLLConfig = module.parent!.filename.includes('webpack.config.renderer.dev.dll');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Warn if the DLL is not built
|
|
||||||
*/
|
|
||||||
if (!requiredByDLLConfig && !(fs.existsSync(webpackPaths.dllPath) && fs.existsSync(manifest))) {
|
|
||||||
console.log(
|
|
||||||
chalk.black.bgYellow.bold(
|
|
||||||
'The DLL files are missing. Sit back while we build them for you with "npm run build-dll"',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
execSync('npm run postinstall');
|
|
||||||
}
|
|
||||||
|
|
||||||
const configuration: webpack.Configuration = {
|
|
||||||
devtool: 'inline-source-map',
|
|
||||||
|
|
||||||
mode: 'development',
|
|
||||||
|
|
||||||
target: ['web', 'electron-renderer'],
|
|
||||||
|
|
||||||
entry: [
|
|
||||||
`webpack-dev-server/client?http://localhost:${port}/dist`,
|
|
||||||
'webpack/hot/only-dev-server',
|
|
||||||
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(),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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);
|
|
||||||
|
|
@ -1,137 +0,0 @@
|
||||||
/**
|
|
||||||
* Build config for electron renderer process
|
|
||||||
*/
|
|
||||||
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
import CssMinimizerPlugin from 'css-minimizer-webpack-plugin';
|
|
||||||
import HtmlWebpackPlugin from 'html-webpack-plugin';
|
|
||||||
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
|
|
||||||
import TerserPlugin from 'terser-webpack-plugin';
|
|
||||||
import webpack from 'webpack';
|
|
||||||
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
|
|
||||||
import { merge } from 'webpack-merge';
|
|
||||||
|
|
||||||
import checkNodeEnv from '../scripts/check-node-env';
|
|
||||||
import deleteSourceMaps from '../scripts/delete-source-maps';
|
|
||||||
import baseConfig from './webpack.config.base';
|
|
||||||
import webpackPaths from './webpack.paths';
|
|
||||||
|
|
||||||
checkNodeEnv('production');
|
|
||||||
deleteSourceMaps();
|
|
||||||
|
|
||||||
const devtoolsConfig =
|
|
||||||
process.env.DEBUG_PROD === 'true'
|
|
||||||
? {
|
|
||||||
devtool: 'source-map',
|
|
||||||
}
|
|
||||||
: {};
|
|
||||||
|
|
||||||
const configuration: webpack.Configuration = {
|
|
||||||
...devtoolsConfig,
|
|
||||||
|
|
||||||
mode: 'production',
|
|
||||||
|
|
||||||
target: ['web', 'electron-renderer'],
|
|
||||||
|
|
||||||
entry: [path.join(webpackPaths.srcRendererPath, 'index.tsx')],
|
|
||||||
|
|
||||||
output: {
|
|
||||||
path: webpackPaths.distRendererPath,
|
|
||||||
publicPath: './',
|
|
||||||
filename: 'renderer.js',
|
|
||||||
library: {
|
|
||||||
type: 'umd',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.s?(a|c)ss$/,
|
|
||||||
use: [
|
|
||||||
MiniCssExtractPlugin.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?(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',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
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',
|
|
||||||
templateParameters: {
|
|
||||||
web: false,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default merge(baseConfig, configuration);
|
|
||||||
|
|
@ -1,147 +0,0 @@
|
||||||
import 'webpack-dev-server';
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin';
|
|
||||||
import HtmlWebpackPlugin from 'html-webpack-plugin';
|
|
||||||
import webpack from 'webpack';
|
|
||||||
import { merge } from 'webpack-merge';
|
|
||||||
|
|
||||||
import checkNodeEnv from '../scripts/check-node-env';
|
|
||||||
import baseConfig from './webpack.config.base';
|
|
||||||
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
|
|
||||||
// at the dev webpack config is not accidentally run in a production environment
|
|
||||||
if (process.env.NODE_ENV === 'production') {
|
|
||||||
checkNodeEnv('development');
|
|
||||||
}
|
|
||||||
|
|
||||||
const port = process.env.PORT || 4343;
|
|
||||||
|
|
||||||
const configuration: webpack.Configuration = {
|
|
||||||
devtool: 'inline-source-map',
|
|
||||||
|
|
||||||
mode: 'development',
|
|
||||||
|
|
||||||
target: ['web', 'electron-renderer'],
|
|
||||||
|
|
||||||
entry: [
|
|
||||||
`webpack-dev-server/client?http://localhost:${port}/dist`,
|
|
||||||
'webpack/hot/only-dev-server',
|
|
||||||
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: [
|
|
||||||
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'),
|
|
||||||
favicon: path.join(webpackPaths.assetsPath, 'icons', 'favicon.ico'),
|
|
||||||
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, // with hot reload, we don't have NGINX injecting variables
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
|
|
||||||
node: {
|
|
||||||
__dirname: false,
|
|
||||||
__filename: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
devServer: {
|
|
||||||
port,
|
|
||||||
compress: true,
|
|
||||||
hot: true,
|
|
||||||
headers: { 'Access-Control-Allow-Origin': '*' },
|
|
||||||
static: {
|
|
||||||
publicPath: '/',
|
|
||||||
},
|
|
||||||
historyApiFallback: {
|
|
||||||
verbose: true,
|
|
||||||
},
|
|
||||||
setupMiddlewares(middlewares) {
|
|
||||||
return middlewares;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default merge(baseConfig, configuration);
|
|
||||||
|
|
@ -1,138 +0,0 @@
|
||||||
/**
|
|
||||||
* Build config for electron renderer process
|
|
||||||
*/
|
|
||||||
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
import CssMinimizerPlugin from 'css-minimizer-webpack-plugin';
|
|
||||||
import HtmlWebpackPlugin from 'html-webpack-plugin';
|
|
||||||
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
|
|
||||||
import TerserPlugin from 'terser-webpack-plugin';
|
|
||||||
import webpack from 'webpack';
|
|
||||||
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
|
|
||||||
import { merge } from 'webpack-merge';
|
|
||||||
|
|
||||||
import checkNodeEnv from '../scripts/check-node-env';
|
|
||||||
import deleteSourceMaps from '../scripts/delete-source-maps';
|
|
||||||
import baseConfig from './webpack.config.base';
|
|
||||||
import webpackPaths from './webpack.paths';
|
|
||||||
|
|
||||||
checkNodeEnv('production');
|
|
||||||
deleteSourceMaps();
|
|
||||||
|
|
||||||
const devtoolsConfig =
|
|
||||||
process.env.DEBUG_PROD === 'true'
|
|
||||||
? {
|
|
||||||
devtool: 'source-map',
|
|
||||||
}
|
|
||||||
: {};
|
|
||||||
|
|
||||||
const configuration: webpack.Configuration = {
|
|
||||||
...devtoolsConfig,
|
|
||||||
|
|
||||||
mode: 'production',
|
|
||||||
|
|
||||||
target: ['web'],
|
|
||||||
|
|
||||||
entry: [path.join(webpackPaths.srcRendererPath, 'index.tsx')],
|
|
||||||
|
|
||||||
output: {
|
|
||||||
path: webpackPaths.distWebPath,
|
|
||||||
publicPath: 'auto',
|
|
||||||
filename: 'renderer.js',
|
|
||||||
library: {
|
|
||||||
type: 'umd',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.s?(a|c)ss$/,
|
|
||||||
use: [
|
|
||||||
MiniCssExtractPlugin.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?(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',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
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'),
|
|
||||||
favicon: path.join(webpackPaths.assetsPath, 'icons', 'favicon.ico'),
|
|
||||||
minify: {
|
|
||||||
collapseWhitespace: true,
|
|
||||||
removeAttributeQuotes: true,
|
|
||||||
removeComments: true,
|
|
||||||
},
|
|
||||||
isBrowser: false,
|
|
||||||
isDevelopment: process.env.NODE_ENV !== 'production',
|
|
||||||
templateParameters: {
|
|
||||||
web: true,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default merge(baseConfig, configuration);
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const rootPath = path.join(__dirname, '../..');
|
|
||||||
|
|
||||||
const dllPath = path.join(__dirname, '../dll');
|
|
||||||
|
|
||||||
const srcPath = path.join(rootPath, 'src');
|
|
||||||
const assetsPath = path.join(rootPath, 'assets');
|
|
||||||
const srcMainPath = path.join(srcPath, 'main');
|
|
||||||
const srcRemotePath = path.join(srcPath, 'remote');
|
|
||||||
const srcRendererPath = path.join(srcPath, 'renderer');
|
|
||||||
|
|
||||||
const releasePath = path.join(rootPath, 'release');
|
|
||||||
const appPath = path.join(releasePath, 'app');
|
|
||||||
const appPackagePath = path.join(appPath, 'package.json');
|
|
||||||
const appNodeModulesPath = path.join(appPath, 'node_modules');
|
|
||||||
const srcNodeModulesPath = path.join(srcPath, 'node_modules');
|
|
||||||
|
|
||||||
const distPath = path.join(appPath, 'dist');
|
|
||||||
const distMainPath = path.join(distPath, 'main');
|
|
||||||
const distRemotePath = path.join(distPath, 'remote');
|
|
||||||
const distRendererPath = path.join(distPath, 'renderer');
|
|
||||||
const distWebPath = path.join(distPath, 'web');
|
|
||||||
|
|
||||||
const buildPath = path.join(releasePath, 'build');
|
|
||||||
|
|
||||||
export default {
|
|
||||||
assetsPath,
|
|
||||||
rootPath,
|
|
||||||
dllPath,
|
|
||||||
srcPath,
|
|
||||||
srcMainPath,
|
|
||||||
srcRemotePath,
|
|
||||||
srcRendererPath,
|
|
||||||
releasePath,
|
|
||||||
appPath,
|
|
||||||
appPackagePath,
|
|
||||||
appNodeModulesPath,
|
|
||||||
srcNodeModulesPath,
|
|
||||||
distPath,
|
|
||||||
distMainPath,
|
|
||||||
distRemotePath,
|
|
||||||
distRendererPath,
|
|
||||||
distWebPath,
|
|
||||||
buildPath,
|
|
||||||
};
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export default 'test-file-stub';
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"rules": {
|
|
||||||
"no-console": "off",
|
|
||||||
"global-require": "off",
|
|
||||||
"import/no-dynamic-require": "off",
|
|
||||||
"import/no-extraneous-dependencies": "off"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
// Check if the renderer and main bundles are built
|
|
||||||
import path from 'path';
|
|
||||||
import chalk from 'chalk';
|
|
||||||
import fs from 'fs';
|
|
||||||
import webpackPaths from '../configs/webpack.paths';
|
|
||||||
|
|
||||||
const mainPath = path.join(webpackPaths.distMainPath, 'main.js');
|
|
||||||
const remotePath = path.join(webpackPaths.distMainPath, 'remote.js');
|
|
||||||
const rendererPath = path.join(webpackPaths.distRendererPath, 'renderer.js');
|
|
||||||
|
|
||||||
if (!fs.existsSync(mainPath)) {
|
|
||||||
throw new Error(
|
|
||||||
chalk.whiteBright.bgRed.bold(
|
|
||||||
'The main process is not built yet. Build it by running "npm run build:main"',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fs.existsSync(remotePath)) {
|
|
||||||
throw new Error(
|
|
||||||
chalk.whiteBright.bgRed.bold(
|
|
||||||
'The remote process is not built yet. Build it by running "npm run build:remote"',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fs.existsSync(rendererPath)) {
|
|
||||||
throw new Error(
|
|
||||||
chalk.whiteBright.bgRed.bold(
|
|
||||||
'The renderer process is not built yet. Build it by running "npm run build:renderer"',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
import fs from 'fs';
|
|
||||||
import chalk from 'chalk';
|
|
||||||
import { execSync } from 'child_process';
|
|
||||||
import { dependencies } from '../../package.json';
|
|
||||||
|
|
||||||
if (dependencies) {
|
|
||||||
const dependenciesKeys = Object.keys(dependencies);
|
|
||||||
const nativeDeps = fs
|
|
||||||
.readdirSync('node_modules')
|
|
||||||
.filter((folder) => fs.existsSync(`node_modules/${folder}/binding.gyp`));
|
|
||||||
if (nativeDeps.length === 0) {
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
// Find the reason for why the dependency is installed. If it is installed
|
|
||||||
// because of a devDependency then that is okay. Warn when it is installed
|
|
||||||
// because of a dependency
|
|
||||||
const { dependencies: dependenciesObject } = JSON.parse(
|
|
||||||
execSync(`npm ls ${nativeDeps.join(' ')} --json`).toString()
|
|
||||||
);
|
|
||||||
const rootDependencies = Object.keys(dependenciesObject);
|
|
||||||
const filteredRootDependencies = rootDependencies.filter((rootDependency) =>
|
|
||||||
dependenciesKeys.includes(rootDependency)
|
|
||||||
);
|
|
||||||
if (filteredRootDependencies.length > 0) {
|
|
||||||
const plural = filteredRootDependencies.length > 1;
|
|
||||||
console.log(`
|
|
||||||
${chalk.whiteBright.bgYellow.bold(
|
|
||||||
'Webpack does not work with native dependencies.'
|
|
||||||
)}
|
|
||||||
${chalk.bold(filteredRootDependencies.join(', '))} ${
|
|
||||||
plural ? 'are native dependencies' : 'is a native dependency'
|
|
||||||
} and should be installed inside of the "./release/app" folder.
|
|
||||||
First, uninstall the packages from "./package.json":
|
|
||||||
${chalk.whiteBright.bgGreen.bold('npm uninstall your-package')}
|
|
||||||
${chalk.bold(
|
|
||||||
'Then, instead of installing the package to the root "./package.json":'
|
|
||||||
)}
|
|
||||||
${chalk.whiteBright.bgRed.bold('npm install your-package')}
|
|
||||||
${chalk.bold('Install the package to "./release/app/package.json"')}
|
|
||||||
${chalk.whiteBright.bgGreen.bold(
|
|
||||||
'cd ./release/app && npm install your-package'
|
|
||||||
)}
|
|
||||||
Read more about native dependencies at:
|
|
||||||
${chalk.bold(
|
|
||||||
'https://electron-react-boilerplate.js.org/docs/adding-dependencies/#module-structure'
|
|
||||||
)}
|
|
||||||
`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log('Native dependencies could not be checked');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import chalk from 'chalk';
|
|
||||||
|
|
||||||
export default function checkNodeEnv(expectedEnv) {
|
|
||||||
if (!expectedEnv) {
|
|
||||||
throw new Error('"expectedEnv" not set');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== expectedEnv) {
|
|
||||||
console.log(
|
|
||||||
chalk.whiteBright.bgRed.bold(
|
|
||||||
`"process.env.NODE_ENV" must be "${expectedEnv}" to use this webpack config`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
process.exit(2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import chalk from 'chalk';
|
|
||||||
import detectPort from 'detect-port';
|
|
||||||
|
|
||||||
const port = process.env.PORT || '4343';
|
|
||||||
|
|
||||||
detectPort(port, (err, availablePort) => {
|
|
||||||
if (port !== String(availablePort)) {
|
|
||||||
throw new Error(
|
|
||||||
chalk.whiteBright.bgRed.bold(
|
|
||||||
`Port "${port}" on "localhost" is already in use. Please use another port. ex: PORT=4343 npm start`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
import rimraf from 'rimraf';
|
|
||||||
import process from 'process';
|
|
||||||
import webpackPaths from '../configs/webpack.paths';
|
|
||||||
|
|
||||||
const args = process.argv.slice(2);
|
|
||||||
const commandMap = {
|
|
||||||
dist: webpackPaths.distPath,
|
|
||||||
release: webpackPaths.releasePath,
|
|
||||||
dll: webpackPaths.dllPath,
|
|
||||||
};
|
|
||||||
|
|
||||||
args.forEach((x) => {
|
|
||||||
const pathToRemove = commandMap[x];
|
|
||||||
if (pathToRemove !== undefined) {
|
|
||||||
rimraf.sync(pathToRemove);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
import path from 'path';
|
|
||||||
import rimraf from 'rimraf';
|
|
||||||
import webpackPaths from '../configs/webpack.paths';
|
|
||||||
|
|
||||||
export default function deleteSourceMaps() {
|
|
||||||
rimraf.sync(path.join(webpackPaths.distMainPath, '*.js.map'));
|
|
||||||
rimraf.sync(path.join(webpackPaths.distRemotePath, '*.js.map'));
|
|
||||||
rimraf.sync(path.join(webpackPaths.distRendererPath, '*.js.map'));
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
import { execSync } from 'child_process';
|
|
||||||
import fs from 'fs';
|
|
||||||
import { dependencies } from '../../release/app/package.json';
|
|
||||||
import webpackPaths from '../configs/webpack.paths';
|
|
||||||
|
|
||||||
if (
|
|
||||||
Object.keys(dependencies || {}).length > 0 &&
|
|
||||||
fs.existsSync(webpackPaths.appNodeModulesPath)
|
|
||||||
) {
|
|
||||||
const electronRebuildCmd =
|
|
||||||
'../../node_modules/.bin/electron-rebuild --force --types prod,dev,optional --module-dir .';
|
|
||||||
const cmd =
|
|
||||||
process.platform === 'win32'
|
|
||||||
? electronRebuildCmd.replace(/\//g, '\\')
|
|
||||||
: electronRebuildCmd;
|
|
||||||
execSync(cmd, {
|
|
||||||
cwd: webpackPaths.appPath,
|
|
||||||
stdio: 'inherit',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
import fs from 'fs';
|
|
||||||
import webpackPaths from '../configs/webpack.paths';
|
|
||||||
|
|
||||||
const { srcNodeModulesPath } = webpackPaths;
|
|
||||||
const { appNodeModulesPath } = webpackPaths;
|
|
||||||
|
|
||||||
if (!fs.existsSync(srcNodeModulesPath) && fs.existsSync(appNodeModulesPath)) {
|
|
||||||
fs.symlinkSync(appNodeModulesPath, srcNodeModulesPath, 'junction');
|
|
||||||
}
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
const { notarize } = require('electron-notarize');
|
|
||||||
const { build } = require('../../package.json');
|
|
||||||
|
|
||||||
exports.default = async function notarizeMacos(context) {
|
|
||||||
const { electronPlatformName, appOutDir } = context;
|
|
||||||
if (electronPlatformName !== 'darwin') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.CI !== 'true') {
|
|
||||||
console.warn('Skipping notarizing step. Packaging is not running in CI');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!('APPLE_ID' in process.env && 'APPLE_ID_PASS' in process.env)) {
|
|
||||||
console.warn(
|
|
||||||
'Skipping notarizing step. APPLE_ID and APPLE_ID_PASS env variables must be set'
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const appName = context.packager.appInfo.productFilename;
|
|
||||||
|
|
||||||
await notarize({
|
|
||||||
appBundleId: build.appId,
|
|
||||||
appPath: `${appOutDir}/${appName}.app`,
|
|
||||||
appleId: process.env.APPLE_ID,
|
|
||||||
appleIdPassword: process.env.APPLE_ID_PASS,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
|
|
||||||
# Runtime data
|
|
||||||
pids
|
|
||||||
*.pid
|
|
||||||
*.seed
|
|
||||||
|
|
||||||
# Coverage directory used by tools like istanbul
|
|
||||||
coverage
|
|
||||||
.eslintcache
|
|
||||||
|
|
||||||
# Dependency directory
|
|
||||||
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
|
|
||||||
node_modules
|
|
||||||
|
|
||||||
# OSX
|
|
||||||
.DS_Store
|
|
||||||
|
|
||||||
src/i18n
|
|
||||||
release/app/dist
|
|
||||||
release/build
|
|
||||||
.erb/dll
|
|
||||||
|
|
||||||
.idea
|
|
||||||
npm-debug.log.*
|
|
||||||
*.css.d.ts
|
|
||||||
*.sass.d.ts
|
|
||||||
*.scss.d.ts
|
|
||||||
|
|
||||||
# eslint ignores hidden directories by default:
|
|
||||||
# https://github.com/eslint/eslint/issues/8429
|
|
||||||
!.erb
|
|
||||||
97
.eslintrc.js
97
.eslintrc.js
|
|
@ -1,97 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
extends: ['erb', 'plugin:typescript-sort-keys/recommended'],
|
|
||||||
ignorePatterns: ['.erb/*', 'server'],
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
parserOptions: {
|
|
||||||
createDefaultProgram: true,
|
|
||||||
ecmaVersion: 12,
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
project: './tsconfig.json',
|
|
||||||
sourceType: 'module',
|
|
||||||
tsconfigRootDir: './',
|
|
||||||
},
|
|
||||||
plugins: ['@typescript-eslint', 'import', 'sort-keys-fix'],
|
|
||||||
rules: {
|
|
||||||
'@typescript-eslint/naming-convention': 'off',
|
|
||||||
'@typescript-eslint/no-explicit-any': 'off',
|
|
||||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
|
||||||
'@typescript-eslint/no-shadow': ['off'],
|
|
||||||
'@typescript-eslint/no-unused-vars': ['error'],
|
|
||||||
'@typescript-eslint/no-use-before-define': ['error'],
|
|
||||||
'default-case': 'off',
|
|
||||||
'import/extensions': 'off',
|
|
||||||
'import/no-absolute-path': 'off',
|
|
||||||
// A temporary hack related to IDE not resolving correct package.json
|
|
||||||
'import/no-extraneous-dependencies': 'off',
|
|
||||||
'import/no-unresolved': 'error',
|
|
||||||
'import/order': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
alphabetize: {
|
|
||||||
caseInsensitive: true,
|
|
||||||
order: 'asc',
|
|
||||||
},
|
|
||||||
groups: ['builtin', 'external', 'internal', ['parent', 'sibling']],
|
|
||||||
'newlines-between': 'never',
|
|
||||||
pathGroups: [
|
|
||||||
{
|
|
||||||
group: 'external',
|
|
||||||
pattern: 'react',
|
|
||||||
position: 'before',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
pathGroupsExcludedImportTypes: ['react'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'import/prefer-default-export': 'off',
|
|
||||||
'jsx-a11y/click-events-have-key-events': 'off',
|
|
||||||
'jsx-a11y/interactive-supports-focus': 'off',
|
|
||||||
'jsx-a11y/media-has-caption': 'off',
|
|
||||||
'no-await-in-loop': 'off',
|
|
||||||
'no-console': 'off',
|
|
||||||
'no-nested-ternary': 'off',
|
|
||||||
'no-restricted-syntax': 'off',
|
|
||||||
'no-shadow': 'off',
|
|
||||||
'no-underscore-dangle': 'off',
|
|
||||||
'no-unused-vars': 'off',
|
|
||||||
'no-use-before-define': 'off',
|
|
||||||
'prefer-destructuring': 'off',
|
|
||||||
'react/function-component-definition': 'off',
|
|
||||||
'react/jsx-filename-extension': [2, { extensions: ['.js', '.jsx', '.ts', '.tsx'] }],
|
|
||||||
'react/jsx-no-useless-fragment': 'off',
|
|
||||||
'react/jsx-props-no-spreading': 'off',
|
|
||||||
'react/jsx-sort-props': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
callbacksLast: true,
|
|
||||||
ignoreCase: false,
|
|
||||||
noSortAlphabetically: false,
|
|
||||||
reservedFirst: true,
|
|
||||||
shorthandFirst: true,
|
|
||||||
shorthandLast: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'react/no-array-index-key': 'off',
|
|
||||||
'react/react-in-jsx-scope': 'off',
|
|
||||||
'react/require-default-props': 'off',
|
|
||||||
'sort-keys-fix/sort-keys-fix': 'warn',
|
|
||||||
},
|
|
||||||
settings: {
|
|
||||||
'import/parsers': {
|
|
||||||
'@typescript-eslint/parser': ['.ts', '.tsx'],
|
|
||||||
},
|
|
||||||
'import/resolver': {
|
|
||||||
// See https://github.com/benmosher/eslint-plugin-import/issues/1396#issuecomment-575727774 for line below
|
|
||||||
node: {
|
|
||||||
extensions: ['.js', '.jsx', '.ts', '.tsx'],
|
|
||||||
},
|
|
||||||
typescript: {
|
|
||||||
alwaysTryTypes: true,
|
|
||||||
project: './tsconfig.json',
|
|
||||||
},
|
|
||||||
webpack: {
|
|
||||||
config: require.resolve('./.erb/configs/webpack.config.eslint.ts'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
33
.gitignore
vendored
33
.gitignore
vendored
|
|
@ -1,31 +1,6 @@
|
||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
|
|
||||||
# Runtime data
|
|
||||||
pids
|
|
||||||
*.pid
|
|
||||||
*.seed
|
|
||||||
|
|
||||||
# Coverage directory used by tools like istanbul
|
|
||||||
coverage
|
|
||||||
.eslintcache
|
|
||||||
|
|
||||||
# Dependency directory
|
|
||||||
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
|
|
||||||
node_modules
|
node_modules
|
||||||
|
dist
|
||||||
# OSX
|
out
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.eslintcache
|
||||||
release/app/dist
|
*.log*
|
||||||
release/build
|
|
||||||
.erb/dll
|
|
||||||
|
|
||||||
.idea
|
|
||||||
npm-debug.log.*
|
|
||||||
*.css.d.ts
|
|
||||||
*.sass.d.ts
|
|
||||||
*.scss.d.ts
|
|
||||||
|
|
||||||
.env*
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
. "$(dirname "$0")/_/husky.sh"
|
|
||||||
|
|
||||||
npx lint-staged
|
|
||||||
6
.prettierignore
Normal file
6
.prettierignore
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
out
|
||||||
|
dist
|
||||||
|
pnpm-lock.yaml
|
||||||
|
LICENSE.md
|
||||||
|
tsconfig.json
|
||||||
|
tsconfig.*.json
|
||||||
22
.prettierrc
22
.prettierrc
|
|
@ -1,22 +0,0 @@
|
||||||
{
|
|
||||||
"printWidth": 100,
|
|
||||||
"semi": true,
|
|
||||||
"singleQuote": true,
|
|
||||||
"tabWidth": 4,
|
|
||||||
"useTabs": false,
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"files": ["**/*.css", "**/*.scss", "**/*.html"],
|
|
||||||
"options": {
|
|
||||||
"singleQuote": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"trailingComma": "all",
|
|
||||||
"bracketSpacing": true,
|
|
||||||
"arrowParens": "always",
|
|
||||||
"proseWrap": "never",
|
|
||||||
"htmlWhitespaceSensitivity": "strict",
|
|
||||||
"endOfLine": "lf",
|
|
||||||
"singleAttributePerLine": true
|
|
||||||
}
|
|
||||||
12
.prettierrc.yaml
Normal file
12
.prettierrc.yaml
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
singleQuote: true
|
||||||
|
semi: true
|
||||||
|
printWidth: 100
|
||||||
|
tabWidth: 4
|
||||||
|
trailingComma: all
|
||||||
|
useTabs: false
|
||||||
|
arrowParens: always
|
||||||
|
proseWrap: never
|
||||||
|
htmlWhitespaceSensitivity: strict
|
||||||
|
endOfLine: lf
|
||||||
|
singleAttributePerLine: true
|
||||||
|
bracketSpacing: true
|
||||||
9
.vscode/extensions.json
vendored
9
.vscode/extensions.json
vendored
|
|
@ -1,10 +1,3 @@
|
||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": ["dbaeumer.vscode-eslint"]
|
||||||
"dbaeumer.vscode-eslint",
|
|
||||||
"EditorConfig.EditorConfig",
|
|
||||||
"stylelint.vscode-stylelint",
|
|
||||||
"esbenp.prettier-vscode",
|
|
||||||
"clinyong.vscode-css-modules",
|
|
||||||
"Huuums.vscode-fast-folder-structure"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
63
.vscode/launch.json
vendored
63
.vscode/launch.json
vendored
|
|
@ -1,28 +1,39 @@
|
||||||
{
|
{
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "Electron: Main",
|
"name": "Debug Main Process",
|
||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"protocol": "inspector",
|
"cwd": "${workspaceRoot}",
|
||||||
"runtimeExecutable": "npm",
|
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite",
|
||||||
"runtimeArgs": ["run start:main --inspect=5858 --remote-debugging-port=9223"],
|
"windows": {
|
||||||
"preLaunchTask": "Start Webpack Dev"
|
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd"
|
||||||
},
|
},
|
||||||
{
|
"runtimeArgs": ["--sourcemap"],
|
||||||
"name": "Electron: Renderer",
|
"env": {
|
||||||
"type": "chrome",
|
"REMOTE_DEBUGGING_PORT": "9222"
|
||||||
"request": "attach",
|
}
|
||||||
"port": 9223,
|
},
|
||||||
"webRoot": "${workspaceFolder}",
|
{
|
||||||
"timeout": 15000
|
"name": "Debug Renderer Process",
|
||||||
}
|
"port": 9222,
|
||||||
],
|
"request": "attach",
|
||||||
"compounds": [
|
"type": "chrome",
|
||||||
{
|
"webRoot": "${workspaceFolder}/src/renderer",
|
||||||
"name": "Electron: All",
|
"timeout": 60000,
|
||||||
"configurations": ["Electron: Main", "Electron: Renderer"]
|
"presentation": {
|
||||||
}
|
"hidden": true
|
||||||
]
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"compounds": [
|
||||||
|
{
|
||||||
|
"name": "Debug All",
|
||||||
|
"configurations": ["Debug Main Process", "Debug Renderer Process"],
|
||||||
|
"presentation": {
|
||||||
|
"order": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
10
.vscode/settings.json
vendored
10
.vscode/settings.json
vendored
|
|
@ -1,4 +1,13 @@
|
||||||
{
|
{
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[json]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
"files.associations": {
|
"files.associations": {
|
||||||
".eslintrc": "jsonc",
|
".eslintrc": "jsonc",
|
||||||
".prettierrc": "jsonc",
|
".prettierrc": "jsonc",
|
||||||
|
|
@ -40,7 +49,6 @@
|
||||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||||
"stylelint.validate": ["css", "scss", "typescript", "typescriptreact"],
|
"stylelint.validate": ["css", "scss", "typescript", "typescriptreact"],
|
||||||
"typescript.updateImportsOnFileMove.enabled": "always",
|
"typescript.updateImportsOnFileMove.enabled": "always",
|
||||||
"[typescript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
|
|
||||||
"[typescriptreact]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
|
"[typescriptreact]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
|
||||||
"typescript.format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": true,
|
"typescript.format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": true,
|
||||||
"folderTemplates.structures": [
|
"folderTemplates.structures": [
|
||||||
|
|
|
||||||
25
.vscode/tasks.json
vendored
25
.vscode/tasks.json
vendored
|
|
@ -1,25 +0,0 @@
|
||||||
{
|
|
||||||
"version": "2.0.0",
|
|
||||||
"tasks": [
|
|
||||||
{
|
|
||||||
"type": "npm",
|
|
||||||
"label": "Start Webpack Dev",
|
|
||||||
"script": "start:renderer",
|
|
||||||
"options": {
|
|
||||||
"cwd": "${workspaceFolder}"
|
|
||||||
},
|
|
||||||
"isBackground": true,
|
|
||||||
"problemMatcher": {
|
|
||||||
"owner": "custom",
|
|
||||||
"pattern": {
|
|
||||||
"regexp": "____________"
|
|
||||||
},
|
|
||||||
"background": {
|
|
||||||
"activeOnStart": true,
|
|
||||||
"beginsPattern": "Compiling\\.\\.\\.$",
|
|
||||||
"endsPattern": "(Compiled successfully|Failed to compile)\\.$"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
12
build/entitlements.mac.plist
Normal file
12
build/entitlements.mac.plist
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.cs.allow-jit</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
BIN
build/icon.icns
Normal file
BIN
build/icon.icns
Normal file
Binary file not shown.
BIN
build/icon.ico
Normal file
BIN
build/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 176 KiB |
BIN
build/icon.png
Normal file
BIN
build/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
3
dev-app-update.yml
Normal file
3
dev-app-update.yml
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
provider: generic
|
||||||
|
url: https://example.com/auto-updates
|
||||||
|
updaterCacheDirName: feishin-updater
|
||||||
43
electron-builder.yml
Normal file
43
electron-builder.yml
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
appId: com.electron.app
|
||||||
|
productName: feishin
|
||||||
|
directories:
|
||||||
|
buildResources: build
|
||||||
|
files:
|
||||||
|
- '!**/.vscode/*'
|
||||||
|
- '!src/*'
|
||||||
|
- '!electron.vite.config.{js,ts,mjs,cjs}'
|
||||||
|
- '!{.eslintcache,eslint.config.mjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
|
||||||
|
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
|
||||||
|
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
|
||||||
|
asarUnpack:
|
||||||
|
- resources/**
|
||||||
|
win:
|
||||||
|
executableName: feishin
|
||||||
|
nsis:
|
||||||
|
artifactName: ${name}-${version}-setup.${ext}
|
||||||
|
shortcutName: ${productName}
|
||||||
|
uninstallDisplayName: ${productName}
|
||||||
|
createDesktopShortcut: always
|
||||||
|
mac:
|
||||||
|
entitlementsInherit: build/entitlements.mac.plist
|
||||||
|
extendInfo:
|
||||||
|
- NSCameraUsageDescription: Application requests access to the device's camera.
|
||||||
|
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
||||||
|
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
|
||||||
|
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
||||||
|
notarize: false
|
||||||
|
dmg:
|
||||||
|
artifactName: ${name}-${version}.${ext}
|
||||||
|
linux:
|
||||||
|
target:
|
||||||
|
- AppImage
|
||||||
|
- snap
|
||||||
|
- deb
|
||||||
|
maintainer: electronjs.org
|
||||||
|
category: Utility
|
||||||
|
appImage:
|
||||||
|
artifactName: ${name}-${version}.${ext}
|
||||||
|
npmRebuild: false
|
||||||
|
publish:
|
||||||
|
provider: generic
|
||||||
|
url: https://example.com/auto-updates
|
||||||
35
electron.vite.config.ts
Normal file
35
electron.vite.config.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import { externalizeDepsPlugin, UserConfig } from 'electron-vite';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
|
||||||
|
const config: UserConfig = {
|
||||||
|
main: {
|
||||||
|
plugins: [externalizeDepsPlugin()],
|
||||||
|
},
|
||||||
|
preload: {
|
||||||
|
plugins: [externalizeDepsPlugin()],
|
||||||
|
},
|
||||||
|
renderer: {
|
||||||
|
css: {
|
||||||
|
modules: {
|
||||||
|
generateScopedName: '[name]__[local]__[hash:base64:5]',
|
||||||
|
localsConvention: 'camelCase',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [react()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'/@/i18n': resolve('src/i18n'),
|
||||||
|
'/@/main': resolve('src/main'),
|
||||||
|
'/@/renderer': resolve('src/renderer'),
|
||||||
|
'/@/renderer/api': resolve('src/renderer/api'),
|
||||||
|
'/@/renderer/components': resolve('src/renderer/components'),
|
||||||
|
'/@/renderer/features': resolve('src/renderer/features'),
|
||||||
|
'/@/renderer/hooks': resolve('src/renderer/hooks'),
|
||||||
|
'/@/shared': resolve('src/shared'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
48
eslint.config.mjs
Normal file
48
eslint.config.mjs
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
import eslintConfigPrettier from '@electron-toolkit/eslint-config-prettier';
|
||||||
|
import tseslint from '@electron-toolkit/eslint-config-ts';
|
||||||
|
import perfectionist from 'eslint-plugin-perfectionist';
|
||||||
|
import eslintPluginReact from 'eslint-plugin-react';
|
||||||
|
import eslintPluginReactHooks from 'eslint-plugin-react-hooks';
|
||||||
|
import eslintPluginReactRefresh from 'eslint-plugin-react-refresh';
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{ ignores: ['**/node_modules', '**/dist', '**/out'] },
|
||||||
|
tseslint.configs.recommended,
|
||||||
|
perfectionist.configs['recommended-natural'],
|
||||||
|
eslintPluginReact.configs.flat.recommended,
|
||||||
|
eslintPluginReact.configs.flat['jsx-runtime'],
|
||||||
|
{
|
||||||
|
settings: {
|
||||||
|
react: {
|
||||||
|
version: 'detect',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
plugins: {
|
||||||
|
'react-hooks': eslintPluginReactHooks,
|
||||||
|
'react-refresh': eslintPluginReactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...eslintPluginReactHooks.configs.recommended.rules,
|
||||||
|
...eslintPluginReactRefresh.configs.vite.rules,
|
||||||
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
|
'@typescript-eslint/no-unused-vars': 'warn',
|
||||||
|
curly: ['error', 'all'],
|
||||||
|
indent: [
|
||||||
|
'error',
|
||||||
|
'tab',
|
||||||
|
{
|
||||||
|
offsetTernaryExpressions: true,
|
||||||
|
SwitchCase: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'no-unused-vars': 'off',
|
||||||
|
'no-use-before-define': 'off',
|
||||||
|
quotes: ['error', 'single'],
|
||||||
|
semi: ['error', 'always'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
eslintConfigPrettier,
|
||||||
|
);
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
server {
|
|
||||||
listen 9180;
|
|
||||||
sendfile on;
|
|
||||||
default_type application/octet-stream;
|
|
||||||
|
|
||||||
gzip on;
|
|
||||||
gzip_http_version 1.1;
|
|
||||||
gzip_disable "MSIE [1-6]\.";
|
|
||||||
gzip_min_length 256;
|
|
||||||
gzip_vary on;
|
|
||||||
gzip_proxied expired no-cache no-store private auth;
|
|
||||||
gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript;
|
|
||||||
gzip_comp_level 9;
|
|
||||||
|
|
||||||
location ${PUBLIC_PATH} {
|
|
||||||
alias /usr/share/nginx/html/;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
6057
package-lock.json
generated
6057
package-lock.json
generated
File diff suppressed because it is too large
Load diff
449
package.json
449
package.json
|
|
@ -1,297 +1,25 @@
|
||||||
{
|
{
|
||||||
"name": "feishin",
|
"name": "feishin",
|
||||||
"productName": "Feishin",
|
"version": "1.0.0",
|
||||||
"description": "Feishin music server",
|
"description": "An Electron application with React and TypeScript",
|
||||||
"version": "0.13.0",
|
"main": "./out/main/index.js",
|
||||||
|
"author": "example.com",
|
||||||
|
"homepage": "https://electron-vite.org",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "concurrently \"npm run build:main\" \"npm run build:renderer\" \"npm run build:remote\"",
|
"format": "prettier --write .",
|
||||||
"build:main": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.main.prod.ts",
|
"lint": "eslint --cache --fix .",
|
||||||
"build:remote": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.remote.prod.ts",
|
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
|
||||||
"build:renderer": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.renderer.prod.ts",
|
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
|
||||||
"build:web": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.web.prod.ts",
|
"typecheck": "npm run typecheck:node && npm run typecheck:web",
|
||||||
"build:docker": "docker build -t jeffvli/feishin .",
|
"start": "electron-vite preview",
|
||||||
"rebuild": "electron-rebuild --parallel --types prod,dev,optional --module-dir release/app",
|
"dev": "electron-vite dev",
|
||||||
"lint": "concurrently \"npm run lint:code\" \"npm run lint:styles\"",
|
"dev:watch": "electron-vite dev --watch",
|
||||||
"lint:code": "cross-env NODE_ENV=development eslint . --ext .js,.jsx,.ts,.tsx --fix",
|
"build": "npm run typecheck && electron-vite build",
|
||||||
"lint:styles": "npx stylelint **/*.tsx --fix",
|
"postinstall": "electron-builder install-app-deps",
|
||||||
"package": "node --import tsx ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never",
|
"build:unpack": "npm run build && electron-builder --dir",
|
||||||
"package:pr": "node --import tsx ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never --win --mac --linux",
|
"build:win": "npm run build && electron-builder --win",
|
||||||
"package:pr:macos": "node --import tsx ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never --mac",
|
"build:mac": "electron-vite build && electron-builder --mac",
|
||||||
"package:pr:windows": "node --import tsx ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never --win",
|
"build:linux": "electron-vite build && electron-builder --linux"
|
||||||
"package:pr:linux": "node --import tsx ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never --linux",
|
|
||||||
"package:dev": "node --import tsx ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never --dir",
|
|
||||||
"postinstall": "node --import tsx .erb/scripts/check-native-dep.js && electron-builder install-app-deps && cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.renderer.dev.dll.ts",
|
|
||||||
"start": "node --import tsx ./.erb/scripts/check-port-in-use.js && npm run start:renderer",
|
|
||||||
"start:main": "cross-env NODE_ENV=development NODE_OPTIONS=\"--import tsx\" electron -r ts-node/register/transpile-only ./src/main/main.ts",
|
|
||||||
"start:preload": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.preload.dev.ts",
|
|
||||||
"start:remote": "cross-env NODE_ENV=developemnt TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.remote.dev.ts",
|
|
||||||
"start:renderer": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./.erb/configs/webpack.config.renderer.dev.ts",
|
|
||||||
"start:web": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./.erb/configs/webpack.config.renderer.web.ts",
|
|
||||||
"test": "jest",
|
|
||||||
"prepare": "husky install",
|
|
||||||
"i18next": "i18next -c src/i18n/i18next-parser.config.js",
|
|
||||||
"prod:buildserver": "pwsh -c \"./scripts/server-build.ps1\"",
|
|
||||||
"prod:publishserver": "pwsh -c \"./scripts/server-publish.ps1\""
|
|
||||||
},
|
|
||||||
"lint-staged": {
|
|
||||||
"*.{js,jsx,ts,tsx}": [
|
|
||||||
"cross-env NODE_ENV=development eslint --cache"
|
|
||||||
],
|
|
||||||
"*.json,.{eslintrc,prettierrc}": [
|
|
||||||
"prettier --ignore-path .eslintignore --parser json --write"
|
|
||||||
],
|
|
||||||
"*.{css,scss}": [
|
|
||||||
"prettier --ignore-path .eslintignore --single-quote --write"
|
|
||||||
],
|
|
||||||
"*.{html,md,yml}": [
|
|
||||||
"prettier --ignore-path .eslintignore --single-quote --write"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"build": {
|
|
||||||
"productName": "Feishin",
|
|
||||||
"appId": "org.jeffvli.feishin",
|
|
||||||
"artifactName": "${productName}-${version}-${os}-${arch}.${ext}",
|
|
||||||
"asar": true,
|
|
||||||
"asarUnpack": "**\\*.{node,dll}",
|
|
||||||
"files": [
|
|
||||||
"dist",
|
|
||||||
"node_modules",
|
|
||||||
"package.json"
|
|
||||||
],
|
|
||||||
"afterSign": ".erb/scripts/notarize.js",
|
|
||||||
"electronVersion": "36.1.0",
|
|
||||||
"mac": {
|
|
||||||
"target": {
|
|
||||||
"target": "default",
|
|
||||||
"arch": [
|
|
||||||
"arm64",
|
|
||||||
"x64"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"icon": "assets/icons/icon.icns",
|
|
||||||
"type": "distribution",
|
|
||||||
"hardenedRuntime": true,
|
|
||||||
"entitlements": "assets/entitlements.mac.plist",
|
|
||||||
"entitlementsInherit": "assets/entitlements.mac.plist",
|
|
||||||
"gatekeeperAssess": false
|
|
||||||
},
|
|
||||||
"dmg": {
|
|
||||||
"contents": [
|
|
||||||
{
|
|
||||||
"x": 130,
|
|
||||||
"y": 220
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"x": 410,
|
|
||||||
"y": 220,
|
|
||||||
"type": "link",
|
|
||||||
"path": "/Applications"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"win": {
|
|
||||||
"target": [
|
|
||||||
"nsis",
|
|
||||||
"zip"
|
|
||||||
],
|
|
||||||
"icon": "assets/icons/icon.ico"
|
|
||||||
},
|
|
||||||
"deb": {
|
|
||||||
"depends": [
|
|
||||||
"libgssapi_krb5.so.2",
|
|
||||||
"libavahi-common.so.3",
|
|
||||||
"libavahi-client.so.3",
|
|
||||||
"libkrb5.so.3",
|
|
||||||
"libkrb5support.so.0",
|
|
||||||
"libkeyutils.so.1",
|
|
||||||
"libcups.so.2"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"rpm": {
|
|
||||||
"depends": [
|
|
||||||
"libgssapi_krb5.so.2",
|
|
||||||
"libavahi-common.so.3",
|
|
||||||
"libavahi-client.so.3",
|
|
||||||
"libkrb5.so.3",
|
|
||||||
"libkrb5support.so.0",
|
|
||||||
"libkeyutils.so.1",
|
|
||||||
"libcups.so.2"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"freebsd": {
|
|
||||||
"depends": [
|
|
||||||
"libgssapi_krb5.so.2",
|
|
||||||
"libavahi-common.so.3",
|
|
||||||
"libavahi-client.so.3",
|
|
||||||
"libkrb5.so.3",
|
|
||||||
"libkrb5support.so.0",
|
|
||||||
"libkeyutils.so.1",
|
|
||||||
"libcups.so.2"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"linux": {
|
|
||||||
"target": [
|
|
||||||
"AppImage",
|
|
||||||
"tar.xz"
|
|
||||||
],
|
|
||||||
"icon": "assets/icons/icon.png",
|
|
||||||
"category": "AudioVideo;Audio;Player"
|
|
||||||
},
|
|
||||||
"directories": {
|
|
||||||
"app": "release/app",
|
|
||||||
"buildResources": "assets",
|
|
||||||
"output": "release/build"
|
|
||||||
},
|
|
||||||
"extraResources": [
|
|
||||||
"./assets/**"
|
|
||||||
],
|
|
||||||
"publish": {
|
|
||||||
"provider": "github",
|
|
||||||
"owner": "jeffvli",
|
|
||||||
"repo": "feishin"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://github.com/jeffvli/feishin.git"
|
|
||||||
},
|
|
||||||
"author": {
|
|
||||||
"name": "jeffvli",
|
|
||||||
"url": "https://github.com/jeffvli/"
|
|
||||||
},
|
|
||||||
"contributors": [],
|
|
||||||
"license": "GPL-3.0",
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/jeffvli/feishin/issues"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"subsonic",
|
|
||||||
"navidrome",
|
|
||||||
"airsonic",
|
|
||||||
"jellyfin",
|
|
||||||
"react",
|
|
||||||
"electron"
|
|
||||||
],
|
|
||||||
"homepage": "https://github.com/jeffvli/feishin",
|
|
||||||
"jest": {
|
|
||||||
"testURL": "http://localhost/",
|
|
||||||
"testEnvironment": "jsdom",
|
|
||||||
"transform": {
|
|
||||||
"\\.(ts|tsx|js|jsx)$": "ts-jest"
|
|
||||||
},
|
|
||||||
"moduleNameMapper": {
|
|
||||||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/.erb/mocks/fileMock.js",
|
|
||||||
"\\.(css|less|sass|scss)$": "identity-obj-proxy"
|
|
||||||
},
|
|
||||||
"moduleFileExtensions": [
|
|
||||||
"js",
|
|
||||||
"jsx",
|
|
||||||
"ts",
|
|
||||||
"tsx",
|
|
||||||
"json"
|
|
||||||
],
|
|
||||||
"moduleDirectories": [
|
|
||||||
"node_modules",
|
|
||||||
"release/app/node_modules"
|
|
||||||
],
|
|
||||||
"testPathIgnorePatterns": [
|
|
||||||
"release/app/dist"
|
|
||||||
],
|
|
||||||
"setupFiles": [
|
|
||||||
"./.erb/scripts/check-build-exists.ts"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@electron/rebuild": "^3.6.0",
|
|
||||||
"@pmmmwh/react-refresh-webpack-plugin": "0.5.5",
|
|
||||||
"@stylelint/postcss-css-in-js": "^0.38.0",
|
|
||||||
"@teamsupercell/typings-for-css-modules-loader": "^2.5.1",
|
|
||||||
"@testing-library/jest-dom": "^5.16.4",
|
|
||||||
"@testing-library/react": "^13.0.0",
|
|
||||||
"@types/dompurify": "^3.0.5",
|
|
||||||
"@types/electron-localshortcut": "^3.1.0",
|
|
||||||
"@types/jest": "^27.4.1",
|
|
||||||
"@types/lodash": "^4.14.188",
|
|
||||||
"@types/md5": "^2.3.2",
|
|
||||||
"@types/node": "^17.0.23",
|
|
||||||
"@types/react": "^18.0.25",
|
|
||||||
"@types/react-dom": "^18.0.8",
|
|
||||||
"@types/react-test-renderer": "^17.0.1",
|
|
||||||
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
|
||||||
"@types/react-window": "^1.8.5",
|
|
||||||
"@types/react-window-infinite-loader": "^1.0.6",
|
|
||||||
"@types/styled-components": "^5.1.26",
|
|
||||||
"@types/terser-webpack-plugin": "^5.0.4",
|
|
||||||
"@types/webpack-bundle-analyzer": "^4.4.1",
|
|
||||||
"@types/webpack-env": "^1.16.3",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^5.47.0",
|
|
||||||
"@typescript-eslint/parser": "^5.47.0",
|
|
||||||
"browserslist-config-erb": "^0.0.3",
|
|
||||||
"chalk": "^4.1.2",
|
|
||||||
"concurrently": "^7.1.0",
|
|
||||||
"core-js": "^3.21.1",
|
|
||||||
"cross-env": "^7.0.3",
|
|
||||||
"css-loader": "^6.7.1",
|
|
||||||
"css-minimizer-webpack-plugin": "^3.4.1",
|
|
||||||
"detect-port": "^1.3.0",
|
|
||||||
"electron": "^36.1.0",
|
|
||||||
"electron-builder": "^24.13.3",
|
|
||||||
"electron-devtools-installer": "^3.2.0",
|
|
||||||
"electron-notarize": "^1.2.1",
|
|
||||||
"electronmon": "^2.0.2",
|
|
||||||
"eslint": "^8.30.0",
|
|
||||||
"eslint-config-airbnb-base": "^15.0.0",
|
|
||||||
"eslint-config-erb": "^4.0.3",
|
|
||||||
"eslint-import-resolver-typescript": "^2.7.1",
|
|
||||||
"eslint-import-resolver-webpack": "^0.13.2",
|
|
||||||
"eslint-plugin-compat": "^4.2.0",
|
|
||||||
"eslint-plugin-import": "^2.26.0",
|
|
||||||
"eslint-plugin-jest": "^26.1.3",
|
|
||||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
|
||||||
"eslint-plugin-promise": "^6.0.0",
|
|
||||||
"eslint-plugin-react": "^7.29.4",
|
|
||||||
"eslint-plugin-react-hooks": "^4.4.0",
|
|
||||||
"eslint-plugin-sort-keys-fix": "^1.1.2",
|
|
||||||
"eslint-plugin-typescript-sort-keys": "^2.1.0",
|
|
||||||
"file-loader": "^6.2.0",
|
|
||||||
"html-webpack-plugin": "^5.5.0",
|
|
||||||
"husky": "^7.0.4",
|
|
||||||
"i18next-parser": "^9.0.2",
|
|
||||||
"identity-obj-proxy": "^3.0.0",
|
|
||||||
"jest": "^27.5.1",
|
|
||||||
"lint-staged": "^12.3.7",
|
|
||||||
"mini-css-extract-plugin": "^2.6.0",
|
|
||||||
"postcss-scss": "^4.0.4",
|
|
||||||
"postcss-styled-syntax": "^0.5.0",
|
|
||||||
"postcss-syntax": "^0.36.2",
|
|
||||||
"prettier": "^3.3.3",
|
|
||||||
"react-refresh": "^0.12.0",
|
|
||||||
"react-refresh-typescript": "^2.0.4",
|
|
||||||
"react-test-renderer": "^18.0.0",
|
|
||||||
"rimraf": "^3.0.2",
|
|
||||||
"sass": "^1.49.11",
|
|
||||||
"sass-loader": "^12.6.0",
|
|
||||||
"style-loader": "^3.3.1",
|
|
||||||
"stylelint": "^15.10.3",
|
|
||||||
"stylelint-config-css-modules": "^4.3.0",
|
|
||||||
"stylelint-config-recess-order": "^4.3.0",
|
|
||||||
"stylelint-config-standard": "^34.0.0",
|
|
||||||
"stylelint-config-standard-scss": "^4.0.0",
|
|
||||||
"stylelint-config-styled-components": "^0.1.1",
|
|
||||||
"terser-webpack-plugin": "^5.3.1",
|
|
||||||
"ts-jest": "^27.1.4",
|
|
||||||
"ts-loader": "^9.2.8",
|
|
||||||
"ts-node": "^10.9.2",
|
|
||||||
"tsconfig-paths-webpack-plugin": "^4.0.0",
|
|
||||||
"tsx": "^4.16.2",
|
|
||||||
"typescript": "^5.2.2",
|
|
||||||
"typescript-plugin-styled-components": "^3.0.0",
|
|
||||||
"url-loader": "^4.1.1",
|
|
||||||
"webpack": "^5.94.0",
|
|
||||||
"webpack-bundle-analyzer": "^4.5.0",
|
|
||||||
"webpack-cli": "^4.9.2",
|
|
||||||
"webpack-dev-server": "^4.8.0",
|
|
||||||
"webpack-merge": "^5.8.0"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ag-grid-community/client-side-row-model": "^28.2.1",
|
"@ag-grid-community/client-side-row-model": "^28.2.1",
|
||||||
|
|
@ -299,6 +27,8 @@
|
||||||
"@ag-grid-community/infinite-row-model": "^28.2.1",
|
"@ag-grid-community/infinite-row-model": "^28.2.1",
|
||||||
"@ag-grid-community/react": "^28.2.1",
|
"@ag-grid-community/react": "^28.2.1",
|
||||||
"@ag-grid-community/styles": "^28.2.1",
|
"@ag-grid-community/styles": "^28.2.1",
|
||||||
|
"@electron-toolkit/preload": "^3.0.1",
|
||||||
|
"@electron-toolkit/utils": "^4.0.0",
|
||||||
"@emotion/react": "^11.10.4",
|
"@emotion/react": "^11.10.4",
|
||||||
"@mantine/core": "^6.0.17",
|
"@mantine/core": "^6.0.17",
|
||||||
"@mantine/dates": "^6.0.17",
|
"@mantine/dates": "^6.0.17",
|
||||||
|
|
@ -315,6 +45,7 @@
|
||||||
"audiomotion-analyzer": "^4.5.0",
|
"audiomotion-analyzer": "^4.5.0",
|
||||||
"auto-text-size": "^0.2.3",
|
"auto-text-size": "^0.2.3",
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
|
"cheerio": "^1.0.0",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"cmdk": "^0.2.0",
|
"cmdk": "^0.2.0",
|
||||||
"dayjs": "^1.11.6",
|
"dayjs": "^1.11.6",
|
||||||
|
|
@ -323,7 +54,7 @@
|
||||||
"electron-localshortcut": "^3.2.1",
|
"electron-localshortcut": "^3.2.1",
|
||||||
"electron-log": "^5.1.1",
|
"electron-log": "^5.1.1",
|
||||||
"electron-store": "^8.1.0",
|
"electron-store": "^8.1.0",
|
||||||
"electron-updater": "^6.3.1",
|
"electron-updater": "^6.3.9",
|
||||||
"fast-average-color": "^9.3.0",
|
"fast-average-color": "^9.3.0",
|
||||||
"format-duration": "^2.0.0",
|
"format-duration": "^2.0.0",
|
||||||
"framer-motion": "^11.0.0",
|
"framer-motion": "^11.0.0",
|
||||||
|
|
@ -336,11 +67,12 @@
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"md5": "^2.3.0",
|
"md5": "^2.3.0",
|
||||||
"memoize-one": "^6.0.0",
|
"memoize-one": "^6.0.0",
|
||||||
|
"mpris-service": "^2.1.2",
|
||||||
"nanoid": "^3.3.3",
|
"nanoid": "^3.3.3",
|
||||||
"net": "^1.0.2",
|
"net": "^1.0.2",
|
||||||
"node-mpv": "github:jeffvli/Node-MPV#32b4d64395289ad710c41d481d2707a7acfc228f",
|
"node-mpv": "github:jeffvli/Node-MPV#32b4d64395289ad710c41d481d2707a7acfc228f",
|
||||||
"overlayscrollbars": "^2.2.1",
|
"overlayscrollbars": "^2.11.1",
|
||||||
"overlayscrollbars-react": "^0.5.1",
|
"overlayscrollbars-react": "^0.5.6",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-error-boundary": "^3.1.4",
|
"react-error-boundary": "^3.1.4",
|
||||||
|
|
@ -356,33 +88,112 @@
|
||||||
"semver": "^7.5.4",
|
"semver": "^7.5.4",
|
||||||
"styled-components": "^6.0.8",
|
"styled-components": "^6.0.8",
|
||||||
"swiper": "^9.3.1",
|
"swiper": "^9.3.1",
|
||||||
|
"ws": "^8.18.2",
|
||||||
"zod": "^3.22.3",
|
"zod": "^3.22.3",
|
||||||
"zustand": "^4.3.9"
|
"zustand": "^4.3.9"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"devDependencies": {
|
||||||
"styled-components": "^6",
|
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
|
||||||
"entities": "2.2.0"
|
"@electron-toolkit/eslint-config-ts": "^3.0.0",
|
||||||
},
|
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||||
"overrides": {
|
"@electron/rebuild": "^3.6.0",
|
||||||
"entities": "2.2.0"
|
"@pmmmwh/react-refresh-webpack-plugin": "0.5.5",
|
||||||
},
|
"@stylelint/postcss-css-in-js": "^0.38.0",
|
||||||
"devEngines": {
|
"@teamsupercell/typings-for-css-modules-loader": "^2.5.1",
|
||||||
"runtime": {
|
"@testing-library/jest-dom": "^5.16.4",
|
||||||
"name": "node",
|
"@testing-library/react": "^13.0.0",
|
||||||
"version": ">=18.x",
|
"@types/dompurify": "^3.0.5",
|
||||||
"onFail": "error"
|
"@types/electron-localshortcut": "^3.1.0",
|
||||||
},
|
"@types/jest": "^27.4.1",
|
||||||
"packageManager": {
|
"@types/lodash": "^4.14.188",
|
||||||
"name": "npm",
|
"@types/md5": "^2.3.2",
|
||||||
"version": ">=7.x",
|
"@types/node": "^22.14.1",
|
||||||
"onFail": "error"
|
"@types/react": "^19.1.1",
|
||||||
}
|
"@types/react-dom": "^19.1.2",
|
||||||
},
|
"@types/react-test-renderer": "^17.0.1",
|
||||||
"browserslist": [],
|
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
||||||
"electronmon": {
|
"@types/react-window": "^1.8.5",
|
||||||
"patterns": [
|
"@types/react-window-infinite-loader": "^1.0.6",
|
||||||
"!server",
|
"@types/styled-components": "^5.1.26",
|
||||||
"!src/renderer"
|
"@types/terser-webpack-plugin": "^5.0.4",
|
||||||
]
|
"@types/webpack-bundle-analyzer": "^4.4.1",
|
||||||
|
"@types/webpack-env": "^1.16.3",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.47.0",
|
||||||
|
"@typescript-eslint/parser": "^5.47.0",
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"browserslist-config-erb": "^0.0.3",
|
||||||
|
"chalk": "^4.1.2",
|
||||||
|
"concurrently": "^7.1.0",
|
||||||
|
"core-js": "^3.21.1",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"css-loader": "^6.7.1",
|
||||||
|
"css-minimizer-webpack-plugin": "^3.4.1",
|
||||||
|
"detect-port": "^1.3.0",
|
||||||
|
"electron": "^35.1.5",
|
||||||
|
"electron-builder": "^25.1.8",
|
||||||
|
"electron-devtools-installer": "^3.2.0",
|
||||||
|
"electron-notarize": "^1.2.1",
|
||||||
|
"electron-vite": "^3.1.0",
|
||||||
|
"electronmon": "^2.0.2",
|
||||||
|
"eslint": "^9.24.0",
|
||||||
|
"eslint-config-airbnb-base": "^15.0.0",
|
||||||
|
"eslint-config-erb": "^4.0.3",
|
||||||
|
"eslint-import-resolver-typescript": "^2.7.1",
|
||||||
|
"eslint-import-resolver-webpack": "^0.13.2",
|
||||||
|
"eslint-plugin-compat": "^4.2.0",
|
||||||
|
"eslint-plugin-import": "^2.26.0",
|
||||||
|
"eslint-plugin-jest": "^26.1.3",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||||
|
"eslint-plugin-perfectionist": "^4.13.0",
|
||||||
|
"eslint-plugin-prettier": "^5.4.0",
|
||||||
|
"eslint-plugin-promise": "^6.0.0",
|
||||||
|
"eslint-plugin-react": "^7.37.5",
|
||||||
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.19",
|
||||||
|
"eslint-plugin-sort-keys-fix": "^1.1.2",
|
||||||
|
"eslint-plugin-typescript-sort-keys": "^2.1.0",
|
||||||
|
"file-loader": "^6.2.0",
|
||||||
|
"html-webpack-plugin": "^5.5.0",
|
||||||
|
"husky": "^7.0.4",
|
||||||
|
"i18next-parser": "^9.0.2",
|
||||||
|
"identity-obj-proxy": "^3.0.0",
|
||||||
|
"jest": "^27.5.1",
|
||||||
|
"lint-staged": "^12.3.7",
|
||||||
|
"mini-css-extract-plugin": "^2.6.0",
|
||||||
|
"postcss-scss": "^4.0.4",
|
||||||
|
"postcss-styled-syntax": "^0.5.0",
|
||||||
|
"postcss-syntax": "^0.36.2",
|
||||||
|
"prettier": "^3.5.3",
|
||||||
|
"react": "^19.1.0",
|
||||||
|
"react-dom": "^19.1.0",
|
||||||
|
"react-refresh": "^0.12.0",
|
||||||
|
"react-refresh-typescript": "^2.0.4",
|
||||||
|
"react-test-renderer": "^18.0.0",
|
||||||
|
"rimraf": "^3.0.2",
|
||||||
|
"sass": "^1.49.11",
|
||||||
|
"sass-embedded": "^1.89.0",
|
||||||
|
"sass-loader": "^12.6.0",
|
||||||
|
"style-loader": "^3.3.1",
|
||||||
|
"stylelint": "^15.10.3",
|
||||||
|
"stylelint-config-css-modules": "^4.3.0",
|
||||||
|
"stylelint-config-recess-order": "^4.3.0",
|
||||||
|
"stylelint-config-standard": "^34.0.0",
|
||||||
|
"stylelint-config-standard-scss": "^4.0.0",
|
||||||
|
"stylelint-config-styled-components": "^0.1.1",
|
||||||
|
"terser-webpack-plugin": "^5.3.1",
|
||||||
|
"ts-jest": "^27.1.4",
|
||||||
|
"ts-loader": "^9.2.8",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"tsconfig-paths-webpack-plugin": "^4.0.0",
|
||||||
|
"tsx": "^4.16.2",
|
||||||
|
"typescript": "^5.8.3",
|
||||||
|
"typescript-plugin-styled-components": "^3.0.0",
|
||||||
|
"url-loader": "^4.1.1",
|
||||||
|
"vite": "^6.2.6",
|
||||||
|
"webpack": "^5.94.0",
|
||||||
|
"webpack-bundle-analyzer": "^4.5.0",
|
||||||
|
"webpack-cli": "^4.9.2",
|
||||||
|
"webpack-dev-server": "^4.8.0",
|
||||||
|
"webpack-merge": "^5.8.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2352
release/app/package-lock.json
generated
2352
release/app/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,30 +0,0 @@
|
||||||
{
|
|
||||||
"name": "feishin",
|
|
||||||
"version": "0.13.0",
|
|
||||||
"description": "",
|
|
||||||
"main": "./dist/main/main.js",
|
|
||||||
"author": {
|
|
||||||
"name": "jeffvli",
|
|
||||||
"url": "https://github.com/jeffvli/"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"electron-rebuild": "node -r ts-node/register ../../.erb/scripts/electron-rebuild.js",
|
|
||||||
"link-modules": "node -r ts-node/register ../../.erb/scripts/link-modules.ts",
|
|
||||||
"postinstall": "npm run electron-rebuild && npm run link-modules"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"cheerio": "^1.0.0-rc.12",
|
|
||||||
"mpris-service": "^2.1.2",
|
|
||||||
"ws": "^8.18.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"electron": "36.1.0"
|
|
||||||
},
|
|
||||||
"resolutions": {
|
|
||||||
"xml2js": "0.5.0"
|
|
||||||
},
|
|
||||||
"overrides": {
|
|
||||||
"xml2js": "0.5.0"
|
|
||||||
},
|
|
||||||
"license": "GPL-3.0"
|
|
||||||
}
|
|
||||||
BIN
resources/icon.png
Normal file
BIN
resources/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
|
|
@ -1,9 +0,0 @@
|
||||||
import '@testing-library/jest-dom';
|
|
||||||
import { render } from '@testing-library/react';
|
|
||||||
import { App } from '../renderer/app';
|
|
||||||
|
|
||||||
describe('App', () => {
|
|
||||||
it('should render', () => {
|
|
||||||
expect(render(<App />)).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,52 +1,53 @@
|
||||||
import { PostProcessorModule, TOptions, StringMap } from 'i18next';
|
import { PostProcessorModule, StringMap, TOptions } from 'i18next';
|
||||||
import i18n from 'i18next';
|
import i18n from 'i18next';
|
||||||
import { initReactI18next } from 'react-i18next';
|
import { initReactI18next } from 'react-i18next';
|
||||||
|
|
||||||
|
import cs from './locales/cs.json';
|
||||||
|
import de from './locales/de.json';
|
||||||
import en from './locales/en.json';
|
import en from './locales/en.json';
|
||||||
import es from './locales/es.json';
|
import es from './locales/es.json';
|
||||||
|
import fa from './locales/fa.json';
|
||||||
|
import fi from './locales/fi.json';
|
||||||
import fr from './locales/fr.json';
|
import fr from './locales/fr.json';
|
||||||
import ja from './locales/ja.json';
|
import hu from './locales/hu.json';
|
||||||
import pl from './locales/pl.json';
|
import id from './locales/id.json';
|
||||||
import zhHans from './locales/zh-Hans.json';
|
|
||||||
import de from './locales/de.json';
|
|
||||||
import it from './locales/it.json';
|
import it from './locales/it.json';
|
||||||
import ru from './locales/ru.json';
|
import ja from './locales/ja.json';
|
||||||
import ptBr from './locales/pt-BR.json';
|
import ko from './locales/ko.json';
|
||||||
import sr from './locales/sr.json';
|
|
||||||
import sv from './locales/sv.json';
|
|
||||||
import cs from './locales/cs.json';
|
|
||||||
import nbNO from './locales/nb-NO.json';
|
import nbNO from './locales/nb-NO.json';
|
||||||
import nl from './locales/nl.json';
|
import nl from './locales/nl.json';
|
||||||
import zhHant from './locales/zh-Hant.json';
|
import pl from './locales/pl.json';
|
||||||
import fa from './locales/fa.json';
|
import ptBr from './locales/pt-BR.json';
|
||||||
import ko from './locales/ko.json';
|
import ru from './locales/ru.json';
|
||||||
|
import sr from './locales/sr.json';
|
||||||
|
import sv from './locales/sv.json';
|
||||||
import ta from './locales/ta.json';
|
import ta from './locales/ta.json';
|
||||||
import id from './locales/id.json';
|
import zhHans from './locales/zh-Hans.json';
|
||||||
import fi from './locales/fi.json';
|
import zhHant from './locales/zh-Hant.json';
|
||||||
import hu from './locales/hu.json';
|
|
||||||
|
|
||||||
const resources = {
|
const resources = {
|
||||||
|
cs: { translation: cs },
|
||||||
|
de: { translation: de },
|
||||||
en: { translation: en },
|
en: { translation: en },
|
||||||
es: { translation: es },
|
es: { translation: es },
|
||||||
de: { translation: de },
|
|
||||||
it: { translation: it },
|
|
||||||
ru: { translation: ru },
|
|
||||||
'pt-BR': { translation: ptBr },
|
|
||||||
fa: { translation: fa },
|
fa: { translation: fa },
|
||||||
|
fi: { translation: fi },
|
||||||
fr: { translation: fr },
|
fr: { translation: fr },
|
||||||
|
hu: { translation: hu },
|
||||||
|
id: { translation: id },
|
||||||
|
it: { translation: it },
|
||||||
ja: { translation: ja },
|
ja: { translation: ja },
|
||||||
ko: { translation: ko },
|
ko: { translation: ko },
|
||||||
|
'nb-NO': { translation: nbNO },
|
||||||
|
nl: { translation: nl },
|
||||||
pl: { translation: pl },
|
pl: { translation: pl },
|
||||||
'zh-Hans': { translation: zhHans },
|
'pt-BR': { translation: ptBr },
|
||||||
'zh-Hant': { translation: zhHant },
|
ru: { translation: ru },
|
||||||
sr: { translation: sr },
|
sr: { translation: sr },
|
||||||
sv: { translation: sv },
|
sv: { translation: sv },
|
||||||
cs: { translation: cs },
|
|
||||||
nl: { translation: nl },
|
|
||||||
'nb-NO': { translation: nbNO },
|
|
||||||
ta: { translation: ta },
|
ta: { translation: ta },
|
||||||
id: { translation: id },
|
'zh-Hans': { translation: zhHans },
|
||||||
fi: { translation: fi },
|
'zh-Hant': { translation: zhHant },
|
||||||
hu: { translation: hu },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const languages = [
|
export const languages = [
|
||||||
|
|
@ -141,35 +142,34 @@ export const languages = [
|
||||||
];
|
];
|
||||||
|
|
||||||
const lowerCasePostProcessor: PostProcessorModule = {
|
const lowerCasePostProcessor: PostProcessorModule = {
|
||||||
type: 'postProcessor',
|
|
||||||
name: 'lowerCase',
|
name: 'lowerCase',
|
||||||
process: (value: string) => {
|
process: (value: string) => {
|
||||||
return value.toLocaleLowerCase();
|
return value.toLocaleLowerCase();
|
||||||
},
|
},
|
||||||
|
type: 'postProcessor',
|
||||||
};
|
};
|
||||||
|
|
||||||
const upperCasePostProcessor: PostProcessorModule = {
|
const upperCasePostProcessor: PostProcessorModule = {
|
||||||
type: 'postProcessor',
|
|
||||||
name: 'upperCase',
|
name: 'upperCase',
|
||||||
process: (value: string) => {
|
process: (value: string) => {
|
||||||
return value.toLocaleUpperCase();
|
return value.toLocaleUpperCase();
|
||||||
},
|
},
|
||||||
|
type: 'postProcessor',
|
||||||
};
|
};
|
||||||
|
|
||||||
const titleCasePostProcessor: PostProcessorModule = {
|
const titleCasePostProcessor: PostProcessorModule = {
|
||||||
type: 'postProcessor',
|
|
||||||
name: 'titleCase',
|
name: 'titleCase',
|
||||||
process: (value: string) => {
|
process: (value: string) => {
|
||||||
return value.replace(/\S\S*/g, (txt) => {
|
return value.replace(/\S\S*/g, (txt) => {
|
||||||
return txt.charAt(0).toLocaleUpperCase() + txt.slice(1).toLowerCase();
|
return txt.charAt(0).toLocaleUpperCase() + txt.slice(1).toLowerCase();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
type: 'postProcessor',
|
||||||
};
|
};
|
||||||
|
|
||||||
const ignoreSentenceCaseLanguages = ['de'];
|
const ignoreSentenceCaseLanguages = ['de'];
|
||||||
|
|
||||||
const sentenceCasePostProcessor: PostProcessorModule = {
|
const sentenceCasePostProcessor: PostProcessorModule = {
|
||||||
type: 'postProcessor',
|
|
||||||
name: 'sentenceCase',
|
name: 'sentenceCase',
|
||||||
process: (value: string, _key: string, _options: TOptions<StringMap>, translator: any) => {
|
process: (value: string, _key: string, _options: TOptions<StringMap>, translator: any) => {
|
||||||
const sentences = value.split('. ');
|
const sentences = value.split('. ');
|
||||||
|
|
@ -185,6 +185,7 @@ const sentenceCasePostProcessor: PostProcessorModule = {
|
||||||
})
|
})
|
||||||
.join('. ');
|
.join('. ');
|
||||||
},
|
},
|
||||||
|
type: 'postProcessor',
|
||||||
};
|
};
|
||||||
i18n.use(lowerCasePostProcessor)
|
i18n.use(lowerCasePostProcessor)
|
||||||
.use(upperCasePostProcessor)
|
.use(upperCasePostProcessor)
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import axios, { AxiosResponse } from 'axios';
|
import axios, { AxiosResponse } from 'axios';
|
||||||
import { load } from 'cheerio';
|
import { load } from 'cheerio';
|
||||||
import { orderSearchResults } from './shared';
|
|
||||||
import {
|
import {
|
||||||
LyricSource,
|
|
||||||
InternetProviderLyricResponse,
|
InternetProviderLyricResponse,
|
||||||
InternetProviderLyricSearchResponse,
|
InternetProviderLyricSearchResponse,
|
||||||
LyricSearchQuery,
|
LyricSearchQuery,
|
||||||
} from '../../../../renderer/api/types';
|
LyricSource,
|
||||||
|
} from '.';
|
||||||
|
import { orderSearchResults } from './shared';
|
||||||
|
|
||||||
const SEARCH_URL = 'https://genius.com/api/search/song';
|
const SEARCH_URL = 'https://genius.com/api/search/song';
|
||||||
|
|
||||||
|
|
@ -17,20 +18,6 @@ export interface GeniusResponse {
|
||||||
response: Response;
|
response: Response;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Meta {
|
|
||||||
status: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Response {
|
|
||||||
next_page: number;
|
|
||||||
sections: Section[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Section {
|
|
||||||
hits: Hit[];
|
|
||||||
type: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Hit {
|
export interface Hit {
|
||||||
highlights: any[];
|
highlights: any[];
|
||||||
index: string;
|
index: string;
|
||||||
|
|
@ -38,6 +25,35 @@ export interface Hit {
|
||||||
type: string;
|
type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Meta {
|
||||||
|
status: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PrimaryArtist {
|
||||||
|
_type: string;
|
||||||
|
api_path: string;
|
||||||
|
header_image_url: string;
|
||||||
|
id: number;
|
||||||
|
image_url: string;
|
||||||
|
index_character: string;
|
||||||
|
is_meme_verified: boolean;
|
||||||
|
is_verified: boolean;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReleaseDateComponents {
|
||||||
|
day: number;
|
||||||
|
month: number;
|
||||||
|
year: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Response {
|
||||||
|
next_page: number;
|
||||||
|
sections: Section[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface Result {
|
export interface Result {
|
||||||
_type: string;
|
_type: string;
|
||||||
annotation_count: number;
|
annotation_count: number;
|
||||||
|
|
@ -69,24 +85,9 @@ export interface Result {
|
||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PrimaryArtist {
|
export interface Section {
|
||||||
_type: string;
|
hits: Hit[];
|
||||||
api_path: string;
|
type: string;
|
||||||
header_image_url: string;
|
|
||||||
id: number;
|
|
||||||
image_url: string;
|
|
||||||
index_character: string;
|
|
||||||
is_meme_verified: boolean;
|
|
||||||
is_verified: boolean;
|
|
||||||
name: string;
|
|
||||||
slug: string;
|
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ReleaseDateComponents {
|
|
||||||
day: number;
|
|
||||||
month: number;
|
|
||||||
year: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Stats {
|
export interface Stats {
|
||||||
|
|
@ -94,6 +95,27 @@ export interface Stats {
|
||||||
unreviewed_annotations: number;
|
unreviewed_annotations: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getLyricsBySongId(url: string): Promise<null | string> {
|
||||||
|
let result: AxiosResponse<string, any>;
|
||||||
|
try {
|
||||||
|
result = await axios.get<string>(url, { responseType: 'text' });
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Genius lyrics request got an error!', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const $ = load(result.data.split('<br/>').join('\n'));
|
||||||
|
const lyricsDiv = $('div.lyrics');
|
||||||
|
|
||||||
|
if (lyricsDiv.length > 0) return lyricsDiv.text().trim();
|
||||||
|
|
||||||
|
const lyricSections = $('div[class^=Lyrics__Container]')
|
||||||
|
.map((_, e) => $(e).text())
|
||||||
|
.toArray()
|
||||||
|
.join('\n');
|
||||||
|
return lyricSections;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getSearchResults(
|
export async function getSearchResults(
|
||||||
params: LyricSearchQuery,
|
params: LyricSearchQuery,
|
||||||
): Promise<InternetProviderLyricSearchResponse[] | null> {
|
): Promise<InternetProviderLyricSearchResponse[] | null> {
|
||||||
|
|
@ -133,9 +155,33 @@ export async function getSearchResults(
|
||||||
return orderSearchResults({ params, results: songResults });
|
return orderSearchResults({ params, results: songResults });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function query(
|
||||||
|
params: LyricSearchQuery,
|
||||||
|
): Promise<InternetProviderLyricResponse | null> {
|
||||||
|
const response = await getSongId(params);
|
||||||
|
if (!response) {
|
||||||
|
console.error('Could not find the song on Genius!');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lyrics = await getLyricsBySongId(response.id);
|
||||||
|
if (!lyrics) {
|
||||||
|
console.error('Could not get lyrics on Genius!');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
artist: response.artist,
|
||||||
|
id: response.id,
|
||||||
|
lyrics,
|
||||||
|
name: response.name,
|
||||||
|
source: LyricSource.GENIUS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function getSongId(
|
async function getSongId(
|
||||||
params: LyricSearchQuery,
|
params: LyricSearchQuery,
|
||||||
): Promise<Omit<InternetProviderLyricResponse, 'lyrics'> | null> {
|
): Promise<null | Omit<InternetProviderLyricResponse, 'lyrics'>> {
|
||||||
let result: AxiosResponse<GeniusResponse>;
|
let result: AxiosResponse<GeniusResponse>;
|
||||||
try {
|
try {
|
||||||
result = await axios.get(SEARCH_URL, {
|
result = await axios.get(SEARCH_URL, {
|
||||||
|
|
@ -162,48 +208,3 @@ async function getSongId(
|
||||||
source: LyricSource.GENIUS,
|
source: LyricSource.GENIUS,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLyricsBySongId(url: string): Promise<string | null> {
|
|
||||||
let result: AxiosResponse<string, any>;
|
|
||||||
try {
|
|
||||||
result = await axios.get<string>(url, { responseType: 'text' });
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Genius lyrics request got an error!', e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const $ = load(result.data.split('<br/>').join('\n'));
|
|
||||||
const lyricsDiv = $('div.lyrics');
|
|
||||||
|
|
||||||
if (lyricsDiv.length > 0) return lyricsDiv.text().trim();
|
|
||||||
|
|
||||||
const lyricSections = $('div[class^=Lyrics__Container]')
|
|
||||||
.map((_, e) => $(e).text())
|
|
||||||
.toArray()
|
|
||||||
.join('\n');
|
|
||||||
return lyricSections;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function query(
|
|
||||||
params: LyricSearchQuery,
|
|
||||||
): Promise<InternetProviderLyricResponse | null> {
|
|
||||||
const response = await getSongId(params);
|
|
||||||
if (!response) {
|
|
||||||
console.error('Could not find the song on Genius!');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lyrics = await getLyricsBySongId(response.id);
|
|
||||||
if (!lyrics) {
|
|
||||||
console.error('Could not get lyrics on Genius!');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
artist: response.artist,
|
|
||||||
id: response.id,
|
|
||||||
lyrics,
|
|
||||||
name: response.name,
|
|
||||||
source: LyricSource.GENIUS,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,76 @@
|
||||||
import { ipcMain } from 'electron';
|
import { ipcMain } from 'electron';
|
||||||
|
|
||||||
|
import { store } from '../settings/index';
|
||||||
import {
|
import {
|
||||||
|
getLyricsBySongId as getGenius,
|
||||||
query as queryGenius,
|
query as queryGenius,
|
||||||
getSearchResults as searchGenius,
|
getSearchResults as searchGenius,
|
||||||
getLyricsBySongId as getGenius,
|
|
||||||
} from './genius';
|
} from './genius';
|
||||||
import {
|
import {
|
||||||
|
getLyricsBySongId as getLrcLib,
|
||||||
query as queryLrclib,
|
query as queryLrclib,
|
||||||
getSearchResults as searchLrcLib,
|
getSearchResults as searchLrcLib,
|
||||||
getLyricsBySongId as getLrcLib,
|
|
||||||
} from './lrclib';
|
} from './lrclib';
|
||||||
import {
|
import {
|
||||||
|
getLyricsBySongId as getNetease,
|
||||||
query as queryNetease,
|
query as queryNetease,
|
||||||
getSearchResults as searchNetease,
|
getSearchResults as searchNetease,
|
||||||
getLyricsBySongId as getNetease,
|
|
||||||
} from './netease';
|
} from './netease';
|
||||||
import {
|
|
||||||
InternetProviderLyricResponse,
|
|
||||||
InternetProviderLyricSearchResponse,
|
|
||||||
LyricSearchQuery,
|
|
||||||
QueueSong,
|
|
||||||
LyricGetQuery,
|
|
||||||
LyricSource,
|
|
||||||
} from '../../../../renderer/api/types';
|
|
||||||
import { store } from '../settings/index';
|
|
||||||
|
|
||||||
type SongFetcher = (params: LyricSearchQuery) => Promise<InternetProviderLyricResponse | null>;
|
export enum LyricSource {
|
||||||
|
GENIUS = 'Genius',
|
||||||
|
LRCLIB = 'lrclib.net',
|
||||||
|
NETEASE = 'NetEase',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FullLyricsMetadata = Omit<InternetProviderLyricResponse, 'id' | 'lyrics' | 'source'> & {
|
||||||
|
lyrics: LyricsResponse;
|
||||||
|
remote: boolean;
|
||||||
|
source: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type InternetProviderLyricResponse = {
|
||||||
|
artist: string;
|
||||||
|
id: string;
|
||||||
|
lyrics: string;
|
||||||
|
name: string;
|
||||||
|
source: LyricSource;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type InternetProviderLyricSearchResponse = {
|
||||||
|
artist: string;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
score?: number;
|
||||||
|
source: LyricSource;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LyricGetQuery = {
|
||||||
|
remoteSongId: string;
|
||||||
|
remoteSource: LyricSource;
|
||||||
|
song: Song;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LyricOverride = Omit<InternetProviderLyricResponse, 'lyrics'>;
|
||||||
|
|
||||||
|
export type LyricSearchQuery = {
|
||||||
|
album?: string;
|
||||||
|
artist?: string;
|
||||||
|
duration?: number;
|
||||||
|
name?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LyricsResponse = string | SynchronizedLyricsArray;
|
||||||
|
|
||||||
|
export type SynchronizedLyricsArray = Array<[number, string]>;
|
||||||
|
|
||||||
|
type CachedLyrics = Record<LyricSource, InternetProviderLyricResponse>;
|
||||||
|
type GetFetcher = (id: string) => Promise<null | string>;
|
||||||
type SearchFetcher = (
|
type SearchFetcher = (
|
||||||
params: LyricSearchQuery,
|
params: LyricSearchQuery,
|
||||||
) => Promise<InternetProviderLyricSearchResponse[] | null>;
|
) => Promise<InternetProviderLyricSearchResponse[] | null>;
|
||||||
type GetFetcher = (id: string) => Promise<string | null>;
|
|
||||||
|
|
||||||
type CachedLyrics = Record<LyricSource, InternetProviderLyricResponse>;
|
type SongFetcher = (params: LyricSearchQuery) => Promise<InternetProviderLyricResponse | null>;
|
||||||
|
|
||||||
const FETCHERS: Record<LyricSource, SongFetcher> = {
|
const FETCHERS: Record<LyricSource, SongFetcher> = {
|
||||||
[LyricSource.GENIUS]: queryGenius,
|
[LyricSource.GENIUS]: queryGenius,
|
||||||
|
|
@ -54,7 +94,7 @@ const MAX_CACHED_ITEMS = 10;
|
||||||
|
|
||||||
const lyricCache = new Map<string, CachedLyrics>();
|
const lyricCache = new Map<string, CachedLyrics>();
|
||||||
|
|
||||||
const getRemoteLyrics = async (song: QueueSong) => {
|
const getRemoteLyrics = async (song: any) => {
|
||||||
const sources = store.get('lyrics', []) as LyricSource[];
|
const sources = store.get('lyrics', []) as LyricSource[];
|
||||||
|
|
||||||
const cached = lyricCache.get(song.id);
|
const cached = lyricCache.get(song.id);
|
||||||
|
|
@ -122,7 +162,7 @@ const searchRemoteLyrics = async (params: LyricSearchQuery) => {
|
||||||
return results;
|
return results;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRemoteLyricsById = async (params: LyricGetQuery): Promise<string | null> => {
|
const getRemoteLyricsById = async (params: LyricGetQuery): Promise<null | string> => {
|
||||||
const { remoteSongId, remoteSource } = params;
|
const { remoteSongId, remoteSource } = params;
|
||||||
const response = await GET_FETCHERS[remoteSource](remoteSongId);
|
const response = await GET_FETCHERS[remoteSource](remoteSongId);
|
||||||
|
|
||||||
|
|
@ -133,7 +173,7 @@ const getRemoteLyricsById = async (params: LyricGetQuery): Promise<string | null
|
||||||
return response;
|
return response;
|
||||||
};
|
};
|
||||||
|
|
||||||
ipcMain.handle('lyric-by-song', async (_event, song: QueueSong) => {
|
ipcMain.handle('lyric-by-song', async (_event, song: any) => {
|
||||||
const lyric = await getRemoteLyrics(song);
|
const lyric = await getRemoteLyrics(song);
|
||||||
return lyric;
|
return lyric;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
// Credits to https://github.com/tranxuanthang/lrcget for API implementation
|
// Credits to https://github.com/tranxuanthang/lrcget for API implementation
|
||||||
import axios, { AxiosResponse } from 'axios';
|
import axios, { AxiosResponse } from 'axios';
|
||||||
import { orderSearchResults } from './shared';
|
|
||||||
import {
|
import {
|
||||||
InternetProviderLyricResponse,
|
InternetProviderLyricResponse,
|
||||||
InternetProviderLyricSearchResponse,
|
InternetProviderLyricSearchResponse,
|
||||||
LyricSearchQuery,
|
LyricSearchQuery,
|
||||||
LyricSource,
|
LyricSource,
|
||||||
} from '../../../../renderer/api/types';
|
} from '.';
|
||||||
|
import { orderSearchResults } from './shared';
|
||||||
|
|
||||||
const FETCH_URL = 'https://lrclib.net/api/get';
|
const FETCH_URL = 'https://lrclib.net/api/get';
|
||||||
const SEEARCH_URL = 'https://lrclib.net/api/search';
|
const SEEARCH_URL = 'https://lrclib.net/api/search';
|
||||||
|
|
@ -29,10 +30,23 @@ export interface LrcLibTrackResponse {
|
||||||
isrc: string;
|
isrc: string;
|
||||||
lang: string;
|
lang: string;
|
||||||
name: string;
|
name: string;
|
||||||
plainLyrics: string | null;
|
plainLyrics: null | string;
|
||||||
releaseDate: string;
|
releaseDate: string;
|
||||||
spotifyId: string;
|
spotifyId: string;
|
||||||
syncedLyrics: string | null;
|
syncedLyrics: null | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getLyricsBySongId(songId: string): Promise<null | string> {
|
||||||
|
let result: AxiosResponse<LrcLibTrackResponse, any>;
|
||||||
|
|
||||||
|
try {
|
||||||
|
result = await axios.get<LrcLibTrackResponse>(`${FETCH_URL}/${songId}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('LrcLib lyrics request got an error!', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data.syncedLyrics || result.data.plainLyrics || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSearchResults(
|
export async function getSearchResults(
|
||||||
|
|
@ -69,19 +83,6 @@ export async function getSearchResults(
|
||||||
return orderSearchResults({ params, results: songResults });
|
return orderSearchResults({ params, results: songResults });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLyricsBySongId(songId: string): Promise<string | null> {
|
|
||||||
let result: AxiosResponse<LrcLibTrackResponse, any>;
|
|
||||||
|
|
||||||
try {
|
|
||||||
result = await axios.get<LrcLibTrackResponse>(`${FETCH_URL}/${songId}`);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('LrcLib lyrics request got an error!', e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.data.syncedLyrics || result.data.plainLyrics || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function query(
|
export async function query(
|
||||||
params: LyricSearchQuery,
|
params: LyricSearchQuery,
|
||||||
): Promise<InternetProviderLyricResponse | null> {
|
): Promise<InternetProviderLyricResponse | null> {
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,18 @@
|
||||||
import axios, { AxiosResponse } from 'axios';
|
import axios, { AxiosResponse } from 'axios';
|
||||||
import { LyricSource } from '../../../../renderer/api/types';
|
|
||||||
import { orderSearchResults } from './shared';
|
import {
|
||||||
import type {
|
|
||||||
InternetProviderLyricResponse,
|
InternetProviderLyricResponse,
|
||||||
InternetProviderLyricSearchResponse,
|
InternetProviderLyricSearchResponse,
|
||||||
LyricSearchQuery,
|
LyricSearchQuery,
|
||||||
} from '/@/renderer/api/types';
|
LyricSource,
|
||||||
|
} from '.';
|
||||||
|
import { orderSearchResults } from './shared';
|
||||||
|
|
||||||
const SEARCH_URL = 'https://music.163.com/api/search/get';
|
const SEARCH_URL = 'https://music.163.com/api/search/get';
|
||||||
const LYRICS_URL = 'https://music.163.com/api/song/lyric';
|
const LYRICS_URL = 'https://music.163.com/api/song/lyric';
|
||||||
|
|
||||||
// Adapted from https://github.com/NyaomiDEV/Sunamu/blob/master/src/main/lyricproviders/netease.ts
|
// Adapted from https://github.com/NyaomiDEV/Sunamu/blob/master/src/main/lyricproviders/netease.ts
|
||||||
|
|
||||||
export interface NetEaseResponse {
|
|
||||||
code: number;
|
|
||||||
result: Result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Result {
|
|
||||||
hasMore: boolean;
|
|
||||||
songCount: number;
|
|
||||||
songs: Song[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Song {
|
|
||||||
album: Album;
|
|
||||||
alias: string[];
|
|
||||||
artists: Artist[];
|
|
||||||
copyrightId: number;
|
|
||||||
duration: number;
|
|
||||||
fee: number;
|
|
||||||
ftype: number;
|
|
||||||
id: number;
|
|
||||||
mark: number;
|
|
||||||
mvid: number;
|
|
||||||
name: string;
|
|
||||||
rUrl: null;
|
|
||||||
rtype: number;
|
|
||||||
status: number;
|
|
||||||
transNames?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Album {
|
export interface Album {
|
||||||
artist: Artist;
|
artist: Artist;
|
||||||
copyrightId: number;
|
copyrightId: number;
|
||||||
|
|
@ -67,6 +39,53 @@ export interface Artist {
|
||||||
trans: null;
|
trans: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface NetEaseResponse {
|
||||||
|
code: number;
|
||||||
|
result: Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Result {
|
||||||
|
hasMore: boolean;
|
||||||
|
songCount: number;
|
||||||
|
songs: Song[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Song {
|
||||||
|
album: Album;
|
||||||
|
alias: string[];
|
||||||
|
artists: Artist[];
|
||||||
|
copyrightId: number;
|
||||||
|
duration: number;
|
||||||
|
fee: number;
|
||||||
|
ftype: number;
|
||||||
|
id: number;
|
||||||
|
mark: number;
|
||||||
|
mvid: number;
|
||||||
|
name: string;
|
||||||
|
rtype: number;
|
||||||
|
rUrl: null;
|
||||||
|
status: number;
|
||||||
|
transNames?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getLyricsBySongId(songId: string): Promise<null | string> {
|
||||||
|
let result: AxiosResponse<any, any>;
|
||||||
|
try {
|
||||||
|
result = await axios.get(LYRICS_URL, {
|
||||||
|
params: {
|
||||||
|
id: songId,
|
||||||
|
kv: '-1',
|
||||||
|
lv: '-1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('NetEase lyrics request got an error!', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data.klyric?.lyric || result.data.lrc?.lyric;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getSearchResults(
|
export async function getSearchResults(
|
||||||
params: LyricSearchQuery,
|
params: LyricSearchQuery,
|
||||||
): Promise<InternetProviderLyricSearchResponse[] | null> {
|
): Promise<InternetProviderLyricSearchResponse[] | null> {
|
||||||
|
|
@ -110,38 +129,6 @@ export async function getSearchResults(
|
||||||
return orderSearchResults({ params, results: songResults });
|
return orderSearchResults({ params, results: songResults });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getMatchedLyrics(
|
|
||||||
params: LyricSearchQuery,
|
|
||||||
): Promise<Omit<InternetProviderLyricResponse, 'lyrics'> | null> {
|
|
||||||
const results = await getSearchResults(params);
|
|
||||||
|
|
||||||
const firstMatch = results?.[0];
|
|
||||||
|
|
||||||
if (!firstMatch || (firstMatch?.score && firstMatch.score > 0.5)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return firstMatch;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getLyricsBySongId(songId: string): Promise<string | null> {
|
|
||||||
let result: AxiosResponse<any, any>;
|
|
||||||
try {
|
|
||||||
result = await axios.get(LYRICS_URL, {
|
|
||||||
params: {
|
|
||||||
id: songId,
|
|
||||||
kv: '-1',
|
|
||||||
lv: '-1',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.error('NetEase lyrics request got an error!', e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.data.klyric?.lyric || result.data.lrc?.lyric;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function query(
|
export async function query(
|
||||||
params: LyricSearchQuery,
|
params: LyricSearchQuery,
|
||||||
): Promise<InternetProviderLyricResponse | null> {
|
): Promise<InternetProviderLyricResponse | null> {
|
||||||
|
|
@ -165,3 +152,17 @@ export async function query(
|
||||||
source: LyricSource.NETEASE,
|
source: LyricSource.NETEASE,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getMatchedLyrics(
|
||||||
|
params: LyricSearchQuery,
|
||||||
|
): Promise<null | Omit<InternetProviderLyricResponse, 'lyrics'>> {
|
||||||
|
const results = await getSearchResults(params);
|
||||||
|
|
||||||
|
const firstMatch = results?.[0];
|
||||||
|
|
||||||
|
if (!firstMatch || (firstMatch?.score && firstMatch.score > 0.5)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return firstMatch;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import Fuse from 'fuse.js';
|
import Fuse from 'fuse.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
InternetProviderLyricSearchResponse,
|
InternetProviderLyricSearchResponse,
|
||||||
LyricSearchQuery,
|
LyricSearchQuery,
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import console from 'console';
|
import console from 'console';
|
||||||
import { rm } from 'fs/promises';
|
|
||||||
import { pid } from 'node:process';
|
|
||||||
import { app, ipcMain } from 'electron';
|
import { app, ipcMain } from 'electron';
|
||||||
|
import { rm } from 'fs/promises';
|
||||||
import uniq from 'lodash/uniq';
|
import uniq from 'lodash/uniq';
|
||||||
import MpvAPI from 'node-mpv';
|
import MpvAPI from 'node-mpv';
|
||||||
import { getMainWindow, sendToastToRenderer } from '../../../main';
|
import { pid } from 'node:process';
|
||||||
|
|
||||||
|
import { getMainWindow, sendToastToRenderer } from '../../../index';
|
||||||
import { createLog, isWindows } from '../../../utils';
|
import { createLog, isWindows } from '../../../utils';
|
||||||
import { store } from '../settings';
|
import { store } from '../settings';
|
||||||
|
|
||||||
|
|
@ -86,7 +87,7 @@ const createMpv = async (data: {
|
||||||
extraParameters?: string[];
|
extraParameters?: string[];
|
||||||
properties?: Record<string, any>;
|
properties?: Record<string, any>;
|
||||||
}): Promise<MpvAPI> => {
|
}): Promise<MpvAPI> => {
|
||||||
const { extraParameters, properties, binaryPath } = data;
|
const { binaryPath, extraParameters, properties } = data;
|
||||||
|
|
||||||
const params = uniq([...DEFAULT_MPV_PARAMETERS(extraParameters), ...(extraParameters || [])]);
|
const params = uniq([...DEFAULT_MPV_PARAMETERS(extraParameters), ...(extraParameters || [])]);
|
||||||
|
|
||||||
|
|
@ -174,7 +175,7 @@ ipcMain.on('player-set-properties', async (_event, data: Record<string, any>) =>
|
||||||
} else {
|
} else {
|
||||||
getMpvInstance()?.setMultipleProperties(data);
|
getMpvInstance()?.setMultipleProperties(data);
|
||||||
}
|
}
|
||||||
} catch (err: NodeMpvError | any) {
|
} catch (err: any | NodeMpvError) {
|
||||||
mpvLog({ action: `Failed to set properties: ${JSON.stringify(data)}` }, err);
|
mpvLog({ action: `Failed to set properties: ${JSON.stringify(data)}` }, err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -199,7 +200,7 @@ ipcMain.handle(
|
||||||
mpvInstance = await createMpv(data);
|
mpvInstance = await createMpv(data);
|
||||||
mpvLog({ action: 'Restarted mpv', toast: 'success' });
|
mpvLog({ action: 'Restarted mpv', toast: 'success' });
|
||||||
setAudioPlayerFallback(false);
|
setAudioPlayerFallback(false);
|
||||||
} catch (err: NodeMpvError | any) {
|
} catch (err: any | NodeMpvError) {
|
||||||
mpvLog({ action: 'Failed to restart mpv, falling back to web player' }, err);
|
mpvLog({ action: 'Failed to restart mpv, falling back to web player' }, err);
|
||||||
setAudioPlayerFallback(true);
|
setAudioPlayerFallback(true);
|
||||||
}
|
}
|
||||||
|
|
@ -215,7 +216,7 @@ ipcMain.handle(
|
||||||
});
|
});
|
||||||
mpvInstance = await createMpv(data);
|
mpvInstance = await createMpv(data);
|
||||||
setAudioPlayerFallback(false);
|
setAudioPlayerFallback(false);
|
||||||
} catch (err: NodeMpvError | any) {
|
} catch (err: any | NodeMpvError) {
|
||||||
mpvLog({ action: 'Failed to initialize mpv, falling back to web player' }, err);
|
mpvLog({ action: 'Failed to initialize mpv, falling back to web player' }, err);
|
||||||
setAudioPlayerFallback(true);
|
setAudioPlayerFallback(true);
|
||||||
}
|
}
|
||||||
|
|
@ -226,7 +227,7 @@ ipcMain.on('player-quit', async () => {
|
||||||
try {
|
try {
|
||||||
await getMpvInstance()?.stop();
|
await getMpvInstance()?.stop();
|
||||||
await quit();
|
await quit();
|
||||||
} catch (err: NodeMpvError | any) {
|
} catch (err: any | NodeMpvError) {
|
||||||
mpvLog({ action: 'Failed to quit mpv' }, err);
|
mpvLog({ action: 'Failed to quit mpv' }, err);
|
||||||
} finally {
|
} finally {
|
||||||
mpvInstance = null;
|
mpvInstance = null;
|
||||||
|
|
@ -245,7 +246,7 @@ ipcMain.handle('player-clean-up', async () => {
|
||||||
ipcMain.on('player-start', async () => {
|
ipcMain.on('player-start', async () => {
|
||||||
try {
|
try {
|
||||||
await getMpvInstance()?.play();
|
await getMpvInstance()?.play();
|
||||||
} catch (err: NodeMpvError | any) {
|
} catch (err: any | NodeMpvError) {
|
||||||
mpvLog({ action: 'Failed to start mpv playback' }, err);
|
mpvLog({ action: 'Failed to start mpv playback' }, err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -254,7 +255,7 @@ ipcMain.on('player-start', async () => {
|
||||||
ipcMain.on('player-play', async () => {
|
ipcMain.on('player-play', async () => {
|
||||||
try {
|
try {
|
||||||
await getMpvInstance()?.play();
|
await getMpvInstance()?.play();
|
||||||
} catch (err: NodeMpvError | any) {
|
} catch (err: any | NodeMpvError) {
|
||||||
mpvLog({ action: 'Failed to start mpv playback' }, err);
|
mpvLog({ action: 'Failed to start mpv playback' }, err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -263,7 +264,7 @@ ipcMain.on('player-play', async () => {
|
||||||
ipcMain.on('player-pause', async () => {
|
ipcMain.on('player-pause', async () => {
|
||||||
try {
|
try {
|
||||||
await getMpvInstance()?.pause();
|
await getMpvInstance()?.pause();
|
||||||
} catch (err: NodeMpvError | any) {
|
} catch (err: any | NodeMpvError) {
|
||||||
mpvLog({ action: 'Failed to pause mpv playback' }, err);
|
mpvLog({ action: 'Failed to pause mpv playback' }, err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -272,7 +273,7 @@ ipcMain.on('player-pause', async () => {
|
||||||
ipcMain.on('player-stop', async () => {
|
ipcMain.on('player-stop', async () => {
|
||||||
try {
|
try {
|
||||||
await getMpvInstance()?.stop();
|
await getMpvInstance()?.stop();
|
||||||
} catch (err: NodeMpvError | any) {
|
} catch (err: any | NodeMpvError) {
|
||||||
mpvLog({ action: 'Failed to stop mpv playback' }, err);
|
mpvLog({ action: 'Failed to stop mpv playback' }, err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -281,7 +282,7 @@ ipcMain.on('player-stop', async () => {
|
||||||
ipcMain.on('player-next', async () => {
|
ipcMain.on('player-next', async () => {
|
||||||
try {
|
try {
|
||||||
await getMpvInstance()?.next();
|
await getMpvInstance()?.next();
|
||||||
} catch (err: NodeMpvError | any) {
|
} catch (err: any | NodeMpvError) {
|
||||||
mpvLog({ action: 'Failed to go to next track' }, err);
|
mpvLog({ action: 'Failed to go to next track' }, err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -290,7 +291,7 @@ ipcMain.on('player-next', async () => {
|
||||||
ipcMain.on('player-previous', async () => {
|
ipcMain.on('player-previous', async () => {
|
||||||
try {
|
try {
|
||||||
await getMpvInstance()?.prev();
|
await getMpvInstance()?.prev();
|
||||||
} catch (err: NodeMpvError | any) {
|
} catch (err: any | NodeMpvError) {
|
||||||
mpvLog({ action: 'Failed to go to previous track' }, err);
|
mpvLog({ action: 'Failed to go to previous track' }, err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -299,7 +300,7 @@ ipcMain.on('player-previous', async () => {
|
||||||
ipcMain.on('player-seek', async (_event, time: number) => {
|
ipcMain.on('player-seek', async (_event, time: number) => {
|
||||||
try {
|
try {
|
||||||
await getMpvInstance()?.seek(time);
|
await getMpvInstance()?.seek(time);
|
||||||
} catch (err: NodeMpvError | any) {
|
} catch (err: any | NodeMpvError) {
|
||||||
mpvLog({ action: `Failed to seek by ${time} seconds` }, err);
|
mpvLog({ action: `Failed to seek by ${time} seconds` }, err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -308,7 +309,7 @@ ipcMain.on('player-seek', async (_event, time: number) => {
|
||||||
ipcMain.on('player-seek-to', async (_event, time: number) => {
|
ipcMain.on('player-seek-to', async (_event, time: number) => {
|
||||||
try {
|
try {
|
||||||
await getMpvInstance()?.goToPosition(time);
|
await getMpvInstance()?.goToPosition(time);
|
||||||
} catch (err: NodeMpvError | any) {
|
} catch (err: any | NodeMpvError) {
|
||||||
mpvLog({ action: `Failed to seek to ${time} seconds` }, err);
|
mpvLog({ action: `Failed to seek to ${time} seconds` }, err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -320,7 +321,7 @@ ipcMain.on('player-set-queue', async (_event, current?: string, next?: string, p
|
||||||
await getMpvInstance()?.clearPlaylist();
|
await getMpvInstance()?.clearPlaylist();
|
||||||
await getMpvInstance()?.pause();
|
await getMpvInstance()?.pause();
|
||||||
return;
|
return;
|
||||||
} catch (err: NodeMpvError | any) {
|
} catch (err: any | NodeMpvError) {
|
||||||
mpvLog({ action: `Failed to clear play queue` }, err);
|
mpvLog({ action: `Failed to clear play queue` }, err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -344,7 +345,7 @@ ipcMain.on('player-set-queue', async (_event, current?: string, next?: string, p
|
||||||
// Only force play if pause is explicitly false
|
// Only force play if pause is explicitly false
|
||||||
await getMpvInstance()?.play();
|
await getMpvInstance()?.play();
|
||||||
}
|
}
|
||||||
} catch (err: NodeMpvError | any) {
|
} catch (err: any | NodeMpvError) {
|
||||||
mpvLog({ action: `Failed to set play queue` }, err);
|
mpvLog({ action: `Failed to set play queue` }, err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -365,7 +366,7 @@ ipcMain.on('player-set-queue-next', async (_event, url?: string) => {
|
||||||
if (url) {
|
if (url) {
|
||||||
await getMpvInstance()?.load(url, 'append');
|
await getMpvInstance()?.load(url, 'append');
|
||||||
}
|
}
|
||||||
} catch (err: NodeMpvError | any) {
|
} catch (err: any | NodeMpvError) {
|
||||||
mpvLog({ action: `Failed to set play queue` }, err);
|
mpvLog({ action: `Failed to set play queue` }, err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -385,7 +386,7 @@ ipcMain.on('player-auto-next', async (_event, url?: string) => {
|
||||||
if (url) {
|
if (url) {
|
||||||
await getMpvInstance()?.load(url, 'append');
|
await getMpvInstance()?.load(url, 'append');
|
||||||
}
|
}
|
||||||
} catch (err: NodeMpvError | any) {
|
} catch (err: any | NodeMpvError) {
|
||||||
mpvLog({ action: `Failed to load next song` }, err);
|
mpvLog({ action: `Failed to load next song` }, err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -398,7 +399,7 @@ ipcMain.on('player-volume', async (_event, value: number) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
await getMpvInstance()?.volume(value);
|
await getMpvInstance()?.volume(value);
|
||||||
} catch (err: NodeMpvError | any) {
|
} catch (err: any | NodeMpvError) {
|
||||||
mpvLog({ action: `Failed to set volume to ${value}` }, err);
|
mpvLog({ action: `Failed to set volume to ${value}` }, err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -407,7 +408,7 @@ ipcMain.on('player-volume', async (_event, value: number) => {
|
||||||
ipcMain.on('player-mute', async (_event, mute: boolean) => {
|
ipcMain.on('player-mute', async (_event, mute: boolean) => {
|
||||||
try {
|
try {
|
||||||
await getMpvInstance()?.mute(mute);
|
await getMpvInstance()?.mute(mute);
|
||||||
} catch (err: NodeMpvError | any) {
|
} catch (err: any | NodeMpvError) {
|
||||||
mpvLog({ action: `Failed to set mute status` }, err);
|
mpvLog({ action: `Failed to set mute status` }, err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -415,7 +416,7 @@ ipcMain.on('player-mute', async (_event, mute: boolean) => {
|
||||||
ipcMain.handle('player-get-time', async (): Promise<number | undefined> => {
|
ipcMain.handle('player-get-time', async (): Promise<number | undefined> => {
|
||||||
try {
|
try {
|
||||||
return getMpvInstance()?.getTimePosition();
|
return getMpvInstance()?.getTimePosition();
|
||||||
} catch (err: NodeMpvError | any) {
|
} catch (err: any | NodeMpvError) {
|
||||||
mpvLog({ action: `Failed to get current time` }, err);
|
mpvLog({ action: `Failed to get current time` }, err);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
@ -442,7 +443,7 @@ app.on('before-quit', async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
await getMpvInstance()?.stop();
|
await getMpvInstance()?.stop();
|
||||||
await quit();
|
await quit();
|
||||||
} catch (err: NodeMpvError | any) {
|
} catch (err: any | NodeMpvError) {
|
||||||
mpvLog({ action: `Failed to cleanly before-quit` }, err);
|
mpvLog({ action: `Failed to cleanly before-quit` }, err);
|
||||||
} finally {
|
} finally {
|
||||||
mpvState = MpvState.DONE;
|
mpvState = MpvState.DONE;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
/* eslint-disable promise/always-return */
|
/* eslint-disable promise/always-return */
|
||||||
import { BrowserWindow, globalShortcut, systemPreferences } from 'electron';
|
import { BrowserWindow, globalShortcut, systemPreferences } from 'electron';
|
||||||
|
|
||||||
import { isMacOS } from '../../../utils';
|
import { isMacOS } from '../../../utils';
|
||||||
import { store } from '../settings';
|
import { store } from '../settings';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,22 @@
|
||||||
import { Stats, promises } from 'fs';
|
|
||||||
import { readFile } from 'fs/promises';
|
|
||||||
import { IncomingMessage, Server, ServerResponse, createServer } from 'http';
|
|
||||||
import { join } from 'path';
|
|
||||||
import { deflate, gzip } from 'zlib';
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { app, ipcMain } from 'electron';
|
import { app, ipcMain } from 'electron';
|
||||||
import { Server as WsServer, WebSocketServer, WebSocket } from 'ws';
|
import { promises, Stats } from 'fs';
|
||||||
import manifest from './manifest.json';
|
import { readFile } from 'fs/promises';
|
||||||
import { ClientEvent, ServerEvent } from '../../../../remote/types';
|
import { createServer, IncomingMessage, Server, ServerResponse } from 'http';
|
||||||
import { PlayerRepeat, PlayerStatus, SongState } from '../../../../renderer/types';
|
import { join } from 'path';
|
||||||
import { getMainWindow } from '../../../main';
|
import { WebSocket, WebSocketServer, Server as WsServer } from 'ws';
|
||||||
|
import { deflate, gzip } from 'zlib';
|
||||||
|
|
||||||
|
import { getMainWindow } from '../../..';
|
||||||
import { isLinux } from '../../../utils';
|
import { isLinux } from '../../../utils';
|
||||||
import type { QueueSong } from '/@/renderer/api/types';
|
import manifest from './manifest.json';
|
||||||
|
|
||||||
let mprisPlayer: any | undefined;
|
let mprisPlayer: any | undefined;
|
||||||
|
|
||||||
if (isLinux()) {
|
if (isLinux()) {
|
||||||
// eslint-disable-next-line global-require
|
|
||||||
mprisPlayer = require('../../linux/mpris').mprisPlayer;
|
mprisPlayer = require('../../linux/mpris').mprisPlayer;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RemoteConfig {
|
|
||||||
enabled: boolean;
|
|
||||||
password: string;
|
|
||||||
port: number;
|
|
||||||
username: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MimeType {
|
interface MimeType {
|
||||||
css: string;
|
css: string;
|
||||||
html: string;
|
html: string;
|
||||||
|
|
@ -34,6 +24,13 @@ interface MimeType {
|
||||||
js: string;
|
js: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RemoteConfig {
|
||||||
|
enabled: boolean;
|
||||||
|
password: string;
|
||||||
|
port: number;
|
||||||
|
username: string;
|
||||||
|
}
|
||||||
|
|
||||||
declare class StatefulWebSocket extends WebSocket {
|
declare class StatefulWebSocket extends WebSocket {
|
||||||
alive: boolean;
|
alive: boolean;
|
||||||
|
|
||||||
|
|
@ -41,7 +38,7 @@ declare class StatefulWebSocket extends WebSocket {
|
||||||
}
|
}
|
||||||
|
|
||||||
let server: Server | undefined;
|
let server: Server | undefined;
|
||||||
let wsServer: WsServer<typeof StatefulWebSocket> | undefined;
|
let wsServer: undefined | WsServer<typeof StatefulWebSocket>;
|
||||||
|
|
||||||
const settings: RemoteConfig = {
|
const settings: RemoteConfig = {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
|
@ -54,14 +51,6 @@ type SendData = ServerEvent & {
|
||||||
client: StatefulWebSocket;
|
client: StatefulWebSocket;
|
||||||
};
|
};
|
||||||
|
|
||||||
function send({ client, event, data }: SendData): void {
|
|
||||||
if (client.readyState === WebSocket.OPEN) {
|
|
||||||
if (client.alive && client.auth) {
|
|
||||||
client.send(JSON.stringify({ data, event }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function broadcast(message: ServerEvent): void {
|
function broadcast(message: ServerEvent): void {
|
||||||
if (wsServer) {
|
if (wsServer) {
|
||||||
for (const client of wsServer.clients) {
|
for (const client of wsServer.clients) {
|
||||||
|
|
@ -70,6 +59,14 @@ function broadcast(message: ServerEvent): void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function send({ client, data, event }: SendData): void {
|
||||||
|
if (client.readyState === WebSocket.OPEN) {
|
||||||
|
if (client.alive && client.auth) {
|
||||||
|
client.send(JSON.stringify({ data, event }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const shutdownServer = () => {
|
const shutdownServer = () => {
|
||||||
if (wsServer) {
|
if (wsServer) {
|
||||||
wsServer.clients.forEach((client) => client.close(4000));
|
wsServer.clients.forEach((client) => client.close(4000));
|
||||||
|
|
@ -121,21 +118,17 @@ const getEncoding = (encoding: string | string[]): Encoding => {
|
||||||
|
|
||||||
const cache = new Map<string, Map<Encoding, [number, Buffer]>>();
|
const cache = new Map<string, Map<Encoding, [number, Buffer]>>();
|
||||||
|
|
||||||
function setOk(
|
function authorize(req: IncomingMessage): boolean {
|
||||||
res: ServerResponse,
|
if (settings.username || settings.password) {
|
||||||
mtimeMs: number,
|
// https://stackoverflow.com/questions/23616371/basic-http-authentication-with-node-and-express-4
|
||||||
extension: keyof MimeType,
|
|
||||||
encoding: Encoding,
|
|
||||||
data?: Buffer,
|
|
||||||
) {
|
|
||||||
res.statusCode = data ? 200 : 304;
|
|
||||||
|
|
||||||
res.setHeader('Content-Type', MIME_TYPES[extension]);
|
const authorization = req.headers.authorization?.split(' ')[1] || '';
|
||||||
res.setHeader('ETag', `"${mtimeMs}"`);
|
const [login, password] = Buffer.from(authorization, 'base64').toString().split(':');
|
||||||
res.setHeader('Cache-Control', 'public');
|
|
||||||
|
|
||||||
if (encoding !== 'none') res.setHeader('Content-Encoding', encoding);
|
return login === settings.username && password === settings.password;
|
||||||
res.end(data);
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function serveFile(
|
async function serveFile(
|
||||||
|
|
@ -252,17 +245,21 @@ async function serveFile(
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
function authorize(req: IncomingMessage): boolean {
|
function setOk(
|
||||||
if (settings.username || settings.password) {
|
res: ServerResponse,
|
||||||
// https://stackoverflow.com/questions/23616371/basic-http-authentication-with-node-and-express-4
|
mtimeMs: number,
|
||||||
|
extension: keyof MimeType,
|
||||||
|
encoding: Encoding,
|
||||||
|
data?: Buffer,
|
||||||
|
) {
|
||||||
|
res.statusCode = data ? 200 : 304;
|
||||||
|
|
||||||
const authorization = req.headers.authorization?.split(' ')[1] || '';
|
res.setHeader('Content-Type', MIME_TYPES[extension]);
|
||||||
const [login, password] = Buffer.from(authorization, 'base64').toString().split(':');
|
res.setHeader('ETag', `"${mtimeMs}"`);
|
||||||
|
res.setHeader('Cache-Control', 'public');
|
||||||
|
|
||||||
return login === settings.username && password === settings.password;
|
if (encoding !== 'none') res.setHeader('Content-Encoding', encoding);
|
||||||
}
|
res.end(data);
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const enableServer = (config: RemoteConfig): Promise<void> => {
|
const enableServer = (config: RemoteConfig): Promise<void> => {
|
||||||
|
|
@ -286,28 +283,28 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
|
||||||
await serveFile(req, 'index', 'html', res);
|
await serveFile(req, 'index', 'html', res);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case '/credentials': {
|
||||||
|
res.statusCode = 200;
|
||||||
|
res.setHeader('Content-Type', 'text/plain');
|
||||||
|
res.end(req.headers.authorization);
|
||||||
|
break;
|
||||||
|
}
|
||||||
case '/favicon.ico': {
|
case '/favicon.ico': {
|
||||||
await serveFile(req, 'favicon', 'ico', res);
|
await serveFile(req, 'favicon', 'ico', res);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case '/remote.css': {
|
|
||||||
await serveFile(req, 'remote', 'css', res);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case '/remote.js': {
|
|
||||||
await serveFile(req, 'remote', 'js', res);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case '/manifest.json': {
|
case '/manifest.json': {
|
||||||
res.statusCode = 200;
|
res.statusCode = 200;
|
||||||
res.setHeader('Content-Type', 'application/json');
|
res.setHeader('Content-Type', 'application/json');
|
||||||
res.end(JSON.stringify(manifest));
|
res.end(JSON.stringify(manifest));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case '/credentials': {
|
case '/remote.css': {
|
||||||
res.statusCode = 200;
|
await serveFile(req, 'remote', 'css', res);
|
||||||
res.setHeader('Content-Type', 'text/plain');
|
break;
|
||||||
res.end(req.headers.authorization);
|
}
|
||||||
|
case '/remote.js': {
|
||||||
|
await serveFile(req, 'remote', 'js', res);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
|
|
@ -371,6 +368,21 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (event) {
|
switch (event) {
|
||||||
|
case 'favorite': {
|
||||||
|
const { favorite, id } = json;
|
||||||
|
if (id && id === currentState.song?.id) {
|
||||||
|
getMainWindow()?.webContents.send('request-favorite', {
|
||||||
|
favorite,
|
||||||
|
id,
|
||||||
|
serverId: currentState.song.serverId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'next': {
|
||||||
|
getMainWindow()?.webContents.send('renderer-player-next');
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'pause': {
|
case 'pause': {
|
||||||
getMainWindow()?.webContents.send('renderer-player-pause');
|
getMainWindow()?.webContents.send('renderer-player-pause');
|
||||||
break;
|
break;
|
||||||
|
|
@ -379,10 +391,6 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
|
||||||
getMainWindow()?.webContents.send('renderer-player-play');
|
getMainWindow()?.webContents.send('renderer-player-play');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'next': {
|
|
||||||
getMainWindow()?.webContents.send('renderer-player-next');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'previous': {
|
case 'previous': {
|
||||||
getMainWindow()?.webContents.send('renderer-player-previous');
|
getMainWindow()?.webContents.send('renderer-player-previous');
|
||||||
break;
|
break;
|
||||||
|
|
@ -421,6 +429,17 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'rating': {
|
||||||
|
const { id, rating } = json;
|
||||||
|
if (id && id === currentState.song?.id) {
|
||||||
|
getMainWindow()?.webContents.send('request-rating', {
|
||||||
|
id,
|
||||||
|
rating,
|
||||||
|
serverId: currentState.song.serverId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'repeat': {
|
case 'repeat': {
|
||||||
getMainWindow()?.webContents.send('renderer-player-toggle-repeat');
|
getMainWindow()?.webContents.send('renderer-player-toggle-repeat');
|
||||||
break;
|
break;
|
||||||
|
|
@ -450,28 +469,6 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'favorite': {
|
|
||||||
const { favorite, id } = json;
|
|
||||||
if (id && id === currentState.song?.id) {
|
|
||||||
getMainWindow()?.webContents.send('request-favorite', {
|
|
||||||
favorite,
|
|
||||||
id,
|
|
||||||
serverId: currentState.song.serverId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'rating': {
|
|
||||||
const { rating, id } = json;
|
|
||||||
if (id && id === currentState.song?.id) {
|
|
||||||
getMainWindow()?.webContents.send('request-rating', {
|
|
||||||
id,
|
|
||||||
rating,
|
|
||||||
serverId: currentState.song.serverId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'position': {
|
case 'position': {
|
||||||
const { position } = json;
|
const { position } = json;
|
||||||
if (mprisPlayer) {
|
if (mprisPlayer) {
|
||||||
|
|
@ -631,12 +628,7 @@ ipcMain.on('update-volume', (_event, volume: number) => {
|
||||||
|
|
||||||
if (mprisPlayer) {
|
if (mprisPlayer) {
|
||||||
mprisPlayer.on('loopStatus', (event: string) => {
|
mprisPlayer.on('loopStatus', (event: string) => {
|
||||||
const repeat =
|
const repeat = event === 'Playlist' ? 'all' : event === 'Track' ? 'one' : 'none';
|
||||||
event === 'Playlist'
|
|
||||||
? PlayerRepeat.ALL
|
|
||||||
: event === 'Track'
|
|
||||||
? PlayerRepeat.ONE
|
|
||||||
: PlayerRepeat.NONE;
|
|
||||||
|
|
||||||
currentState.repeat = repeat;
|
currentState.repeat = repeat;
|
||||||
broadcast({ data: repeat, event: 'repeat' });
|
broadcast({ data: repeat, event: 'repeat' });
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
|
import type { TitleTheme } from '/@/renderer/types';
|
||||||
|
|
||||||
import { ipcMain, nativeTheme, safeStorage } from 'electron';
|
import { ipcMain, nativeTheme, safeStorage } from 'electron';
|
||||||
import Store from 'electron-store';
|
import Store from 'electron-store';
|
||||||
import type { TitleTheme } from '/@/renderer/types';
|
|
||||||
|
|
||||||
export const store = new Store();
|
export const store = new Store();
|
||||||
|
|
||||||
|
|
@ -12,7 +13,7 @@ ipcMain.on('settings-set', (__event, data: { property: string; value: any }) =>
|
||||||
store.set(`${data.property}`, data.value);
|
store.set(`${data.property}`, data.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('password-get', (_event, server: string): string | null => {
|
ipcMain.handle('password-get', (_event, server: string): null | string => {
|
||||||
if (safeStorage.isEncryptionAvailable()) {
|
if (safeStorage.isEncryptionAvailable()) {
|
||||||
const servers = store.get('server') as Record<string, string> | undefined;
|
const servers = store.get('server') as Record<string, string> | undefined;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import './core';
|
import './core';
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-dynamic-require
|
// require(`./${process.platform}`)
|
||||||
require(`./${process.platform}`);
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
import { ipcMain } from 'electron';
|
import { ipcMain } from 'electron';
|
||||||
import Player from 'mpris-service';
|
import Player from 'mpris-service';
|
||||||
|
|
||||||
import { PlayerRepeat, PlayerStatus } from '../../../renderer/types';
|
import { PlayerRepeat, PlayerStatus } from '../../../renderer/types';
|
||||||
import { getMainWindow } from '../../main';
|
import { getMainWindow } from '../../main';
|
||||||
|
|
||||||
import { QueueSong } from '/@/renderer/api/types';
|
import { QueueSong } from '/@/renderer/api/types';
|
||||||
|
|
||||||
const mprisPlayer = Player({
|
const mprisPlayer = Player({
|
||||||
|
|
@ -124,8 +126,8 @@ ipcMain.on('update-playback', (_event, status: PlayerStatus) => {
|
||||||
|
|
||||||
const REPEAT_TO_MPRIS: Record<PlayerRepeat, string> = {
|
const REPEAT_TO_MPRIS: Record<PlayerRepeat, string> = {
|
||||||
[PlayerRepeat.ALL]: 'Playlist',
|
[PlayerRepeat.ALL]: 'Playlist',
|
||||||
[PlayerRepeat.ONE]: 'Track',
|
|
||||||
[PlayerRepeat.NONE]: 'None',
|
[PlayerRepeat.NONE]: 'None',
|
||||||
|
[PlayerRepeat.ONE]: 'Track',
|
||||||
};
|
};
|
||||||
|
|
||||||
ipcMain.on('update-repeat', (_event, arg: PlayerRepeat) => {
|
ipcMain.on('update-repeat', (_event, arg: PlayerRepeat) => {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
// Dummy file to satisfy the build system
|
||||||
|
|
||||||
|
export {};
|
||||||
|
|
@ -1,51 +1,40 @@
|
||||||
/* eslint global-require: off, no-console: off, promise/always-return: off */
|
import { electronApp, is, optimizer } from '@electron-toolkit/utils';
|
||||||
|
import { constants } from 'buffer';
|
||||||
/**
|
|
||||||
* This module executes inside of electron's main process. You can start
|
|
||||||
* electron renderer process from here and communicate with the other processes
|
|
||||||
* through IPC.
|
|
||||||
*
|
|
||||||
* When running `npm run build` or `npm run build:main`, this file is compiled to
|
|
||||||
* `./src/main.js` using webpack. This gives us some performance wins.
|
|
||||||
*/
|
|
||||||
import { access, constants, readFile, writeFile } from 'fs';
|
|
||||||
import path, { join } from 'path';
|
|
||||||
import { deflate, inflate } from 'zlib';
|
|
||||||
import {
|
import {
|
||||||
app,
|
app,
|
||||||
BrowserWindow,
|
BrowserWindow,
|
||||||
shell,
|
BrowserWindowConstructorOptions,
|
||||||
ipcMain,
|
|
||||||
globalShortcut,
|
globalShortcut,
|
||||||
Tray,
|
ipcMain,
|
||||||
Menu,
|
Menu,
|
||||||
nativeImage,
|
nativeImage,
|
||||||
nativeTheme,
|
nativeTheme,
|
||||||
BrowserWindowConstructorOptions,
|
|
||||||
protocol,
|
|
||||||
net,
|
net,
|
||||||
|
protocol,
|
||||||
Rectangle,
|
Rectangle,
|
||||||
screen,
|
screen,
|
||||||
|
shell,
|
||||||
|
Tray,
|
||||||
} from 'electron';
|
} from 'electron';
|
||||||
import electronLocalShortcut from 'electron-localshortcut';
|
import electronLocalShortcut from 'electron-localshortcut';
|
||||||
import log from 'electron-log/main';
|
import log from 'electron-log/main';
|
||||||
import { autoUpdater } from 'electron-updater';
|
import { autoUpdater } from 'electron-updater';
|
||||||
|
import { access, readFile, writeFile } from 'fs';
|
||||||
|
import path, { join } from 'path';
|
||||||
|
import { deflate, inflate } from 'zlib';
|
||||||
|
|
||||||
import { disableMediaKeys, enableMediaKeys } from './features/core/player/media-keys';
|
import { disableMediaKeys, enableMediaKeys } from './features/core/player/media-keys';
|
||||||
import { store } from './features/core/settings/index';
|
import { store } from './features/core/settings';
|
||||||
import MenuBuilder from './menu';
|
import MenuBuilder from './menu';
|
||||||
import {
|
import {
|
||||||
|
autoUpdaterLogInterface,
|
||||||
|
createLog,
|
||||||
hotkeyToElectronAccelerator,
|
hotkeyToElectronAccelerator,
|
||||||
isLinux,
|
isLinux,
|
||||||
isMacOS,
|
isMacOS,
|
||||||
isWindows,
|
isWindows,
|
||||||
resolveHtmlPath,
|
|
||||||
createLog,
|
|
||||||
autoUpdaterLogInterface,
|
|
||||||
} from './utils';
|
} from './utils';
|
||||||
import './features';
|
import './features';
|
||||||
import type { TitleTheme } from '/@/renderer/types';
|
|
||||||
|
|
||||||
declare module 'node-mpv';
|
|
||||||
|
|
||||||
export default class AppUpdater {
|
export default class AppUpdater {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -72,7 +61,7 @@ if (isLinux() && !process.argv.some((a) => a.startsWith('--password-store='))) {
|
||||||
}
|
}
|
||||||
|
|
||||||
let mainWindow: BrowserWindow | null = null;
|
let mainWindow: BrowserWindow | null = null;
|
||||||
let tray: Tray | null = null;
|
let tray: null | Tray = null;
|
||||||
let exitFromTray = false;
|
let exitFromTray = false;
|
||||||
let forceQuit = false;
|
let forceQuit = false;
|
||||||
|
|
||||||
|
|
@ -117,7 +106,7 @@ export const sendToastToRenderer = ({
|
||||||
type,
|
type,
|
||||||
}: {
|
}: {
|
||||||
message: string;
|
message: string;
|
||||||
type: 'success' | 'error' | 'warning' | 'info';
|
type: 'error' | 'info' | 'success' | 'warning';
|
||||||
}) => {
|
}) => {
|
||||||
getMainWindow()?.webContents.send('toast-from-main', {
|
getMainWindow()?.webContents.send('toast-from-main', {
|
||||||
message,
|
message,
|
||||||
|
|
@ -208,7 +197,7 @@ const createTray = () => {
|
||||||
tray.setContextMenu(contextMenu);
|
tray.setContextMenu(contextMenu);
|
||||||
};
|
};
|
||||||
|
|
||||||
const createWindow = async (first = true) => {
|
async function createWindow(first = true): Promise<void> {
|
||||||
if (isDevelopment) {
|
if (isDevelopment) {
|
||||||
await installExtensions().catch(console.log);
|
await installExtensions().catch(console.log);
|
||||||
}
|
}
|
||||||
|
|
@ -233,6 +222,7 @@ const createWindow = async (first = true) => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Create the browser window.
|
||||||
mainWindow = new BrowserWindow({
|
mainWindow = new BrowserWindow({
|
||||||
autoHideMenuBar: true,
|
autoHideMenuBar: true,
|
||||||
frame: false,
|
frame: false,
|
||||||
|
|
@ -247,9 +237,8 @@ const createWindow = async (first = true) => {
|
||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
devTools: true,
|
devTools: true,
|
||||||
nodeIntegration: true,
|
nodeIntegration: true,
|
||||||
preload: app.isPackaged
|
preload: join(__dirname, '../preload/index.js'),
|
||||||
? path.join(__dirname, 'preload.js')
|
sandbox: false,
|
||||||
: path.join(__dirname, '../../.erb/dll/preload.js'),
|
|
||||||
webSecurity: !store.get('ignore_cors'),
|
webSecurity: !store.get('ignore_cors'),
|
||||||
},
|
},
|
||||||
width: 1440,
|
width: 1440,
|
||||||
|
|
@ -374,11 +363,11 @@ const createWindow = async (first = true) => {
|
||||||
enableMediaKeys(mainWindow);
|
enableMediaKeys(mainWindow);
|
||||||
}
|
}
|
||||||
|
|
||||||
mainWindow.loadURL(resolveHtmlPath('index.html'));
|
|
||||||
|
|
||||||
const startWindowMinimized = store.get('window_start_minimized', false) as boolean;
|
const startWindowMinimized = store.get('window_start_minimized', false) as boolean;
|
||||||
|
|
||||||
mainWindow.on('ready-to-show', () => {
|
mainWindow.on('ready-to-show', () => {
|
||||||
|
// mainWindow.show()
|
||||||
|
|
||||||
if (!mainWindow) {
|
if (!mainWindow) {
|
||||||
throw new Error('"mainWindow" is not defined');
|
throw new Error('"mainWindow" is not defined');
|
||||||
}
|
}
|
||||||
|
|
@ -404,7 +393,7 @@ const createWindow = async (first = true) => {
|
||||||
mainWindow = null;
|
mainWindow = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
let saved = false;
|
const saved = false;
|
||||||
|
|
||||||
mainWindow.on('close', (event) => {
|
mainWindow.on('close', (event) => {
|
||||||
store.set('bounds', mainWindow?.getNormalBounds());
|
store.set('bounds', mainWindow?.getNormalBounds());
|
||||||
|
|
@ -484,13 +473,25 @@ const createWindow = async (first = true) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (store.get('disable_auto_updates') !== true) {
|
if (store.get('disable_auto_updates') !== true) {
|
||||||
// eslint-disable-next-line
|
|
||||||
new AppUpdater();
|
new AppUpdater();
|
||||||
}
|
}
|
||||||
|
|
||||||
const theme = store.get('theme') as TitleTheme | undefined;
|
const theme = store.get('theme') as TitleTheme | undefined;
|
||||||
nativeTheme.themeSource = theme || 'dark';
|
nativeTheme.themeSource = theme || 'dark';
|
||||||
};
|
|
||||||
|
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||||
|
shell.openExternal(details.url);
|
||||||
|
return { action: 'deny' };
|
||||||
|
});
|
||||||
|
|
||||||
|
// HMR for renderer base on electron-vite cli.
|
||||||
|
// Load the remote URL for development or the local html file for production.
|
||||||
|
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
||||||
|
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']);
|
||||||
|
} else {
|
||||||
|
mainWindow.loadFile(join(__dirname, '../renderer/index.html'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
app.commandLine.appendSwitch('disable-features', 'HardwareMediaKeyHandling,MediaSessionService');
|
app.commandLine.appendSwitch('disable-features', 'HardwareMediaKeyHandling,MediaSessionService');
|
||||||
|
|
||||||
|
|
@ -519,6 +520,8 @@ enum BindingActions {
|
||||||
}
|
}
|
||||||
|
|
||||||
const HOTKEY_ACTIONS: Record<BindingActions, () => void> = {
|
const HOTKEY_ACTIONS: Record<BindingActions, () => void> = {
|
||||||
|
[BindingActions.GLOBAL_SEARCH]: () => {},
|
||||||
|
[BindingActions.LOCAL_SEARCH]: () => {},
|
||||||
[BindingActions.MUTE]: () => getMainWindow()?.webContents.send('renderer-player-volume-mute'),
|
[BindingActions.MUTE]: () => getMainWindow()?.webContents.send('renderer-player-volume-mute'),
|
||||||
[BindingActions.NEXT]: () => getMainWindow()?.webContents.send('renderer-player-next'),
|
[BindingActions.NEXT]: () => getMainWindow()?.webContents.send('renderer-player-next'),
|
||||||
[BindingActions.PAUSE]: () => getMainWindow()?.webContents.send('renderer-player-pause'),
|
[BindingActions.PAUSE]: () => getMainWindow()?.webContents.send('renderer-player-pause'),
|
||||||
|
|
@ -533,16 +536,14 @@ const HOTKEY_ACTIONS: Record<BindingActions, () => void> = {
|
||||||
[BindingActions.SKIP_FORWARD]: () =>
|
[BindingActions.SKIP_FORWARD]: () =>
|
||||||
getMainWindow()?.webContents.send('renderer-player-skip-forward'),
|
getMainWindow()?.webContents.send('renderer-player-skip-forward'),
|
||||||
[BindingActions.STOP]: () => getMainWindow()?.webContents.send('renderer-player-stop'),
|
[BindingActions.STOP]: () => getMainWindow()?.webContents.send('renderer-player-stop'),
|
||||||
|
[BindingActions.TOGGLE_FULLSCREEN_PLAYER]: () => {},
|
||||||
|
[BindingActions.TOGGLE_QUEUE]: () => {},
|
||||||
[BindingActions.TOGGLE_REPEAT]: () =>
|
[BindingActions.TOGGLE_REPEAT]: () =>
|
||||||
getMainWindow()?.webContents.send('renderer-player-toggle-repeat'),
|
getMainWindow()?.webContents.send('renderer-player-toggle-repeat'),
|
||||||
[BindingActions.VOLUME_UP]: () =>
|
|
||||||
getMainWindow()?.webContents.send('renderer-player-volume-up'),
|
|
||||||
[BindingActions.VOLUME_DOWN]: () =>
|
[BindingActions.VOLUME_DOWN]: () =>
|
||||||
getMainWindow()?.webContents.send('renderer-player-volume-down'),
|
getMainWindow()?.webContents.send('renderer-player-volume-down'),
|
||||||
[BindingActions.GLOBAL_SEARCH]: () => {},
|
[BindingActions.VOLUME_UP]: () =>
|
||||||
[BindingActions.LOCAL_SEARCH]: () => {},
|
getMainWindow()?.webContents.send('renderer-player-volume-up'),
|
||||||
[BindingActions.TOGGLE_QUEUE]: () => {},
|
|
||||||
[BindingActions.TOGGLE_FULLSCREEN_PLAYER]: () => {},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
ipcMain.on(
|
ipcMain.on(
|
||||||
|
|
@ -585,7 +586,7 @@ ipcMain.on(
|
||||||
_event,
|
_event,
|
||||||
data: {
|
data: {
|
||||||
message: string;
|
message: string;
|
||||||
type: 'debug' | 'verbose' | 'success' | 'error' | 'warning' | 'info';
|
type: 'debug' | 'error' | 'info' | 'success' | 'verbose' | 'warning';
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
createLog(data);
|
createLog(data);
|
||||||
|
|
@ -597,7 +598,6 @@ 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();
|
||||||
|
|
@ -632,22 +632,22 @@ if (!singleInstance) {
|
||||||
|
|
||||||
app.whenReady()
|
app.whenReady()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
protocol.handle('feishin', async (request) => {
|
// protocol.handle('feishin', async (request) => {
|
||||||
const filePath = `file://${request.url.slice('feishin://'.length)}`;
|
// const filePath = `file://${request.url.slice('feishin://'.length)}`
|
||||||
const response = await net.fetch(filePath);
|
// const response = await net.fetch(filePath)
|
||||||
const contentType = response.headers.get('content-type');
|
// const contentType = response.headers.get('content-type')
|
||||||
|
|
||||||
if (!contentType || !FONT_HEADERS.includes(contentType)) {
|
// if (!contentType || !FONT_HEADERS.includes(contentType)) {
|
||||||
getMainWindow()?.webContents.send('custom-font-error', filePath);
|
// getMainWindow()?.webContents.send('custom-font-error', filePath)
|
||||||
|
|
||||||
return new Response(null, {
|
// return new Response(null, {
|
||||||
status: 403,
|
// status: 403,
|
||||||
statusText: 'Forbidden',
|
// statusText: 'Forbidden'
|
||||||
});
|
// })
|
||||||
}
|
// }
|
||||||
|
|
||||||
return response;
|
// return response
|
||||||
});
|
// })
|
||||||
|
|
||||||
createWindow();
|
createWindow();
|
||||||
if (store.get('window_enable_tray', true)) {
|
if (store.get('window_enable_tray', true)) {
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { app, Menu, shell, BrowserWindow, MenuItemConstructorOptions } from 'electron';
|
import { app, BrowserWindow, Menu, MenuItemConstructorOptions, shell } from 'electron';
|
||||||
|
|
||||||
interface DarwinMenuItemConstructorOptions extends MenuItemConstructorOptions {
|
interface DarwinMenuItemConstructorOptions extends MenuItemConstructorOptions {
|
||||||
selector?: string;
|
selector?: string;
|
||||||
|
|
@ -12,37 +12,6 @@ export default class MenuBuilder {
|
||||||
this.mainWindow = mainWindow;
|
this.mainWindow = mainWindow;
|
||||||
}
|
}
|
||||||
|
|
||||||
buildMenu(): Menu {
|
|
||||||
if (process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true') {
|
|
||||||
this.setupDevelopmentEnvironment();
|
|
||||||
}
|
|
||||||
|
|
||||||
const template =
|
|
||||||
process.platform === 'darwin'
|
|
||||||
? this.buildDarwinTemplate()
|
|
||||||
: this.buildDefaultTemplate();
|
|
||||||
|
|
||||||
const menu = Menu.buildFromTemplate(template);
|
|
||||||
Menu.setApplicationMenu(menu);
|
|
||||||
|
|
||||||
return menu;
|
|
||||||
}
|
|
||||||
|
|
||||||
setupDevelopmentEnvironment(): void {
|
|
||||||
this.mainWindow.webContents.on('context-menu', (_, props) => {
|
|
||||||
const { x, y } = props;
|
|
||||||
|
|
||||||
Menu.buildFromTemplate([
|
|
||||||
{
|
|
||||||
click: () => {
|
|
||||||
this.mainWindow.webContents.inspectElement(x, y);
|
|
||||||
},
|
|
||||||
label: 'Inspect element',
|
|
||||||
},
|
|
||||||
]).popup({ window: this.mainWindow });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
buildDarwinTemplate(): MenuItemConstructorOptions[] {
|
buildDarwinTemplate(): MenuItemConstructorOptions[] {
|
||||||
const subMenuAbout: DarwinMenuItemConstructorOptions = {
|
const subMenuAbout: DarwinMenuItemConstructorOptions = {
|
||||||
label: 'Electron',
|
label: 'Electron',
|
||||||
|
|
@ -276,4 +245,35 @@ export default class MenuBuilder {
|
||||||
|
|
||||||
return templateDefault;
|
return templateDefault;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildMenu(): Menu {
|
||||||
|
if (process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true') {
|
||||||
|
this.setupDevelopmentEnvironment();
|
||||||
|
}
|
||||||
|
|
||||||
|
const template =
|
||||||
|
process.platform === 'darwin'
|
||||||
|
? this.buildDarwinTemplate()
|
||||||
|
: this.buildDefaultTemplate();
|
||||||
|
|
||||||
|
const menu = Menu.buildFromTemplate(template);
|
||||||
|
Menu.setApplicationMenu(menu);
|
||||||
|
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
|
||||||
|
setupDevelopmentEnvironment(): void {
|
||||||
|
this.mainWindow.webContents.on('context-menu', (_, props) => {
|
||||||
|
const { x, y } = props;
|
||||||
|
|
||||||
|
Menu.buildFromTemplate([
|
||||||
|
{
|
||||||
|
click: () => {
|
||||||
|
this.mainWindow.webContents.inspectElement(x, y);
|
||||||
|
},
|
||||||
|
label: 'Inspect element',
|
||||||
|
},
|
||||||
|
]).popup({ window: this.mainWindow });
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
import { contextBridge } from 'electron';
|
|
||||||
import { browser } from './preload/browser';
|
|
||||||
import { discordRpc } from './preload/discord-rpc';
|
|
||||||
import { ipc } from './preload/ipc';
|
|
||||||
import { localSettings } from './preload/local-settings';
|
|
||||||
import { lyrics } from './preload/lyrics';
|
|
||||||
import { mpris } from './preload/mpris';
|
|
||||||
import { mpvPlayer, mpvPlayerListener } from './preload/mpv-player';
|
|
||||||
import { remote } from './preload/remote';
|
|
||||||
import { utils } from './preload/utils';
|
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('electron', {
|
|
||||||
browser,
|
|
||||||
discordRpc,
|
|
||||||
ipc,
|
|
||||||
localSettings,
|
|
||||||
lyrics,
|
|
||||||
mpris,
|
|
||||||
mpvPlayer,
|
|
||||||
mpvPlayerListener,
|
|
||||||
remote,
|
|
||||||
utils,
|
|
||||||
});
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
/* eslint import/prefer-default-export: off, import/no-mutable-exports: off */
|
import log from 'electron-log/main';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import process from 'process';
|
import process from 'process';
|
||||||
import { URL } from 'url';
|
import { URL } from 'url';
|
||||||
import log from 'electron-log/main';
|
|
||||||
|
|
||||||
export let resolveHtmlPath: (htmlFileName: string) => string;
|
export let resolveHtmlPath: (htmlFileName: string) => string;
|
||||||
|
|
||||||
|
|
@ -76,7 +75,7 @@ const logColor = {
|
||||||
|
|
||||||
export const createLog = (data: {
|
export const createLog = (data: {
|
||||||
message: string;
|
message: string;
|
||||||
type: 'debug' | 'verbose' | 'success' | 'error' | 'warning' | 'info';
|
type: 'debug' | 'error' | 'info' | 'success' | 'verbose' | 'warning';
|
||||||
}) => {
|
}) => {
|
||||||
logMethod[data.type](`%c${data.message}`, `color: ${logColor[data.type]}`);
|
logMethod[data.type](`%c${data.message}`, `color: ${logColor[data.type]}`);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
10
src/preload/index.d.ts
vendored
Normal file
10
src/preload/index.d.ts
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { ElectronAPI } from '@electron-toolkit/preload';
|
||||||
|
|
||||||
|
import { PreloadApi } from './index';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
api: PreloadApi;
|
||||||
|
electron: ElectronAPI;
|
||||||
|
}
|
||||||
|
}
|
||||||
45
src/preload/index.ts
Normal file
45
src/preload/index.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { electronAPI } from '@electron-toolkit/preload';
|
||||||
|
import { contextBridge } from 'electron';
|
||||||
|
|
||||||
|
import { browser } from './browser';
|
||||||
|
import { discordRpc } from './discord-rpc';
|
||||||
|
import { ipc } from './ipc';
|
||||||
|
import { localSettings } from './local-settings';
|
||||||
|
import { lyrics } from './lyrics';
|
||||||
|
import { mpris } from './mpris';
|
||||||
|
import { mpvPlayer, mpvPlayerListener } from './mpv-player';
|
||||||
|
import { remote } from './remote';
|
||||||
|
import { utils } from './utils';
|
||||||
|
|
||||||
|
// Custom APIs for renderer
|
||||||
|
const api = {
|
||||||
|
browser,
|
||||||
|
discordRpc,
|
||||||
|
ipc,
|
||||||
|
localSettings,
|
||||||
|
lyrics,
|
||||||
|
mpris,
|
||||||
|
mpvPlayer,
|
||||||
|
mpvPlayerListener,
|
||||||
|
remote,
|
||||||
|
utils,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PreloadApi = typeof api;
|
||||||
|
|
||||||
|
// Use `contextBridge` APIs to expose Electron APIs to
|
||||||
|
// renderer only if context isolation is enabled, otherwise
|
||||||
|
// just add to the DOM global.
|
||||||
|
if (process.contextIsolated) {
|
||||||
|
try {
|
||||||
|
contextBridge.exposeInMainWorld('electron', electronAPI);
|
||||||
|
contextBridge.exposeInMainWorld('api', api);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// @ts-ignore (define in dts)
|
||||||
|
window.electron = electronAPI;
|
||||||
|
// @ts-ignore (define in dts)
|
||||||
|
window.api = api;
|
||||||
|
}
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
import { IpcRendererEvent, ipcRenderer, webFrame } from 'electron';
|
import { ipcRenderer, IpcRendererEvent, webFrame } from 'electron';
|
||||||
import Store from 'electron-store';
|
import Store from 'electron-store';
|
||||||
import { toServerType, type TitleTheme } from '/@/renderer/types';
|
|
||||||
|
|
||||||
const store = new Store();
|
const store = new Store();
|
||||||
|
|
||||||
const set = (
|
const set = (
|
||||||
property: string,
|
property: string,
|
||||||
value: string | Record<string, unknown> | boolean | string[] | undefined,
|
value: boolean | Record<string, unknown> | string | string[] | undefined,
|
||||||
) => {
|
) => {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
store.delete(property);
|
store.delete(property);
|
||||||
|
|
@ -32,7 +31,7 @@ const disableMediaKeys = () => {
|
||||||
ipcRenderer.send('global-media-keys-disable');
|
ipcRenderer.send('global-media-keys-disable');
|
||||||
};
|
};
|
||||||
|
|
||||||
const passwordGet = async (server: string): Promise<string | null> => {
|
const passwordGet = async (server: string): Promise<null | string> => {
|
||||||
return ipcRenderer.invoke('password-get', server);
|
return ipcRenderer.invoke('password-get', server);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -56,6 +55,19 @@ const themeSet = (theme: TitleTheme): void => {
|
||||||
ipcRenderer.send('theme-set', theme);
|
ipcRenderer.send('theme-set', theme);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const toServerType = (value?: string): null | string => {
|
||||||
|
switch (value?.toLowerCase()) {
|
||||||
|
case 'jellyfin':
|
||||||
|
return 'jellyfin';
|
||||||
|
case 'navidrome':
|
||||||
|
return 'navidrome';
|
||||||
|
case 'subsonic':
|
||||||
|
return 'subsonic';
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const SERVER_TYPE = toServerType(process.env.SERVER_TYPE);
|
const SERVER_TYPE = toServerType(process.env.SERVER_TYPE);
|
||||||
|
|
||||||
const env = {
|
const env = {
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { ipcRenderer } from 'electron';
|
import { ipcRenderer } from 'electron';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
InternetProviderLyricSearchResponse,
|
InternetProviderLyricSearchResponse,
|
||||||
LyricGetQuery,
|
LyricGetQuery,
|
||||||
LyricSearchQuery,
|
LyricSearchQuery,
|
||||||
LyricSource,
|
LyricSource,
|
||||||
QueueSong,
|
} from '../main/features/core/lyrics';
|
||||||
} from '/@/renderer/api/types';
|
|
||||||
|
|
||||||
const getRemoteLyricsBySong = (song: QueueSong) => {
|
const getRemoteLyricsBySong = (song: QueueSong) => {
|
||||||
const result = ipcRenderer.invoke('lyric-by-song', song);
|
const result = ipcRenderer.invoke('lyric-by-song', song);
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { IpcRendererEvent, ipcRenderer } from 'electron';
|
import { ipcRenderer, IpcRendererEvent } from 'electron';
|
||||||
import type { PlayerRepeat } from '/@/renderer/types';
|
|
||||||
|
|
||||||
const updatePosition = (timeSec: number) => {
|
const updatePosition = (timeSec: number) => {
|
||||||
ipcRenderer.send('mpris-update-position', timeSec);
|
ipcRenderer.send('mpris-update-position', timeSec);
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { ipcRenderer, IpcRendererEvent } from 'electron';
|
import { ipcRenderer, IpcRendererEvent } from 'electron';
|
||||||
import { PlayerData } from '/@/renderer/store';
|
|
||||||
|
|
||||||
const initialize = (data: { extraParameters?: string[]; properties?: Record<string, any> }) => {
|
const initialize = (data: { extraParameters?: string[]; properties?: Record<string, any> }) => {
|
||||||
return ipcRenderer.invoke('player-initialize', data);
|
return ipcRenderer.invoke('player-initialize', data);
|
||||||
|
|
@ -187,8 +186,8 @@ export const mpvPlayerListener = {
|
||||||
rendererNext,
|
rendererNext,
|
||||||
rendererPause,
|
rendererPause,
|
||||||
rendererPlay,
|
rendererPlay,
|
||||||
rendererPlayPause,
|
|
||||||
rendererPlayerFallback,
|
rendererPlayerFallback,
|
||||||
|
rendererPlayPause,
|
||||||
rendererPrevious,
|
rendererPrevious,
|
||||||
rendererQuit,
|
rendererQuit,
|
||||||
rendererSkipBackward,
|
rendererSkipBackward,
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
import { IpcRendererEvent, ipcRenderer } from 'electron';
|
import { ipcRenderer, IpcRendererEvent } from 'electron';
|
||||||
import { QueueSong } from '/@/renderer/api/types';
|
|
||||||
import { PlayerStatus } from '/@/renderer/types';
|
|
||||||
|
|
||||||
const requestFavorite = (
|
const requestFavorite = (
|
||||||
cb: (
|
cb: (
|
||||||
|
|
@ -29,12 +27,12 @@ const requestVolume = (cb: (event: IpcRendererEvent, data: { volume: number }) =
|
||||||
ipcRenderer.on('request-volume', cb);
|
ipcRenderer.on('request-volume', cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setRemoteEnabled = (enabled: boolean): Promise<string | null> => {
|
const setRemoteEnabled = (enabled: boolean): Promise<null | string> => {
|
||||||
const result = ipcRenderer.invoke('remote-enable', enabled);
|
const result = ipcRenderer.invoke('remote-enable', enabled);
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
const setRemotePort = (port: number): Promise<string | null> => {
|
const setRemotePort = (port: number): Promise<null | string> => {
|
||||||
const result = ipcRenderer.invoke('remote-port', port);
|
const result = ipcRenderer.invoke('remote-port', port);
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
@ -56,7 +54,7 @@ const updateSetting = (
|
||||||
port: number,
|
port: number,
|
||||||
username: string,
|
username: string,
|
||||||
password: string,
|
password: string,
|
||||||
): Promise<string | null> => {
|
): Promise<null | string> => {
|
||||||
return ipcRenderer.invoke('remote-settings', enabled, port, username, password);
|
return ipcRenderer.invoke('remote-settings', enabled, port, username, password);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { IpcRendererEvent, ipcRenderer } from 'electron';
|
import { ipcRenderer, IpcRendererEvent } from 'electron';
|
||||||
import { isMacOS, isWindows, isLinux } from '../utils';
|
|
||||||
import { PlayerState } from '/@/renderer/store';
|
import { isLinux, isMacOS, isWindows } from '../main/utils';
|
||||||
|
|
||||||
const saveQueue = (data: Record<string, any>) => {
|
const saveQueue = (data: Record<string, any>) => {
|
||||||
ipcRenderer.send('player-save-queue', data);
|
ipcRenderer.send('player-save-queue', data);
|
||||||
|
|
@ -18,7 +18,7 @@ const onSaveQueue = (cb: (event: IpcRendererEvent) => void) => {
|
||||||
ipcRenderer.on('renderer-save-queue', cb);
|
ipcRenderer.on('renderer-save-queue', cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onRestoreQueue = (cb: (event: IpcRendererEvent, data: Partial<PlayerState>) => void) => {
|
const onRestoreQueue = (cb: (event: IpcRendererEvent, data: Partial<any>) => void) => {
|
||||||
ipcRenderer.on('renderer-restore-queue', cb);
|
ipcRenderer.on('renderer-restore-queue', cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -29,7 +29,7 @@ const playerErrorListener = (cb: (event: IpcRendererEvent, data: { code: number
|
||||||
const mainMessageListener = (
|
const mainMessageListener = (
|
||||||
cb: (
|
cb: (
|
||||||
event: IpcRendererEvent,
|
event: IpcRendererEvent,
|
||||||
data: { message: string; type: 'success' | 'error' | 'warning' | 'info' },
|
data: { message: string; type: 'error' | 'info' | 'success' | 'warning' },
|
||||||
) => void,
|
) => void,
|
||||||
) => {
|
) => {
|
||||||
ipcRenderer.on('toast-from-main', cb);
|
ipcRenderer.on('toast-from-main', cb);
|
||||||
|
|
@ -40,7 +40,7 @@ const logger = (
|
||||||
event: IpcRendererEvent,
|
event: IpcRendererEvent,
|
||||||
data: {
|
data: {
|
||||||
message: string;
|
message: string;
|
||||||
type: 'debug' | 'verbose' | 'error' | 'warning' | 'info';
|
type: 'debug' | 'error' | 'info' | 'verbose' | 'warning';
|
||||||
},
|
},
|
||||||
) => void,
|
) => void,
|
||||||
) => {
|
) => {
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { useAuthStore } from '/@/renderer/store';
|
import type { AuthenticationResponse, ControllerEndpoint, ServerType } from '/@/renderer/api/types';
|
||||||
import { toast } from '/@/renderer/components/toast/index';
|
|
||||||
import type { ServerType, ControllerEndpoint, AuthenticationResponse } from '/@/renderer/api/types';
|
import i18n from '/@/i18n/i18n';
|
||||||
|
import { JellyfinController } from '/@/renderer/api/jellyfin/jellyfin-controller';
|
||||||
import { NavidromeController } from '/@/renderer/api/navidrome/navidrome-controller';
|
import { NavidromeController } from '/@/renderer/api/navidrome/navidrome-controller';
|
||||||
import { SubsonicController } from '/@/renderer/api/subsonic/subsonic-controller';
|
import { SubsonicController } from '/@/renderer/api/subsonic/subsonic-controller';
|
||||||
import { JellyfinController } from '/@/renderer/api/jellyfin/jellyfin-controller';
|
import { toast } from '/@/renderer/components/toast/index';
|
||||||
import i18n from '/@/i18n/i18n';
|
import { useAuthStore } from '/@/renderer/store';
|
||||||
|
|
||||||
type ApiController = {
|
type ApiController = {
|
||||||
jellyfin: ControllerEndpoint;
|
jellyfin: ControllerEndpoint;
|
||||||
|
|
|
||||||
|
|
@ -1,97 +1,56 @@
|
||||||
export type JFBasePaginatedResponse = {
|
export enum JFAlbumArtistListSort {
|
||||||
StartIndex: number;
|
ALBUM = 'Album,SortName',
|
||||||
TotalRecordCount: number;
|
DURATION = 'Runtime,AlbumArtist,Album,SortName',
|
||||||
};
|
NAME = 'SortName,Name',
|
||||||
|
RANDOM = 'Random,SortName',
|
||||||
export interface JFMusicFolderListResponse extends JFBasePaginatedResponse {
|
RECENTLY_ADDED = 'DateCreated,SortName',
|
||||||
Items: JFMusicFolder[];
|
RELEASE_DATE = 'PremiereDate,AlbumArtist,Album,SortName',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type JFMusicFolderList = JFMusicFolder[];
|
export enum JFAlbumListSort {
|
||||||
|
ALBUM_ARTIST = 'AlbumArtist,SortName',
|
||||||
export interface JFGenreListResponse extends JFBasePaginatedResponse {
|
COMMUNITY_RATING = 'CommunityRating,SortName',
|
||||||
Items: JFGenre[];
|
CRITIC_RATING = 'CriticRating,SortName',
|
||||||
|
NAME = 'SortName',
|
||||||
|
PLAY_COUNT = 'PlayCount',
|
||||||
|
RANDOM = 'Random,SortName',
|
||||||
|
RECENTLY_ADDED = 'DateCreated,SortName',
|
||||||
|
RELEASE_DATE = 'ProductionYear,PremiereDate,SortName',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type JFGenreList = JFGenreListResponse;
|
export enum JFArtistListSort {
|
||||||
|
ALBUM = 'Album,SortName',
|
||||||
|
DURATION = 'Runtime,AlbumArtist,Album,SortName',
|
||||||
|
NAME = 'SortName,Name',
|
||||||
|
RANDOM = 'Random,SortName',
|
||||||
|
RECENTLY_ADDED = 'DateCreated,SortName',
|
||||||
|
RELEASE_DATE = 'PremiereDate,AlbumArtist,Album,SortName',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum JFCollectionType {
|
||||||
|
MUSIC = 'music',
|
||||||
|
PLAYLISTS = 'playlists',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum JFExternalType {
|
||||||
|
MUSICBRAINZ = 'MusicBrainz',
|
||||||
|
THEAUDIODB = 'TheAudioDb',
|
||||||
|
}
|
||||||
|
|
||||||
export enum JFGenreListSort {
|
export enum JFGenreListSort {
|
||||||
NAME = 'SortName',
|
NAME = 'SortName',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type JFAlbumArtistDetailResponse = JFAlbumArtist;
|
export enum JFImageType {
|
||||||
|
LOGO = 'Logo',
|
||||||
export type JFAlbumArtistDetail = JFAlbumArtistDetailResponse;
|
PRIMARY = 'Primary',
|
||||||
|
|
||||||
export interface JFAlbumArtistListResponse extends JFBasePaginatedResponse {
|
|
||||||
Items: JFAlbumArtist[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type JFAlbumArtistList = {
|
export enum JFItemType {
|
||||||
items: JFAlbumArtist[];
|
AUDIO = 'Audio',
|
||||||
startIndex: number;
|
MUSICALBUM = 'MusicAlbum',
|
||||||
totalRecordCount: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface JFArtistListResponse extends JFBasePaginatedResponse {
|
|
||||||
Items: JFAlbumArtist[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type JFArtistList = JFArtistListResponse;
|
|
||||||
|
|
||||||
export interface JFAlbumListResponse extends JFBasePaginatedResponse {
|
|
||||||
Items: JFAlbum[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type JFAlbumList = {
|
|
||||||
items: JFAlbum[];
|
|
||||||
startIndex: number;
|
|
||||||
totalRecordCount: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type JFAlbumDetailResponse = JFAlbum;
|
|
||||||
|
|
||||||
export type JFAlbumDetail = JFAlbum & { songs?: JFSong[] };
|
|
||||||
|
|
||||||
export interface JFSongListResponse extends JFBasePaginatedResponse {
|
|
||||||
Items: JFSong[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type JFSongList = {
|
|
||||||
items: JFSong[];
|
|
||||||
startIndex: number;
|
|
||||||
totalRecordCount: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type JFAddToPlaylistResponse = {
|
|
||||||
added: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type JFAddToPlaylistParams = {
|
|
||||||
ids: string[];
|
|
||||||
userId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type JFAddToPlaylist = null;
|
|
||||||
|
|
||||||
export type JFRemoveFromPlaylistResponse = null;
|
|
||||||
|
|
||||||
export type JFRemoveFromPlaylistParams = {
|
|
||||||
entryIds: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type JFRemoveFromPlaylist = null;
|
|
||||||
|
|
||||||
export interface JFPlaylistListResponse extends JFBasePaginatedResponse {
|
|
||||||
Items: JFPlaylist[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type JFPlaylistList = {
|
|
||||||
items: JFPlaylist[];
|
|
||||||
startIndex: number;
|
|
||||||
totalRecordCount: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export enum JFPlaylistListSort {
|
export enum JFPlaylistListSort {
|
||||||
ALBUM_ARTIST = 'AlbumArtist,SortName',
|
ALBUM_ARTIST = 'AlbumArtist,SortName',
|
||||||
DURATION = 'Runtime',
|
DURATION = 'Runtime',
|
||||||
|
|
@ -100,124 +59,34 @@ export enum JFPlaylistListSort {
|
||||||
SONG_COUNT = 'ChildCount',
|
SONG_COUNT = 'ChildCount',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type JFPlaylistDetailResponse = JFPlaylist;
|
export enum JFSongListSort {
|
||||||
|
ALBUM = 'Album,SortName',
|
||||||
|
ALBUM_ARTIST = 'AlbumArtist,Album,SortName',
|
||||||
|
ARTIST = 'Artist,Album,SortName',
|
||||||
|
COMMUNITY_RATING = 'CommunityRating,SortName',
|
||||||
|
DURATION = 'Runtime,AlbumArtist,Album,SortName',
|
||||||
|
NAME = 'Name',
|
||||||
|
PLAY_COUNT = 'PlayCount,SortName',
|
||||||
|
RANDOM = 'Random,SortName',
|
||||||
|
RECENTLY_ADDED = 'DateCreated,SortName',
|
||||||
|
RECENTLY_PLAYED = 'DatePlayed,SortName',
|
||||||
|
RELEASE_DATE = 'PremiereDate,AlbumArtist,Album,SortName',
|
||||||
|
}
|
||||||
|
|
||||||
export type JFPlaylistDetail = JFPlaylist & { songs?: JFSong[] };
|
export enum JFSortOrder {
|
||||||
|
ASC = 'Ascending',
|
||||||
|
DESC = 'Descending',
|
||||||
|
}
|
||||||
|
|
||||||
export type JFPlaylist = {
|
export type JFAddToPlaylist = null;
|
||||||
BackdropImageTags: string[];
|
|
||||||
ChannelId: null;
|
export type JFAddToPlaylistParams = {
|
||||||
ChildCount?: number;
|
ids: string[];
|
||||||
DateCreated: string;
|
userId: string;
|
||||||
GenreItems: GenreItem[];
|
|
||||||
Genres: string[];
|
|
||||||
Id: string;
|
|
||||||
ImageBlurHashes: ImageBlurHashes;
|
|
||||||
ImageTags: ImageTags;
|
|
||||||
IsFolder: boolean;
|
|
||||||
LocationType: string;
|
|
||||||
MediaType: string;
|
|
||||||
Name: string;
|
|
||||||
Overview?: string;
|
|
||||||
RunTimeTicks: number;
|
|
||||||
ServerId: string;
|
|
||||||
Type: string;
|
|
||||||
UserData: UserData;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type JFRequestParams = {
|
export type JFAddToPlaylistResponse = {
|
||||||
albumArtistIds?: string;
|
added: number;
|
||||||
artistIds?: string;
|
|
||||||
enableImageTypes?: string;
|
|
||||||
enableTotalRecordCount?: boolean;
|
|
||||||
enableUserData?: boolean;
|
|
||||||
excludeItemTypes?: string;
|
|
||||||
fields?: string;
|
|
||||||
imageTypeLimit?: number;
|
|
||||||
includeItemTypes?: string;
|
|
||||||
isFavorite?: boolean;
|
|
||||||
limit?: number;
|
|
||||||
parentId?: string;
|
|
||||||
recursive?: boolean;
|
|
||||||
searchTerm?: string;
|
|
||||||
sortBy?: string;
|
|
||||||
sortOrder?: 'Ascending' | 'Descending';
|
|
||||||
startIndex?: number;
|
|
||||||
userId?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type JFMusicFolder = {
|
|
||||||
BackdropImageTags: string[];
|
|
||||||
ChannelId: null;
|
|
||||||
CollectionType: string;
|
|
||||||
Id: string;
|
|
||||||
ImageBlurHashes: ImageBlurHashes;
|
|
||||||
ImageTags: ImageTags;
|
|
||||||
IsFolder: boolean;
|
|
||||||
LocationType: string;
|
|
||||||
Name: string;
|
|
||||||
ServerId: string;
|
|
||||||
Type: string;
|
|
||||||
UserData: UserData;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type JFGenre = {
|
|
||||||
BackdropImageTags: any[];
|
|
||||||
ChannelId: null;
|
|
||||||
Id: string;
|
|
||||||
ImageBlurHashes: any;
|
|
||||||
ImageTags: ImageTags;
|
|
||||||
LocationType: string;
|
|
||||||
Name: string;
|
|
||||||
ServerId: string;
|
|
||||||
Type: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type JFAlbumArtist = {
|
|
||||||
BackdropImageTags: string[];
|
|
||||||
ChannelId: null;
|
|
||||||
DateCreated: string;
|
|
||||||
ExternalUrls: ExternalURL[];
|
|
||||||
GenreItems: GenreItem[];
|
|
||||||
Genres: string[];
|
|
||||||
Id: string;
|
|
||||||
ImageBlurHashes: any;
|
|
||||||
ImageTags: ImageTags;
|
|
||||||
LocationType: string;
|
|
||||||
Name: string;
|
|
||||||
Overview?: string;
|
|
||||||
RunTimeTicks: number;
|
|
||||||
ServerId: string;
|
|
||||||
Type: string;
|
|
||||||
UserData: {
|
|
||||||
IsFavorite: boolean;
|
|
||||||
Key: string;
|
|
||||||
PlayCount: number;
|
|
||||||
PlaybackPositionTicks: number;
|
|
||||||
Played: boolean;
|
|
||||||
};
|
|
||||||
} & {
|
|
||||||
similarArtists: {
|
|
||||||
items: JFAlbumArtist[];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type JFArtist = {
|
|
||||||
BackdropImageTags: string[];
|
|
||||||
ChannelId: null;
|
|
||||||
DateCreated: string;
|
|
||||||
ExternalUrls: ExternalURL[];
|
|
||||||
GenreItems: GenreItem[];
|
|
||||||
Genres: string[];
|
|
||||||
Id: string;
|
|
||||||
ImageBlurHashes: any;
|
|
||||||
ImageTags: string[];
|
|
||||||
LocationType: string;
|
|
||||||
Name: string;
|
|
||||||
Overview?: string;
|
|
||||||
RunTimeTicks: number;
|
|
||||||
ServerId: string;
|
|
||||||
Type: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type JFAlbum = {
|
export type JFAlbum = {
|
||||||
|
|
@ -251,6 +120,244 @@ export type JFAlbum = {
|
||||||
songs?: JFSong[];
|
songs?: JFSong[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type JFAlbumArtist = {
|
||||||
|
BackdropImageTags: string[];
|
||||||
|
ChannelId: null;
|
||||||
|
DateCreated: string;
|
||||||
|
ExternalUrls: ExternalURL[];
|
||||||
|
GenreItems: GenreItem[];
|
||||||
|
Genres: string[];
|
||||||
|
Id: string;
|
||||||
|
ImageBlurHashes: any;
|
||||||
|
ImageTags: ImageTags;
|
||||||
|
LocationType: string;
|
||||||
|
Name: string;
|
||||||
|
Overview?: string;
|
||||||
|
RunTimeTicks: number;
|
||||||
|
ServerId: string;
|
||||||
|
Type: string;
|
||||||
|
UserData: {
|
||||||
|
IsFavorite: boolean;
|
||||||
|
Key: string;
|
||||||
|
PlaybackPositionTicks: number;
|
||||||
|
PlayCount: number;
|
||||||
|
Played: boolean;
|
||||||
|
};
|
||||||
|
} & {
|
||||||
|
similarArtists: {
|
||||||
|
items: JFAlbumArtist[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type JFAlbumArtistDetail = JFAlbumArtistDetailResponse;
|
||||||
|
|
||||||
|
export type JFAlbumArtistDetailResponse = JFAlbumArtist;
|
||||||
|
|
||||||
|
export type JFAlbumArtistList = {
|
||||||
|
items: JFAlbumArtist[];
|
||||||
|
startIndex: number;
|
||||||
|
totalRecordCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type JFAlbumArtistListParams = JFBaseParams &
|
||||||
|
JFPaginationParams & {
|
||||||
|
filters?: string;
|
||||||
|
genres?: string;
|
||||||
|
sortBy?: JFAlbumArtistListSort;
|
||||||
|
years?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface JFAlbumArtistListResponse extends JFBasePaginatedResponse {
|
||||||
|
Items: JFAlbumArtist[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type JFAlbumDetail = JFAlbum & { songs?: JFSong[] };
|
||||||
|
|
||||||
|
export type JFAlbumDetailResponse = JFAlbum;
|
||||||
|
|
||||||
|
export type JFAlbumList = {
|
||||||
|
items: JFAlbum[];
|
||||||
|
startIndex: number;
|
||||||
|
totalRecordCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type JFAlbumListParams = JFBaseParams &
|
||||||
|
JFPaginationParams & {
|
||||||
|
albumArtistIds?: string;
|
||||||
|
artistIds?: string;
|
||||||
|
filters?: string;
|
||||||
|
genreIds?: string;
|
||||||
|
genres?: string;
|
||||||
|
includeItemTypes: 'MusicAlbum';
|
||||||
|
isFavorite?: boolean;
|
||||||
|
searchTerm?: string;
|
||||||
|
sortBy?: JFAlbumListSort;
|
||||||
|
tags?: string;
|
||||||
|
years?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface JFAlbumListResponse extends JFBasePaginatedResponse {
|
||||||
|
Items: JFAlbum[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type JFArtist = {
|
||||||
|
BackdropImageTags: string[];
|
||||||
|
ChannelId: null;
|
||||||
|
DateCreated: string;
|
||||||
|
ExternalUrls: ExternalURL[];
|
||||||
|
GenreItems: GenreItem[];
|
||||||
|
Genres: string[];
|
||||||
|
Id: string;
|
||||||
|
ImageBlurHashes: any;
|
||||||
|
ImageTags: string[];
|
||||||
|
LocationType: string;
|
||||||
|
Name: string;
|
||||||
|
Overview?: string;
|
||||||
|
RunTimeTicks: number;
|
||||||
|
ServerId: string;
|
||||||
|
Type: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type JFArtistList = JFArtistListResponse;
|
||||||
|
|
||||||
|
export type JFArtistListParams = JFBaseParams &
|
||||||
|
JFPaginationParams & {
|
||||||
|
filters?: string;
|
||||||
|
genres?: string;
|
||||||
|
sortBy?: JFArtistListSort;
|
||||||
|
years?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface JFArtistListResponse extends JFBasePaginatedResponse {
|
||||||
|
Items: JFAlbumArtist[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JFAuthenticate {
|
||||||
|
AccessToken: string;
|
||||||
|
ServerId: string;
|
||||||
|
SessionInfo: SessionInfo;
|
||||||
|
User: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type JFBasePaginatedResponse = {
|
||||||
|
StartIndex: number;
|
||||||
|
TotalRecordCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type JFCreatePlaylist = JFCreatePlaylistResponse;
|
||||||
|
|
||||||
|
export type JFCreatePlaylistResponse = {
|
||||||
|
Id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type JFGenericItem = {
|
||||||
|
Id: string;
|
||||||
|
Name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type JFGenre = {
|
||||||
|
BackdropImageTags: any[];
|
||||||
|
ChannelId: null;
|
||||||
|
Id: string;
|
||||||
|
ImageBlurHashes: any;
|
||||||
|
ImageTags: ImageTags;
|
||||||
|
LocationType: string;
|
||||||
|
Name: string;
|
||||||
|
ServerId: string;
|
||||||
|
Type: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type JFGenreList = JFGenreListResponse;
|
||||||
|
|
||||||
|
export interface JFGenreListResponse extends JFBasePaginatedResponse {
|
||||||
|
Items: JFGenre[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type JFMusicFolder = {
|
||||||
|
BackdropImageTags: string[];
|
||||||
|
ChannelId: null;
|
||||||
|
CollectionType: string;
|
||||||
|
Id: string;
|
||||||
|
ImageBlurHashes: ImageBlurHashes;
|
||||||
|
ImageTags: ImageTags;
|
||||||
|
IsFolder: boolean;
|
||||||
|
LocationType: string;
|
||||||
|
Name: string;
|
||||||
|
ServerId: string;
|
||||||
|
Type: string;
|
||||||
|
UserData: UserData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type JFMusicFolderList = JFMusicFolder[];
|
||||||
|
|
||||||
|
export interface JFMusicFolderListResponse extends JFBasePaginatedResponse {
|
||||||
|
Items: JFMusicFolder[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type JFPlaylist = {
|
||||||
|
BackdropImageTags: string[];
|
||||||
|
ChannelId: null;
|
||||||
|
ChildCount?: number;
|
||||||
|
DateCreated: string;
|
||||||
|
GenreItems: GenreItem[];
|
||||||
|
Genres: string[];
|
||||||
|
Id: string;
|
||||||
|
ImageBlurHashes: ImageBlurHashes;
|
||||||
|
ImageTags: ImageTags;
|
||||||
|
IsFolder: boolean;
|
||||||
|
LocationType: string;
|
||||||
|
MediaType: string;
|
||||||
|
Name: string;
|
||||||
|
Overview?: string;
|
||||||
|
RunTimeTicks: number;
|
||||||
|
ServerId: string;
|
||||||
|
Type: string;
|
||||||
|
UserData: UserData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type JFPlaylistDetail = JFPlaylist & { songs?: JFSong[] };
|
||||||
|
|
||||||
|
export type JFPlaylistDetailResponse = JFPlaylist;
|
||||||
|
|
||||||
|
export type JFPlaylistList = {
|
||||||
|
items: JFPlaylist[];
|
||||||
|
startIndex: number;
|
||||||
|
totalRecordCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface JFPlaylistListResponse extends JFBasePaginatedResponse {
|
||||||
|
Items: JFPlaylist[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type JFRemoveFromPlaylist = null;
|
||||||
|
|
||||||
|
export type JFRemoveFromPlaylistParams = {
|
||||||
|
entryIds: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type JFRemoveFromPlaylistResponse = null;
|
||||||
|
|
||||||
|
export type JFRequestParams = {
|
||||||
|
albumArtistIds?: string;
|
||||||
|
artistIds?: string;
|
||||||
|
enableImageTypes?: string;
|
||||||
|
enableTotalRecordCount?: boolean;
|
||||||
|
enableUserData?: boolean;
|
||||||
|
excludeItemTypes?: string;
|
||||||
|
fields?: string;
|
||||||
|
imageTypeLimit?: number;
|
||||||
|
includeItemTypes?: string;
|
||||||
|
isFavorite?: boolean;
|
||||||
|
limit?: number;
|
||||||
|
parentId?: string;
|
||||||
|
recursive?: boolean;
|
||||||
|
searchTerm?: string;
|
||||||
|
sortBy?: string;
|
||||||
|
sortOrder?: 'Ascending' | 'Descending';
|
||||||
|
startIndex?: number;
|
||||||
|
userId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type JFSong = {
|
export type JFSong = {
|
||||||
Album: string;
|
Album: string;
|
||||||
AlbumArtist: string;
|
AlbumArtist: string;
|
||||||
|
|
@ -285,23 +392,56 @@ export type JFSong = {
|
||||||
UserData?: UserData;
|
UserData?: UserData;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ImageBlurHashes = {
|
export type JFSongList = {
|
||||||
Backdrop?: any;
|
items: JFSong[];
|
||||||
Logo?: any;
|
startIndex: number;
|
||||||
Primary?: any;
|
totalRecordCount: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ImageTags = {
|
export type JFSongListParams = JFBaseParams &
|
||||||
Logo?: string;
|
JFPaginationParams & {
|
||||||
Primary?: string;
|
albumArtistIds?: string;
|
||||||
|
albumIds?: string;
|
||||||
|
artistIds?: string;
|
||||||
|
contributingArtistIds?: string;
|
||||||
|
filters?: string;
|
||||||
|
genreIds?: string;
|
||||||
|
genres?: string;
|
||||||
|
ids?: string;
|
||||||
|
includeItemTypes: 'Audio';
|
||||||
|
searchTerm?: string;
|
||||||
|
sortBy?: JFSongListSort;
|
||||||
|
years?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface JFSongListResponse extends JFBasePaginatedResponse {
|
||||||
|
Items: JFSong[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type Capabilities = {
|
||||||
|
PlayableMediaTypes: any[];
|
||||||
|
SupportedCommands: any[];
|
||||||
|
SupportsContentUploading: boolean;
|
||||||
|
SupportsMediaControl: boolean;
|
||||||
|
SupportsPersistentIdentifier: boolean;
|
||||||
|
SupportsSync: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type UserData = {
|
type Configuration = {
|
||||||
IsFavorite: boolean;
|
DisplayCollectionsView: boolean;
|
||||||
Key: string;
|
DisplayMissingEpisodes: boolean;
|
||||||
PlayCount: number;
|
EnableLocalPassword: boolean;
|
||||||
PlaybackPositionTicks: number;
|
EnableNextEpisodeAutoPlay: boolean;
|
||||||
Played: boolean;
|
GroupedFolders: any[];
|
||||||
|
HidePlayedInLatest: boolean;
|
||||||
|
LatestItemsExcludes: any[];
|
||||||
|
MyMediaExcludes: any[];
|
||||||
|
OrderedViews: any[];
|
||||||
|
PlayDefaultAudioTrack: boolean;
|
||||||
|
RememberAudioSelections: boolean;
|
||||||
|
RememberSubtitleSelections: boolean;
|
||||||
|
SubtitleLanguagePreference: string;
|
||||||
|
SubtitleMode: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ExternalURL = {
|
type ExternalURL = {
|
||||||
|
|
@ -314,9 +454,32 @@ type GenreItem = {
|
||||||
Name: string;
|
Name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type JFGenericItem = {
|
type ImageBlurHashes = {
|
||||||
Id: string;
|
Backdrop?: any;
|
||||||
Name: string;
|
Logo?: any;
|
||||||
|
Primary?: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ImageTags = {
|
||||||
|
Logo?: string;
|
||||||
|
Primary?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type JFBaseParams = {
|
||||||
|
enableImageTypes?: JFImageType[];
|
||||||
|
fields?: string;
|
||||||
|
imageTypeLimit?: number;
|
||||||
|
parentId?: string;
|
||||||
|
recursive?: boolean;
|
||||||
|
searchTerm?: string;
|
||||||
|
userId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type JFPaginationParams = {
|
||||||
|
limit?: number;
|
||||||
|
nameStartsWith?: string;
|
||||||
|
sortOrder?: JFSortOrder;
|
||||||
|
startIndex?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type MediaSources = {
|
type MediaSources = {
|
||||||
|
|
@ -380,32 +543,53 @@ type MediaStream = {
|
||||||
Width?: number;
|
Width?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum JFExternalType {
|
type PlayState = {
|
||||||
MUSICBRAINZ = 'MusicBrainz',
|
CanSeek: boolean;
|
||||||
THEAUDIODB = 'TheAudioDb',
|
IsMuted: boolean;
|
||||||
}
|
IsPaused: boolean;
|
||||||
|
RepeatMode: string;
|
||||||
|
};
|
||||||
|
|
||||||
export enum JFImageType {
|
type Policy = {
|
||||||
LOGO = 'Logo',
|
AccessSchedules: any[];
|
||||||
PRIMARY = 'Primary',
|
AuthenticationProviderId: string;
|
||||||
}
|
BlockedChannels: any[];
|
||||||
|
BlockedMediaFolders: any[];
|
||||||
export enum JFItemType {
|
BlockedTags: any[];
|
||||||
AUDIO = 'Audio',
|
BlockUnratedItems: any[];
|
||||||
MUSICALBUM = 'MusicAlbum',
|
EnableAllChannels: boolean;
|
||||||
}
|
EnableAllDevices: boolean;
|
||||||
|
EnableAllFolders: boolean;
|
||||||
export enum JFCollectionType {
|
EnableAudioPlaybackTranscoding: boolean;
|
||||||
MUSIC = 'music',
|
EnableContentDeletion: boolean;
|
||||||
PLAYLISTS = 'playlists',
|
EnableContentDeletionFromFolders: any[];
|
||||||
}
|
EnableContentDownloading: boolean;
|
||||||
|
EnabledChannels: any[];
|
||||||
export interface JFAuthenticate {
|
EnabledDevices: any[];
|
||||||
AccessToken: string;
|
EnabledFolders: any[];
|
||||||
ServerId: string;
|
EnableLiveTvAccess: boolean;
|
||||||
SessionInfo: SessionInfo;
|
EnableLiveTvManagement: boolean;
|
||||||
User: User;
|
EnableMediaConversion: boolean;
|
||||||
}
|
EnableMediaPlayback: boolean;
|
||||||
|
EnablePlaybackRemuxing: boolean;
|
||||||
|
EnablePublicSharing: boolean;
|
||||||
|
EnableRemoteAccess: boolean;
|
||||||
|
EnableRemoteControlOfOtherUsers: boolean;
|
||||||
|
EnableSharedDeviceControl: boolean;
|
||||||
|
EnableSyncTranscoding: boolean;
|
||||||
|
EnableUserPreferenceAccess: boolean;
|
||||||
|
EnableVideoPlaybackTranscoding: boolean;
|
||||||
|
ForceRemoteSourceTranscoding: boolean;
|
||||||
|
InvalidLoginAttemptCount: number;
|
||||||
|
IsAdministrator: boolean;
|
||||||
|
IsDisabled: boolean;
|
||||||
|
IsHidden: boolean;
|
||||||
|
LoginAttemptsBeforeLockout: number;
|
||||||
|
MaxActiveSessions: number;
|
||||||
|
PasswordResetProviderId: string;
|
||||||
|
RemoteClientBitrateLimit: number;
|
||||||
|
SyncPlayAccess: string;
|
||||||
|
};
|
||||||
|
|
||||||
type SessionInfo = {
|
type SessionInfo = {
|
||||||
AdditionalUsers: any[];
|
AdditionalUsers: any[];
|
||||||
|
|
@ -421,8 +605,8 @@ type SessionInfo = {
|
||||||
LastPlaybackCheckIn: string;
|
LastPlaybackCheckIn: string;
|
||||||
NowPlayingQueue: any[];
|
NowPlayingQueue: any[];
|
||||||
NowPlayingQueueFullItems: any[];
|
NowPlayingQueueFullItems: any[];
|
||||||
PlayState: PlayState;
|
|
||||||
PlayableMediaTypes: any[];
|
PlayableMediaTypes: any[];
|
||||||
|
PlayState: PlayState;
|
||||||
RemoteEndPoint: string;
|
RemoteEndPoint: string;
|
||||||
ServerId: string;
|
ServerId: string;
|
||||||
SupportedCommands: any[];
|
SupportedCommands: any[];
|
||||||
|
|
@ -432,22 +616,6 @@ type SessionInfo = {
|
||||||
UserName: string;
|
UserName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Capabilities = {
|
|
||||||
PlayableMediaTypes: any[];
|
|
||||||
SupportedCommands: any[];
|
|
||||||
SupportsContentUploading: boolean;
|
|
||||||
SupportsMediaControl: boolean;
|
|
||||||
SupportsPersistentIdentifier: boolean;
|
|
||||||
SupportsSync: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
type PlayState = {
|
|
||||||
CanSeek: boolean;
|
|
||||||
IsMuted: boolean;
|
|
||||||
IsPaused: boolean;
|
|
||||||
RepeatMode: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type User = {
|
type User = {
|
||||||
Configuration: Configuration;
|
Configuration: Configuration;
|
||||||
EnableAutoLogin: boolean;
|
EnableAutoLogin: boolean;
|
||||||
|
|
@ -462,178 +630,10 @@ type User = {
|
||||||
ServerId: string;
|
ServerId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Configuration = {
|
type UserData = {
|
||||||
DisplayCollectionsView: boolean;
|
IsFavorite: boolean;
|
||||||
DisplayMissingEpisodes: boolean;
|
Key: string;
|
||||||
EnableLocalPassword: boolean;
|
PlaybackPositionTicks: number;
|
||||||
EnableNextEpisodeAutoPlay: boolean;
|
PlayCount: number;
|
||||||
GroupedFolders: any[];
|
Played: boolean;
|
||||||
HidePlayedInLatest: boolean;
|
|
||||||
LatestItemsExcludes: any[];
|
|
||||||
MyMediaExcludes: any[];
|
|
||||||
OrderedViews: any[];
|
|
||||||
PlayDefaultAudioTrack: boolean;
|
|
||||||
RememberAudioSelections: boolean;
|
|
||||||
RememberSubtitleSelections: boolean;
|
|
||||||
SubtitleLanguagePreference: string;
|
|
||||||
SubtitleMode: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type Policy = {
|
|
||||||
AccessSchedules: any[];
|
|
||||||
AuthenticationProviderId: string;
|
|
||||||
BlockUnratedItems: any[];
|
|
||||||
BlockedChannels: any[];
|
|
||||||
BlockedMediaFolders: any[];
|
|
||||||
BlockedTags: any[];
|
|
||||||
EnableAllChannels: boolean;
|
|
||||||
EnableAllDevices: boolean;
|
|
||||||
EnableAllFolders: boolean;
|
|
||||||
EnableAudioPlaybackTranscoding: boolean;
|
|
||||||
EnableContentDeletion: boolean;
|
|
||||||
EnableContentDeletionFromFolders: any[];
|
|
||||||
EnableContentDownloading: boolean;
|
|
||||||
EnableLiveTvAccess: boolean;
|
|
||||||
EnableLiveTvManagement: boolean;
|
|
||||||
EnableMediaConversion: boolean;
|
|
||||||
EnableMediaPlayback: boolean;
|
|
||||||
EnablePlaybackRemuxing: boolean;
|
|
||||||
EnablePublicSharing: boolean;
|
|
||||||
EnableRemoteAccess: boolean;
|
|
||||||
EnableRemoteControlOfOtherUsers: boolean;
|
|
||||||
EnableSharedDeviceControl: boolean;
|
|
||||||
EnableSyncTranscoding: boolean;
|
|
||||||
EnableUserPreferenceAccess: boolean;
|
|
||||||
EnableVideoPlaybackTranscoding: boolean;
|
|
||||||
EnabledChannels: any[];
|
|
||||||
EnabledDevices: any[];
|
|
||||||
EnabledFolders: any[];
|
|
||||||
ForceRemoteSourceTranscoding: boolean;
|
|
||||||
InvalidLoginAttemptCount: number;
|
|
||||||
IsAdministrator: boolean;
|
|
||||||
IsDisabled: boolean;
|
|
||||||
IsHidden: boolean;
|
|
||||||
LoginAttemptsBeforeLockout: number;
|
|
||||||
MaxActiveSessions: number;
|
|
||||||
PasswordResetProviderId: string;
|
|
||||||
RemoteClientBitrateLimit: number;
|
|
||||||
SyncPlayAccess: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type JFBaseParams = {
|
|
||||||
enableImageTypes?: JFImageType[];
|
|
||||||
fields?: string;
|
|
||||||
imageTypeLimit?: number;
|
|
||||||
parentId?: string;
|
|
||||||
recursive?: boolean;
|
|
||||||
searchTerm?: string;
|
|
||||||
userId?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type JFPaginationParams = {
|
|
||||||
limit?: number;
|
|
||||||
nameStartsWith?: string;
|
|
||||||
sortOrder?: JFSortOrder;
|
|
||||||
startIndex?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export enum JFSortOrder {
|
|
||||||
ASC = 'Ascending',
|
|
||||||
DESC = 'Descending',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum JFAlbumListSort {
|
|
||||||
ALBUM_ARTIST = 'AlbumArtist,SortName',
|
|
||||||
COMMUNITY_RATING = 'CommunityRating,SortName',
|
|
||||||
CRITIC_RATING = 'CriticRating,SortName',
|
|
||||||
NAME = 'SortName',
|
|
||||||
PLAY_COUNT = 'PlayCount',
|
|
||||||
RANDOM = 'Random,SortName',
|
|
||||||
RECENTLY_ADDED = 'DateCreated,SortName',
|
|
||||||
RELEASE_DATE = 'ProductionYear,PremiereDate,SortName',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type JFAlbumListParams = {
|
|
||||||
albumArtistIds?: string;
|
|
||||||
artistIds?: string;
|
|
||||||
filters?: string;
|
|
||||||
genreIds?: string;
|
|
||||||
genres?: string;
|
|
||||||
includeItemTypes: 'MusicAlbum';
|
|
||||||
isFavorite?: boolean;
|
|
||||||
searchTerm?: string;
|
|
||||||
sortBy?: JFAlbumListSort;
|
|
||||||
tags?: string;
|
|
||||||
years?: string;
|
|
||||||
} & JFBaseParams &
|
|
||||||
JFPaginationParams;
|
|
||||||
|
|
||||||
export enum JFSongListSort {
|
|
||||||
ALBUM = 'Album,SortName',
|
|
||||||
ALBUM_ARTIST = 'AlbumArtist,Album,SortName',
|
|
||||||
ARTIST = 'Artist,Album,SortName',
|
|
||||||
COMMUNITY_RATING = 'CommunityRating,SortName',
|
|
||||||
DURATION = 'Runtime,AlbumArtist,Album,SortName',
|
|
||||||
NAME = 'Name',
|
|
||||||
PLAY_COUNT = 'PlayCount,SortName',
|
|
||||||
RANDOM = 'Random,SortName',
|
|
||||||
RECENTLY_ADDED = 'DateCreated,SortName',
|
|
||||||
RECENTLY_PLAYED = 'DatePlayed,SortName',
|
|
||||||
RELEASE_DATE = 'PremiereDate,AlbumArtist,Album,SortName',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type JFSongListParams = {
|
|
||||||
albumArtistIds?: string;
|
|
||||||
albumIds?: string;
|
|
||||||
artistIds?: string;
|
|
||||||
contributingArtistIds?: string;
|
|
||||||
filters?: string;
|
|
||||||
genreIds?: string;
|
|
||||||
genres?: string;
|
|
||||||
ids?: string;
|
|
||||||
includeItemTypes: 'Audio';
|
|
||||||
searchTerm?: string;
|
|
||||||
sortBy?: JFSongListSort;
|
|
||||||
years?: string;
|
|
||||||
} & JFBaseParams &
|
|
||||||
JFPaginationParams;
|
|
||||||
|
|
||||||
export enum JFAlbumArtistListSort {
|
|
||||||
ALBUM = 'Album,SortName',
|
|
||||||
DURATION = 'Runtime,AlbumArtist,Album,SortName',
|
|
||||||
NAME = 'SortName,Name',
|
|
||||||
RANDOM = 'Random,SortName',
|
|
||||||
RECENTLY_ADDED = 'DateCreated,SortName',
|
|
||||||
RELEASE_DATE = 'PremiereDate,AlbumArtist,Album,SortName',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type JFAlbumArtistListParams = {
|
|
||||||
filters?: string;
|
|
||||||
genres?: string;
|
|
||||||
sortBy?: JFAlbumArtistListSort;
|
|
||||||
years?: string;
|
|
||||||
} & JFBaseParams &
|
|
||||||
JFPaginationParams;
|
|
||||||
|
|
||||||
export enum JFArtistListSort {
|
|
||||||
ALBUM = 'Album,SortName',
|
|
||||||
DURATION = 'Runtime,AlbumArtist,Album,SortName',
|
|
||||||
NAME = 'SortName,Name',
|
|
||||||
RANDOM = 'Random,SortName',
|
|
||||||
RECENTLY_ADDED = 'DateCreated,SortName',
|
|
||||||
RELEASE_DATE = 'PremiereDate,AlbumArtist,Album,SortName',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type JFArtistListParams = {
|
|
||||||
filters?: string;
|
|
||||||
genres?: string;
|
|
||||||
sortBy?: JFArtistListSort;
|
|
||||||
years?: string;
|
|
||||||
} & JFBaseParams &
|
|
||||||
JFPaginationParams;
|
|
||||||
|
|
||||||
export type JFCreatePlaylistResponse = {
|
|
||||||
Id: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type JFCreatePlaylist = JFCreatePlaylistResponse;
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,17 @@
|
||||||
import { useAuthStore } from '/@/renderer/store';
|
|
||||||
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 { ServerListItem } from '/@/renderer/api/types';
|
|
||||||
import omitBy from 'lodash/omitBy';
|
import omitBy from 'lodash/omitBy';
|
||||||
|
import qs from 'qs';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { authenticationFailure, getClientType } from '/@/renderer/api/utils';
|
|
||||||
import i18n from '/@/i18n/i18n';
|
|
||||||
import packageJson from '../../../../package.json';
|
import packageJson from '../../../../package.json';
|
||||||
|
|
||||||
|
import i18n from '/@/i18n/i18n';
|
||||||
|
import { jfType } from '/@/renderer/api/jellyfin/jellyfin-types';
|
||||||
|
import { ServerListItem } from '/@/renderer/api/types';
|
||||||
|
import { authenticationFailure, getClientType } from '/@/renderer/api/utils';
|
||||||
|
import { useAuthStore } from '/@/renderer/store';
|
||||||
|
|
||||||
const c = initContract();
|
const c = initContract();
|
||||||
|
|
||||||
export const contract = c.router({
|
export const contract = c.router({
|
||||||
|
|
@ -356,14 +358,14 @@ export const createAuthHeader = (): string => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const jfApiClient = (args: {
|
export const jfApiClient = (args: {
|
||||||
server: ServerListItem | null;
|
server: null | ServerListItem;
|
||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
url?: string;
|
url?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const { server, url, signal } = args;
|
const { server, signal, url } = args;
|
||||||
|
|
||||||
return initClient(contract, {
|
return initClient(contract, {
|
||||||
api: async ({ path, method, headers, body }) => {
|
api: async ({ body, headers, method, path }) => {
|
||||||
let baseUrl: string | undefined;
|
let baseUrl: string | undefined;
|
||||||
let token: string | undefined;
|
let token: string | undefined;
|
||||||
|
|
||||||
|
|
@ -395,7 +397,7 @@ export const jfApiClient = (args: {
|
||||||
headers: result.headers as any,
|
headers: result.headers as any,
|
||||||
status: result.status,
|
status: result.status,
|
||||||
};
|
};
|
||||||
} catch (e: Error | AxiosError | any) {
|
} catch (e: any | AxiosError | Error) {
|
||||||
if (isAxiosError(e)) {
|
if (isAxiosError(e)) {
|
||||||
if (e.code === 'ERR_NETWORK') {
|
if (e.code === 'ERR_NETWORK') {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,25 @@
|
||||||
|
import chunk from 'lodash/chunk';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { jfNormalize } from './jellyfin-normalize';
|
||||||
|
|
||||||
|
import { ServerFeature } from '/@/renderer/api/features-types';
|
||||||
|
import { JFSongListSort, JFSortOrder } from '/@/renderer/api/jellyfin.types';
|
||||||
|
import { jfApiClient } from '/@/renderer/api/jellyfin/jellyfin-api';
|
||||||
|
import { jfType } from '/@/renderer/api/jellyfin/jellyfin-types';
|
||||||
import {
|
import {
|
||||||
albumArtistListSortMap,
|
albumArtistListSortMap,
|
||||||
sortOrderMap,
|
|
||||||
albumListSortMap,
|
albumListSortMap,
|
||||||
songListSortMap,
|
|
||||||
playlistListSortMap,
|
|
||||||
genreListSortMap,
|
|
||||||
Song,
|
|
||||||
Played,
|
|
||||||
ControllerEndpoint,
|
ControllerEndpoint,
|
||||||
|
genreListSortMap,
|
||||||
LibraryItem,
|
LibraryItem,
|
||||||
|
Played,
|
||||||
|
playlistListSortMap,
|
||||||
|
Song,
|
||||||
|
songListSortMap,
|
||||||
|
sortOrderMap,
|
||||||
} from '/@/renderer/api/types';
|
} from '/@/renderer/api/types';
|
||||||
import { jfApiClient } from '/@/renderer/api/jellyfin/jellyfin-api';
|
import { getFeatures, hasFeature, VersionInfo } from '/@/renderer/api/utils';
|
||||||
import { jfNormalize } from './jellyfin-normalize';
|
|
||||||
import { jfType } from '/@/renderer/api/jellyfin/jellyfin-types';
|
|
||||||
import { z } from 'zod';
|
|
||||||
import { JFSongListSort, JFSortOrder } from '/@/renderer/api/jellyfin.types';
|
|
||||||
import { ServerFeature } from '/@/renderer/api/features-types';
|
|
||||||
import { VersionInfo, getFeatures, hasFeature } from '/@/renderer/api/utils';
|
|
||||||
import chunk from 'lodash/chunk';
|
|
||||||
|
|
||||||
const formatCommaDelimitedString = (value: string[]) => {
|
const formatCommaDelimitedString = (value: string[]) => {
|
||||||
return value.join(',');
|
return value.join(',');
|
||||||
|
|
@ -41,7 +43,7 @@ const VERSION_INFO: VersionInfo = [
|
||||||
|
|
||||||
export const JellyfinController: ControllerEndpoint = {
|
export const JellyfinController: ControllerEndpoint = {
|
||||||
addToPlaylist: async (args) => {
|
addToPlaylist: async (args) => {
|
||||||
const { query, body, apiClientProps } = args;
|
const { apiClientProps, body, query } = args;
|
||||||
|
|
||||||
if (!apiClientProps.server?.userId) {
|
if (!apiClientProps.server?.userId) {
|
||||||
throw new Error('No userId found');
|
throw new Error('No userId found');
|
||||||
|
|
@ -89,7 +91,7 @@ export const JellyfinController: ControllerEndpoint = {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
createFavorite: async (args) => {
|
createFavorite: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
if (!apiClientProps.server?.userId) {
|
if (!apiClientProps.server?.userId) {
|
||||||
throw new Error('No userId found');
|
throw new Error('No userId found');
|
||||||
|
|
@ -108,7 +110,7 @@ export const JellyfinController: ControllerEndpoint = {
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
createPlaylist: async (args) => {
|
createPlaylist: async (args) => {
|
||||||
const { body, apiClientProps } = args;
|
const { apiClientProps, body } = args;
|
||||||
|
|
||||||
if (!apiClientProps.server?.userId) {
|
if (!apiClientProps.server?.userId) {
|
||||||
throw new Error('No userId found');
|
throw new Error('No userId found');
|
||||||
|
|
@ -132,7 +134,7 @@ export const JellyfinController: ControllerEndpoint = {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
deleteFavorite: async (args) => {
|
deleteFavorite: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
if (!apiClientProps.server?.userId) {
|
if (!apiClientProps.server?.userId) {
|
||||||
throw new Error('No userId found');
|
throw new Error('No userId found');
|
||||||
|
|
@ -151,7 +153,7 @@ export const JellyfinController: ControllerEndpoint = {
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
deletePlaylist: async (args) => {
|
deletePlaylist: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
const res = await jfApiClient(apiClientProps).deletePlaylist({
|
const res = await jfApiClient(apiClientProps).deletePlaylist({
|
||||||
params: {
|
params: {
|
||||||
|
|
@ -166,7 +168,7 @@ export const JellyfinController: ControllerEndpoint = {
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
getAlbumArtistDetail: async (args) => {
|
getAlbumArtistDetail: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
if (!apiClientProps.server?.userId) {
|
if (!apiClientProps.server?.userId) {
|
||||||
throw new Error('No userId found');
|
throw new Error('No userId found');
|
||||||
|
|
@ -201,7 +203,7 @@ export const JellyfinController: ControllerEndpoint = {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
getAlbumArtistList: async (args) => {
|
getAlbumArtistList: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
const res = await jfApiClient(apiClientProps).getAlbumArtistList({
|
const res = await jfApiClient(apiClientProps).getAlbumArtistList({
|
||||||
query: {
|
query: {
|
||||||
|
|
@ -236,7 +238,7 @@ export const JellyfinController: ControllerEndpoint = {
|
||||||
query: { ...query, limit: 1, startIndex: 0 },
|
query: { ...query, limit: 1, startIndex: 0 },
|
||||||
}).then((result) => result!.totalRecordCount!),
|
}).then((result) => result!.totalRecordCount!),
|
||||||
getAlbumDetail: async (args) => {
|
getAlbumDetail: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
if (!apiClientProps.server?.userId) {
|
if (!apiClientProps.server?.userId) {
|
||||||
throw new Error('No userId found');
|
throw new Error('No userId found');
|
||||||
|
|
@ -274,7 +276,7 @@ export const JellyfinController: ControllerEndpoint = {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
getAlbumList: async (args) => {
|
getAlbumList: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
if (!apiClientProps.server?.userId) {
|
if (!apiClientProps.server?.userId) {
|
||||||
throw new Error('No userId found');
|
throw new Error('No userId found');
|
||||||
|
|
@ -334,7 +336,7 @@ export const JellyfinController: ControllerEndpoint = {
|
||||||
query: { ...query, limit: 1, startIndex: 0 },
|
query: { ...query, limit: 1, startIndex: 0 },
|
||||||
}).then((result) => result!.totalRecordCount!),
|
}).then((result) => result!.totalRecordCount!),
|
||||||
getArtistList: async (args) => {
|
getArtistList: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
const res = await jfApiClient(apiClientProps).getArtistList({
|
const res = await jfApiClient(apiClientProps).getArtistList({
|
||||||
query: {
|
query: {
|
||||||
|
|
@ -404,7 +406,7 @@ export const JellyfinController: ControllerEndpoint = {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getLyrics: async (args) => {
|
getLyrics: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
if (!apiClientProps.server?.userId) {
|
if (!apiClientProps.server?.userId) {
|
||||||
throw new Error('No userId found');
|
throw new Error('No userId found');
|
||||||
|
|
@ -453,7 +455,7 @@ export const JellyfinController: ControllerEndpoint = {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getPlaylistDetail: async (args) => {
|
getPlaylistDetail: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
if (!apiClientProps.server?.userId) {
|
if (!apiClientProps.server?.userId) {
|
||||||
throw new Error('No userId found');
|
throw new Error('No userId found');
|
||||||
|
|
@ -477,7 +479,7 @@ export const JellyfinController: ControllerEndpoint = {
|
||||||
return jfNormalize.playlist(res.body, apiClientProps.server);
|
return jfNormalize.playlist(res.body, apiClientProps.server);
|
||||||
},
|
},
|
||||||
getPlaylistList: async (args) => {
|
getPlaylistList: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
if (!apiClientProps.server?.userId) {
|
if (!apiClientProps.server?.userId) {
|
||||||
throw new Error('No userId found');
|
throw new Error('No userId found');
|
||||||
|
|
@ -515,7 +517,7 @@ export const JellyfinController: ControllerEndpoint = {
|
||||||
query: { ...query, limit: 1, startIndex: 0 },
|
query: { ...query, limit: 1, startIndex: 0 },
|
||||||
}).then((result) => result!.totalRecordCount!),
|
}).then((result) => result!.totalRecordCount!),
|
||||||
getPlaylistSongList: async (args) => {
|
getPlaylistSongList: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
if (!apiClientProps.server?.userId) {
|
if (!apiClientProps.server?.userId) {
|
||||||
throw new Error('No userId found');
|
throw new Error('No userId found');
|
||||||
|
|
@ -547,7 +549,7 @@ export const JellyfinController: ControllerEndpoint = {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getRandomSongList: async (args) => {
|
getRandomSongList: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
if (!apiClientProps.server?.userId) {
|
if (!apiClientProps.server?.userId) {
|
||||||
throw new Error('No userId found');
|
throw new Error('No userId found');
|
||||||
|
|
@ -668,7 +670,7 @@ export const JellyfinController: ControllerEndpoint = {
|
||||||
}, []);
|
}, []);
|
||||||
},
|
},
|
||||||
getSongDetail: async (args) => {
|
getSongDetail: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
const res = await jfApiClient(apiClientProps).getSongDetail({
|
const res = await jfApiClient(apiClientProps).getSongDetail({
|
||||||
params: {
|
params: {
|
||||||
|
|
@ -684,7 +686,7 @@ export const JellyfinController: ControllerEndpoint = {
|
||||||
return jfNormalize.song(res.body, apiClientProps.server, '');
|
return jfNormalize.song(res.body, apiClientProps.server, '');
|
||||||
},
|
},
|
||||||
getSongList: async (args) => {
|
getSongList: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
if (!apiClientProps.server?.userId) {
|
if (!apiClientProps.server?.userId) {
|
||||||
throw new Error('No userId found');
|
throw new Error('No userId found');
|
||||||
|
|
@ -864,7 +866,7 @@ export const JellyfinController: ControllerEndpoint = {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getTranscodingUrl: (args) => {
|
getTranscodingUrl: (args) => {
|
||||||
const { base, format, bitrate } = args.query;
|
const { base, bitrate, format } = args.query;
|
||||||
let url = base.replace('transcodingProtocol=hls', 'transcodingProtocol=http');
|
let url = base.replace('transcodingProtocol=hls', 'transcodingProtocol=http');
|
||||||
if (format) {
|
if (format) {
|
||||||
url = url.replace('audioCodec=aac', `audioCodec=${format}`);
|
url = url.replace('audioCodec=aac', `audioCodec=${format}`);
|
||||||
|
|
@ -892,7 +894,7 @@ export const JellyfinController: ControllerEndpoint = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
removeFromPlaylist: async (args) => {
|
removeFromPlaylist: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
const chunks = chunk(query.songId, MAX_ITEMS_PER_PLAYLIST_ADD);
|
const chunks = chunk(query.songId, MAX_ITEMS_PER_PLAYLIST_ADD);
|
||||||
|
|
||||||
|
|
@ -914,7 +916,7 @@ export const JellyfinController: ControllerEndpoint = {
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
scrobble: async (args) => {
|
scrobble: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
const position = query.position && Math.round(query.position);
|
const position = query.position && Math.round(query.position);
|
||||||
|
|
||||||
|
|
@ -978,7 +980,7 @@ export const JellyfinController: ControllerEndpoint = {
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
search: async (args) => {
|
search: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
if (!apiClientProps.server?.userId) {
|
if (!apiClientProps.server?.userId) {
|
||||||
throw new Error('No userId found');
|
throw new Error('No userId found');
|
||||||
|
|
@ -1071,7 +1073,7 @@ export const JellyfinController: ControllerEndpoint = {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
updatePlaylist: async (args) => {
|
updatePlaylist: async (args) => {
|
||||||
const { query, body, apiClientProps } = args;
|
const { apiClientProps, body, query } = args;
|
||||||
|
|
||||||
if (!apiClientProps.server?.userId) {
|
if (!apiClientProps.server?.userId) {
|
||||||
throw new Error('No userId found');
|
throw new Error('No userId found');
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,19 @@
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { JFAlbum, JFPlaylist, JFMusicFolder, JFGenre } from '/@/renderer/api/jellyfin.types';
|
|
||||||
|
import { JFAlbum, JFGenre, JFMusicFolder, JFPlaylist } from '/@/renderer/api/jellyfin.types';
|
||||||
import { jfType } from '/@/renderer/api/jellyfin/jellyfin-types';
|
import { jfType } from '/@/renderer/api/jellyfin/jellyfin-types';
|
||||||
import {
|
import {
|
||||||
Song,
|
|
||||||
LibraryItem,
|
|
||||||
Album,
|
Album,
|
||||||
AlbumArtist,
|
AlbumArtist,
|
||||||
Playlist,
|
|
||||||
MusicFolder,
|
|
||||||
Genre,
|
Genre,
|
||||||
|
LibraryItem,
|
||||||
|
MusicFolder,
|
||||||
|
Playlist,
|
||||||
|
RelatedArtist,
|
||||||
ServerListItem,
|
ServerListItem,
|
||||||
ServerType,
|
ServerType,
|
||||||
RelatedArtist,
|
Song,
|
||||||
} from '/@/renderer/api/types';
|
} from '/@/renderer/api/types';
|
||||||
|
|
||||||
const getStreamUrl = (args: {
|
const getStreamUrl = (args: {
|
||||||
|
|
@ -21,9 +22,9 @@ const getStreamUrl = (args: {
|
||||||
eTag?: string;
|
eTag?: string;
|
||||||
id: string;
|
id: string;
|
||||||
mediaSourceId?: string;
|
mediaSourceId?: string;
|
||||||
server: ServerListItem | null;
|
server: null | ServerListItem;
|
||||||
}) => {
|
}) => {
|
||||||
const { id, server, deviceId } = args;
|
const { deviceId, id, server } = args;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
`${server?.url}/audio` +
|
`${server?.url}/audio` +
|
||||||
|
|
@ -122,9 +123,9 @@ const getPlaylistCoverArtUrl = (args: { baseUrl: string; item: JFPlaylist; size:
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
type AlbumOrSong = z.infer<typeof jfType._response.song> | z.infer<typeof jfType._response.album>;
|
type AlbumOrSong = z.infer<typeof jfType._response.album> | z.infer<typeof jfType._response.song>;
|
||||||
|
|
||||||
const getPeople = (item: AlbumOrSong): Record<string, RelatedArtist[]> | null => {
|
const getPeople = (item: AlbumOrSong): null | Record<string, RelatedArtist[]> => {
|
||||||
if (item.People) {
|
if (item.People) {
|
||||||
const participants: Record<string, RelatedArtist[]> = {};
|
const participants: Record<string, RelatedArtist[]> = {};
|
||||||
|
|
||||||
|
|
@ -151,7 +152,7 @@ const getPeople = (item: AlbumOrSong): Record<string, RelatedArtist[]> | null =>
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getTags = (item: AlbumOrSong): Record<string, string[]> | null => {
|
const getTags = (item: AlbumOrSong): null | Record<string, string[]> => {
|
||||||
if (item.Tags) {
|
if (item.Tags) {
|
||||||
const tags: Record<string, string[]> = {};
|
const tags: Record<string, string[]> = {};
|
||||||
for (const tag of item.Tags) {
|
for (const tag of item.Tags) {
|
||||||
|
|
@ -166,7 +167,7 @@ const getTags = (item: AlbumOrSong): Record<string, string[]> | null => {
|
||||||
|
|
||||||
const normalizeSong = (
|
const normalizeSong = (
|
||||||
item: z.infer<typeof jfType._response.song>,
|
item: z.infer<typeof jfType._response.song>,
|
||||||
server: ServerListItem | null,
|
server: null | ServerListItem,
|
||||||
deviceId: string,
|
deviceId: string,
|
||||||
imageSize?: number,
|
imageSize?: number,
|
||||||
): Song => {
|
): Song => {
|
||||||
|
|
@ -252,7 +253,7 @@ const normalizeSong = (
|
||||||
|
|
||||||
const normalizeAlbum = (
|
const normalizeAlbum = (
|
||||||
item: z.infer<typeof jfType._response.album>,
|
item: z.infer<typeof jfType._response.album>,
|
||||||
server: ServerListItem | null,
|
server: null | ServerListItem,
|
||||||
imageSize?: number,
|
imageSize?: number,
|
||||||
): Album => {
|
): Album => {
|
||||||
return {
|
return {
|
||||||
|
|
@ -312,7 +313,7 @@ const normalizeAlbumArtist = (
|
||||||
item: z.infer<typeof jfType._response.albumArtist> & {
|
item: z.infer<typeof jfType._response.albumArtist> & {
|
||||||
similarArtists?: z.infer<typeof jfType._response.albumArtistList>;
|
similarArtists?: z.infer<typeof jfType._response.albumArtistList>;
|
||||||
},
|
},
|
||||||
server: ServerListItem | null,
|
server: null | ServerListItem,
|
||||||
imageSize?: number,
|
imageSize?: number,
|
||||||
): AlbumArtist => {
|
): AlbumArtist => {
|
||||||
const similarArtists =
|
const similarArtists =
|
||||||
|
|
@ -361,7 +362,7 @@ const normalizeAlbumArtist = (
|
||||||
|
|
||||||
const normalizePlaylist = (
|
const normalizePlaylist = (
|
||||||
item: z.infer<typeof jfType._response.playlist>,
|
item: z.infer<typeof jfType._response.playlist>,
|
||||||
server: ServerListItem | null,
|
server: null | ServerListItem,
|
||||||
imageSize?: number,
|
imageSize?: number,
|
||||||
): Playlist => {
|
): Playlist => {
|
||||||
const imageUrl = getPlaylistCoverArtUrl({
|
const imageUrl = getPlaylistCoverArtUrl({
|
||||||
|
|
@ -445,7 +446,7 @@ const getGenreCoverArtUrl = (args: {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeGenre = (item: JFGenre, server: ServerListItem | null): Genre => {
|
const normalizeGenre = (item: JFGenre, server: null | ServerListItem): Genre => {
|
||||||
return {
|
return {
|
||||||
albumCount: undefined,
|
albumCount: undefined,
|
||||||
id: item.Id,
|
id: item.Id,
|
||||||
|
|
|
||||||
|
|
@ -95,8 +95,8 @@ const imageBlurHashes = z.object({
|
||||||
const userData = z.object({
|
const userData = z.object({
|
||||||
IsFavorite: z.boolean(),
|
IsFavorite: z.boolean(),
|
||||||
Key: z.string(),
|
Key: z.string(),
|
||||||
PlayCount: z.number(),
|
|
||||||
PlaybackPositionTicks: z.number(),
|
PlaybackPositionTicks: z.number(),
|
||||||
|
PlayCount: z.number(),
|
||||||
Played: z.boolean(),
|
Played: z.boolean(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -187,13 +187,13 @@ const sessionInfo = z.object({
|
||||||
LastPlaybackCheckIn: z.string(),
|
LastPlaybackCheckIn: z.string(),
|
||||||
NowPlayingQueue: z.array(z.any()),
|
NowPlayingQueue: z.array(z.any()),
|
||||||
NowPlayingQueueFullItems: z.array(z.any()),
|
NowPlayingQueueFullItems: z.array(z.any()),
|
||||||
|
PlayableMediaTypes: z.array(z.any()),
|
||||||
PlayState: z.object({
|
PlayState: z.object({
|
||||||
CanSeek: z.boolean(),
|
CanSeek: z.boolean(),
|
||||||
IsMuted: z.boolean(),
|
IsMuted: z.boolean(),
|
||||||
IsPaused: z.boolean(),
|
IsPaused: z.boolean(),
|
||||||
RepeatMode: z.string(),
|
RepeatMode: z.string(),
|
||||||
}),
|
}),
|
||||||
PlayableMediaTypes: z.array(z.any()),
|
|
||||||
RemoteEndPoint: z.string(),
|
RemoteEndPoint: z.string(),
|
||||||
ServerId: z.string(),
|
ServerId: z.string(),
|
||||||
SupportedCommands: z.array(z.any()),
|
SupportedCommands: z.array(z.any()),
|
||||||
|
|
@ -223,10 +223,10 @@ const configuration = z.object({
|
||||||
const policy = z.object({
|
const policy = z.object({
|
||||||
AccessSchedules: z.array(z.any()),
|
AccessSchedules: z.array(z.any()),
|
||||||
AuthenticationProviderId: z.string(),
|
AuthenticationProviderId: z.string(),
|
||||||
BlockUnratedItems: z.array(z.any()),
|
|
||||||
BlockedChannels: z.array(z.any()),
|
BlockedChannels: z.array(z.any()),
|
||||||
BlockedMediaFolders: z.array(z.any()),
|
BlockedMediaFolders: z.array(z.any()),
|
||||||
BlockedTags: z.array(z.any()),
|
BlockedTags: z.array(z.any()),
|
||||||
|
BlockUnratedItems: z.array(z.any()),
|
||||||
EnableAllChannels: z.boolean(),
|
EnableAllChannels: z.boolean(),
|
||||||
EnableAllDevices: z.boolean(),
|
EnableAllDevices: z.boolean(),
|
||||||
EnableAllFolders: z.boolean(),
|
EnableAllFolders: z.boolean(),
|
||||||
|
|
@ -234,6 +234,9 @@ const policy = z.object({
|
||||||
EnableContentDeletion: z.boolean(),
|
EnableContentDeletion: z.boolean(),
|
||||||
EnableContentDeletionFromFolders: z.array(z.any()),
|
EnableContentDeletionFromFolders: z.array(z.any()),
|
||||||
EnableContentDownloading: z.boolean(),
|
EnableContentDownloading: z.boolean(),
|
||||||
|
EnabledChannels: z.array(z.any()),
|
||||||
|
EnabledDevices: z.array(z.any()),
|
||||||
|
EnabledFolders: z.array(z.any()),
|
||||||
EnableLiveTvAccess: z.boolean(),
|
EnableLiveTvAccess: z.boolean(),
|
||||||
EnableLiveTvManagement: z.boolean(),
|
EnableLiveTvManagement: z.boolean(),
|
||||||
EnableMediaConversion: z.boolean(),
|
EnableMediaConversion: z.boolean(),
|
||||||
|
|
@ -246,9 +249,6 @@ const policy = z.object({
|
||||||
EnableSyncTranscoding: z.boolean(),
|
EnableSyncTranscoding: z.boolean(),
|
||||||
EnableUserPreferenceAccess: z.boolean(),
|
EnableUserPreferenceAccess: z.boolean(),
|
||||||
EnableVideoPlaybackTranscoding: z.boolean(),
|
EnableVideoPlaybackTranscoding: z.boolean(),
|
||||||
EnabledChannels: z.array(z.any()),
|
|
||||||
EnabledDevices: z.array(z.any()),
|
|
||||||
EnabledFolders: z.array(z.any()),
|
|
||||||
ForceRemoteSourceTranscoding: z.boolean(),
|
ForceRemoteSourceTranscoding: z.boolean(),
|
||||||
InvalidLoginAttemptCount: z.number(),
|
InvalidLoginAttemptCount: z.number(),
|
||||||
IsAdministrator: z.boolean(),
|
IsAdministrator: z.boolean(),
|
||||||
|
|
@ -414,8 +414,8 @@ const song = z.object({
|
||||||
ImageTags: imageTags,
|
ImageTags: imageTags,
|
||||||
IndexNumber: z.number(),
|
IndexNumber: z.number(),
|
||||||
IsFolder: z.boolean(),
|
IsFolder: z.boolean(),
|
||||||
LUFS: z.number().optional(),
|
|
||||||
LocationType: z.string(),
|
LocationType: z.string(),
|
||||||
|
LUFS: z.number().optional(),
|
||||||
MediaSources: z.array(mediaSources),
|
MediaSources: z.array(mediaSources),
|
||||||
MediaType: z.string(),
|
MediaType: z.string(),
|
||||||
Name: z.string(),
|
Name: z.string(),
|
||||||
|
|
@ -654,8 +654,8 @@ const favorite = z.object({
|
||||||
Key: z.string(),
|
Key: z.string(),
|
||||||
LastPlayedDate: z.string(),
|
LastPlayedDate: z.string(),
|
||||||
Likes: z.boolean(),
|
Likes: z.boolean(),
|
||||||
PlayCount: z.number(),
|
|
||||||
PlaybackPositionTicks: z.number(),
|
PlaybackPositionTicks: z.number(),
|
||||||
|
PlayCount: z.number(),
|
||||||
Played: z.boolean(),
|
Played: z.boolean(),
|
||||||
PlayedPercentage: z.number(),
|
PlayedPercentage: z.number(),
|
||||||
Rating: z.number(),
|
Rating: z.number(),
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,77 @@
|
||||||
import { SSArtistInfo } from '/@/renderer/api/subsonic.types';
|
import { SSArtistInfo } from '/@/renderer/api/subsonic.types';
|
||||||
|
|
||||||
export type NDAuthenticate = {
|
export enum NDAlbumArtistListSort {
|
||||||
id: string;
|
ALBUM_COUNT = 'albumCount',
|
||||||
isAdmin: boolean;
|
FAVORITED = 'starred_at',
|
||||||
name: string;
|
NAME = 'name',
|
||||||
subsonicSalt: string;
|
PLAY_COUNT = 'playCount',
|
||||||
subsonicToken: string;
|
RATING = 'rating',
|
||||||
token: string;
|
SONG_COUNT = 'songCount',
|
||||||
username: string;
|
}
|
||||||
|
|
||||||
|
export enum NDAlbumListSort {
|
||||||
|
ALBUM_ARTIST = 'album_artist',
|
||||||
|
ARTIST = 'artist',
|
||||||
|
DURATION = 'duration',
|
||||||
|
NAME = 'name',
|
||||||
|
PLAY_COUNT = 'play_count',
|
||||||
|
PLAY_DATE = 'play_date',
|
||||||
|
RANDOM = 'random',
|
||||||
|
RATING = 'rating',
|
||||||
|
RECENTLY_ADDED = 'recently_added',
|
||||||
|
SONG_COUNT = 'songCount',
|
||||||
|
STARRED = 'starred_at',
|
||||||
|
YEAR = 'max_year',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum NDGenreListSort {
|
||||||
|
NAME = 'name',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum NDPlaylistListSort {
|
||||||
|
DURATION = 'duration',
|
||||||
|
NAME = 'name',
|
||||||
|
OWNER = 'owner_name',
|
||||||
|
PUBLIC = 'public',
|
||||||
|
SONG_COUNT = 'songCount',
|
||||||
|
UPDATED_AT = 'updatedAt',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum NDSongListSort {
|
||||||
|
ALBUM = 'album',
|
||||||
|
ALBUM_ARTIST = 'order_album_artist_name',
|
||||||
|
ALBUM_SONGS = 'album',
|
||||||
|
ARTIST = 'artist',
|
||||||
|
BPM = 'bpm',
|
||||||
|
CHANNELS = 'channels',
|
||||||
|
COMMENT = 'comment',
|
||||||
|
DURATION = 'duration',
|
||||||
|
FAVORITED = 'starred_at',
|
||||||
|
GENRE = 'genre',
|
||||||
|
ID = 'id',
|
||||||
|
PLAY_COUNT = 'playCount',
|
||||||
|
PLAY_DATE = 'playDate',
|
||||||
|
RANDOM = 'random',
|
||||||
|
RATING = 'rating',
|
||||||
|
RECENTLY_ADDED = 'createdAt',
|
||||||
|
TITLE = 'title',
|
||||||
|
TRACK = 'track',
|
||||||
|
YEAR = 'year',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum NDSortOrder {
|
||||||
|
ASC = 'ASC',
|
||||||
|
DESC = 'DESC',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NDAddToPlaylist = null;
|
||||||
|
|
||||||
|
export type NDAddToPlaylistBody = {
|
||||||
|
ids: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NDUser = {
|
export type NDAddToPlaylistResponse = {
|
||||||
createdAt: string;
|
added: number;
|
||||||
email: string;
|
|
||||||
id: string;
|
|
||||||
isAdmin: boolean;
|
|
||||||
lastAccessAt: string;
|
|
||||||
lastLoginAt: string;
|
|
||||||
name: string;
|
|
||||||
updatedAt: string;
|
|
||||||
userName: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type NDGenre = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NDAlbum = {
|
export type NDAlbum = {
|
||||||
|
|
@ -61,6 +108,193 @@ export type NDAlbum = {
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
} & { songs?: NDSong[] };
|
} & { songs?: NDSong[] };
|
||||||
|
|
||||||
|
export type NDAlbumArtist = {
|
||||||
|
albumCount: number;
|
||||||
|
biography: string;
|
||||||
|
externalInfoUpdatedAt: string;
|
||||||
|
externalUrl: string;
|
||||||
|
fullText: string;
|
||||||
|
genres: NDGenre[];
|
||||||
|
id: string;
|
||||||
|
largeImageUrl?: string;
|
||||||
|
mbzArtistId: string;
|
||||||
|
mediumImageUrl?: string;
|
||||||
|
name: string;
|
||||||
|
orderArtistName: string;
|
||||||
|
playCount: number;
|
||||||
|
playDate: string;
|
||||||
|
rating: number;
|
||||||
|
size: number;
|
||||||
|
smallImageUrl?: string;
|
||||||
|
songCount: number;
|
||||||
|
starred: boolean;
|
||||||
|
starredAt: string;
|
||||||
|
} & {
|
||||||
|
similarArtists?: SSArtistInfo['similarArtist'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NDAlbumArtistDetail = NDAlbumArtist;
|
||||||
|
|
||||||
|
export type NDAlbumArtistDetailResponse = NDAlbumArtist;
|
||||||
|
|
||||||
|
export type NDAlbumArtistList = {
|
||||||
|
items: NDAlbumArtist[];
|
||||||
|
startIndex: number;
|
||||||
|
totalRecordCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NDAlbumArtistListParams = NDOrder &
|
||||||
|
NDPagination & {
|
||||||
|
_sort?: NDAlbumArtistListSort;
|
||||||
|
genre_id?: string;
|
||||||
|
starred?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NDAlbumDetail = NDAlbum & { songs?: NDSongListResponse };
|
||||||
|
|
||||||
|
export type NDAlbumDetailResponse = NDAlbum;
|
||||||
|
|
||||||
|
export type NDAlbumList = {
|
||||||
|
items: NDAlbum[];
|
||||||
|
startIndex: number;
|
||||||
|
totalRecordCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NDAlbumListParams = NDOrder &
|
||||||
|
NDPagination & {
|
||||||
|
_sort?: NDAlbumListSort;
|
||||||
|
album_id?: string;
|
||||||
|
artist_id?: string;
|
||||||
|
compilation?: boolean;
|
||||||
|
genre_id?: string;
|
||||||
|
has_rating?: boolean;
|
||||||
|
id?: string;
|
||||||
|
name?: string;
|
||||||
|
recently_played?: boolean;
|
||||||
|
starred?: boolean;
|
||||||
|
year?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NDAlbumListResponse = NDAlbum[];
|
||||||
|
|
||||||
|
export type NDArtistListResponse = NDAlbumArtist[];
|
||||||
|
|
||||||
|
export type NDAuthenticate = {
|
||||||
|
id: string;
|
||||||
|
isAdmin: boolean;
|
||||||
|
name: string;
|
||||||
|
subsonicSalt: string;
|
||||||
|
subsonicToken: string;
|
||||||
|
token: string;
|
||||||
|
username: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NDAuthenticationResponse = NDAuthenticate;
|
||||||
|
|
||||||
|
export type NDCreatePlaylist = NDCreatePlaylistResponse;
|
||||||
|
|
||||||
|
export type NDCreatePlaylistParams = {
|
||||||
|
comment?: string;
|
||||||
|
name: string;
|
||||||
|
public?: boolean;
|
||||||
|
rules?: null | Record<string, any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NDCreatePlaylistResponse = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NDDeletePlaylist = NDDeletePlaylistResponse;
|
||||||
|
|
||||||
|
export type NDDeletePlaylistParams = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NDDeletePlaylistResponse = null;
|
||||||
|
|
||||||
|
export type NDGenre = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NDGenreList = NDGenre[];
|
||||||
|
|
||||||
|
export type NDGenreListParams = NDOrder &
|
||||||
|
NDPagination & {
|
||||||
|
_sort?: NDGenreListSort;
|
||||||
|
id?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NDGenreListResponse = NDGenre[];
|
||||||
|
|
||||||
|
export type NDOrder = {
|
||||||
|
_order?: NDSortOrder;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NDPagination = {
|
||||||
|
_end?: number;
|
||||||
|
_start?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NDPlaylist = {
|
||||||
|
comment: string;
|
||||||
|
createdAt: string;
|
||||||
|
duration: number;
|
||||||
|
evaluatedAt: string;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
ownerId: string;
|
||||||
|
ownerName: string;
|
||||||
|
path: string;
|
||||||
|
public: boolean;
|
||||||
|
rules: null | Record<string, any>;
|
||||||
|
size: number;
|
||||||
|
songCount: number;
|
||||||
|
sync: boolean;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NDPlaylistDetail = NDPlaylist;
|
||||||
|
|
||||||
|
export type NDPlaylistDetailResponse = NDPlaylist;
|
||||||
|
|
||||||
|
export type NDPlaylistList = {
|
||||||
|
items: NDPlaylist[];
|
||||||
|
startIndex: number;
|
||||||
|
totalRecordCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NDPlaylistListParams = NDOrder &
|
||||||
|
NDPagination & {
|
||||||
|
_sort?: NDPlaylistListSort;
|
||||||
|
owner_id?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NDPlaylistListResponse = NDPlaylist[];
|
||||||
|
|
||||||
|
export type NDPlaylistSong = NDSong & {
|
||||||
|
mediaFileId: string;
|
||||||
|
playlistId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NDPlaylistSongList = {
|
||||||
|
items: NDPlaylistSong[];
|
||||||
|
startIndex: number;
|
||||||
|
totalRecordCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NDPlaylistSongListResponse = NDPlaylistSong[];
|
||||||
|
|
||||||
|
export type NDRemoveFromPlaylist = null;
|
||||||
|
|
||||||
|
export type NDRemoveFromPlaylistParams = {
|
||||||
|
id: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NDRemoveFromPlaylistResponse = {
|
||||||
|
ids: string[];
|
||||||
|
};
|
||||||
|
|
||||||
export type NDSong = {
|
export type NDSong = {
|
||||||
album: string;
|
album: string;
|
||||||
albumArtist: string;
|
albumArtist: string;
|
||||||
|
|
@ -107,275 +341,41 @@ export type NDSong = {
|
||||||
year: number;
|
year: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NDAlbumArtist = {
|
|
||||||
albumCount: number;
|
|
||||||
biography: string;
|
|
||||||
externalInfoUpdatedAt: string;
|
|
||||||
externalUrl: string;
|
|
||||||
fullText: string;
|
|
||||||
genres: NDGenre[];
|
|
||||||
id: string;
|
|
||||||
largeImageUrl?: string;
|
|
||||||
mbzArtistId: string;
|
|
||||||
mediumImageUrl?: string;
|
|
||||||
name: string;
|
|
||||||
orderArtistName: string;
|
|
||||||
playCount: number;
|
|
||||||
playDate: string;
|
|
||||||
rating: number;
|
|
||||||
size: number;
|
|
||||||
smallImageUrl?: string;
|
|
||||||
songCount: number;
|
|
||||||
starred: boolean;
|
|
||||||
starredAt: string;
|
|
||||||
} & {
|
|
||||||
similarArtists?: SSArtistInfo['similarArtist'];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type NDAuthenticationResponse = NDAuthenticate;
|
|
||||||
|
|
||||||
export type NDAlbumArtistList = {
|
|
||||||
items: NDAlbumArtist[];
|
|
||||||
startIndex: number;
|
|
||||||
totalRecordCount: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type NDAlbumArtistDetail = NDAlbumArtist;
|
|
||||||
|
|
||||||
export type NDAlbumArtistDetailResponse = NDAlbumArtist;
|
|
||||||
|
|
||||||
export type NDGenreList = NDGenre[];
|
|
||||||
|
|
||||||
export type NDGenreListResponse = NDGenre[];
|
|
||||||
|
|
||||||
export type NDAlbumDetailResponse = NDAlbum;
|
|
||||||
|
|
||||||
export type NDAlbumDetail = NDAlbum & { songs?: NDSongListResponse };
|
|
||||||
|
|
||||||
export type NDAlbumListResponse = NDAlbum[];
|
|
||||||
|
|
||||||
export type NDAlbumList = {
|
|
||||||
items: NDAlbum[];
|
|
||||||
startIndex: number;
|
|
||||||
totalRecordCount: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type NDSongDetail = NDSong;
|
export type NDSongDetail = NDSong;
|
||||||
|
|
||||||
export type NDSongDetailResponse = NDSong;
|
export type NDSongDetailResponse = NDSong;
|
||||||
|
|
||||||
export type NDSongListResponse = NDSong[];
|
|
||||||
|
|
||||||
export type NDSongList = {
|
export type NDSongList = {
|
||||||
items: NDSong[];
|
items: NDSong[];
|
||||||
startIndex: number;
|
startIndex: number;
|
||||||
totalRecordCount: number;
|
totalRecordCount: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NDArtistListResponse = NDAlbumArtist[];
|
export type NDSongListParams = NDOrder &
|
||||||
|
NDPagination & {
|
||||||
|
_sort?: NDSongListSort;
|
||||||
|
album_id?: string[];
|
||||||
|
artist_id?: string[];
|
||||||
|
genre_id?: string;
|
||||||
|
starred?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export type NDPagination = {
|
export type NDSongListResponse = NDSong[];
|
||||||
_end?: number;
|
|
||||||
_start?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export enum NDSortOrder {
|
|
||||||
ASC = 'ASC',
|
|
||||||
DESC = 'DESC',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type NDOrder = {
|
|
||||||
_order?: NDSortOrder;
|
|
||||||
};
|
|
||||||
|
|
||||||
export enum NDGenreListSort {
|
|
||||||
NAME = 'name',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type NDGenreListParams = {
|
|
||||||
_sort?: NDGenreListSort;
|
|
||||||
id?: string;
|
|
||||||
} & NDPagination &
|
|
||||||
NDOrder;
|
|
||||||
|
|
||||||
export enum NDAlbumListSort {
|
|
||||||
ALBUM_ARTIST = 'album_artist',
|
|
||||||
ARTIST = 'artist',
|
|
||||||
DURATION = 'duration',
|
|
||||||
NAME = 'name',
|
|
||||||
PLAY_COUNT = 'play_count',
|
|
||||||
PLAY_DATE = 'play_date',
|
|
||||||
RANDOM = 'random',
|
|
||||||
RATING = 'rating',
|
|
||||||
RECENTLY_ADDED = 'recently_added',
|
|
||||||
SONG_COUNT = 'songCount',
|
|
||||||
STARRED = 'starred_at',
|
|
||||||
YEAR = 'max_year',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type NDAlbumListParams = {
|
|
||||||
_sort?: NDAlbumListSort;
|
|
||||||
album_id?: string;
|
|
||||||
artist_id?: string;
|
|
||||||
compilation?: boolean;
|
|
||||||
genre_id?: string;
|
|
||||||
has_rating?: boolean;
|
|
||||||
id?: string;
|
|
||||||
name?: string;
|
|
||||||
recently_played?: boolean;
|
|
||||||
starred?: boolean;
|
|
||||||
year?: number;
|
|
||||||
} & NDPagination &
|
|
||||||
NDOrder;
|
|
||||||
|
|
||||||
export enum NDSongListSort {
|
|
||||||
ALBUM = 'album',
|
|
||||||
ALBUM_ARTIST = 'order_album_artist_name',
|
|
||||||
ALBUM_SONGS = 'album',
|
|
||||||
ARTIST = 'artist',
|
|
||||||
BPM = 'bpm',
|
|
||||||
CHANNELS = 'channels',
|
|
||||||
COMMENT = 'comment',
|
|
||||||
DURATION = 'duration',
|
|
||||||
FAVORITED = 'starred_at',
|
|
||||||
GENRE = 'genre',
|
|
||||||
ID = 'id',
|
|
||||||
PLAY_COUNT = 'playCount',
|
|
||||||
PLAY_DATE = 'playDate',
|
|
||||||
RANDOM = 'random',
|
|
||||||
RATING = 'rating',
|
|
||||||
RECENTLY_ADDED = 'createdAt',
|
|
||||||
TITLE = 'title',
|
|
||||||
TRACK = 'track',
|
|
||||||
YEAR = 'year',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type NDSongListParams = {
|
|
||||||
_sort?: NDSongListSort;
|
|
||||||
album_id?: string[];
|
|
||||||
artist_id?: string[];
|
|
||||||
genre_id?: string;
|
|
||||||
starred?: boolean;
|
|
||||||
} & NDPagination &
|
|
||||||
NDOrder;
|
|
||||||
|
|
||||||
export enum NDAlbumArtistListSort {
|
|
||||||
ALBUM_COUNT = 'albumCount',
|
|
||||||
FAVORITED = 'starred_at',
|
|
||||||
NAME = 'name',
|
|
||||||
PLAY_COUNT = 'playCount',
|
|
||||||
RATING = 'rating',
|
|
||||||
SONG_COUNT = 'songCount',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type NDAlbumArtistListParams = {
|
|
||||||
_sort?: NDAlbumArtistListSort;
|
|
||||||
genre_id?: string;
|
|
||||||
starred?: boolean;
|
|
||||||
} & NDPagination &
|
|
||||||
NDOrder;
|
|
||||||
|
|
||||||
export type NDAddToPlaylistResponse = {
|
|
||||||
added: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type NDAddToPlaylistBody = {
|
|
||||||
ids: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type NDAddToPlaylist = null;
|
|
||||||
|
|
||||||
export type NDRemoveFromPlaylistResponse = {
|
|
||||||
ids: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type NDRemoveFromPlaylistParams = {
|
|
||||||
id: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type NDRemoveFromPlaylist = null;
|
|
||||||
|
|
||||||
export type NDCreatePlaylistParams = {
|
|
||||||
comment?: string;
|
|
||||||
name: string;
|
|
||||||
public?: boolean;
|
|
||||||
rules?: Record<string, any> | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type NDCreatePlaylistResponse = {
|
|
||||||
id: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type NDCreatePlaylist = NDCreatePlaylistResponse;
|
|
||||||
|
|
||||||
export type NDUpdatePlaylistParams = Partial<NDPlaylist>;
|
export type NDUpdatePlaylistParams = Partial<NDPlaylist>;
|
||||||
|
|
||||||
export type NDUpdatePlaylistResponse = NDPlaylist;
|
export type NDUpdatePlaylistResponse = NDPlaylist;
|
||||||
|
|
||||||
export type NDDeletePlaylistParams = {
|
export type NDUser = {
|
||||||
id: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type NDDeletePlaylistResponse = null;
|
|
||||||
|
|
||||||
export type NDDeletePlaylist = NDDeletePlaylistResponse;
|
|
||||||
|
|
||||||
export type NDPlaylist = {
|
|
||||||
comment: string;
|
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
duration: number;
|
email: string;
|
||||||
evaluatedAt: string;
|
|
||||||
id: string;
|
id: string;
|
||||||
|
isAdmin: boolean;
|
||||||
|
lastAccessAt: string;
|
||||||
|
lastLoginAt: string;
|
||||||
name: string;
|
name: string;
|
||||||
ownerId: string;
|
|
||||||
ownerName: string;
|
|
||||||
path: string;
|
|
||||||
public: boolean;
|
|
||||||
rules: Record<string, any> | null;
|
|
||||||
size: number;
|
|
||||||
songCount: number;
|
|
||||||
sync: boolean;
|
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
};
|
userName: string;
|
||||||
|
|
||||||
export type NDPlaylistDetail = NDPlaylist;
|
|
||||||
|
|
||||||
export type NDPlaylistDetailResponse = NDPlaylist;
|
|
||||||
|
|
||||||
export type NDPlaylistList = {
|
|
||||||
items: NDPlaylist[];
|
|
||||||
startIndex: number;
|
|
||||||
totalRecordCount: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type NDPlaylistListResponse = NDPlaylist[];
|
|
||||||
|
|
||||||
export enum NDPlaylistListSort {
|
|
||||||
DURATION = 'duration',
|
|
||||||
NAME = 'name',
|
|
||||||
OWNER = 'owner_name',
|
|
||||||
PUBLIC = 'public',
|
|
||||||
SONG_COUNT = 'songCount',
|
|
||||||
UPDATED_AT = 'updatedAt',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type NDPlaylistListParams = {
|
|
||||||
_sort?: NDPlaylistListSort;
|
|
||||||
owner_id?: string;
|
|
||||||
} & NDPagination &
|
|
||||||
NDOrder;
|
|
||||||
|
|
||||||
export type NDPlaylistSong = NDSong & {
|
|
||||||
mediaFileId: string;
|
|
||||||
playlistId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type NDPlaylistSongListResponse = NDPlaylistSong[];
|
|
||||||
|
|
||||||
export type NDPlaylistSongList = {
|
|
||||||
items: NDPlaylistSong[];
|
|
||||||
startIndex: number;
|
|
||||||
totalRecordCount: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NDSongQueryFields = [
|
export const NDSongQueryFields = [
|
||||||
|
|
@ -515,12 +515,9 @@ export const NDSongQueryNumberOperators = [
|
||||||
{ label: 'is in the range', value: 'inTheRange' },
|
{ label: 'is in the range', value: 'inTheRange' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export type NDUserListParams = {
|
export enum NDUserListSort {
|
||||||
_sort?: NDUserListSort;
|
NAME = 'name',
|
||||||
} & NDPagination &
|
}
|
||||||
NDOrder;
|
|
||||||
|
|
||||||
export type NDUserListResponse = NDUser[];
|
|
||||||
|
|
||||||
export type NDUserList = {
|
export type NDUserList = {
|
||||||
items: NDUser[];
|
items: NDUser[];
|
||||||
|
|
@ -528,6 +525,9 @@ export type NDUserList = {
|
||||||
totalRecordCount: number;
|
totalRecordCount: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum NDUserListSort {
|
export type NDUserListParams = NDOrder &
|
||||||
NAME = 'name',
|
NDPagination & {
|
||||||
}
|
_sort?: NDUserListSort;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NDUserListResponse = NDUser[];
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,19 @@
|
||||||
import { initClient, initContract } from '@ts-rest/core';
|
import { initClient, initContract } from '@ts-rest/core';
|
||||||
import axios, { Method, AxiosError, AxiosResponse, isAxiosError } from 'axios';
|
import axios, { AxiosError, AxiosResponse, isAxiosError, Method } from 'axios';
|
||||||
import isElectron from 'is-electron';
|
import isElectron from 'is-electron';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import omitBy from 'lodash/omitBy';
|
import omitBy from 'lodash/omitBy';
|
||||||
import qs from 'qs';
|
import qs from 'qs';
|
||||||
import { ndType } from './navidrome-types';
|
|
||||||
import { authenticationFailure, resultWithHeaders } from '/@/renderer/api/utils';
|
|
||||||
import { useAuthStore } from '/@/renderer/store';
|
|
||||||
import { ServerListItem } from '/@/renderer/api/types';
|
|
||||||
import { toast } from '/@/renderer/components/toast';
|
|
||||||
import i18n from '/@/i18n/i18n';
|
|
||||||
|
|
||||||
const localSettings = isElectron() ? window.electron.localSettings : null;
|
import { ndType } from './navidrome-types';
|
||||||
|
|
||||||
|
import i18n from '/@/i18n/i18n';
|
||||||
|
import { ServerListItem } from '/@/renderer/api/types';
|
||||||
|
import { authenticationFailure, resultWithHeaders } from '/@/renderer/api/utils';
|
||||||
|
import { toast } from '/@/renderer/components/toast';
|
||||||
|
import { useAuthStore } from '/@/renderer/store';
|
||||||
|
|
||||||
|
const localSettings = isElectron() ? window.api.localSettings : null;
|
||||||
|
|
||||||
const c = initContract();
|
const c = initContract();
|
||||||
|
|
||||||
|
|
@ -275,7 +277,7 @@ axiosClient.interceptors.response.use(
|
||||||
// eslint-disable-next-line promise/no-promise-in-callback
|
// eslint-disable-next-line promise/no-promise-in-callback
|
||||||
return localSettings
|
return localSettings
|
||||||
.passwordGet(currentServer.id)
|
.passwordGet(currentServer.id)
|
||||||
.then(async (password: string | null) => {
|
.then(async (password: null | string) => {
|
||||||
authSuccess = false;
|
authSuccess = false;
|
||||||
|
|
||||||
if (password === null) {
|
if (password === null) {
|
||||||
|
|
@ -367,14 +369,14 @@ axiosClient.interceptors.response.use(
|
||||||
);
|
);
|
||||||
|
|
||||||
export const ndApiClient = (args: {
|
export const ndApiClient = (args: {
|
||||||
server: ServerListItem | null;
|
server: null | ServerListItem;
|
||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
url?: string;
|
url?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const { server, url, signal } = args;
|
const { server, signal, url } = args;
|
||||||
|
|
||||||
return initClient(contract, {
|
return initClient(contract, {
|
||||||
api: async ({ path, method, headers, body }) => {
|
api: async ({ body, headers, method, path }) => {
|
||||||
let baseUrl: string | undefined;
|
let baseUrl: string | undefined;
|
||||||
let token: string | undefined;
|
let token: string | undefined;
|
||||||
|
|
||||||
|
|
@ -406,7 +408,7 @@ export const ndApiClient = (args: {
|
||||||
headers: result.headers as any,
|
headers: result.headers as any,
|
||||||
status: result.status,
|
status: result.status,
|
||||||
};
|
};
|
||||||
} catch (e: Error | AxiosError | any) {
|
} catch (e: any | AxiosError | Error) {
|
||||||
if (isAxiosError(e)) {
|
if (isAxiosError(e)) {
|
||||||
if (e.code === 'ERR_NETWORK') {
|
if (e.code === 'ERR_NETWORK') {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,29 @@
|
||||||
|
import {
|
||||||
|
albumArtistListSortMap,
|
||||||
|
albumListSortMap,
|
||||||
|
AuthenticationResponse,
|
||||||
|
ControllerEndpoint,
|
||||||
|
genreListSortMap,
|
||||||
|
playlistListSortMap,
|
||||||
|
PlaylistSongListArgs,
|
||||||
|
PlaylistSongListResponse,
|
||||||
|
ServerListItem,
|
||||||
|
Song,
|
||||||
|
songListSortMap,
|
||||||
|
sortOrderMap,
|
||||||
|
userListSortMap,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
|
import { ServerFeature, ServerFeatures } from '/@/renderer/api/features-types';
|
||||||
|
import { NDSongListSort } from '/@/renderer/api/navidrome.types';
|
||||||
import { ndApiClient } from '/@/renderer/api/navidrome/navidrome-api';
|
import { ndApiClient } from '/@/renderer/api/navidrome/navidrome-api';
|
||||||
import { ndNormalize } from '/@/renderer/api/navidrome/navidrome-normalize';
|
import { ndNormalize } from '/@/renderer/api/navidrome/navidrome-normalize';
|
||||||
import { ndType } from '/@/renderer/api/navidrome/navidrome-types';
|
import { ndType } from '/@/renderer/api/navidrome/navidrome-types';
|
||||||
import { ssApiClient } from '/@/renderer/api/subsonic/subsonic-api';
|
import { ssApiClient } from '/@/renderer/api/subsonic/subsonic-api';
|
||||||
import {
|
|
||||||
albumArtistListSortMap,
|
|
||||||
sortOrderMap,
|
|
||||||
AuthenticationResponse,
|
|
||||||
userListSortMap,
|
|
||||||
albumListSortMap,
|
|
||||||
songListSortMap,
|
|
||||||
playlistListSortMap,
|
|
||||||
PlaylistSongListArgs,
|
|
||||||
PlaylistSongListResponse,
|
|
||||||
genreListSortMap,
|
|
||||||
Song,
|
|
||||||
ControllerEndpoint,
|
|
||||||
ServerListItem,
|
|
||||||
} from '../types';
|
|
||||||
import { VersionInfo, getFeatures, hasFeature } from '/@/renderer/api/utils';
|
|
||||||
import { ServerFeature, ServerFeatures } from '/@/renderer/api/features-types';
|
|
||||||
import { SubsonicExtensions } from '/@/renderer/api/subsonic/subsonic-types';
|
|
||||||
import { NDSongListSort } from '/@/renderer/api/navidrome.types';
|
|
||||||
import { ssNormalize } from '/@/renderer/api/subsonic/subsonic-normalize';
|
|
||||||
import { SubsonicController } from '/@/renderer/api/subsonic/subsonic-controller';
|
import { SubsonicController } from '/@/renderer/api/subsonic/subsonic-controller';
|
||||||
|
import { ssNormalize } from '/@/renderer/api/subsonic/subsonic-normalize';
|
||||||
|
import { SubsonicExtensions } from '/@/renderer/api/subsonic/subsonic-types';
|
||||||
|
import { getFeatures, hasFeature, VersionInfo } from '/@/renderer/api/utils';
|
||||||
|
|
||||||
const VERSION_INFO: VersionInfo = [
|
const VERSION_INFO: VersionInfo = [
|
||||||
['0.55.0', { [ServerFeature.BFR]: [1] }],
|
['0.55.0', { [ServerFeature.BFR]: [1] }],
|
||||||
|
|
@ -48,7 +49,7 @@ const NAVIDROME_ROLES: Array<string | { label: string; value: string }> = [
|
||||||
|
|
||||||
const EXCLUDED_TAGS = new Set<string>(['disctotal', 'genre', 'tracktotal']);
|
const EXCLUDED_TAGS = new Set<string>(['disctotal', 'genre', 'tracktotal']);
|
||||||
|
|
||||||
const excludeMissing = (server: ServerListItem | null) => {
|
const excludeMissing = (server: null | ServerListItem) => {
|
||||||
if (hasFeature(server, ServerFeature.BFR)) {
|
if (hasFeature(server, ServerFeature.BFR)) {
|
||||||
return { missing: false };
|
return { missing: false };
|
||||||
}
|
}
|
||||||
|
|
@ -58,7 +59,7 @@ const excludeMissing = (server: ServerListItem | null) => {
|
||||||
|
|
||||||
export const NavidromeController: ControllerEndpoint = {
|
export const NavidromeController: ControllerEndpoint = {
|
||||||
addToPlaylist: async (args) => {
|
addToPlaylist: async (args) => {
|
||||||
const { body, query, apiClientProps } = args;
|
const { apiClientProps, body, query } = args;
|
||||||
|
|
||||||
const res = await ndApiClient(apiClientProps).addToPlaylist({
|
const res = await ndApiClient(apiClientProps).addToPlaylist({
|
||||||
body: {
|
body: {
|
||||||
|
|
@ -98,7 +99,7 @@ export const NavidromeController: ControllerEndpoint = {
|
||||||
},
|
},
|
||||||
createFavorite: SubsonicController.createFavorite,
|
createFavorite: SubsonicController.createFavorite,
|
||||||
createPlaylist: async (args) => {
|
createPlaylist: async (args) => {
|
||||||
const { body, apiClientProps } = args;
|
const { apiClientProps, body } = args;
|
||||||
|
|
||||||
const res = await ndApiClient(apiClientProps).createPlaylist({
|
const res = await ndApiClient(apiClientProps).createPlaylist({
|
||||||
body: {
|
body: {
|
||||||
|
|
@ -120,7 +121,7 @@ export const NavidromeController: ControllerEndpoint = {
|
||||||
},
|
},
|
||||||
deleteFavorite: SubsonicController.deleteFavorite,
|
deleteFavorite: SubsonicController.deleteFavorite,
|
||||||
deletePlaylist: async (args) => {
|
deletePlaylist: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
const res = await ndApiClient(apiClientProps).deletePlaylist({
|
const res = await ndApiClient(apiClientProps).deletePlaylist({
|
||||||
params: {
|
params: {
|
||||||
|
|
@ -135,7 +136,7 @@ export const NavidromeController: ControllerEndpoint = {
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
getAlbumArtistDetail: async (args) => {
|
getAlbumArtistDetail: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
const res = await ndApiClient(apiClientProps).getAlbumArtistDetail({
|
const res = await ndApiClient(apiClientProps).getAlbumArtistDetail({
|
||||||
params: {
|
params: {
|
||||||
|
|
@ -176,7 +177,7 @@ export const NavidromeController: ControllerEndpoint = {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
getAlbumArtistList: async (args) => {
|
getAlbumArtistList: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
const res = await ndApiClient(apiClientProps).getAlbumArtistList({
|
const res = await ndApiClient(apiClientProps).getAlbumArtistList({
|
||||||
query: {
|
query: {
|
||||||
|
|
@ -217,7 +218,7 @@ export const NavidromeController: ControllerEndpoint = {
|
||||||
query: { ...query, limit: 1, startIndex: 0 },
|
query: { ...query, limit: 1, startIndex: 0 },
|
||||||
}).then((result) => result!.totalRecordCount!),
|
}).then((result) => result!.totalRecordCount!),
|
||||||
getAlbumDetail: async (args) => {
|
getAlbumDetail: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
const albumRes = await ndApiClient(apiClientProps).getAlbumDetail({
|
const albumRes = await ndApiClient(apiClientProps).getAlbumDetail({
|
||||||
params: {
|
params: {
|
||||||
|
|
@ -245,7 +246,7 @@ export const NavidromeController: ControllerEndpoint = {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
getAlbumInfo: async (args) => {
|
getAlbumInfo: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
const albumInfo = await ssApiClient(apiClientProps).getAlbumInfo2({
|
const albumInfo = await ssApiClient(apiClientProps).getAlbumInfo2({
|
||||||
query: {
|
query: {
|
||||||
|
|
@ -265,7 +266,7 @@ export const NavidromeController: ControllerEndpoint = {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getAlbumList: async (args) => {
|
getAlbumList: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
const res = await ndApiClient(apiClientProps).getAlbumList({
|
const res = await ndApiClient(apiClientProps).getAlbumList({
|
||||||
query: {
|
query: {
|
||||||
|
|
@ -299,7 +300,7 @@ export const NavidromeController: ControllerEndpoint = {
|
||||||
query: { ...query, limit: 1, startIndex: 0 },
|
query: { ...query, limit: 1, startIndex: 0 },
|
||||||
}).then((result) => result!.totalRecordCount!),
|
}).then((result) => result!.totalRecordCount!),
|
||||||
getArtistList: async (args) => {
|
getArtistList: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
const res = await ndApiClient(apiClientProps).getAlbumArtistList({
|
const res = await ndApiClient(apiClientProps).getAlbumArtistList({
|
||||||
query: {
|
query: {
|
||||||
|
|
@ -341,7 +342,7 @@ export const NavidromeController: ControllerEndpoint = {
|
||||||
}).then((result) => result!.totalRecordCount!),
|
}).then((result) => result!.totalRecordCount!),
|
||||||
getDownloadUrl: SubsonicController.getDownloadUrl,
|
getDownloadUrl: SubsonicController.getDownloadUrl,
|
||||||
getGenreList: async (args) => {
|
getGenreList: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
const res = await ndApiClient(apiClientProps).getGenreList({
|
const res = await ndApiClient(apiClientProps).getGenreList({
|
||||||
query: {
|
query: {
|
||||||
|
|
@ -366,7 +367,7 @@ export const NavidromeController: ControllerEndpoint = {
|
||||||
getLyrics: SubsonicController.getLyrics,
|
getLyrics: SubsonicController.getLyrics,
|
||||||
getMusicFolderList: SubsonicController.getMusicFolderList,
|
getMusicFolderList: SubsonicController.getMusicFolderList,
|
||||||
getPlaylistDetail: async (args) => {
|
getPlaylistDetail: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
const res = await ndApiClient(apiClientProps).getPlaylistDetail({
|
const res = await ndApiClient(apiClientProps).getPlaylistDetail({
|
||||||
params: {
|
params: {
|
||||||
|
|
@ -381,7 +382,7 @@ export const NavidromeController: ControllerEndpoint = {
|
||||||
return ndNormalize.playlist(res.body.data, apiClientProps.server);
|
return ndNormalize.playlist(res.body.data, apiClientProps.server);
|
||||||
},
|
},
|
||||||
getPlaylistList: async (args) => {
|
getPlaylistList: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
const customQuery = query._custom?.navidrome;
|
const customQuery = query._custom?.navidrome;
|
||||||
|
|
||||||
// Smart playlists only became available in 0.48.0. Do not filter for previous versions
|
// Smart playlists only became available in 0.48.0. Do not filter for previous versions
|
||||||
|
|
@ -420,7 +421,7 @@ export const NavidromeController: ControllerEndpoint = {
|
||||||
query: { ...query, limit: 1, startIndex: 0 },
|
query: { ...query, limit: 1, startIndex: 0 },
|
||||||
}).then((result) => result!.totalRecordCount!),
|
}).then((result) => result!.totalRecordCount!),
|
||||||
getPlaylistSongList: async (args: PlaylistSongListArgs): Promise<PlaylistSongListResponse> => {
|
getPlaylistSongList: async (args: PlaylistSongListArgs): Promise<PlaylistSongListResponse> => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
const res = await ndApiClient(apiClientProps).getPlaylistSongList({
|
const res = await ndApiClient(apiClientProps).getPlaylistSongList({
|
||||||
params: {
|
params: {
|
||||||
|
|
@ -548,7 +549,7 @@ export const NavidromeController: ControllerEndpoint = {
|
||||||
}, []);
|
}, []);
|
||||||
},
|
},
|
||||||
getSongDetail: async (args) => {
|
getSongDetail: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
const res = await ndApiClient(apiClientProps).getSongDetail({
|
const res = await ndApiClient(apiClientProps).getSongDetail({
|
||||||
params: {
|
params: {
|
||||||
|
|
@ -563,7 +564,7 @@ export const NavidromeController: ControllerEndpoint = {
|
||||||
return ndNormalize.song(res.body.data, apiClientProps.server);
|
return ndNormalize.song(res.body.data, apiClientProps.server);
|
||||||
},
|
},
|
||||||
getSongList: async (args) => {
|
getSongList: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
const res = await ndApiClient(apiClientProps).getSongList({
|
const res = await ndApiClient(apiClientProps).getSongList({
|
||||||
query: {
|
query: {
|
||||||
|
|
@ -642,7 +643,7 @@ export const NavidromeController: ControllerEndpoint = {
|
||||||
getTopSongs: SubsonicController.getTopSongs,
|
getTopSongs: SubsonicController.getTopSongs,
|
||||||
getTranscodingUrl: SubsonicController.getTranscodingUrl,
|
getTranscodingUrl: SubsonicController.getTranscodingUrl,
|
||||||
getUserList: async (args) => {
|
getUserList: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
const res = await ndApiClient(apiClientProps).getUserList({
|
const res = await ndApiClient(apiClientProps).getUserList({
|
||||||
query: {
|
query: {
|
||||||
|
|
@ -682,7 +683,7 @@ export const NavidromeController: ControllerEndpoint = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
removeFromPlaylist: async (args) => {
|
removeFromPlaylist: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
const res = await ndApiClient(apiClientProps).removeFromPlaylist({
|
const res = await ndApiClient(apiClientProps).removeFromPlaylist({
|
||||||
params: {
|
params: {
|
||||||
|
|
@ -703,7 +704,7 @@ export const NavidromeController: ControllerEndpoint = {
|
||||||
search: SubsonicController.search,
|
search: SubsonicController.search,
|
||||||
setRating: SubsonicController.setRating,
|
setRating: SubsonicController.setRating,
|
||||||
shareItem: async (args) => {
|
shareItem: async (args) => {
|
||||||
const { body, apiClientProps } = args;
|
const { apiClientProps, body } = args;
|
||||||
|
|
||||||
const res = await ndApiClient(apiClientProps).shareItem({
|
const res = await ndApiClient(apiClientProps).shareItem({
|
||||||
body: {
|
body: {
|
||||||
|
|
@ -724,7 +725,7 @@ export const NavidromeController: ControllerEndpoint = {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
updatePlaylist: async (args) => {
|
updatePlaylist: async (args) => {
|
||||||
const { query, body, apiClientProps } = args;
|
const { apiClientProps, body, query } = args;
|
||||||
|
|
||||||
const res = await ndApiClient(apiClientProps).updatePlaylist({
|
const res = await ndApiClient(apiClientProps).updatePlaylist({
|
||||||
body: {
|
body: {
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,24 @@
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
|
import z from 'zod';
|
||||||
|
|
||||||
|
import { ndType } from './navidrome-types';
|
||||||
|
|
||||||
|
import { NDGenre } from '/@/renderer/api/navidrome.types';
|
||||||
|
import { ssType } from '/@/renderer/api/subsonic/subsonic-types';
|
||||||
import {
|
import {
|
||||||
Song,
|
|
||||||
LibraryItem,
|
|
||||||
Album,
|
Album,
|
||||||
Playlist,
|
|
||||||
User,
|
|
||||||
AlbumArtist,
|
AlbumArtist,
|
||||||
Genre,
|
Genre,
|
||||||
|
LibraryItem,
|
||||||
|
Playlist,
|
||||||
|
RelatedArtist,
|
||||||
ServerListItem,
|
ServerListItem,
|
||||||
ServerType,
|
ServerType,
|
||||||
RelatedArtist,
|
Song,
|
||||||
|
User,
|
||||||
} from '/@/renderer/api/types';
|
} from '/@/renderer/api/types';
|
||||||
import z from 'zod';
|
|
||||||
import { ndType } from './navidrome-types';
|
|
||||||
import { ssType } from '/@/renderer/api/subsonic/subsonic-types';
|
|
||||||
import { NDGenre } from '/@/renderer/api/navidrome.types';
|
|
||||||
|
|
||||||
const getImageUrl = (args: { url: string | null }) => {
|
const getImageUrl = (args: { url: null | string }) => {
|
||||||
const { url } = args;
|
const { url } = args;
|
||||||
if (url === '/app/artist-placeholder.webp') {
|
if (url === '/app/artist-placeholder.webp') {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -51,19 +53,19 @@ interface WithDate {
|
||||||
playDate?: string;
|
playDate?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizePlayDate = (item: WithDate): string | null => {
|
const normalizePlayDate = (item: WithDate): null | string => {
|
||||||
return !item.playDate || item.playDate.includes('0001-') ? null : item.playDate;
|
return !item.playDate || item.playDate.includes('0001-') ? null : item.playDate;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getArtists = (
|
const getArtists = (
|
||||||
item:
|
item:
|
||||||
| z.infer<typeof ndType._response.song>
|
| z.infer<typeof ndType._response.album>
|
||||||
| z.infer<typeof ndType._response.playlistSong>
|
| z.infer<typeof ndType._response.playlistSong>
|
||||||
| z.infer<typeof ndType._response.album>,
|
| z.infer<typeof ndType._response.song>,
|
||||||
) => {
|
) => {
|
||||||
let albumArtists: RelatedArtist[] | undefined;
|
let albumArtists: RelatedArtist[] | undefined;
|
||||||
let artists: RelatedArtist[] | undefined;
|
let artists: RelatedArtist[] | undefined;
|
||||||
let participants: Record<string, RelatedArtist[]> | null = null;
|
let participants: null | Record<string, RelatedArtist[]> = null;
|
||||||
|
|
||||||
if (item.participants) {
|
if (item.participants) {
|
||||||
participants = {};
|
participants = {};
|
||||||
|
|
@ -120,8 +122,8 @@ const getArtists = (
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeSong = (
|
const normalizeSong = (
|
||||||
item: z.infer<typeof ndType._response.song> | z.infer<typeof ndType._response.playlistSong>,
|
item: z.infer<typeof ndType._response.playlistSong> | z.infer<typeof ndType._response.song>,
|
||||||
server: ServerListItem | null,
|
server: null | ServerListItem,
|
||||||
imageSize?: number,
|
imageSize?: number,
|
||||||
): Song => {
|
): Song => {
|
||||||
let id;
|
let id;
|
||||||
|
|
@ -204,7 +206,7 @@ const normalizeAlbum = (
|
||||||
item: z.infer<typeof ndType._response.album> & {
|
item: z.infer<typeof ndType._response.album> & {
|
||||||
songs?: z.infer<typeof ndType._response.songList>;
|
songs?: z.infer<typeof ndType._response.songList>;
|
||||||
},
|
},
|
||||||
server: ServerListItem | null,
|
server: null | ServerListItem,
|
||||||
imageSize?: number,
|
imageSize?: number,
|
||||||
): Album => {
|
): Album => {
|
||||||
const imageUrl = getCoverArtUrl({
|
const imageUrl = getCoverArtUrl({
|
||||||
|
|
@ -268,7 +270,7 @@ const normalizeAlbumArtist = (
|
||||||
item: z.infer<typeof ndType._response.albumArtist> & {
|
item: z.infer<typeof ndType._response.albumArtist> & {
|
||||||
similarArtists?: z.infer<typeof ssType._response.artistInfo>['artistInfo']['similarArtist'];
|
similarArtists?: z.infer<typeof ssType._response.artistInfo>['artistInfo']['similarArtist'];
|
||||||
},
|
},
|
||||||
server: ServerListItem | null,
|
server: null | ServerListItem,
|
||||||
): AlbumArtist => {
|
): AlbumArtist => {
|
||||||
let imageUrl = getImageUrl({ url: item?.largeImageUrl || null });
|
let imageUrl = getImageUrl({ url: item?.largeImageUrl || null });
|
||||||
|
|
||||||
|
|
@ -332,7 +334,7 @@ const normalizeAlbumArtist = (
|
||||||
|
|
||||||
const normalizePlaylist = (
|
const normalizePlaylist = (
|
||||||
item: z.infer<typeof ndType._response.playlist>,
|
item: z.infer<typeof ndType._response.playlist>,
|
||||||
server: ServerListItem | null,
|
server: null | ServerListItem,
|
||||||
imageSize?: number,
|
imageSize?: number,
|
||||||
): Playlist => {
|
): Playlist => {
|
||||||
const imageUrl = getCoverArtUrl({
|
const imageUrl = getCoverArtUrl({
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
NDAlbumArtistListSort,
|
NDAlbumArtistListSort,
|
||||||
NDAlbumListSort,
|
NDAlbumListSort,
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,30 @@
|
||||||
import { QueryFunctionContext } from '@tanstack/react-query';
|
import { QueryFunctionContext } from '@tanstack/react-query';
|
||||||
import { LyricSource } from './types';
|
|
||||||
import type {
|
import type {
|
||||||
AlbumListQuery,
|
|
||||||
SongListQuery,
|
|
||||||
AlbumDetailQuery,
|
|
||||||
AlbumArtistListQuery,
|
|
||||||
ArtistListQuery,
|
|
||||||
PlaylistListQuery,
|
|
||||||
PlaylistDetailQuery,
|
|
||||||
PlaylistSongListQuery,
|
|
||||||
UserListQuery,
|
|
||||||
AlbumArtistDetailQuery,
|
AlbumArtistDetailQuery,
|
||||||
TopSongListQuery,
|
AlbumArtistListQuery,
|
||||||
SearchQuery,
|
AlbumDetailQuery,
|
||||||
SongDetailQuery,
|
AlbumListQuery,
|
||||||
RandomSongListQuery,
|
ArtistListQuery,
|
||||||
LyricsQuery,
|
|
||||||
LyricSearchQuery,
|
|
||||||
GenreListQuery,
|
GenreListQuery,
|
||||||
|
LyricSearchQuery,
|
||||||
|
LyricsQuery,
|
||||||
|
PlaylistDetailQuery,
|
||||||
|
PlaylistListQuery,
|
||||||
|
PlaylistSongListQuery,
|
||||||
|
RandomSongListQuery,
|
||||||
|
SearchQuery,
|
||||||
SimilarSongsQuery,
|
SimilarSongsQuery,
|
||||||
|
SongDetailQuery,
|
||||||
|
SongListQuery,
|
||||||
|
TopSongListQuery,
|
||||||
|
UserListQuery,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
|
import { LyricSource } from './types';
|
||||||
|
|
||||||
export const splitPaginatedQuery = (key: any) => {
|
export const splitPaginatedQuery = (key: any) => {
|
||||||
const { startIndex, limit, ...filter } = key || {};
|
const { limit, startIndex, ...filter } = key || {};
|
||||||
|
|
||||||
if (startIndex !== undefined || limit !== undefined) {
|
if (startIndex !== undefined || limit !== undefined) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -51,7 +53,7 @@ export const queryKeys: Record<
|
||||||
> = {
|
> = {
|
||||||
albumArtists: {
|
albumArtists: {
|
||||||
count: (serverId: string, query?: AlbumArtistListQuery) => {
|
count: (serverId: string, query?: AlbumArtistListQuery) => {
|
||||||
const { pagination, filter } = splitPaginatedQuery(query);
|
const { filter, pagination } = splitPaginatedQuery(query);
|
||||||
|
|
||||||
if (query && pagination) {
|
if (query && pagination) {
|
||||||
return [serverId, 'albumArtists', 'count', filter, pagination] as const;
|
return [serverId, 'albumArtists', 'count', filter, pagination] as const;
|
||||||
|
|
@ -68,7 +70,7 @@ export const queryKeys: Record<
|
||||||
return [serverId, 'albumArtists', 'detail'] as const;
|
return [serverId, 'albumArtists', 'detail'] as const;
|
||||||
},
|
},
|
||||||
list: (serverId: string, query?: AlbumArtistListQuery) => {
|
list: (serverId: string, query?: AlbumArtistListQuery) => {
|
||||||
const { pagination, filter } = splitPaginatedQuery(query);
|
const { filter, pagination } = splitPaginatedQuery(query);
|
||||||
if (query && pagination) {
|
if (query && pagination) {
|
||||||
return [serverId, 'albumArtists', 'list', filter, pagination] as const;
|
return [serverId, 'albumArtists', 'list', filter, pagination] as const;
|
||||||
}
|
}
|
||||||
|
|
@ -87,7 +89,7 @@ export const queryKeys: Record<
|
||||||
},
|
},
|
||||||
albums: {
|
albums: {
|
||||||
count: (serverId: string, query?: AlbumListQuery, artistId?: string) => {
|
count: (serverId: string, query?: AlbumListQuery, artistId?: string) => {
|
||||||
const { pagination, filter } = splitPaginatedQuery(query);
|
const { filter, pagination } = splitPaginatedQuery(query);
|
||||||
|
|
||||||
if (query && pagination && artistId) {
|
if (query && pagination && artistId) {
|
||||||
return [serverId, 'albums', 'count', artistId, filter, pagination] as const;
|
return [serverId, 'albums', 'count', artistId, filter, pagination] as const;
|
||||||
|
|
@ -110,7 +112,7 @@ export const queryKeys: Record<
|
||||||
detail: (serverId: string, query?: AlbumDetailQuery) =>
|
detail: (serverId: string, query?: AlbumDetailQuery) =>
|
||||||
[serverId, 'albums', 'detail', query] as const,
|
[serverId, 'albums', 'detail', query] as const,
|
||||||
list: (serverId: string, query?: AlbumListQuery, artistId?: string) => {
|
list: (serverId: string, query?: AlbumListQuery, artistId?: string) => {
|
||||||
const { pagination, filter } = splitPaginatedQuery(query);
|
const { filter, pagination } = splitPaginatedQuery(query);
|
||||||
|
|
||||||
if (query && pagination && artistId) {
|
if (query && pagination && artistId) {
|
||||||
return [serverId, 'albums', 'list', artistId, filter, pagination] as const;
|
return [serverId, 'albums', 'list', artistId, filter, pagination] as const;
|
||||||
|
|
@ -144,7 +146,7 @@ export const queryKeys: Record<
|
||||||
},
|
},
|
||||||
artists: {
|
artists: {
|
||||||
list: (serverId: string, query?: ArtistListQuery) => {
|
list: (serverId: string, query?: ArtistListQuery) => {
|
||||||
const { pagination, filter } = splitPaginatedQuery(query);
|
const { filter, pagination } = splitPaginatedQuery(query);
|
||||||
if (query && pagination) {
|
if (query && pagination) {
|
||||||
return [serverId, 'artists', 'list', filter, pagination] as const;
|
return [serverId, 'artists', 'list', filter, pagination] as const;
|
||||||
}
|
}
|
||||||
|
|
@ -159,7 +161,7 @@ export const queryKeys: Record<
|
||||||
},
|
},
|
||||||
genres: {
|
genres: {
|
||||||
list: (serverId: string, query?: GenreListQuery) => {
|
list: (serverId: string, query?: GenreListQuery) => {
|
||||||
const { pagination, filter } = splitPaginatedQuery(query);
|
const { filter, pagination } = splitPaginatedQuery(query);
|
||||||
if (query && pagination) {
|
if (query && pagination) {
|
||||||
return [serverId, 'genres', 'list', filter, pagination] as const;
|
return [serverId, 'genres', 'list', filter, pagination] as const;
|
||||||
}
|
}
|
||||||
|
|
@ -177,7 +179,7 @@ export const queryKeys: Record<
|
||||||
},
|
},
|
||||||
playlists: {
|
playlists: {
|
||||||
detail: (serverId: string, id?: string, query?: PlaylistDetailQuery) => {
|
detail: (serverId: string, id?: string, query?: PlaylistDetailQuery) => {
|
||||||
const { pagination, filter } = splitPaginatedQuery(query);
|
const { filter, pagination } = splitPaginatedQuery(query);
|
||||||
if (query && pagination) {
|
if (query && pagination) {
|
||||||
return [serverId, 'playlists', id, 'detail', filter, pagination] as const;
|
return [serverId, 'playlists', id, 'detail', filter, pagination] as const;
|
||||||
}
|
}
|
||||||
|
|
@ -190,7 +192,7 @@ export const queryKeys: Record<
|
||||||
return [serverId, 'playlists', 'detail'] as const;
|
return [serverId, 'playlists', 'detail'] as const;
|
||||||
},
|
},
|
||||||
detailSongList: (serverId: string, id: string, query?: PlaylistSongListQuery) => {
|
detailSongList: (serverId: string, id: string, query?: PlaylistSongListQuery) => {
|
||||||
const { pagination, filter } = splitPaginatedQuery(query);
|
const { filter, pagination } = splitPaginatedQuery(query);
|
||||||
|
|
||||||
if (query && id && pagination) {
|
if (query && id && pagination) {
|
||||||
return [serverId, 'playlists', id, 'detailSongList', filter, pagination] as const;
|
return [serverId, 'playlists', id, 'detailSongList', filter, pagination] as const;
|
||||||
|
|
@ -205,7 +207,7 @@ export const queryKeys: Record<
|
||||||
return [serverId, 'playlists', 'detailSongList'] as const;
|
return [serverId, 'playlists', 'detailSongList'] as const;
|
||||||
},
|
},
|
||||||
list: (serverId: string, query?: PlaylistListQuery) => {
|
list: (serverId: string, query?: PlaylistListQuery) => {
|
||||||
const { pagination, filter } = splitPaginatedQuery(query);
|
const { filter, pagination } = splitPaginatedQuery(query);
|
||||||
if (query && pagination) {
|
if (query && pagination) {
|
||||||
return [serverId, 'playlists', 'list', filter, pagination] as const;
|
return [serverId, 'playlists', 'list', filter, pagination] as const;
|
||||||
}
|
}
|
||||||
|
|
@ -218,7 +220,7 @@ export const queryKeys: Record<
|
||||||
},
|
},
|
||||||
root: (serverId: string) => [serverId, 'playlists'] as const,
|
root: (serverId: string) => [serverId, 'playlists'] as const,
|
||||||
songList: (serverId: string, id?: string, query?: PlaylistSongListQuery) => {
|
songList: (serverId: string, id?: string, query?: PlaylistSongListQuery) => {
|
||||||
const { pagination, filter } = splitPaginatedQuery(query);
|
const { filter, pagination } = splitPaginatedQuery(query);
|
||||||
if (query && id && pagination) {
|
if (query && id && pagination) {
|
||||||
return [serverId, 'playlists', id, 'songList', filter, pagination] as const;
|
return [serverId, 'playlists', id, 'songList', filter, pagination] as const;
|
||||||
}
|
}
|
||||||
|
|
@ -246,7 +248,7 @@ export const queryKeys: Record<
|
||||||
},
|
},
|
||||||
songs: {
|
songs: {
|
||||||
count: (serverId: string, query?: SongListQuery) => {
|
count: (serverId: string, query?: SongListQuery) => {
|
||||||
const { pagination, filter } = splitPaginatedQuery(query);
|
const { filter, pagination } = splitPaginatedQuery(query);
|
||||||
if (query && pagination) {
|
if (query && pagination) {
|
||||||
return [serverId, 'songs', 'count', filter, pagination] as const;
|
return [serverId, 'songs', 'count', filter, pagination] as const;
|
||||||
}
|
}
|
||||||
|
|
@ -262,7 +264,7 @@ export const queryKeys: Record<
|
||||||
return [serverId, 'songs', 'detail'] as const;
|
return [serverId, 'songs', 'detail'] as const;
|
||||||
},
|
},
|
||||||
list: (serverId: string, query?: SongListQuery) => {
|
list: (serverId: string, query?: SongListQuery) => {
|
||||||
const { pagination, filter } = splitPaginatedQuery(query);
|
const { filter, pagination } = splitPaginatedQuery(query);
|
||||||
if (query && pagination) {
|
if (query && pagination) {
|
||||||
return [serverId, 'songs', 'list', filter, pagination] as const;
|
return [serverId, 'songs', 'list', filter, pagination] as const;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,13 @@
|
||||||
export type SSBaseResponse = {
|
export type SSAlbum = SSAlbumListEntry & {
|
||||||
serverVersion?: 'string';
|
song: SSSong[];
|
||||||
status: 'string';
|
|
||||||
type?: 'string';
|
|
||||||
version: 'string';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SSMusicFolderList = SSMusicFolder[];
|
export type SSAlbumArtistDetail = SSAlbumArtistListEntry & { album: SSAlbumListEntry[] };
|
||||||
|
|
||||||
export type SSMusicFolderListResponse = {
|
|
||||||
musicFolders: {
|
|
||||||
musicFolder: SSMusicFolder[];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SSGenreList = SSGenre[];
|
|
||||||
|
|
||||||
export type SSGenreListResponse = {
|
|
||||||
genres: {
|
|
||||||
genre: SSGenre[];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SSAlbumArtistDetailParams = {
|
export type SSAlbumArtistDetailParams = {
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SSAlbumArtistDetail = SSAlbumArtistListEntry & { album: SSAlbumListEntry[] };
|
|
||||||
|
|
||||||
export type SSAlbumArtistDetailResponse = {
|
export type SSAlbumArtistDetailResponse = {
|
||||||
artist: SSAlbumArtistListEntry & {
|
artist: SSAlbumArtistListEntry & {
|
||||||
album: SSAlbumListEntry[];
|
album: SSAlbumListEntry[];
|
||||||
|
|
@ -36,7 +17,19 @@ export type SSAlbumArtistDetailResponse = {
|
||||||
export type SSAlbumArtistList = {
|
export type SSAlbumArtistList = {
|
||||||
items: SSAlbumArtistListEntry[];
|
items: SSAlbumArtistListEntry[];
|
||||||
startIndex: number;
|
startIndex: number;
|
||||||
totalRecordCount: number | null;
|
totalRecordCount: null | number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SSAlbumArtistListEntry = {
|
||||||
|
albumCount: string;
|
||||||
|
artistImageUrl?: string;
|
||||||
|
coverArt?: string;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SSAlbumArtistListParams = {
|
||||||
|
musicFolderId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SSAlbumArtistListResponse = {
|
export type SSAlbumArtistListResponse = {
|
||||||
|
|
@ -47,72 +40,16 @@ export type SSAlbumArtistListResponse = {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SSAlbumList = {
|
|
||||||
items: SSAlbumListEntry[];
|
|
||||||
startIndex: number;
|
|
||||||
totalRecordCount: number | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SSAlbumListResponse = {
|
|
||||||
albumList2: {
|
|
||||||
album: SSAlbumListEntry[];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SSAlbumDetail = Omit<SSAlbum, 'song'> & { songs: SSSong[] };
|
export type SSAlbumDetail = Omit<SSAlbum, 'song'> & { songs: SSSong[] };
|
||||||
|
|
||||||
export type SSAlbumDetailResponse = {
|
export type SSAlbumDetailResponse = {
|
||||||
album: SSAlbum;
|
album: SSAlbum;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SSArtistInfoParams = {
|
export type SSAlbumList = {
|
||||||
count?: number;
|
items: SSAlbumListEntry[];
|
||||||
id: string;
|
startIndex: number;
|
||||||
includeNotPresent?: boolean;
|
totalRecordCount: null | number;
|
||||||
};
|
|
||||||
|
|
||||||
export type SSArtistInfoResponse = {
|
|
||||||
artistInfo2: SSArtistInfo;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SSArtistInfo = {
|
|
||||||
biography: string;
|
|
||||||
largeImageUrl?: string;
|
|
||||||
lastFmUrl?: string;
|
|
||||||
mediumImageUrl?: string;
|
|
||||||
musicBrainzId?: string;
|
|
||||||
similarArtist?: {
|
|
||||||
albumCount: string;
|
|
||||||
artistImageUrl?: string;
|
|
||||||
coverArt?: string;
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
}[];
|
|
||||||
smallImageUrl?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SSMusicFolder = {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SSGenre = {
|
|
||||||
albumCount?: number;
|
|
||||||
songCount?: number;
|
|
||||||
value: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SSArtistIndex = {
|
|
||||||
artist: SSAlbumArtistListEntry[];
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SSAlbumArtistListEntry = {
|
|
||||||
albumCount: string;
|
|
||||||
artistImageUrl?: string;
|
|
||||||
coverArt?: string;
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SSAlbumListEntry = {
|
export type SSAlbumListEntry = {
|
||||||
|
|
@ -135,9 +72,111 @@ export type SSAlbumListEntry = {
|
||||||
year: number;
|
year: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SSAlbum = {
|
export type SSAlbumListParams = {
|
||||||
song: SSSong[];
|
fromYear?: number;
|
||||||
} & SSAlbumListEntry;
|
genre?: string;
|
||||||
|
musicFolderId?: string;
|
||||||
|
offset?: number;
|
||||||
|
size?: number;
|
||||||
|
toYear?: number;
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SSAlbumListResponse = {
|
||||||
|
albumList2: {
|
||||||
|
album: SSAlbumListEntry[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SSArtistIndex = {
|
||||||
|
artist: SSAlbumArtistListEntry[];
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SSArtistInfo = {
|
||||||
|
biography: string;
|
||||||
|
largeImageUrl?: string;
|
||||||
|
lastFmUrl?: string;
|
||||||
|
mediumImageUrl?: string;
|
||||||
|
musicBrainzId?: string;
|
||||||
|
similarArtist?: {
|
||||||
|
albumCount: string;
|
||||||
|
artistImageUrl?: string;
|
||||||
|
coverArt?: string;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}[];
|
||||||
|
smallImageUrl?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SSArtistInfoParams = {
|
||||||
|
count?: number;
|
||||||
|
id: string;
|
||||||
|
includeNotPresent?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SSArtistInfoResponse = {
|
||||||
|
artistInfo2: SSArtistInfo;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SSBaseResponse = {
|
||||||
|
serverVersion?: 'string';
|
||||||
|
status: 'string';
|
||||||
|
type?: 'string';
|
||||||
|
version: 'string';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SSFavorite = null;
|
||||||
|
|
||||||
|
export type SSFavoriteParams = {
|
||||||
|
albumId?: string;
|
||||||
|
artistId?: string;
|
||||||
|
id?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SSFavoriteResponse = null;
|
||||||
|
|
||||||
|
export type SSGenre = {
|
||||||
|
albumCount?: number;
|
||||||
|
songCount?: number;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SSGenreList = SSGenre[];
|
||||||
|
|
||||||
|
export type SSGenreListResponse = {
|
||||||
|
genres: {
|
||||||
|
genre: SSGenre[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SSMusicFolder = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SSMusicFolderList = SSMusicFolder[];
|
||||||
|
|
||||||
|
export type SSMusicFolderListResponse = {
|
||||||
|
musicFolders: {
|
||||||
|
musicFolder: SSMusicFolder[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SSRating = null;
|
||||||
|
|
||||||
|
export type SSRatingParams = {
|
||||||
|
id: string;
|
||||||
|
rating: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SSRatingResponse = null;
|
||||||
|
|
||||||
|
export type SSScrobbleParams = {
|
||||||
|
id: string;
|
||||||
|
submission?: boolean;
|
||||||
|
time?: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type SSSong = {
|
export type SSSong = {
|
||||||
album: string;
|
album: string;
|
||||||
|
|
@ -167,39 +206,12 @@ export type SSSong = {
|
||||||
year: number;
|
year: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SSAlbumListParams = {
|
export type SSTopSongList = {
|
||||||
fromYear?: number;
|
items: SSSong[];
|
||||||
genre?: string;
|
startIndex: number;
|
||||||
musicFolderId?: string;
|
totalRecordCount: null | number;
|
||||||
offset?: number;
|
|
||||||
size?: number;
|
|
||||||
toYear?: number;
|
|
||||||
type: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SSAlbumArtistListParams = {
|
|
||||||
musicFolderId?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SSFavoriteParams = {
|
|
||||||
albumId?: string;
|
|
||||||
artistId?: string;
|
|
||||||
id?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SSFavorite = null;
|
|
||||||
|
|
||||||
export type SSFavoriteResponse = null;
|
|
||||||
|
|
||||||
export type SSRatingParams = {
|
|
||||||
id: string;
|
|
||||||
rating: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SSRating = null;
|
|
||||||
|
|
||||||
export type SSRatingResponse = null;
|
|
||||||
|
|
||||||
export type SSTopSongListParams = {
|
export type SSTopSongListParams = {
|
||||||
artist: string;
|
artist: string;
|
||||||
count?: number;
|
count?: number;
|
||||||
|
|
@ -210,15 +222,3 @@ export type SSTopSongListResponse = {
|
||||||
song: SSSong[];
|
song: SSSong[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SSTopSongList = {
|
|
||||||
items: SSSong[];
|
|
||||||
startIndex: number;
|
|
||||||
totalRecordCount: number | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SSScrobbleParams = {
|
|
||||||
id: string;
|
|
||||||
submission?: boolean;
|
|
||||||
time?: number;
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import { initClient, initContract } from '@ts-rest/core';
|
import { initClient, initContract } from '@ts-rest/core';
|
||||||
import axios, { Method, AxiosError, isAxiosError, AxiosResponse } from 'axios';
|
import axios, { AxiosError, AxiosResponse, isAxiosError, Method } from 'axios';
|
||||||
import omitBy from 'lodash/omitBy';
|
import omitBy from 'lodash/omitBy';
|
||||||
import qs from 'qs';
|
import qs from 'qs';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import i18n from '/@/i18n/i18n';
|
||||||
import { ssType } from '/@/renderer/api/subsonic/subsonic-types';
|
import { ssType } from '/@/renderer/api/subsonic/subsonic-types';
|
||||||
import { ServerListItem } from '/@/renderer/api/types';
|
import { ServerListItem } from '/@/renderer/api/types';
|
||||||
import { toast } from '/@/renderer/components/toast/index';
|
import { toast } from '/@/renderer/components/toast/index';
|
||||||
import i18n from '/@/i18n/i18n';
|
|
||||||
|
|
||||||
const c = initContract();
|
const c = initContract();
|
||||||
|
|
||||||
|
|
@ -284,15 +285,15 @@ const silentlyTransformResponse = (data: any) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ssApiClient = (args: {
|
export const ssApiClient = (args: {
|
||||||
server: ServerListItem | null;
|
server: null | ServerListItem;
|
||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
silent?: boolean;
|
silent?: boolean;
|
||||||
url?: string;
|
url?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const { server, url, signal, silent } = args;
|
const { server, signal, silent, url } = args;
|
||||||
|
|
||||||
return initClient(contract, {
|
return initClient(contract, {
|
||||||
api: async ({ path, method, headers, body }) => {
|
api: async ({ body, headers, method, path }) => {
|
||||||
let baseUrl: string | undefined;
|
let baseUrl: string | undefined;
|
||||||
const authParams: Record<string, any> = {};
|
const authParams: Record<string, any> = {};
|
||||||
|
|
||||||
|
|
@ -339,7 +340,7 @@ export const ssApiClient = (args: {
|
||||||
headers: result.headers as any,
|
headers: result.headers as any,
|
||||||
status: result.status,
|
status: result.status,
|
||||||
};
|
};
|
||||||
} catch (e: Error | AxiosError | any) {
|
} catch (e: any | AxiosError | Error) {
|
||||||
if (isAxiosError(e)) {
|
if (isAxiosError(e)) {
|
||||||
if (e.code === 'ERR_NETWORK') {
|
if (e.code === 'ERR_NETWORK') {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|
|
||||||
|
|
@ -2,44 +2,45 @@ import dayjs from 'dayjs';
|
||||||
import filter from 'lodash/filter';
|
import filter from 'lodash/filter';
|
||||||
import orderBy from 'lodash/orderBy';
|
import orderBy from 'lodash/orderBy';
|
||||||
import md5 from 'md5';
|
import md5 from 'md5';
|
||||||
|
|
||||||
|
import { ServerFeatures } from '/@/renderer/api/features-types';
|
||||||
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 { AlbumListSortType, SubsonicExtensions } from '/@/renderer/api/subsonic/subsonic-types';
|
import { AlbumListSortType, SubsonicExtensions } from '/@/renderer/api/subsonic/subsonic-types';
|
||||||
import {
|
import {
|
||||||
LibraryItem,
|
|
||||||
Song,
|
|
||||||
ControllerEndpoint,
|
|
||||||
sortSongList,
|
|
||||||
sortAlbumArtistList,
|
|
||||||
PlaylistListSort,
|
|
||||||
GenreListSort,
|
|
||||||
AlbumListSort,
|
AlbumListSort,
|
||||||
|
ControllerEndpoint,
|
||||||
|
GenreListSort,
|
||||||
|
LibraryItem,
|
||||||
|
PlaylistListSort,
|
||||||
|
Song,
|
||||||
|
sortAlbumArtistList,
|
||||||
sortAlbumList,
|
sortAlbumList,
|
||||||
SortOrder,
|
SortOrder,
|
||||||
|
sortSongList,
|
||||||
} 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 ALBUM_LIST_SORT_MAPPING: Record<AlbumListSort, AlbumListSortType | undefined> = {
|
const ALBUM_LIST_SORT_MAPPING: Record<AlbumListSort, AlbumListSortType | undefined> = {
|
||||||
[AlbumListSort.RANDOM]: AlbumListSortType.RANDOM,
|
|
||||||
[AlbumListSort.ALBUM_ARTIST]: AlbumListSortType.ALPHABETICAL_BY_ARTIST,
|
[AlbumListSort.ALBUM_ARTIST]: AlbumListSortType.ALPHABETICAL_BY_ARTIST,
|
||||||
[AlbumListSort.PLAY_COUNT]: AlbumListSortType.FREQUENT,
|
|
||||||
[AlbumListSort.RECENTLY_ADDED]: AlbumListSortType.NEWEST,
|
|
||||||
[AlbumListSort.FAVORITED]: AlbumListSortType.STARRED,
|
|
||||||
[AlbumListSort.YEAR]: AlbumListSortType.BY_YEAR,
|
|
||||||
[AlbumListSort.NAME]: AlbumListSortType.ALPHABETICAL_BY_NAME,
|
|
||||||
[AlbumListSort.COMMUNITY_RATING]: undefined,
|
|
||||||
[AlbumListSort.DURATION]: undefined,
|
|
||||||
[AlbumListSort.CRITIC_RATING]: undefined,
|
|
||||||
[AlbumListSort.RATING]: undefined,
|
|
||||||
[AlbumListSort.ARTIST]: undefined,
|
[AlbumListSort.ARTIST]: undefined,
|
||||||
|
[AlbumListSort.COMMUNITY_RATING]: undefined,
|
||||||
|
[AlbumListSort.CRITIC_RATING]: undefined,
|
||||||
|
[AlbumListSort.DURATION]: undefined,
|
||||||
|
[AlbumListSort.FAVORITED]: AlbumListSortType.STARRED,
|
||||||
|
[AlbumListSort.NAME]: AlbumListSortType.ALPHABETICAL_BY_NAME,
|
||||||
|
[AlbumListSort.PLAY_COUNT]: AlbumListSortType.FREQUENT,
|
||||||
|
[AlbumListSort.RANDOM]: AlbumListSortType.RANDOM,
|
||||||
|
[AlbumListSort.RATING]: undefined,
|
||||||
|
[AlbumListSort.RECENTLY_ADDED]: AlbumListSortType.NEWEST,
|
||||||
[AlbumListSort.RECENTLY_PLAYED]: AlbumListSortType.RECENT,
|
[AlbumListSort.RECENTLY_PLAYED]: AlbumListSortType.RECENT,
|
||||||
[AlbumListSort.RELEASE_DATE]: undefined,
|
[AlbumListSort.RELEASE_DATE]: undefined,
|
||||||
[AlbumListSort.SONG_COUNT]: undefined,
|
[AlbumListSort.SONG_COUNT]: undefined,
|
||||||
|
[AlbumListSort.YEAR]: AlbumListSortType.BY_YEAR,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SubsonicController: ControllerEndpoint = {
|
export const SubsonicController: ControllerEndpoint = {
|
||||||
addToPlaylist: async ({ body, query, apiClientProps }) => {
|
addToPlaylist: async ({ apiClientProps, body, query }) => {
|
||||||
const res = await ssApiClient(apiClientProps).updatePlaylist({
|
const res = await ssApiClient(apiClientProps).updatePlaylist({
|
||||||
query: {
|
query: {
|
||||||
playlistId: query.id,
|
playlistId: query.id,
|
||||||
|
|
@ -98,7 +99,7 @@ export const SubsonicController: ControllerEndpoint = {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
createFavorite: async (args) => {
|
createFavorite: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
const res = await ssApiClient(apiClientProps).createFavorite({
|
const res = await ssApiClient(apiClientProps).createFavorite({
|
||||||
query: {
|
query: {
|
||||||
|
|
@ -114,7 +115,7 @@ export const SubsonicController: ControllerEndpoint = {
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
createPlaylist: async ({ body, apiClientProps }) => {
|
createPlaylist: async ({ apiClientProps, body }) => {
|
||||||
const res = await ssApiClient(apiClientProps).createPlaylist({
|
const res = await ssApiClient(apiClientProps).createPlaylist({
|
||||||
query: {
|
query: {
|
||||||
name: body.name,
|
name: body.name,
|
||||||
|
|
@ -131,7 +132,7 @@ export const SubsonicController: ControllerEndpoint = {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
deleteFavorite: async (args) => {
|
deleteFavorite: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
const res = await ssApiClient(apiClientProps).removeFavorite({
|
const res = await ssApiClient(apiClientProps).removeFavorite({
|
||||||
query: {
|
query: {
|
||||||
|
|
@ -148,7 +149,7 @@ export const SubsonicController: ControllerEndpoint = {
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
deletePlaylist: async (args) => {
|
deletePlaylist: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
const res = await ssApiClient(apiClientProps).deletePlaylist({
|
const res = await ssApiClient(apiClientProps).deletePlaylist({
|
||||||
query: {
|
query: {
|
||||||
|
|
@ -163,7 +164,7 @@ export const SubsonicController: ControllerEndpoint = {
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
getAlbumArtistDetail: async (args) => {
|
getAlbumArtistDetail: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
const artistInfoRes = await ssApiClient(apiClientProps).getArtistInfo({
|
const artistInfoRes = await ssApiClient(apiClientProps).getArtistInfo({
|
||||||
query: {
|
query: {
|
||||||
|
|
@ -198,7 +199,7 @@ export const SubsonicController: ControllerEndpoint = {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getAlbumArtistList: async (args) => {
|
getAlbumArtistList: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
const res = await ssApiClient(apiClientProps).getArtists({
|
const res = await ssApiClient(apiClientProps).getArtists({
|
||||||
query: {
|
query: {
|
||||||
|
|
@ -237,7 +238,7 @@ export const SubsonicController: ControllerEndpoint = {
|
||||||
getAlbumArtistListCount: (args) =>
|
getAlbumArtistListCount: (args) =>
|
||||||
SubsonicController.getAlbumArtistList(args).then((res) => res!.totalRecordCount!),
|
SubsonicController.getAlbumArtistList(args).then((res) => res!.totalRecordCount!),
|
||||||
getAlbumDetail: async (args) => {
|
getAlbumDetail: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
const res = await ssApiClient(apiClientProps).getAlbum({
|
const res = await ssApiClient(apiClientProps).getAlbum({
|
||||||
query: {
|
query: {
|
||||||
|
|
@ -252,7 +253,7 @@ export const SubsonicController: ControllerEndpoint = {
|
||||||
return ssNormalize.album(res.body.album, apiClientProps.server);
|
return ssNormalize.album(res.body.album, apiClientProps.server);
|
||||||
},
|
},
|
||||||
getAlbumList: async (args) => {
|
getAlbumList: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
if (query.searchTerm) {
|
if (query.searchTerm) {
|
||||||
const res = await ssApiClient(apiClientProps).search3({
|
const res = await ssApiClient(apiClientProps).search3({
|
||||||
|
|
@ -398,7 +399,7 @@ export const SubsonicController: ControllerEndpoint = {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getAlbumListCount: async (args) => {
|
getAlbumListCount: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
if (query.searchTerm) {
|
if (query.searchTerm) {
|
||||||
let fetchNextPage = true;
|
let fetchNextPage = true;
|
||||||
|
|
@ -516,7 +517,7 @@ export const SubsonicController: ControllerEndpoint = {
|
||||||
return totalRecordCount;
|
return totalRecordCount;
|
||||||
},
|
},
|
||||||
getArtistList: async (args) => {
|
getArtistList: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
const res = await ssApiClient(apiClientProps).getArtists({
|
const res = await ssApiClient(apiClientProps).getArtists({
|
||||||
query: {
|
query: {
|
||||||
|
|
@ -570,7 +571,7 @@ export const SubsonicController: ControllerEndpoint = {
|
||||||
'&c=Feishin'
|
'&c=Feishin'
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
getGenreList: async ({ query, apiClientProps }) => {
|
getGenreList: async ({ apiClientProps, query }) => {
|
||||||
const sortOrder = query.sortOrder.toLowerCase() as 'asc' | 'desc';
|
const sortOrder = query.sortOrder.toLowerCase() as 'asc' | 'desc';
|
||||||
|
|
||||||
const res = await ssApiClient(apiClientProps).getGenres({});
|
const res = await ssApiClient(apiClientProps).getGenres({});
|
||||||
|
|
@ -624,7 +625,7 @@ export const SubsonicController: ControllerEndpoint = {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getPlaylistDetail: async (args) => {
|
getPlaylistDetail: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
const res = await ssApiClient(apiClientProps).getPlaylist({
|
const res = await ssApiClient(apiClientProps).getPlaylist({
|
||||||
query: {
|
query: {
|
||||||
|
|
@ -638,7 +639,7 @@ export const SubsonicController: ControllerEndpoint = {
|
||||||
|
|
||||||
return ssNormalize.playlist(res.body.playlist, apiClientProps.server);
|
return ssNormalize.playlist(res.body.playlist, apiClientProps.server);
|
||||||
},
|
},
|
||||||
getPlaylistList: async ({ query, apiClientProps }) => {
|
getPlaylistList: async ({ apiClientProps, query }) => {
|
||||||
const sortOrder = query.sortOrder.toLowerCase() as 'asc' | 'desc';
|
const sortOrder = query.sortOrder.toLowerCase() as 'asc' | 'desc';
|
||||||
|
|
||||||
const res = await ssApiClient(apiClientProps).getPlaylists({});
|
const res = await ssApiClient(apiClientProps).getPlaylists({});
|
||||||
|
|
@ -686,7 +687,7 @@ export const SubsonicController: ControllerEndpoint = {
|
||||||
totalRecordCount: results.length,
|
totalRecordCount: results.length,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getPlaylistListCount: async ({ query, apiClientProps }) => {
|
getPlaylistListCount: async ({ apiClientProps, query }) => {
|
||||||
const res = await ssApiClient(apiClientProps).getPlaylists({});
|
const res = await ssApiClient(apiClientProps).getPlaylists({});
|
||||||
|
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
|
|
@ -705,7 +706,7 @@ export const SubsonicController: ControllerEndpoint = {
|
||||||
|
|
||||||
return results.length;
|
return results.length;
|
||||||
},
|
},
|
||||||
getPlaylistSongList: async ({ query, apiClientProps }) => {
|
getPlaylistSongList: async ({ apiClientProps, query }) => {
|
||||||
const res = await ssApiClient(apiClientProps).getPlaylist({
|
const res = await ssApiClient(apiClientProps).getPlaylist({
|
||||||
query: {
|
query: {
|
||||||
id: query.id,
|
id: query.id,
|
||||||
|
|
@ -731,7 +732,7 @@ export const SubsonicController: ControllerEndpoint = {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getRandomSongList: async (args) => {
|
getRandomSongList: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
const res = await ssApiClient(apiClientProps).getRandomSongList({
|
const res = await ssApiClient(apiClientProps).getRandomSongList({
|
||||||
query: {
|
query: {
|
||||||
|
|
@ -842,7 +843,7 @@ export const SubsonicController: ControllerEndpoint = {
|
||||||
}, []);
|
}, []);
|
||||||
},
|
},
|
||||||
getSongDetail: async (args) => {
|
getSongDetail: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
const res = await ssApiClient(apiClientProps).getSong({
|
const res = await ssApiClient(apiClientProps).getSong({
|
||||||
query: {
|
query: {
|
||||||
|
|
@ -856,7 +857,7 @@ export const SubsonicController: ControllerEndpoint = {
|
||||||
|
|
||||||
return ssNormalize.song(res.body.song, apiClientProps.server);
|
return ssNormalize.song(res.body.song, apiClientProps.server);
|
||||||
},
|
},
|
||||||
getSongList: async ({ query, apiClientProps }) => {
|
getSongList: async ({ apiClientProps, query }) => {
|
||||||
const fromAlbumPromises = [];
|
const fromAlbumPromises = [];
|
||||||
const artistDetailPromises = [];
|
const artistDetailPromises = [];
|
||||||
let results: any[] = [];
|
let results: any[] = [];
|
||||||
|
|
@ -1028,7 +1029,7 @@ export const SubsonicController: ControllerEndpoint = {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getSongListCount: async (args) => {
|
getSongListCount: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
let fetchNextPage = true;
|
let fetchNextPage = true;
|
||||||
let startIndex = 0;
|
let startIndex = 0;
|
||||||
|
|
@ -1196,7 +1197,7 @@ export const SubsonicController: ControllerEndpoint = {
|
||||||
return totalRecordCount;
|
return totalRecordCount;
|
||||||
},
|
},
|
||||||
getStructuredLyrics: async (args) => {
|
getStructuredLyrics: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
const res = await ssApiClient(apiClientProps).getStructuredLyrics({
|
const res = await ssApiClient(apiClientProps).getStructuredLyrics({
|
||||||
query: {
|
query: {
|
||||||
|
|
@ -1238,7 +1239,7 @@ export const SubsonicController: ControllerEndpoint = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
getTopSongs: async (args) => {
|
getTopSongs: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
const res = await ssApiClient(apiClientProps).getTopSongsList({
|
const res = await ssApiClient(apiClientProps).getTopSongsList({
|
||||||
query: {
|
query: {
|
||||||
|
|
@ -1261,7 +1262,7 @@ export const SubsonicController: ControllerEndpoint = {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getTranscodingUrl: (args) => {
|
getTranscodingUrl: (args) => {
|
||||||
const { base, format, bitrate } = args.query;
|
const { base, bitrate, format } = args.query;
|
||||||
let url = base;
|
let url = base;
|
||||||
if (format) {
|
if (format) {
|
||||||
url += `&format=${format}`;
|
url += `&format=${format}`;
|
||||||
|
|
@ -1272,7 +1273,7 @@ export const SubsonicController: ControllerEndpoint = {
|
||||||
|
|
||||||
return url;
|
return url;
|
||||||
},
|
},
|
||||||
removeFromPlaylist: async ({ query, apiClientProps }) => {
|
removeFromPlaylist: async ({ apiClientProps, query }) => {
|
||||||
const res = await ssApiClient(apiClientProps).updatePlaylist({
|
const res = await ssApiClient(apiClientProps).updatePlaylist({
|
||||||
query: {
|
query: {
|
||||||
playlistId: query.id,
|
playlistId: query.id,
|
||||||
|
|
@ -1287,7 +1288,7 @@ export const SubsonicController: ControllerEndpoint = {
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
scrobble: async (args) => {
|
scrobble: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
const res = await ssApiClient(apiClientProps).scrobble({
|
const res = await ssApiClient(apiClientProps).scrobble({
|
||||||
query: {
|
query: {
|
||||||
|
|
@ -1304,7 +1305,7 @@ export const SubsonicController: ControllerEndpoint = {
|
||||||
},
|
},
|
||||||
|
|
||||||
search: async (args) => {
|
search: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
const res = await ssApiClient(apiClientProps).search3({
|
const res = await ssApiClient(apiClientProps).search3({
|
||||||
query: {
|
query: {
|
||||||
|
|
@ -1335,7 +1336,7 @@ export const SubsonicController: ControllerEndpoint = {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
setRating: async (args) => {
|
setRating: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
const itemIds = query.item.map((item) => item.id);
|
const itemIds = query.item.map((item) => item.id);
|
||||||
|
|
||||||
|
|
@ -1351,7 +1352,7 @@ export const SubsonicController: ControllerEndpoint = {
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
updatePlaylist: async (args) => {
|
updatePlaylist: async (args) => {
|
||||||
const { body, query, apiClientProps } = args;
|
const { apiClientProps, body, query } = args;
|
||||||
|
|
||||||
const res = await ssApiClient(apiClientProps).updatePlaylist({
|
const res = await ssApiClient(apiClientProps).updatePlaylist({
|
||||||
query: {
|
query: {
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,17 @@
|
||||||
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 {
|
import {
|
||||||
QueueSong,
|
|
||||||
LibraryItem,
|
|
||||||
AlbumArtist,
|
|
||||||
Album,
|
Album,
|
||||||
|
AlbumArtist,
|
||||||
|
Genre,
|
||||||
|
LibraryItem,
|
||||||
|
Playlist,
|
||||||
|
QueueSong,
|
||||||
|
RelatedArtist,
|
||||||
ServerListItem,
|
ServerListItem,
|
||||||
ServerType,
|
ServerType,
|
||||||
Playlist,
|
|
||||||
Genre,
|
|
||||||
RelatedArtist,
|
|
||||||
} from '/@/renderer/api/types';
|
} from '/@/renderer/api/types';
|
||||||
|
|
||||||
const getCoverArtUrl = (args: {
|
const getCoverArtUrl = (args: {
|
||||||
|
|
@ -37,9 +38,9 @@ const getCoverArtUrl = (args: {
|
||||||
|
|
||||||
const getArtists = (
|
const getArtists = (
|
||||||
item:
|
item:
|
||||||
| z.infer<typeof ssType._response.song>
|
|
||||||
| z.infer<typeof ssType._response.album>
|
| z.infer<typeof ssType._response.album>
|
||||||
| z.infer<typeof ssType._response.albumListEntry>,
|
| z.infer<typeof ssType._response.albumListEntry>
|
||||||
|
| z.infer<typeof ssType._response.song>,
|
||||||
) => {
|
) => {
|
||||||
const albumArtists: RelatedArtist[] = item.albumArtists
|
const albumArtists: RelatedArtist[] = item.albumArtists
|
||||||
? item.albumArtists.map((item) => ({
|
? item.albumArtists.map((item) => ({
|
||||||
|
|
@ -69,7 +70,7 @@ const getArtists = (
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
let participants: Record<string, RelatedArtist[]> | null = null;
|
let participants: null | Record<string, RelatedArtist[]> = null;
|
||||||
|
|
||||||
if (item.contributors) {
|
if (item.contributors) {
|
||||||
participants = {};
|
participants = {};
|
||||||
|
|
@ -98,9 +99,9 @@ const getArtists = (
|
||||||
|
|
||||||
const getGenres = (
|
const getGenres = (
|
||||||
item:
|
item:
|
||||||
| z.infer<typeof ssType._response.song>
|
|
||||||
| z.infer<typeof ssType._response.album>
|
| z.infer<typeof ssType._response.album>
|
||||||
| z.infer<typeof ssType._response.albumListEntry>,
|
| z.infer<typeof ssType._response.albumListEntry>
|
||||||
|
| z.infer<typeof ssType._response.song>,
|
||||||
): Genre[] => {
|
): Genre[] => {
|
||||||
return item.genres
|
return item.genres
|
||||||
? item.genres.map((genre) => ({
|
? item.genres.map((genre) => ({
|
||||||
|
|
@ -123,7 +124,7 @@ const getGenres = (
|
||||||
|
|
||||||
const normalizeSong = (
|
const normalizeSong = (
|
||||||
item: z.infer<typeof ssType._response.song>,
|
item: z.infer<typeof ssType._response.song>,
|
||||||
server: ServerListItem | null,
|
server: null | ServerListItem,
|
||||||
size?: number,
|
size?: number,
|
||||||
): QueueSong => {
|
): QueueSong => {
|
||||||
const imageUrl =
|
const imageUrl =
|
||||||
|
|
@ -194,7 +195,7 @@ const normalizeAlbumArtist = (
|
||||||
item:
|
item:
|
||||||
| z.infer<typeof ssType._response.albumArtist>
|
| z.infer<typeof ssType._response.albumArtist>
|
||||||
| z.infer<typeof ssType._response.artistListEntry>,
|
| z.infer<typeof ssType._response.artistListEntry>,
|
||||||
server: ServerListItem | null,
|
server: null | ServerListItem,
|
||||||
imageSize?: number,
|
imageSize?: number,
|
||||||
): AlbumArtist => {
|
): AlbumArtist => {
|
||||||
const imageUrl =
|
const imageUrl =
|
||||||
|
|
@ -229,7 +230,7 @@ const normalizeAlbumArtist = (
|
||||||
|
|
||||||
const normalizeAlbum = (
|
const normalizeAlbum = (
|
||||||
item: z.infer<typeof ssType._response.album> | z.infer<typeof ssType._response.albumListEntry>,
|
item: z.infer<typeof ssType._response.album> | z.infer<typeof ssType._response.albumListEntry>,
|
||||||
server: ServerListItem | null,
|
server: null | ServerListItem,
|
||||||
imageSize?: number,
|
imageSize?: number,
|
||||||
): Album => {
|
): Album => {
|
||||||
const imageUrl =
|
const imageUrl =
|
||||||
|
|
@ -280,7 +281,7 @@ const normalizePlaylist = (
|
||||||
item:
|
item:
|
||||||
| z.infer<typeof ssType._response.playlist>
|
| z.infer<typeof ssType._response.playlist>
|
||||||
| z.infer<typeof ssType._response.playlistListEntry>,
|
| z.infer<typeof ssType._response.playlistListEntry>,
|
||||||
server: ServerListItem | null,
|
server: null | ServerListItem,
|
||||||
): Playlist => {
|
): Playlist => {
|
||||||
return {
|
return {
|
||||||
description: item.comment || null,
|
description: item.comment || null,
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -3,10 +3,11 @@ import isElectron from 'is-electron';
|
||||||
import semverCoerce from 'semver/functions/coerce';
|
import semverCoerce from 'semver/functions/coerce';
|
||||||
import semverGte from 'semver/functions/gte';
|
import semverGte from 'semver/functions/gte';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { ServerFeature } from '/@/renderer/api/features-types';
|
||||||
|
import { ServerListItem } from '/@/renderer/api/types';
|
||||||
import { toast } from '/@/renderer/components/toast';
|
import { toast } from '/@/renderer/components/toast';
|
||||||
import { useAuthStore } from '/@/renderer/store';
|
import { useAuthStore } from '/@/renderer/store';
|
||||||
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) => {
|
||||||
|
|
@ -29,7 +30,7 @@ export const resultSubsonicBaseResponse = <ItemType extends z.ZodRawShape>(
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const authenticationFailure = (currentServer: ServerListItem | null) => {
|
export const authenticationFailure = (currentServer: null | ServerListItem) => {
|
||||||
toast.error({
|
toast.error({
|
||||||
message: 'Your session has expired.',
|
message: 'Your session has expired.',
|
||||||
});
|
});
|
||||||
|
|
@ -43,7 +44,7 @@ export const authenticationFailure = (currentServer: ServerListItem | null) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hasFeature = (server: ServerListItem | null, feature: ServerFeature): boolean => {
|
export const hasFeature = (server: null | ServerListItem, feature: ServerFeature): boolean => {
|
||||||
if (!server || !server.features) {
|
if (!server || !server.features) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,39 @@
|
||||||
import { useEffect, useMemo, useState, useRef } from 'react';
|
|
||||||
import { ClientSideRowModelModule } from '@ag-grid-community/client-side-row-model';
|
import { ClientSideRowModelModule } from '@ag-grid-community/client-side-row-model';
|
||||||
import { ModuleRegistry } from '@ag-grid-community/core';
|
import { ModuleRegistry } from '@ag-grid-community/core';
|
||||||
import { InfiniteRowModelModule } from '@ag-grid-community/infinite-row-model';
|
import { InfiniteRowModelModule } from '@ag-grid-community/infinite-row-model';
|
||||||
import { MantineProvider } from '@mantine/core';
|
import { MantineProvider } from '@mantine/core';
|
||||||
import isElectron from 'is-electron';
|
import isElectron from 'is-electron';
|
||||||
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { initSimpleImg } from 'react-simple-img';
|
import { initSimpleImg } from 'react-simple-img';
|
||||||
|
|
||||||
|
import i18n from '../i18n/i18n';
|
||||||
import { toast } from './components';
|
import { toast } from './components';
|
||||||
import { useTheme } from './hooks';
|
import { useTheme } from './hooks';
|
||||||
import { IsUpdatedDialog } from './is-updated-dialog';
|
|
||||||
import { AppRouter } from './router/app-router';
|
import { AppRouter } from './router/app-router';
|
||||||
|
import './styles/global.scss';
|
||||||
|
|
||||||
|
import '@ag-grid-community/styles/ag-grid.css';
|
||||||
|
import 'overlayscrollbars/overlayscrollbars.css';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
useCssSettings,
|
||||||
useHotkeySettings,
|
useHotkeySettings,
|
||||||
usePlaybackSettings,
|
usePlaybackSettings,
|
||||||
useRemoteSettings,
|
useRemoteSettings,
|
||||||
useSettingsStore,
|
useSettingsStore,
|
||||||
} from './store/settings.store';
|
} from './store/settings.store';
|
||||||
import './styles/global.scss';
|
|
||||||
import { ContextMenuProvider } from '/@/renderer/features/context-menu';
|
import { ContextMenuProvider } from '/@/renderer/features/context-menu';
|
||||||
import { useHandlePlayQueueAdd } from '/@/renderer/features/player/hooks/use-handle-playqueue-add';
|
|
||||||
import { PlayQueueHandlerContext } from '/@/renderer/features/player';
|
|
||||||
import { getMpvProperties } from '/@/renderer/features/settings/components/playback/mpv-settings';
|
|
||||||
import { PlayerState, useCssSettings, usePlayerStore, useQueueControls } from '/@/renderer/store';
|
|
||||||
import { FontType, PlaybackType, PlayerStatus, WebAudio } from '/@/renderer/types';
|
|
||||||
import '@ag-grid-community/styles/ag-grid.css';
|
|
||||||
import { WebAudioContext } from '/@/renderer/features/player/context/webaudio-context';
|
|
||||||
import { useDiscordRpc } from '/@/renderer/features/discord-rpc/use-discord-rpc';
|
import { useDiscordRpc } from '/@/renderer/features/discord-rpc/use-discord-rpc';
|
||||||
import i18n from '/@/i18n/i18n';
|
import { PlayQueueHandlerContext } from '/@/renderer/features/player';
|
||||||
import { useServerVersion } from '/@/renderer/hooks/use-server-version';
|
import { WebAudioContext } from '/@/renderer/features/player/context/webaudio-context';
|
||||||
|
import { useHandlePlayQueueAdd } from '/@/renderer/features/player/hooks/use-handle-playqueue-add';
|
||||||
import { updateSong } from '/@/renderer/features/player/update-remote-song';
|
import { updateSong } from '/@/renderer/features/player/update-remote-song';
|
||||||
|
import { getMpvProperties } from '/@/renderer/features/settings/components/playback/mpv-settings';
|
||||||
|
import { useServerVersion } from '/@/renderer/hooks/use-server-version';
|
||||||
|
import { IsUpdatedDialog } from '/@/renderer/is-updated-dialog';
|
||||||
|
import { PlayerState, usePlayerStore, useQueueControls } from '/@/renderer/store';
|
||||||
|
import { FontType, PlaybackType, PlayerStatus, WebAudio } from '/@/renderer/types';
|
||||||
import { sanitizeCss } from '/@/renderer/utils/sanitize';
|
import { sanitizeCss } from '/@/renderer/utils/sanitize';
|
||||||
import { setQueue } from '/@/renderer/utils/set-transcoded-queue-data';
|
import { setQueue } from '/@/renderer/utils/set-transcoded-queue-data';
|
||||||
|
|
||||||
|
|
@ -35,10 +41,10 @@ ModuleRegistry.registerModules([ClientSideRowModelModule, InfiniteRowModelModule
|
||||||
|
|
||||||
initSimpleImg({ threshold: 0.05 }, true);
|
initSimpleImg({ threshold: 0.05 }, true);
|
||||||
|
|
||||||
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
const mpvPlayer = isElectron() ? window.api.mpvPlayer : null;
|
||||||
const ipc = isElectron() ? window.electron.ipc : null;
|
const ipc = isElectron() ? window.api.ipc : null;
|
||||||
const remote = isElectron() ? window.electron.remote : null;
|
const remote = isElectron() ? window.api.remote : null;
|
||||||
const utils = isElectron() ? window.electron.utils : null;
|
const utils = isElectron() ? window.api.utils : null;
|
||||||
|
|
||||||
export const App = () => {
|
export const App = () => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
@ -46,7 +52,7 @@ export const App = () => {
|
||||||
const language = useSettingsStore((store) => store.general.language);
|
const language = useSettingsStore((store) => store.general.language);
|
||||||
const nativeImageAspect = useSettingsStore((store) => store.general.nativeAspectRatio);
|
const nativeImageAspect = useSettingsStore((store) => store.general.nativeAspectRatio);
|
||||||
const { builtIn, custom, system, type } = useSettingsStore((state) => state.font);
|
const { builtIn, custom, system, type } = useSettingsStore((state) => state.font);
|
||||||
const { enabled, content } = useCssSettings();
|
const { content, enabled } = useCssSettings();
|
||||||
const { type: playbackType } = usePlaybackSettings();
|
const { type: playbackType } = usePlaybackSettings();
|
||||||
const { bindings } = useHotkeySettings();
|
const { bindings } = useHotkeySettings();
|
||||||
const handlePlayQueueAdd = useHandlePlayQueueAdd();
|
const handlePlayQueueAdd = useHandlePlayQueueAdd();
|
||||||
|
|
@ -230,10 +236,8 @@ export const App = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MantineProvider
|
<MantineProvider
|
||||||
withGlobalStyles
|
|
||||||
withNormalizeCSS
|
|
||||||
theme={{
|
theme={{
|
||||||
colorScheme: theme as 'light' | 'dark',
|
colorScheme: theme as 'dark' | 'light',
|
||||||
components: {
|
components: {
|
||||||
Modal: {
|
Modal: {
|
||||||
styles: {
|
styles: {
|
||||||
|
|
@ -282,6 +286,8 @@ export const App = () => {
|
||||||
xs: '0rem',
|
xs: '0rem',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
withGlobalStyles
|
||||||
|
withNormalizeCSS
|
||||||
>
|
>
|
||||||
<PlayQueueHandlerContext.Provider value={providerValue}>
|
<PlayQueueHandlerContext.Provider value={providerValue}>
|
||||||
<ContextMenuProvider>
|
<ContextMenuProvider>
|
||||||
|
|
|
||||||
31
src/renderer/assets/assets.d.ts
vendored
Normal file
31
src/renderer/assets/assets.d.ts
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
type Styles = Record<string, string>;
|
||||||
|
|
||||||
|
declare module '*.svg' {
|
||||||
|
const content: string;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.png' {
|
||||||
|
const content: string;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.jpg' {
|
||||||
|
const content: string;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.scss' {
|
||||||
|
const content: Styles;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.sass' {
|
||||||
|
const content: Styles;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.css' {
|
||||||
|
const content: Styles;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
10
src/renderer/assets/entitlements.mac.plist
Normal file
10
src/renderer/assets/entitlements.mac.plist
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.allow-jit</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue