Compare commits

..

114 Commits

Author SHA1 Message Date
jeffvli 7d29a692ef remove unused import 2025-06-24 22:34:06 -07:00
jeffvli 3f9eb446f7 update to v0.15.1 2025-06-24 22:27:12 -07:00
jeffvli d8f7b49ab6 increase size of play button icon 2025-06-24 22:22:15 -07:00
jeffvli 35e70a3eff fix synchronized lyric styles not applying 2025-06-24 22:20:26 -07:00
jeffvli ef9c16e940 attempt fix on docker build 2025-06-24 22:16:16 -07:00
Kendall Garner 0b39c35132 make item modal links have heavier font width 2025-06-24 21:39:47 -07:00
Kendall Garner 9f5b4e5410 remove unused length in visualizer 2025-06-24 21:20:41 -07:00
jeffvli dbf840b185 fix actionbar not growing to width of container 2025-06-24 20:33:50 -07:00
jeffvli e0f0524eb9 adjust button styles on playerbar 2025-06-24 20:31:33 -07:00
jeffvli 8598313d12 fix styling on web titlebar style 2025-06-24 20:14:15 -07:00
jeffvli c84dd648ea various clean up and fixes 2025-06-24 18:43:37 -07:00
jeffvli 01885c1a9b decrease spacing on playerbar details 2025-06-24 18:38:10 -07:00
jeffvli 5121f57171 use native img for sidebar image 2025-06-24 18:38:10 -07:00
jeffvli 3d7ee10328 add standalone fast-average-color function 2025-06-24 18:38:10 -07:00
jeffvli 4db47b4d37 switch image loading to lazy by default 2025-06-24 18:38:10 -07:00
jeffvli 786a693526 add animation presets 2025-06-24 18:38:10 -07:00
jeffvli 1faef6a1a7 fix unused var on visualizer 2025-06-24 18:38:10 -07:00
jeffvli 1598642389 re-add page fade in 2025-06-24 18:38:10 -07:00
Kendall Garner 8c4a7f4f91 only show lastfm/listenbrainz if configured 2025-06-24 17:58:43 -07:00
jeffvli 5878f89339 set sidebar items open by default 2025-06-24 15:04:22 -07:00
jeffvli 4acbb1820d set fullscreen player badges to transparent 2025-06-24 14:52:40 -07:00
jeffvli 01f5745629 update visualizer sizing and z-index 2025-06-24 14:52:27 -07:00
jeffvli 73dd781a88 fix regression on image blur in fullscreen player 2025-06-24 14:47:50 -07:00
jeffvli d777be6251 increase minRows on custom css input 2025-06-24 14:42:16 -07:00
jeffvli 6689e84f67 fix and update remote design 2025-06-24 14:36:14 -07:00
jeffvli ad533a1d9c update to v0.15.0 2025-06-24 00:07:51 -07:00
Hosted Weblate b691891e62 Translated using Weblate (Finnish)
Currently translated at 100.0% (668 of 668 strings)

Co-authored-by: jonoafi <joona@jonottaa.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/fi/
Translation: feishin/Translation
2025-06-24 09:06:37 +02:00
Hosted Weblate 53fa265af9 Translated using Weblate (Spanish)
Currently translated at 100.0% (668 of 668 strings)

Co-authored-by: Fordas <fordas15@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/es/
Translation: feishin/Translation
2025-06-24 09:06:36 +02:00
Hosted Weblate 55f6a382d4 Translated using Weblate (Czech)
Currently translated at 100.0% (668 of 668 strings)

Co-authored-by: Fjuro <git@alius.cz>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/cs/
Translation: feishin/Translation
2025-06-24 09:06:36 +02:00
Hosted Weblate 9c30acdd56 Translated using Weblate (Portuguese (Brazil))
Currently translated at 62.1% (414 of 666 strings)

Co-authored-by: Brunno Hofmann <brunno.hofmann@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/pt_BR/
Translation: feishin/Translation
2025-06-24 09:06:35 +02:00
jeffvli b29d3e7f78 disable netease translation by default 2025-06-24 00:06:19 -07:00
Jeff c1330d92b2 Migrate to Mantine v8 and Design Changes (#961)
* mantine v8 migration

* various design changes and improvements
2025-06-24 00:04:36 -07:00
Benjamin bea55d48a8 discord rpc changes (#958) 2025-06-21 12:38:06 -07:00
et21ff ae41fe99bb lyrics: add translation lyrics for netease.ts (#951)
* lyrics: add translation lyrics for netease.ts
2025-06-21 12:19:23 -07:00
Pyx e3751229b6 update readme because subsonic is supported now (#960)
* Update README.md
2025-06-20 18:53:44 -07:00
Kendall Garner 87c9963354 fix subsonic album artist and album list count 2025-06-20 18:35:11 -07:00
Kendall Garner b7fb7c7f94 improve library header loading 2025-06-20 17:57:15 -07:00
Hosted Weblate b8ceb174b3 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (666 of 666 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: 無情天 <kofzhanganguo@126.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/zh_Hans/
Translation: feishin/Translation
2025-06-11 02:38:13 +02:00
Hosted Weblate 48f085b0ac Translated using Weblate (Finnish)
Currently translated at 100.0% (666 of 666 strings)

Co-authored-by: jonoafi <joona@jonottaa.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/fi/
Translation: feishin/Translation
2025-06-11 02:38:13 +02:00
Hosted Weblate dfc0639f95 Translated using Weblate (French)
Currently translated at 100.0% (666 of 666 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: KosmoMoustache <kosmomoustache@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/fr/
Translation: feishin/Translation
2025-06-11 02:38:13 +02:00
Hosted Weblate 80ffd1a925 Translated using Weblate (Spanish)
Currently translated at 100.0% (666 of 666 strings)

Co-authored-by: Fordas <fordas15@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/es/
Translation: feishin/Translation
2025-06-11 02:38:13 +02:00
Hosted Weblate c87bb65023 Translated using Weblate (Czech)
Currently translated at 100.0% (666 of 666 strings)

Co-authored-by: Fjuro <git@alius.cz>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/cs/
Translation: feishin/Translation
2025-06-11 02:38:13 +02:00
jeffvli 5ae21bd224 fix icon alignment for context menu items 2025-06-10 17:37:43 -07:00
jeffvli 12d293a74c remove trailing space 2025-06-10 17:34:00 -07:00
et21ff 62f4bb0d7b fix(player): Improve MPV stability and seek performance (#953) 2025-06-10 17:22:40 -07:00
Pyx 9f11061433 disable visualizer background (#949)
* disable visualizer background
2025-06-09 18:14:59 -07:00
Hans Yulian aba64b10d0 Feature: Shuffle Button (#941) 2025-06-09 02:02:03 -07:00
jeffvli c20e30e387 include sourcemap in vite build 2025-06-09 01:28:27 -07:00
jeffvli c4b4300845 fix lyrics offset type conversion (#948) 2025-06-09 01:28:27 -07:00
Kendall Garner f1e5ed41bc also gate by external link 2025-06-07 20:54:23 -07:00
Kendall Garner 9b79502022 config option for listenbrainz/lastfm links 2025-06-07 20:36:41 -07:00
jeffvli 636c227a83 replace and fix position of current track play icon 2025-06-03 01:05:19 -07:00
jeffvli e8a94a0b1c bump to node 23 image 2025-06-02 21:39:12 -07:00
jeffvli fa93dfd771 add pnpm install to alpine image 2025-06-02 21:37:27 -07:00
jeffvli 8629994eb6 fix docker build issues
- pnpm-lock instead of package-lock
- fix build out directory
2025-06-02 21:32:00 -07:00
jeffvli c54423a667 fix wrong lockfile copy 2025-06-02 21:30:57 -07:00
jeffvli d28fc9f630 allow workflow_dispath on docker deploy 2025-06-02 21:23:07 -07:00
jeffvli bd2b39fdfb re-add ng.conf.template file 2025-06-02 21:20:57 -07:00
jeffvli 2912cd72ef fix build artifactName to include arch 2025-06-02 20:49:55 -07:00
jeffvli 9d31d952b7 update to v0.14.0 2025-06-02 20:34:30 -07:00
jeffvli 8f692b6f4d call remote shutdown on app quit 2025-06-02 20:32:05 -07:00
jeffvli 7562c619d2 fix mpv path save dialog (#930) (#940) 2025-06-02 20:17:55 -07:00
Kendall Garner 6b91ee4a25 fix album genre filter 2025-06-02 19:38:39 -07:00
Kendall Garner 9e689468f9 info for playlists, show id, fix playlist duration 2025-06-02 00:26:36 -07:00
Kendall Garner 1cda4363ef add lockfile 2025-05-28 22:00:26 -07:00
Kendall Garner b6941df7a7 fix editorconfig and downgrade react player back to lazy 2025-05-28 21:57:47 -07:00
jeffvli 608b322f9e remove maintenance notice 2025-05-28 14:49:32 -07:00
jeffvli daee582e92 fix web player playback
- add missing forwardRef on AudioPlayer component
- bump react-player to latest
2025-05-28 10:39:25 -07:00
jeffvli 6525a8a725 fix remote dev path 2025-05-27 18:28:52 -07:00
jeffvli ee1896c345 fix version number 2025-05-27 01:47:18 -07:00
Jeff ec625c2c65 Merge pull request #933 from jeffvli/experimental/vite
Migrate from Webpack to Vite
2025-05-26 18:10:48 -07:00
jeffvli 52e4423cf1 update readme with new pnpm scripts 2025-05-26 17:20:45 -07:00
jeffvli a9f7e808cb update the docker build for pnpm 2025-05-26 17:20:45 -07:00
jeffvli 9c26187b45 fixes for pnpm v10 2025-05-26 17:20:45 -07:00
jeffvli 90afa11f20 set custom userData path for dev 2025-05-26 17:20:45 -07:00
jeffvli 6463ea937b add vite build for web 2025-05-26 17:20:45 -07:00
jeffvli ac682428e6 various cleanup 2025-05-26 17:20:45 -07:00
jeffvli 48917547b2 add vite build for remote 2025-05-26 17:20:45 -07:00
jeffvli 74554d9725 use lowercase in package productName 2025-05-26 17:20:45 -07:00
jeffvli 0d42a6ea49 update workflows for new build 2025-05-26 17:20:45 -07:00
jeffvli faadff0211 migrate to pnpm 2025-05-26 17:20:44 -07:00
jeffvli 91715ebf7d change build dir 2025-05-26 17:20:28 -07:00
jeffvli 1808f160b4 clean up dependencies 2025-05-26 17:20:28 -07:00
jeffvli e10a92a5c2 add legacy-peer-deps default 2025-05-26 17:20:02 -07:00
jeffvli 930165d006 fix all imports for new structure 2025-05-26 17:20:02 -07:00
jeffvli 249eaf89f8 update linter rules 2025-05-26 17:18:56 -07:00
jeffvli bf1cddae9d remove unusued paths from tsconfig node 2025-05-26 17:18:56 -07:00
jeffvli 9db2e51d2d reorganize global types to shared directory 2025-05-26 17:18:56 -07:00
jeffvli 26c02e03c5 update package.json and electron builder 2025-05-26 17:18:56 -07:00
jeffvli 1cf587bc8f restructure files onto electron-vite boilerplate 2025-05-26 17:18:55 -07:00
Hosted Weblate 91ce2cd8a1 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (662 of 662 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: 無情天 <kofzhanganguo@126.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/zh_Hans/
Translation: feishin/Translation
2025-05-26 11:11:24 +02:00
Hosted Weblate 4f61e82068 Translated using Weblate (Finnish)
Currently translated at 100.0% (659 of 659 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: jonoafi <joona@jonottaa.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/fi/
Translation: feishin/Translation
2025-05-26 11:11:24 +02:00
Hosted Weblate 1e6673fabd Translated using Weblate (French)
Currently translated at 100.0% (662 of 662 strings)

Co-authored-by: KosmoMoustache <hosted.weblate.org@kosmo.ovh>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/fr/
Translation: feishin/Translation
2025-05-26 11:11:24 +02:00
Hosted Weblate 02951c92af Translated using Weblate (Spanish)
Currently translated at 100.0% (662 of 662 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (659 of 659 strings)

Co-authored-by: Fordas <fordas15@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/es/
Translation: feishin/Translation
2025-05-26 11:11:24 +02:00
Hosted Weblate 05f8fb3114 Translated using Weblate (Czech)
Currently translated at 100.0% (662 of 662 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (659 of 659 strings)

Co-authored-by: Fjuro <git@alius.cz>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/cs/
Translation: feishin/Translation
2025-05-26 11:11:24 +02:00
jeffvli 169da10c1b fix release version 2025-05-26 02:11:14 -07:00
jeffvli 5a79fee77e use missing UTC transform on navidrome dates (#928) 2025-05-26 02:08:08 -07:00
jeffvli 7ef80f14b0 fix casing on filtered list route titels (#929) 2025-05-26 01:58:16 -07:00
jeffvli 36cc37e39f update to v0.13.0 2025-05-26 01:47:13 -07:00
Kendall Garner d4e7c6bd18 fix copypasta 2025-05-20 08:40:24 -07:00
Kendall Garner 90f79b4ae7 add multiselect with invalid data handling to jellyfin album 2025-05-18 18:27:17 -07:00
Kendall Garner cf74625bfc warn if a value in select no longer exists 2025-05-18 10:59:45 -07:00
Kendall Garner f068d6e4b8 actually add type to query key 2025-05-18 09:29:13 -07:00
Kendall Garner e1aa8d74f3 Tag filter support
- Jellyfin: Uses `/items/filters` to get list of boolean tags. Notably, does not use this same filter for genres. Separate filter for song/album
- Navidrome: Uses `/api/tags`, which appears to be album-level as multiple independent selects. Same filter for song/album
2025-05-18 09:23:52 -07:00
Kendall Garner b0d86ee5c9 Support tags, and better participants for servers
- Parses `tags` for Navidrome (mapping string: string[])
- Parses `Tags` (and fetches for it) for Jellyfin (map a string to empty, and display as a bool)
- Clean parsing of participants for Navidrome/Subsonic
- Only show `People` for Jellyfin, not clickable
2025-05-17 21:35:58 -07:00
Kendall Garner 89e27ec6ff remove console.log 2025-05-16 11:50:26 -07:00
Kendall Garner 39c714a137 navidrome cover art workaround 2025-05-15 19:10:15 -07:00
Kendall Garner a8fb7ff11e fullscreen header image on click 2025-05-14 08:25:02 -07:00
jeffvli 9b95f47a91 update to v0.12.7 2025-05-12 18:27:37 -07:00
Hosted Weblate 2267e9bc9d Translated using Weblate (Czech)
Currently translated at 100.0% (657 of 657 strings)

Co-authored-by: Fjuro <fjuro@alius.cz>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/cs/
Translation: feishin/Translation
2025-05-13 03:24:48 +02:00
Kendall Garner 089311c673 add migrate from v8 (#925) 2025-05-12 18:24:42 -07:00
Kendall Garner 773f349b66 don't show song count if not present for home carousel 2025-05-09 19:08:36 -07:00
Kendall Garner 3980c8ea97 save the package-logk.json changes as well 2025-05-08 08:23:58 -07:00
Kendall Garner 257a5ceef0 force xmljs to 0.5.0 2025-05-08 08:08:31 -07:00
706 changed files with 29845 additions and 61886 deletions
+2 -5
View File
@@ -1,12 +1,9 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
trim_trailing_whitespace = true
-7
View File
@@ -1,7 +0,0 @@
{
"rules": {
"no-console": "off",
"global-require": "off",
"import/no-dynamic-require": "off"
}
}
-64
View File
@@ -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;
-3
View File
@@ -1,3 +0,0 @@
/* eslint import/no-unresolved: off, import/no-self-import: off */
module.exports = require('./webpack.config.renderer.dev').default;
-84
View File
@@ -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);
-127
View File
@@ -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);
-142
View File
@@ -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);
-201
View File
@@ -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);
-147
View File
@@ -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);
-138
View File
@@ -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);
-46
View File
@@ -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
View File
@@ -1 +0,0 @@
export default 'test-file-stub';
-8
View File
@@ -1,8 +0,0 @@
{
"rules": {
"no-console": "off",
"global-require": "off",
"import/no-dynamic-require": "off",
"import/no-extraneous-dependencies": "off"
}
}
-33
View File
@@ -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"',
),
);
}
-54
View File
@@ -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');
}
}
-16
View File
@@ -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);
}
}
-16
View File
@@ -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);
}
});
-17
View File
@@ -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);
}
});
-9
View File
@@ -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'));
}
-20
View File
@@ -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',
});
}
-9
View File
@@ -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');
}
-30
View File
@@ -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,
});
};
-34
View File
@@ -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
View File
@@ -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'),
},
},
},
};
+4 -3
View File
@@ -3,6 +3,7 @@ name: Publish Docker to GHCR
permissions: write-all
on:
workflow_dispatch:
push:
tags:
- 'v*.*.*'
@@ -49,6 +50,6 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: |
linux/amd64
linux/arm/v7
linux/arm64/v8
linux/amd64
linux/arm/v7
linux/arm64/v8
+4 -6
View File
@@ -24,11 +24,9 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=latest,enable={{is_default_branch}}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Setup Docker buildx
@@ -41,6 +39,6 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: |
linux/amd64
linux/arm/v7
linux/arm64/v8
linux/amd64
linux/arm/v7
linux/arm64/v8
+12 -16
View File
@@ -14,17 +14,15 @@ jobs:
- name: Checkout git repo
uses: actions/checkout@v1
- name: Install Node and NPM
uses: actions/setup-node@v1
- name: Install Node and PNPM
uses: pnpm/action-setup@v4
with:
node-version: 18
cache: npm
version: 9
- name: Install dependencies
run: |
npm install --legacy-peer-deps
run: pnpm install
- name: Publish releases
- name: Build and Publish releases
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: nick-invision/retry@v2.8.2
@@ -33,12 +31,11 @@ jobs:
max_attempts: 3
retry_on: error
command: |
npm run postinstall
npm run build
npm exec electron-builder -- --publish always --linux
on_retry_command: npm cache clean --force
pnpm run package:linux
pnpm run publish:linux
on_retry_command: pnpm cache delete
- name: Publish releases (arm64)
- name: Build and Publish releases (arm64)
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: nick-invision/retry@v2.8.2
@@ -47,7 +44,6 @@ jobs:
max_attempts: 3
retry_on: error
command: |
npm run postinstall
npm run build
npm exec electron-builder -- --publish always --arm64
on_retry_command: npm cache clean --force
pnpm run package:linux-arm64
pnpm run publish:linux-arm64
on_retry_command: pnpm cache delete
+8 -11
View File
@@ -14,17 +14,15 @@ jobs:
- name: Checkout git repo
uses: actions/checkout@v1
- name: Install Node and NPM
uses: actions/setup-node@v1
- name: Install Node and PNPM
uses: pnpm/action-setup@v4
with:
node-version: 18
cache: npm
version: 9
- name: Install dependencies
run: |
npm install --legacy-peer-deps
run: pnpm install
- name: Publish releases
- name: Build and Publish releases
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: nick-invision/retry@v2.8.2
@@ -33,7 +31,6 @@ jobs:
max_attempts: 3
retry_on: error
command: |
npm run postinstall
npm run build
npm exec electron-builder -- --publish always --mac
on_retry_command: npm cache clean --force
pnpm run package:mac
pnpm run publish:mac
on_retry_command: pnpm cache delete
+13 -15
View File
@@ -17,15 +17,13 @@ jobs:
- name: Checkout git repo
uses: actions/checkout@v3
- name: Install Node and NPM
uses: actions/setup-node@v3
- name: Install Node and PNPM
uses: pnpm/action-setup@v4
with:
node-version: 18
cache: npm
version: 9
- name: Install dependencies
run: |
npm install --legacy-peer-deps
run: pnpm install
- name: Build for Windows
if: ${{ matrix.os == 'windows-latest' }}
@@ -35,7 +33,7 @@ jobs:
max_attempts: 3
retry_on: error
command: |
npm run package:pr:windows
pnpm run package:win:pr
- name: Build for Linux
if: ${{ matrix.os == 'ubuntu-latest' }}
@@ -45,7 +43,7 @@ jobs:
max_attempts: 3
retry_on: error
command: |
npm run package:pr:linux
pnpm run package:linux:pr
- name: Build for MacOS
if: ${{ matrix.os == 'macos-latest' }}
@@ -55,41 +53,41 @@ jobs:
max_attempts: 3
retry_on: error
command: |
npm run package:pr:macos
pnpm run package:mac:pr
- name: Zip Windows Binaries
if: ${{ matrix.os == 'windows-latest' }}
shell: pwsh
run: |
Compress-Archive -Path "release/build/*.exe" -DestinationPath "release/build/windows-binaries.zip" -Force
Compress-Archive -Path "dist/*.exe" -DestinationPath "dist/windows-binaries.zip" -Force
- name: Zip Linux Binaries
if: ${{ matrix.os == 'ubuntu-latest' }}
run: |
zip -r release/build/linux-binaries.zip release/build/*.{AppImage,deb,rpm}
zip -r dist/linux-binaries.zip dist/*.{AppImage,deb,rpm}
- name: Zip MacOS Binaries
if: ${{ matrix.os == 'macos-latest' }}
run: |
zip -r release/build/macos-binaries.zip release/build/*.dmg
zip -r dist/macos-binaries.zip dist/*.dmg
- name: Upload Windows Binaries
if: ${{ matrix.os == 'windows-latest' }}
uses: actions/upload-artifact@v4
with:
name: windows-binaries
path: release/build/windows-binaries.zip
path: dist/windows-binaries.zip
- name: Upload Linux Binaries
if: ${{ matrix.os == 'ubuntu-latest' }}
uses: actions/upload-artifact@v4
with:
name: linux-binaries
path: release/build/linux-binaries.zip
path: dist/linux-binaries.zip
- name: Upload MacOS Binaries
if: ${{ matrix.os == 'macos-latest' }}
uses: actions/upload-artifact@v4
with:
name: macos-binaries
path: release/build/macos-binaries.zip
path: dist/macos-binaries.zip
+8 -11
View File
@@ -14,17 +14,15 @@ jobs:
- name: Checkout git repo
uses: actions/checkout@v1
- name: Install Node and NPM
uses: actions/setup-node@v1
- name: Install Node and PNPM
uses: pnpm/action-setup@v4
with:
node-version: 18
cache: npm
version: 9
- name: Install dependencies
run: |
npm install --legacy-peer-deps
run: pnpm install
- name: Publish releases
- name: Build and Publish releases
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: nick-invision/retry@v2.8.2
@@ -33,7 +31,6 @@ jobs:
max_attempts: 3
retry_on: error
command: |
npm run postinstall
npm run build
npm exec electron-builder -- --publish always --win
on_retry_command: npm cache clean --force
pnpm run package:win
pnpm run publish:win
on_retry_command: pnpm cache delete
+7 -15
View File
@@ -14,21 +14,13 @@ jobs:
- name: Check out Git repository
uses: actions/checkout@v1
- name: Install Node.js and NPM
uses: actions/setup-node@v2
- name: Install Node.js and PNPM
uses: pnpm/action-setup@v4
with:
node-version: 16
cache: npm
version: 9
- name: npm install
run: |
npm install --legacy-peer-deps
- name: Install dependencies
run: pnpm install
- name: npm test
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
npm run lint
npm run package
npm exec tsc
npm test
- name: Lint Files
run: pnpm run lint
+5 -29
View File
@@ -1,31 +1,7 @@
# 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
dist
out
.DS_Store
release/app/dist
release/build
.erb/dll
.idea
npm-debug.log.*
*.css.d.ts
*.sass.d.ts
*.scss.d.ts
.env*
.eslintcache
*.log*
release
-4
View File
@@ -1,4 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged
+2
View File
@@ -0,0 +1,2 @@
legacy-peer-deps=true
only-built-dependencies=electron,esbuild
+6
View File
@@ -0,0 +1,6 @@
out
dist
pnpm-lock.yaml
LICENSE.md
tsconfig.json
tsconfig.*.json
-22
View File
@@ -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
}
+14
View File
@@ -0,0 +1,14 @@
singleQuote: true
semi: true
printWidth: 100
tabWidth: 4
trailingComma: all
useTabs: false
arrowParens: always
proseWrap: never
htmlWhitespaceSensitivity: strict
endOfLine: lf
singleAttributePerLine: true
bracketSpacing: true
plugins:
- prettier-plugin-packagejson
+9 -7
View File
@@ -1,17 +1,19 @@
{
"customSyntax": "postcss-styled-syntax",
"extends": [
"stylelint-config-standard",
"stylelint-config-styled-components",
"stylelint-config-css-modules",
"stylelint-config-recess-order"
],
"rules": {
"declaration-empty-line-before": null,
"declaration-block-no-redundant-longhand-properties": null,
"selector-class-pattern": null,
"block-no-empty": null,
"selector-type-case": ["lower", { "ignoreTypes": ["/^\\$\\w+/"] }],
"selector-type-no-unknown": [true, { "ignoreTypes": ["/-styled-mixin/", "/^\\$\\w+/"] }],
"declaration-colon-newline-after": null,
"property-no-vendor-prefix": null
"declaration-block-no-shorthand-property-overrides": null,
"declaration-block-no-redundant-longhand-properties": null,
"at-rule-no-unknown": [true, { "ignoreAtRules": ["mixin"] }],
"function-no-unknown": [true, { "ignoreFunctions": ["darken", "alpha", "lighten"] }],
"declaration-property-value-no-unknown": null,
"no-descending-specificity": null,
"no-empty-source": null
}
}
+1 -8
View File
@@ -1,10 +1,3 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
"EditorConfig.EditorConfig",
"stylelint.vscode-stylelint",
"esbenp.prettier-vscode",
"clinyong.vscode-css-modules",
"Huuums.vscode-fast-folder-structure"
]
"recommendations": ["dbaeumer.vscode-eslint"]
}
+37 -26
View File
@@ -1,28 +1,39 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Electron: Main",
"type": "node",
"request": "launch",
"protocol": "inspector",
"runtimeExecutable": "npm",
"runtimeArgs": ["run start:main --inspect=5858 --remote-debugging-port=9223"],
"preLaunchTask": "Start Webpack Dev"
},
{
"name": "Electron: Renderer",
"type": "chrome",
"request": "attach",
"port": 9223,
"webRoot": "${workspaceFolder}",
"timeout": 15000
}
],
"compounds": [
{
"name": "Electron: All",
"configurations": ["Electron: Main", "Electron: Renderer"]
}
]
"version": "0.2.0",
"configurations": [
{
"name": "Debug Main Process",
"type": "node",
"request": "launch",
"cwd": "${workspaceRoot}",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite",
"windows": {
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd"
},
"runtimeArgs": ["--sourcemap"],
"env": {
"REMOTE_DEBUGGING_PORT": "9222"
}
},
{
"name": "Debug Renderer Process",
"port": 9222,
"request": "attach",
"type": "chrome",
"webRoot": "${workspaceFolder}/src/renderer",
"timeout": 60000,
"presentation": {
"hidden": true
}
}
],
"compounds": [
{
"name": "Debug All",
"configurations": ["Debug Main Process", "Debug Renderer Process"],
"presentation": {
"order": 1
}
}
]
}
+22 -10
View File
@@ -1,4 +1,13 @@
{
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"files.associations": {
".eslintrc": "jsonc",
".prettierrc": "jsonc",
@@ -9,7 +18,7 @@
{ "directory": "./", "changeProcessCWD": true },
{ "directory": "./server", "changeProcessCWD": true }
],
"typescript.tsserver.experimental.enableProjectDiagnostics": true,
"typescript.tsserver.experimental.enableProjectDiagnostics": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.fixAll.stylelint": "explicit",
@@ -17,10 +26,6 @@
"source.formatDocument": "explicit"
},
"css.validate": true,
"less.validate": false,
"scss.validate": true,
"scss.lint.unknownAtRules": "warning",
"scss.lint.unknownProperties": "warning",
"javascript.validate.enable": false,
"javascript.format.enable": false,
"typescript.format.enable": false,
@@ -33,14 +38,21 @@
"npm-debug.log.*": true,
"test/**/__snapshots__": true,
"package-lock.json": true,
"*.{css,sass,scss}.d.ts": true
"*.{css,sass,scss}.d.ts": true,
"out/**/*": true,
"dist/**/*": true
},
"i18n-ally.localesPaths": ["src/i18n", "src/i18n/locales"],
"typescript.tsdk": "node_modules\\typescript\\lib",
"typescript.preferences.importModuleSpecifier": "non-relative",
"stylelint.validate": ["css", "scss", "typescript", "typescriptreact"],
"stylelint.config": null,
"stylelint.validate": ["css", "postcss"],
"typescript.updateImportsOnFileMove.enabled": "always",
"[typescript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
"typescript.preferences.autoImportFileExcludePatterns": [
"@mantine/core",
"@mantine/modals",
"@mantine/dates"
],
"[typescriptreact]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
"typescript.format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": true,
"folderTemplates.structures": [
@@ -53,14 +65,14 @@
"template": "Functional Component with CSS Modules"
},
{
"fileName": "<FTName | kebabcase>.module.scss"
"fileName": "<FTName | kebabcase>.module.css"
}
]
}
],
"folderTemplates.fileTemplates": {
"Functional Component with CSS Modules": [
"import styles from './<FTName | kebabcase>.module.scss';",
"import styles from './<FTName | kebabcase>.module.css';",
"",
"interface <FTName | pascalcase>Props {}",
"",
-25
View File
@@ -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)\\.$"
}
}
}
]
}
+11 -8
View File
@@ -1,19 +1,22 @@
# --- Builder stage
FROM node:18-alpine as builder
FROM node:23-alpine as builder
WORKDIR /app
#Copy package.json first to cache node_modules
COPY package.json package-lock.json .
# Scripts include electron-specific dependencies, which we don't need
RUN npm install --legacy-peer-deps --ignore-scripts
#Copy code and build with cached modules
# Copy package.json first to cache node_modules
COPY package.json pnpm-lock.yaml .
RUN npm install -g pnpm
RUN pnpm install
# Copy code and build with cached modules
COPY . .
RUN npm run build:web
RUN pnpm run build:web
# --- Production stage
FROM nginx:alpine-slim
COPY --chown=nginx:nginx --from=builder /app/release/app/dist/web /usr/share/nginx/html
COPY --chown=nginx:nginx --from=builder /app/out/web /usr/share/nginx/html
COPY ./settings.js.template /etc/nginx/templates/settings.js.template
COPY ng.conf.template /etc/nginx/templates/default.conf.template
+47 -32
View File
@@ -29,25 +29,17 @@
---
## MAINTENANCE NOTICE
Feishin is currently undergoing a major rewrite. New feature requests will not be accepted. The rewrite is being actively developed at the [audioling](https://github.com/audioling/audioling) repository.
Follow the repository or join the discord/matrix server for updates.
---
Rewrite of [Sonixd](https://github.com/jeffvli/sonixd).
## Features
- [x] MPV player backend
- [x] Web player backend
- [x] Modern UI
- [x] Scrobble playback to your server
- [x] Smart playlist editor (Navidrome)
- [x] Synchronized and unsynchronized lyrics support
- [ ] [Request a feature](https://github.com/jeffvli/feishin/issues) or [view taskboard](https://github.com/users/jeffvli/projects/5/views/1)
- [x] MPV player backend
- [x] Web player backend
- [x] Modern UI
- [x] Scrobble playback to your server
- [x] Smart playlist editor (Navidrome)
- [x] Synchronized and unsynchronized lyrics support
- [ ] [Request a feature](https://github.com/jeffvli/feishin/issues) or [view taskboard](https://github.com/users/jeffvli/projects/5/views/1)
## Screenshots
@@ -109,8 +101,8 @@ services:
2. After restarting the app, you will be prompted to select a server. Click the `Open menu` button and select `Manage servers`. Click the `Add server` button in the popup and fill out all applicable details. You will need to enter the full URL to your server, including the protocol and port if applicable (e.g. `https://navidrome.my-server.com` or `http://192.168.0.1:4533`).
- **Navidrome** - For the best experience, select "Save password" when creating the server and configure the `SessionTimeout` setting in your Navidrome config to a larger value (e.g. 72h).
- **Linux users** - The default password store uses `libsecret`. `kwallet4/5/6` are also supported, but must be explicitly set in Settings > Window > Passwords/secret score.
- **Navidrome** - For the best experience, select "Save password" when creating the server and configure the `SessionTimeout` setting in your Navidrome config to a larger value (e.g. 72h).
- **Linux users** - The default password store uses `libsecret`. `kwallet4/5/6` are also supported, but must be explicitly set in Settings > Window > Passwords/secret score.
3. _Optional_ - If you want to host Feishin on a subpath (not `/`), then pass in the following environment variable: `PUBLIC_PATH=PATH`. For example, to host on `/feishin`, pass in `PUBLIC_PATH=/feishin`.
@@ -124,20 +116,20 @@ First thing to do is check that your MPV binary path is correct. Navigate to the
### What music servers does Feishin support?
Feishin supports any music server that implements a [Navidrome](https://www.navidrome.org/) or [Jellyfin](https://jellyfin.org/) API. **Subsonic API is not currently supported**. This will likely be added in [later when the new Subsonic API is decided on](https://support.symfonium.app/t/subsonic-servers-participation/1233).
Feishin supports any music server that implements a [Navidrome](https://www.navidrome.org/), [Jellyfin](https://jellyfin.org/), or [OpenSubsonic compatible](https://opensubsonic.netlify.app/) API.
- [Navidrome](https://github.com/navidrome/navidrome)
- [Jellyfin](https://github.com/jellyfin/jellyfin)
- Subsonic-compatible servers
- [Airsonic-Advanced](https://github.com/airsonic-advanced/airsonic-advanced)
- [Ampache](https://ampache.org)
- [Astiga](https://asti.ga/)
- [Funkwhale](https://www.funkwhale.audio/)
- [Gonic](https://github.com/sentriz/gonic)
- [LMS](https://github.com/epoupon/lms)
- [Nextcloud Music](https://apps.nextcloud.com/apps/music)
- [Supysonic](https://github.com/spl0k/supysonic)
- More (?)
- [Navidrome](https://github.com/navidrome/navidrome)
- [Jellyfin](https://github.com/jellyfin/jellyfin)
- [OpenSubsonic](https://opensubsonic.netlify.app/) compatible servers, such as...
- [Airsonic-Advanced](https://github.com/airsonic-advanced/airsonic-advanced)
- [Ampache](https://ampache.org)
- [Astiga](https://asti.ga/)
- [Funkwhale](https://www.funkwhale.audio/)
- [Gonic](https://github.com/sentriz/gonic)
- [LMS](https://github.com/epoupon/lms)
- [Nextcloud Music](https://apps.nextcloud.com/apps/music)
- [Supysonic](https://github.com/spl0k/supysonic)
- More (?)
### I have the issue "The SUID sandbox helper binary was found, but is not configured correctly" on Linux
@@ -152,9 +144,32 @@ Ubunutu 24.04 specifically introduced breaking changes that affect how namespace
## Development
Built and tested using Node `v16.15.0`.
Built and tested using Node `v23.11.0`.
This project is built off of [electron-react-boilerplate](https://github.com/electron-react-boilerplate/electron-react-boilerplate) v4.6.0.
This project is built off of [electron-vite](https://github.com/alex8088/electron-vite)
- `pnpm run dev` - Start the development server
- `pnpm run dev:watch` - Start the development server in watch mode (for main / preload HMR)
- `pnpm run start` - Starts the app in production preview mode
- `pnpm run build` - Builds the app for desktop
- `pnpm run build:electron` - Build the electron app (main, preload, and renderer)
- `pnpm run build:remote` - Build the remote app (remote)
- `pnpm run build:web` - Build the standalone web app (renderer)
- `pnpm run package` - Package the project
- `pnpm run package:dev` - Package the project for development
- `pnpm run package:linux` - Package the project for Linux
- `pnpm run package:mac` - Package the project for Mac
- `pnpm run package:win` - Package the project for Windows
- `pnpm run publish:linux` - Publish the project for Linux
- `pnpm run publish:linux-arm64` - Publish the project for Linux ARM64
- `pnpm run publish:mac` - Publish the project for Mac
- `pnpm run publish:win` - Publish the project for Windows
- `pnpm run typecheck` - Type check the project
- `pnpm run typecheck:node` - Type check the project with tsconfig.node.json
- `pnpm run typecheck:web` - Type check the project with tsconfig.web.json
- `pnpm run lint` - Lint the project
- `pnpm run lint:fix` - Lint the project and fix linting errors
- `pnpm run i18next` - Generate i18n files
## Translation
+3 -1
View File
@@ -2,9 +2,11 @@
<!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-jit</key>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
</dict>
</plist>
+3
View File
@@ -0,0 +1,3 @@
provider: generic
url: https://example.com/auto-updates
updaterCacheDirName: feishin-updater
+75
View File
@@ -0,0 +1,75 @@
appId: org.jeffvli.feishin
productName: Feishin
artifactName: ${productName}-${version}-${os}-${arch}.${ext}
electronVersion: 35.1.5
directories:
buildResources: assets
files:
- 'out/**/*'
- 'package.json'
extraResources:
- assets/**
asarUnpack:
- resources/**
win:
target:
- zip
- nsis
icon: assets/icons/icon.png
nsis:
shortcutName: ${productName}
uninstallDisplayName: ${productName}
createDesktopShortcut: always
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
notarize: false
dmg:
contents: [{ x: 130, y: 220 }, { x: 410, y: 220, type: link, path: /Applications }]
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
category: AudioVideo;Audio;Player
icon: assets/icons/icon.png
npmRebuild: false
publish:
provider: github
owner: jeffvli
repo: feishin
+66
View File
@@ -0,0 +1,66 @@
import react from '@vitejs/plugin-react';
import { externalizeDepsPlugin, UserConfig } from 'electron-vite';
import { resolve } from 'path';
import conditionalImportPlugin from 'vite-plugin-conditional-import';
import dynamicImportPlugin from 'vite-plugin-dynamic-import';
import { ViteEjsPlugin } from 'vite-plugin-ejs';
const currentOSEnv = process.platform;
const config: UserConfig = {
main: {
build: {
rollupOptions: {
external: ['source-map-support'],
},
sourcemap: true,
},
define: {
'import.meta.env.IS_LINUX': JSON.stringify(currentOSEnv === 'linux'),
'import.meta.env.IS_MACOS': JSON.stringify(currentOSEnv === 'darwin'),
'import.meta.env.IS_WIN': JSON.stringify(currentOSEnv === 'win32'),
},
plugins: [
externalizeDepsPlugin(),
dynamicImportPlugin(),
conditionalImportPlugin({
currentEnv: currentOSEnv,
envs: ['win32', 'linux', 'darwin'],
}),
],
resolve: {
alias: {
'/@/main': resolve('src/main'),
'/@/shared': resolve('src/shared'),
},
},
},
preload: {
plugins: [externalizeDepsPlugin()],
resolve: {
alias: {
'/@/preload': resolve('src/preload'),
'/@/shared': resolve('src/shared'),
},
},
},
renderer: {
css: {
modules: {
generateScopedName: 'fs-[name]-[local]',
localsConvention: 'camelCase',
},
},
plugins: [react(), ViteEjsPlugin({ web: false })],
resolve: {
alias: {
'/@/i18n': resolve('src/i18n'),
'/@/remote': resolve('src/remote'),
'/@/renderer': resolve('src/renderer'),
'/@/shared': resolve('src/shared'),
},
},
},
};
export default config;
+53
View File
@@ -0,0 +1,53 @@
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-duplicate-enum-values': 'off',
'@typescript-eslint/no-explicit-any': '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'],
'react-refresh/only-export-components': 'off',
'react/display-name': 'off',
semi: ['error', 'always'],
'single-attribute-per-line': 'off',
},
},
eslintConfigPrettier,
);
+1 -1
View File
@@ -24,4 +24,4 @@ server {
location ${PUBLIC_PATH}/settings.js {
alias /etc/nginx/conf.d/settings.js;
}
}
}
-40780
View File
File diff suppressed because it is too large Load Diff
+117 -327
View File
@@ -1,297 +1,57 @@
{
"name": "feishin",
"productName": "Feishin",
"description": "Feishin music server",
"version": "0.12.6",
"scripts": {
"build": "concurrently \"npm run build:main\" \"npm run build:renderer\" \"npm run build:remote\"",
"build:main": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.main.prod.ts",
"build:remote": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.remote.prod.ts",
"build:renderer": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.renderer.prod.ts",
"build:web": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.web.prod.ts",
"build:docker": "docker build -t jeffvli/feishin .",
"rebuild": "electron-rebuild --parallel --types prod,dev,optional --module-dir release/app",
"lint": "concurrently \"npm run lint:code\" \"npm run lint:styles\"",
"lint:code": "cross-env NODE_ENV=development eslint . --ext .js,.jsx,.ts,.tsx --fix",
"lint:styles": "npx stylelint **/*.tsx --fix",
"package": "node --import tsx ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never",
"package:pr": "node --import tsx ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never --win --mac --linux",
"package:pr:macos": "node --import tsx ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never --mac",
"package:pr:windows": "node --import tsx ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never --win",
"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"
},
"version": "0.15.1",
"description": "A modern self-hosted music player.",
"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"
]
"bugs": {
"url": "https://github.com/jeffvli/feishin/issues"
},
"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"
"license": "GPL-3.0",
"author": {
"name": "jeffvli",
"url": "https://github.com/jeffvli/"
},
"main": "./out/main/index.js",
"scripts": {
"build": "pnpm run typecheck && pnpm run build:electron && pnpm run build:remote",
"build:electron": "electron-vite build",
"build:remote": "vite build --config remote.vite.config.ts",
"build:web": "vite build --config web.vite.config.ts",
"dev": "electron-vite dev",
"dev:remote": "vite dev --config remote.vite.config.ts",
"dev:watch": "electron-vite dev --watch",
"i18next": "i18next -c src/i18n/i18next-parser.config.js",
"postinstall": "electron-builder install-app-deps",
"lint": "pnpm run lint-code && pnpm run lint-styles",
"lint-code": "eslint --cache .",
"lint-code:fix": "eslint --cache --fix .",
"lint-styles": "stylelint 'src/**/*.{css,scss}'",
"lint-styles:fix": "stylelint 'src/**/*.{css,scss}' --fix",
"lint:fix": "pnpm run lint-code:fix && pnpm run lint-styles:fix",
"package": "pnpm run build && electron-builder",
"package:dev": "pnpm run build && electron-builder --dir",
"package:linux": "pnpm run build && electron-builder --linux",
"package:linux-arm64:pr": "pnpm run build && electron-builder --linux --arm64 --publish never",
"package:linux:pr": "pnpm run build && electron-builder --linux --publish never",
"package:mac": "pnpm run build && electron-builder --mac",
"package:mac:pr": "pnpm run build && electron-builder --mac --publish never",
"package:win": "pnpm run build && electron-builder --win",
"package:win:pr": "pnpm run build && electron-builder --win --publish never",
"publish:linux": "electron-builder --publish always --linux",
"publish:linux-arm64": "electron-builder --publish always --linux --arm64",
"publish:mac": "electron-builder --publish always --mac",
"publish:win": "electron-builder --publish always --win",
"start": "electron-vite preview",
"typecheck": "pnpm run typecheck:node && pnpm run typecheck:web",
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false"
},
"dependencies": {
"@ag-grid-community/client-side-row-model": "^28.2.1",
@@ -299,14 +59,18 @@
"@ag-grid-community/infinite-row-model": "^28.2.1",
"@ag-grid-community/react": "^28.2.1",
"@ag-grid-community/styles": "^28.2.1",
"@emotion/react": "^11.10.4",
"@mantine/core": "^6.0.17",
"@mantine/dates": "^6.0.17",
"@mantine/form": "^6.0.17",
"@mantine/hooks": "^6.0.17",
"@mantine/modals": "^6.0.17",
"@mantine/notifications": "^6.0.17",
"@mantine/utils": "^6.0.17",
"@atlaskit/pragmatic-drag-and-drop": "1.4.0",
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^2.1.0",
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
"@electron-toolkit/preload": "^3.0.1",
"@electron-toolkit/utils": "^4.0.0",
"@mantine/colors-generator": "^8.1.1",
"@mantine/core": "^8.1.1",
"@mantine/dates": "^8.1.1",
"@mantine/form": "^8.1.1",
"@mantine/hooks": "^8.1.1",
"@mantine/modals": "^8.1.1",
"@mantine/notifications": "^8.1.1",
"@tanstack/react-query": "^4.32.1",
"@tanstack/react-query-devtools": "^4.32.1",
"@tanstack/react-query-persist-client": "^4.32.1",
@@ -315,6 +79,7 @@
"audiomotion-analyzer": "^4.5.0",
"auto-text-size": "^0.2.3",
"axios": "^1.6.0",
"cheerio": "^1.0.0",
"clsx": "^2.0.0",
"cmdk": "^0.2.0",
"dayjs": "^1.11.6",
@@ -323,12 +88,10 @@
"electron-localshortcut": "^3.2.1",
"electron-log": "^5.1.1",
"electron-store": "^8.1.0",
"electron-updater": "^6.3.1",
"electron-updater": "^6.3.9",
"fast-average-color": "^9.3.0",
"format-duration": "^2.0.0",
"framer-motion": "^11.0.0",
"fuse.js": "^6.6.2",
"history": "^5.3.0",
"i18next": "^21.10.0",
"idb-keyval": "^6.2.1",
"immer": "^9.0.21",
@@ -336,53 +99,80 @@
"lodash": "^4.17.21",
"md5": "^2.3.0",
"memoize-one": "^6.0.0",
"motion": "^12.18.1",
"mpris-service": "^2.1.2",
"nanoid": "^3.3.3",
"net": "^1.0.2",
"node-mpv": "github:jeffvli/Node-MPV#32b4d64395289ad710c41d481d2707a7acfc228f",
"overlayscrollbars": "^2.2.1",
"overlayscrollbars-react": "^0.5.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"overlayscrollbars": "^2.11.1",
"overlayscrollbars-react": "^0.5.6",
"qs": "^6.14.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-error-boundary": "^3.1.4",
"react-i18next": "^11.18.6",
"react-icons": "^4.10.1",
"react-icons": "^5.5.0",
"react-image": "^4.1.0",
"react-loading-skeleton": "^3.5.0",
"react-player": "^2.11.0",
"react-router": "^6.16.0",
"react-router-dom": "^6.16.0",
"react-simple-img": "^3.0.0",
"react-virtualized-auto-sizer": "^1.0.17",
"react-window": "^1.8.9",
"react-window-infinite-loader": "^1.0.9",
"semver": "^7.5.4",
"styled-components": "^6.0.8",
"swiper": "^9.3.1",
"use-sync-external-store": "^1.5.0",
"ws": "^8.18.2",
"zod": "^3.22.3",
"zustand": "^4.3.9"
"zustand": "^5.0.5"
},
"resolutions": {
"styled-components": "^6",
"entities": "2.2.0"
"devDependencies": {
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
"@electron-toolkit/eslint-config-ts": "^3.0.0",
"@electron-toolkit/tsconfig": "^1.0.1",
"@types/electron-localshortcut": "^3.1.0",
"@types/lodash": "^4.17.18",
"@types/md5": "^2.3.5",
"@types/node": "^22.15.32",
"@types/react": "^18.3.23",
"@types/react-dom": "^18.3.7",
"@types/react-window": "^1.8.5",
"@types/react-window-infinite-loader": "^1.0.6",
"@types/source-map-support": "^0.5.10",
"@types/ws": "^8.18.1",
"@vitejs/plugin-react": "^4.3.4",
"concurrently": "^7.1.0",
"cross-env": "^7.0.3",
"electron": "^35.1.5",
"electron-builder": "^26.0.12",
"electron-devtools-installer": "^3.2.0",
"electron-vite": "^3.1.0",
"eslint": "^9.24.0",
"eslint-plugin-perfectionist": "^4.13.0",
"eslint-plugin-prettier": "^5.4.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"i18next-parser": "^9.0.2",
"postcss-preset-mantine": "^1.17.0",
"prettier": "^3.5.3",
"prettier-plugin-packagejson": "^2.5.14",
"sass-embedded": "^1.89.0",
"stylelint": "^16.14.1",
"stylelint-config-css-modules": "^4.4.0",
"stylelint-config-recess-order": "^7.1.0",
"stylelint-config-standard": "^38.0.0",
"typescript": "^5.8.3",
"vite": "^6.3.5",
"vite-plugin-conditional-import": "^0.1.7",
"vite-plugin-dynamic-import": "^1.6.0",
"vite-plugin-ejs": "^1.7.0"
},
"overrides": {
"entities": "2.2.0"
},
"devEngines": {
"runtime": {
"name": "node",
"version": ">=18.x",
"onFail": "error"
},
"packageManager": {
"name": "npm",
"version": ">=7.x",
"onFail": "error"
}
},
"browserslist": [],
"electronmon": {
"patterns": [
"!server",
"!src/renderer"
"pnpm": {
"onlyBuiltDependencies": [
"electron",
"esbuild"
]
}
},
"productName": "feishin"
}
+9656
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -0,0 +1,5 @@
module.exports = {
plugins: {
'postcss-preset-mantine': {},
},
};
-2351
View File
File diff suppressed because it is too large Load Diff
-24
View File
@@ -1,24 +0,0 @@
{
"name": "feishin",
"version": "0.12.6",
"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"
},
"license": "GPL-3.0"
}
+51
View File
@@ -0,0 +1,51 @@
import react from '@vitejs/plugin-react';
import path from 'path';
import { defineConfig, normalizePath } from 'vite';
import { ViteEjsPlugin } from 'vite-plugin-ejs';
import { version } from './package.json';
export default defineConfig({
build: {
emptyOutDir: true,
outDir: path.resolve(__dirname, './out/remote'),
rollupOptions: {
input: {
favicon: normalizePath(path.resolve(__dirname, './assets/icons/favicon.ico')),
index: normalizePath(path.resolve(__dirname, './src/remote/index.html')),
manifest: normalizePath(path.resolve(__dirname, './src/remote/manifest.json')),
remote: normalizePath(path.resolve(__dirname, './src/remote/index.tsx')),
worker: normalizePath(path.resolve(__dirname, './src/remote/service-worker.ts')),
},
output: {
assetFileNames: '[name].[ext]',
chunkFileNames: '[name].js',
entryFileNames: '[name].js',
},
},
sourcemap: true,
},
css: {
modules: {
generateScopedName: 'fs-[name]-[local]',
localsConvention: 'camelCase',
},
},
plugins: [
react(),
ViteEjsPlugin({
prod: process.env.NODE_ENV === 'production',
root: normalizePath(path.resolve(__dirname, './src/remote')),
version,
}),
],
resolve: {
alias: {
'/@/i18n': path.resolve(__dirname, './src/i18n'),
'/@/remote': path.resolve(__dirname, './src/remote'),
'/@/renderer': path.resolve(__dirname, './src/renderer'),
'/@/shared': path.resolve(__dirname, './src/shared'),
},
},
root: path.resolve(__dirname, './src/remote'),
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

-9
View File
@@ -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();
});
});
+33 -32
View File
@@ -1,52 +1,53 @@
import { PostProcessorModule, TOptions, StringMap } from 'i18next';
import { PostProcessorModule, StringMap, TOptions } from 'i18next';
import i18n from '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 es from './locales/es.json';
import fa from './locales/fa.json';
import fi from './locales/fi.json';
import fr from './locales/fr.json';
import ja from './locales/ja.json';
import pl from './locales/pl.json';
import zhHans from './locales/zh-Hans.json';
import de from './locales/de.json';
import hu from './locales/hu.json';
import id from './locales/id.json';
import it from './locales/it.json';
import ru from './locales/ru.json';
import ptBr from './locales/pt-BR.json';
import sr from './locales/sr.json';
import sv from './locales/sv.json';
import cs from './locales/cs.json';
import ja from './locales/ja.json';
import ko from './locales/ko.json';
import nbNO from './locales/nb-NO.json';
import nl from './locales/nl.json';
import zhHant from './locales/zh-Hant.json';
import fa from './locales/fa.json';
import ko from './locales/ko.json';
import pl from './locales/pl.json';
import ptBr from './locales/pt-BR.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 id from './locales/id.json';
import fi from './locales/fi.json';
import hu from './locales/hu.json';
import zhHans from './locales/zh-Hans.json';
import zhHant from './locales/zh-Hant.json';
const resources = {
cs: { translation: cs },
de: { translation: de },
en: { translation: en },
es: { translation: es },
de: { translation: de },
it: { translation: it },
ru: { translation: ru },
'pt-BR': { translation: ptBr },
fa: { translation: fa },
fi: { translation: fi },
fr: { translation: fr },
hu: { translation: hu },
id: { translation: id },
it: { translation: it },
ja: { translation: ja },
ko: { translation: ko },
'nb-NO': { translation: nbNO },
nl: { translation: nl },
pl: { translation: pl },
'zh-Hans': { translation: zhHans },
'zh-Hant': { translation: zhHant },
'pt-BR': { translation: ptBr },
ru: { translation: ru },
sr: { translation: sr },
sv: { translation: sv },
cs: { translation: cs },
nl: { translation: nl },
'nb-NO': { translation: nbNO },
ta: { translation: ta },
id: { translation: id },
fi: { translation: fi },
hu: { translation: hu },
'zh-Hans': { translation: zhHans },
'zh-Hant': { translation: zhHant },
};
export const languages = [
@@ -141,35 +142,34 @@ export const languages = [
];
const lowerCasePostProcessor: PostProcessorModule = {
type: 'postProcessor',
name: 'lowerCase',
process: (value: string) => {
return value.toLocaleLowerCase();
},
type: 'postProcessor',
};
const upperCasePostProcessor: PostProcessorModule = {
type: 'postProcessor',
name: 'upperCase',
process: (value: string) => {
return value.toLocaleUpperCase();
},
type: 'postProcessor',
};
const titleCasePostProcessor: PostProcessorModule = {
type: 'postProcessor',
name: 'titleCase',
process: (value: string) => {
return value.replace(/\S\S*/g, (txt) => {
return txt.charAt(0).toLocaleUpperCase() + txt.slice(1).toLowerCase();
});
},
type: 'postProcessor',
};
const ignoreSentenceCaseLanguages = ['de'];
const sentenceCasePostProcessor: PostProcessorModule = {
type: 'postProcessor',
name: 'sentenceCase',
process: (value: string, _key: string, _options: TOptions<StringMap>, translator: any) => {
const sentences = value.split('. ');
@@ -185,6 +185,7 @@ const sentenceCasePostProcessor: PostProcessorModule = {
})
.join('. ');
},
type: 'postProcessor',
};
i18n.use(lowerCasePostProcessor)
.use(upperCasePostProcessor)
+1 -1
View File
@@ -5,7 +5,7 @@ module.exports = {
createOldCatalogs: true,
customValueTemplate: null,
defaultNamespace: 'translation',
defaultValue: function (locale, namespace, key, value) {
defaultValue: function (locale, namespace, key) {
return key;
},
failOnUpdate: false,
+16 -5
View File
@@ -257,7 +257,15 @@
"translationTargetLanguage": "cílový jazyk překladu",
"translationTargetLanguage_description": "cílový jazyk pro překlad",
"lastfmApiKey": "klíč API {{lastfm}}",
"lastfmApiKey_description": "klíč API pro {{lastfm}}. vyžadováno pro obaly alb"
"lastfmApiKey_description": "klíč API pro {{lastfm}}. vyžadováno pro obaly alb",
"discordServeImage": "načítat obrázky {{discord}} ze serveru",
"discordServeImage_description": "sdílet obaly alb pro {{discord}} rich presence ze samotného serveru, dostupné pouze pro jellyfin a navidrome",
"lastfm": "zobrazit odkazy na last.fm",
"lastfm_description": "na stránkách umělců a alb zobrazit odkazy na last.fm",
"musicbrainz": "zobrazit odkazy na musicbrainz",
"musicbrainz_description": "na stránkách umělců a alb, kde existuje mbid, zobrazit odkazy na musicbrainz",
"neteaseTranslation": "Povolit překlady NetEase",
"neteaseTranslation_description": "Pokud je povoleno, načte a zobrazí přeložené texty ze služby NetEase, pokud jsou dostupné."
},
"action": {
"editPlaylist": "upravit $t(entity.playlist_one)",
@@ -375,7 +383,9 @@
"codec": "kodek",
"trackPeak": "vrchol skladby",
"preview": "náhled",
"translation": "překlad"
"translation": "překlad",
"additionalParticipants": "další přispívající",
"tags": "štítky"
},
"table": {
"config": {
@@ -474,7 +484,8 @@
"loginRateError": "příliš mnoho pokusů o přihlášení, zkuste to znovu za pár vteřin",
"badAlbum": "tuto stránku vidíte, protože tato skladba není součástí alba. tento problém může nastat, pokud máte skladbu na nejvyšší úrovni vaší složky s hudbou. jellyfin seskupuje skladby pouze, pokud se nacházejí ve složce.",
"networkError": "vyskytla se chyba sítě",
"openError": "nepodařilo se otevřít soubor"
"openError": "nepodařilo se otevřít soubor",
"badValue": "neplatná možnost „{{value}}“. tato možnost již neexistuje"
},
"filter": {
"mostPlayed": "nejvíce přehráváno",
@@ -747,8 +758,8 @@
"folderWithCount_few": "{{count}} složky",
"folderWithCount_other": "{{count}} složek",
"albumArtist_one": "umělec alba",
"albumArtist_few": "umělci alba",
"albumArtist_other": "umělců alba",
"albumArtist_few": "umělci alb",
"albumArtist_other": "umělci alb",
"track_one": "skladba",
"track_few": "skladby",
"track_other": "skladby",
+17
View File
@@ -27,6 +27,9 @@
"action_one": "action",
"action_other": "actions",
"add": "add",
"additionalParticipants": "additional participants",
"newVersion": "a new version has been installed ({{version}})",
"viewReleaseNotes": "view release notes",
"albumGain": "album gain",
"albumPeak": "album peak",
"areYouSure": "are you sure?",
@@ -106,6 +109,7 @@
"share": "share",
"size": "size",
"sortOrder": "order",
"tags": "tags",
"title": "title",
"trackNumber": "track",
"trackGain": "track gain",
@@ -158,6 +162,7 @@
"audioDeviceFetchError": "an error occurred when trying to get audio devices",
"authenticationFailed": "authentication failed",
"badAlbum": "you are seeing this page because this song is not part of an album. you are most likely seeing this issue if you have a song at the top level of your music folder. jellyfin only groups tracks if they are in a folder.",
"badValue": "invalid option \"{{value}}\". this value no longer exists",
"credentialsRequired": "credentials required",
"endpointNotImplementedError": "endpoint {{endpoint}} is not implemented for {{serverType}}",
"genericError": "an error occurred",
@@ -265,6 +270,7 @@
"title": "lyric search"
},
"queryEditor": {
"title": "query editor",
"input_optionMatchAll": "match all",
"input_optionMatchAny": "match any"
},
@@ -418,6 +424,7 @@
"folders": "$t(entity.folder_other)",
"genres": "$t(entity.genre_other)",
"home": "$t(common.home)",
"myLibrary": "my library",
"nowPlaying": "now playing",
"playlists": "$t(entity.playlist_other)",
"search": "$t(common.search)",
@@ -513,6 +520,8 @@
"discordListening_description": "show status as listening instead of playing",
"discordRichPresence": "{{discord}} rich presence",
"discordRichPresence_description": "enable playback status in {{discord}} rich presence. Image keys are: {{icon}}, {{playing}}, and {{paused}} ",
"discordServeImage": "serve {{discord}} images from server",
"discordServeImage_description": "share cover art for {{discord}} rich presence from server itself, only available for jellyfin and navidrome",
"discordUpdateInterval": "{{discord}} rich presence update interval",
"discordUpdateInterval_description": "the time in seconds between each update (minimum 15 seconds)",
"doubleClickBehavior": "queue all searched tracks when double clicking",
@@ -582,6 +591,8 @@
"imageAspectRatio_description": "if enabled, cover art will be shown using their native aspect ratio. for art that is not 1:1, the remaining space will be empty",
"language": "language",
"language_description": "sets the language for the application ($t(common.restartRequired))",
"lastfm": "show last.fm links",
"lastfm_description": "show links to last.fm on artist/album pages",
"lastfmApiKey": "{{lastfm}} API key",
"lastfmApiKey_description": "the API key for {{lastfm}}. required for cover art",
"lyricFetch": "fetch lyrics from the internet",
@@ -600,6 +611,10 @@
"mpvExecutablePath_description": "sets the path to the mpv executable. if left empty, the default path will be used",
"mpvExtraParameters": "mpv parameters",
"mpvExtraParameters_help": "one per line",
"musicbrainz": "show musicbrainz links",
"musicbrainz_description": "show links to musicbrainz on artist/album pages, where mbid exists",
"neteaseTranslation": "Enable NetEase translations",
"neteaseTranslation_description": "When enabled, fetches and displays translated lyrics from NetEase if available.",
"passwordStore": "passwords/secret store",
"passwordStore_description": "what password/secret store to use. change this if you are having issues storing passwords.",
"playbackStyle": "playback style",
@@ -763,6 +778,8 @@
},
"view": {
"card": "card",
"grid": "grid",
"list": "list",
"poster": "poster",
"table": "table"
}
+14 -3
View File
@@ -257,7 +257,15 @@
"translationTargetLanguage": "idioma final de la traducción",
"translationTargetLanguage_description": "lengua de destino de la traducción",
"lastfmApiKey_description": "la clave API para {{lastfm}}. Requerida para la portada",
"lastfmApiKey": "Clave API para {{lastfm}}"
"lastfmApiKey": "Clave API para {{lastfm}}",
"discordServeImage": "Servir imágenes de {{discord}} desde el servidor",
"discordServeImage_description": "Comparte el arte de la portada para el estado de actividad de {{discord}} desde el propio servidor, solo disponible para Jellyfin y Navidrome",
"lastfm": "Mostrar enlaces de last.fm",
"lastfm_description": "Muestra enlaces a last.fm en las páginas de artistas/álbumes",
"musicbrainz": "Mostrar enlaces de MusicBrainz",
"musicbrainz_description": "Muestra enlaces a MusicBrainz en las páginas de artistas/álbumes, donde exista mbid",
"neteaseTranslation": "Activar traducciones de NetEase",
"neteaseTranslation_description": "Cuando se habilita, busca y muestra letras traducidas desde NetEase si está disponible."
},
"action": {
"editPlaylist": "editar $t(entity.playlist_one)",
@@ -375,7 +383,9 @@
"share": "Compartir",
"trackGain": "Ganancia de pista",
"preview": "Vista previa",
"translation": "traducción"
"translation": "traducción",
"additionalParticipants": "Participantes adicionales",
"tags": "Etiquetas"
},
"error": {
"remotePortWarning": "reiniciar el servidor para aplicar el nuevo puerto",
@@ -399,7 +409,8 @@
"loginRateError": "demasiados intentos de inicio de sesión, por favor inténtalo en unos segundos",
"badAlbum": "Estás viendo esta página porque esta canción no forma parte de un álbum. Este problema puede ocurrir si tienes una canción en el nivel superior de tu carpeta de música. Jellyfin solo agrupa pistas si están en una carpeta.",
"networkError": "Ocurrió un error de red",
"openError": "No se pudo abrir el archivo"
"openError": "No se pudo abrir el archivo",
"badValue": "Opción inválida \"{{value}}\". Este valor ya no existe"
},
"filter": {
"mostPlayed": "más reproducido",
+14 -3
View File
@@ -88,7 +88,9 @@
"albumGain": "albumin vahvistus (gain)",
"albumPeak": "albumin huippu (peak)",
"trackGain": "raidan vahvistus (gain)",
"trackPeak": "kappaleen huippu (peak)"
"trackPeak": "kappaleen huippu (peak)",
"additionalParticipants": "muut osallistujat",
"tags": "tägit"
},
"entity": {
"album_one": "albumi",
@@ -173,7 +175,8 @@
"localFontAccessDenied": "paikallisiin fontteihin pääsy on kielletty",
"playbackError": "mediaa toistaessa tapahtui virhe",
"remotePortWarning": "käynnistä palvelin uudestaan ottaaksesi uuden portin käyttöön",
"endpointNotImplementedError": "päätepiste {{endpoint}} ei ole toteutettu {{serverType}} varten"
"endpointNotImplementedError": "päätepiste {{endpoint}} ei ole toteutettu {{serverType}} varten",
"badValue": "kelpaamaton optio \"{{value}}\". tätä arvoa ei ole enää olemassa"
},
"filter": {
"album": "$t(entity.album_one)",
@@ -504,7 +507,15 @@
"webAudio_description": "käytä web-ääntä. tämä mahdollistaa edistyneet ominaisuudet, kuten replaygainin. poista käytöstä, jos koet ongelmia",
"startMinimized": "käynnistä pienennettynä",
"useSystemTheme": "käytä järjestelmän teemaa",
"volumeWheelStep": "äänenvoimakkuusrullan askel"
"volumeWheelStep": "äänenvoimakkuusrullan askel",
"discordServeImage": "jaa {{discord}} kuvat palvelimelta",
"discordServeImage_description": "jaa kansikuvat {{discord}}n rich presenceä varten suoraan palvelimelta. saatavilla vain jellyfinille ja navidromelle",
"musicbrainz_description": "näytä linkit musicbrainz sivulle artistin/albumin sivuilla, jos musicbrainz-id löytyy",
"lastfm": "näytä last.fm linkit",
"lastfm_description": "näytä linkit last.fm sivulle artistin/albumin sivuilla",
"musicbrainz": "näytä musicbrainz linkit",
"neteaseTranslation": "Ota NetEasen käännökset käyttöön",
"neteaseTranslation_description": "Käytöss ollessa noutaa ja näyttää käännetyt sanat NetEasesta, jos ne ovat saatavilla."
},
"page": {
"itemDetail": {
+12 -3
View File
@@ -148,7 +148,9 @@
"trackGain": "gain de la piste",
"trackPeak": "crête de la piste",
"codec": "codec",
"translation": "traduction"
"translation": "traduction",
"additionalParticipants": "participants additionnels",
"tags": "tags"
},
"error": {
"remotePortWarning": "redémarrer le serveur pour appliquer le nouveau port",
@@ -172,7 +174,8 @@
"loginRateError": "trop de tentative de connexion, merci de réessayer dans quelques secondes",
"openError": "impossible d'ouvrir le fichier",
"networkError": "une erreur de réseau est survenue",
"badAlbum": "vous voyez cette page parce que cette chanson ne fait pas parti d'un album. vous rencontrez probablement cette erreur si vous avez une chanson qui n'est pas dans votre répertoire de musique. jellyfin gère les chansons uniquement si elles sont dans un sous-dossier, qui est lui-même dans un dossier \"Musique(s)\"."
"badAlbum": "vous voyez cette page parce que cette chanson ne fait pas parti d'un album. vous rencontrez probablement cette erreur si vous avez une chanson qui n'est pas dans votre répertoire de musique. jellyfin gère les chansons uniquement si elles sont dans un sous-dossier, qui est lui-même dans un dossier \"Musique(s)\".",
"badValue": "option {{value}} invalide. Cette valeur n'existe plus"
},
"filter": {
"mostPlayed": "plus joués",
@@ -593,7 +596,13 @@
"doubleClickBehavior_description": "si vrai, toutes les pistes correspondantes dans une recherche de piste seront mises en file d'attente. sinon, seule celle sur laquelle vous avez cliqué sera mise en file d'attente",
"albumBackgroundBlur": "taille du flou de l'image d'arrière-plan de l'album",
"lastfmApiKey": "clé API {{lastfm}}",
"lastfmApiKey_description": "la clé API pour {{lastfm}} est requise pour la pochette d'album"
"lastfmApiKey_description": "la clé API pour {{lastfm}} . requise pour la pochette d'album",
"discordServeImage": "servir l'image {{discord}} depuis le serveur",
"discordServeImage_description": "partage pochette du status d'activité {{discord}} depuis le serveur lui même, disponible uniquement pour jellyfin et navidrome",
"lastfm": "affiche les liens de last.fm",
"musicbrainz_description": "affiches les liens vers musicbrainz sur les pages des artistes/albums, quand mbid existes",
"lastfm_description": "affiche les liens vers last.fm sur les pages des artistes/albums",
"musicbrainz": "affiches les liens musicbrainz"
},
"form": {
"deletePlaylist": {
+15 -3
View File
@@ -91,7 +91,9 @@
"albumGain": "ganho do álbum",
"trackPeak": "pico da faixa",
"albumPeak": "pico do álbum",
"trackGain": "ganho da faixa"
"trackGain": "ganho da faixa",
"additionalParticipants": "participantes adicionais",
"tags": "tags"
},
"action": {
"goToPage": "vá para página",
@@ -205,7 +207,16 @@
"buttonSize_description": "o tamanho dos botões da barra de reprodução",
"albumBackgroundBlur": "tamanho de desfoque da imagem de fundo do álbum",
"albumBackgroundBlur_description": "ajusta a quantidade de desfoque aplicada para a imagem de fundo do álbum",
"albumBackground": "imagem de fundo do álbum"
"albumBackground": "imagem de fundo do álbum",
"contextMenu_description": "permite esconder itens exibidos no menu quando você clica em um item com o botão direito. itens não selecionados serão escondidos",
"customCssEnable": "habilitar css customizado",
"customCssEnable_description": "permite escrever css customizado.",
"crossfadeDuration": "duraçao de crossfade",
"customCss": "css customizado",
"crossfadeDuration_description": "define a duração do efeito crossfade",
"customCssNotice": "Aviso: apesar de existir alguma higienização (url() e content: não são permitidas), o uso de CSS personalizado ainda pode representar riscos ao alterar a interface.",
"crossfadeStyle": "estilo do crossfade",
"crossfadeStyle_description": "seleciona qual estilo de crossfade usado no player de áudio"
},
"table": {
"config": {
@@ -524,6 +535,7 @@
"loginRateError": "muitas tentativas de login, tente novamente em alguns segundos",
"badAlbum": "você está vendo este erro por que está música não é parte de algum album. um motivo comum para você estar vendo este erro é se a sua música estiver na raiz da sua pasta de músicas. o jellyfin apenas agrupa as músicas se elas estiveram na mesma pasta.",
"networkError": "ocorreu um erro na internet",
"openError": "não foi possível abrir o arquivo"
"openError": "não foi possível abrir o arquivo",
"badValue": "opção inválida \"{{value}}\". este valor não existe no momento"
}
}
+14 -3
View File
@@ -109,7 +109,9 @@
"codec": "编解码器",
"share": "分享",
"preview": "预览",
"translation": "翻译"
"translation": "翻译",
"additionalParticipants": "其他参与者",
"tags": "标签"
},
"entity": {
"albumArtist_other": "专辑艺术家",
@@ -178,6 +180,8 @@
"followLyric_description": "滚动歌词到当前播放位置",
"audioExclusiveMode": "音频独占模式",
"font": "字体",
"neteaseTranslation": "启用网易云歌词翻译",
"neteaseTranslation_description": "启用后,在获取歌词时将包含并显示网易云音乐提供的翻译(如果存在)。",
"crossfadeDuration_description": "设置淡入淡出持续时间",
"audioDevice": "音频设备",
"enableRemote": "启用远程控制服务器",
@@ -389,7 +393,13 @@
"translationTargetLanguage": "目标翻译语言",
"translationTargetLanguage_description": "目标翻译语言",
"lastfmApiKey": "{{lastfm}} API 密钥",
"lastfmApiKey_description": "{{lastfm}} 的 API 密钥。封面艺术图所需"
"lastfmApiKey_description": "{{lastfm}} 的 API 密钥。封面艺术图所需",
"discordServeImage": "从服务器提供 {{discord}} 图像",
"discordServeImage_description": "分享 {{discord}} 封面艺术图,来自 rich presence 服务器,仅适用于 jellyfin 和 navidrome",
"musicbrainz": "显示 musicbrainz 链接",
"musicbrainz_description": "在 mbid 的艺术家/专辑页面上显示 musicbrainz 的链接",
"lastfm": "显示 last.fm 链接",
"lastfm_description": "在艺术家/专辑页面上显示 last.fm 的链接"
},
"error": {
"remotePortWarning": "重启服务器使新端口生效",
@@ -413,7 +423,8 @@
"loginRateError": "登录请求尝试次数过多,请稍后再试",
"badAlbum": "您看到此页面是因为这首歌不是专辑的一部分。如果您的音乐文件夹顶层有一首歌曲,您很可能会遇到此问题。jellyfin 仅对位于文件夹中的曲目进行分组。",
"networkError": "发生网络错误",
"openError": "无法打开文件"
"openError": "无法打开文件",
"badValue": "无效的选项 \"{{value}}\". 此值不再存在"
},
"filter": {
"mostPlayed": "最多播放过",
+82 -81
View File
@@ -1,12 +1,13 @@
import axios, { AxiosResponse } from 'axios';
import { load } from 'cheerio';
import { orderSearchResults } from './shared';
import {
LyricSource,
InternetProviderLyricResponse,
InternetProviderLyricSearchResponse,
LyricSearchQuery,
} from '../../../../renderer/api/types';
LyricSource,
} from '.';
import { orderSearchResults } from './shared';
const SEARCH_URL = 'https://genius.com/api/search/song';
@@ -17,20 +18,6 @@ export interface GeniusResponse {
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 {
highlights: any[];
index: string;
@@ -38,6 +25,35 @@ export interface Hit {
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 {
_type: string;
annotation_count: number;
@@ -69,24 +85,9 @@ export interface Result {
url: string;
}
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 Section {
hits: Hit[];
type: string;
}
export interface Stats {
@@ -94,6 +95,27 @@ export interface Stats {
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(
params: LyricSearchQuery,
): Promise<InternetProviderLyricSearchResponse[] | null> {
@@ -133,9 +155,33 @@ export async function getSearchResults(
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(
params: LyricSearchQuery,
): Promise<Omit<InternetProviderLyricResponse, 'lyrics'> | null> {
): Promise<null | Omit<InternetProviderLyricResponse, 'lyrics'>> {
let result: AxiosResponse<GeniusResponse>;
try {
result = await axios.get(SEARCH_URL, {
@@ -162,48 +208,3 @@ async function getSongId(
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,
};
}
+68 -24
View File
@@ -1,36 +1,78 @@
import { ipcMain } from 'electron';
import { store } from '../settings';
import {
getLyricsBySongId as getGenius,
query as queryGenius,
getSearchResults as searchGenius,
getLyricsBySongId as getGenius,
} from './genius';
import {
getLyricsBySongId as getLrcLib,
query as queryLrclib,
getSearchResults as searchLrcLib,
getLyricsBySongId as getLrcLib,
} from './lrclib';
import {
getLyricsBySongId as getNetease,
query as queryNetease,
getSearchResults as searchNetease,
getLyricsBySongId as getNetease,
} 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>;
import { Song } from '/@/shared/types/domain-types';
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 = (
params: LyricSearchQuery,
) => 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> = {
[LyricSource.GENIUS]: queryGenius,
@@ -54,10 +96,10 @@ const MAX_CACHED_ITEMS = 10;
const lyricCache = new Map<string, CachedLyrics>();
const getRemoteLyrics = async (song: QueueSong) => {
const getRemoteLyrics = async (song: Song) => {
const sources = store.get('lyrics', []) as LyricSource[];
const cached = lyricCache.get(song.id);
const cached = lyricCache.get(song.id.toString());
if (cached) {
for (const source of sources) {
@@ -66,16 +108,16 @@ const getRemoteLyrics = async (song: QueueSong) => {
}
}
let lyricsFromSource = null;
let lyricsFromSource: InternetProviderLyricResponse | null = null;
for (const source of sources) {
const params = {
album: song.album || song.name,
artist: song.artistName,
artist: song.artists[0].name,
duration: song.duration / 1000.0,
name: song.name,
};
const response = await FETCHERS[source](params);
const response = await FETCHERS[source](params as unknown as LyricSearchQuery);
if (response) {
const newResult = cached
@@ -87,10 +129,12 @@ const getRemoteLyrics = async (song: QueueSong) => {
if (lyricCache.size === MAX_CACHED_ITEMS && cached === undefined) {
const toRemove = lyricCache.keys().next().value;
lyricCache.delete(toRemove);
if (toRemove) {
lyricCache.delete(toRemove);
}
}
lyricCache.set(song.id, newResult);
lyricCache.set(song.id.toString(), newResult);
lyricsFromSource = response;
break;
@@ -122,7 +166,7 @@ const searchRemoteLyrics = async (params: LyricSearchQuery) => {
return results;
};
const getRemoteLyricsById = async (params: LyricGetQuery): Promise<string | null> => {
const getRemoteLyricsById = async (params: LyricGetQuery): Promise<null | string> => {
const { remoteSongId, remoteSource } = params;
const response = await GET_FETCHERS[remoteSource](remoteSongId);
@@ -133,7 +177,7 @@ const getRemoteLyricsById = async (params: LyricGetQuery): Promise<string | null
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);
return lyric;
});
+18 -17
View File
@@ -1,12 +1,13 @@
// Credits to https://github.com/tranxuanthang/lrcget for API implementation
import axios, { AxiosResponse } from 'axios';
import { orderSearchResults } from './shared';
import {
InternetProviderLyricResponse,
InternetProviderLyricSearchResponse,
LyricSearchQuery,
LyricSource,
} from '../../../../renderer/api/types';
} from '.';
import { orderSearchResults } from './shared';
const FETCH_URL = 'https://lrclib.net/api/get';
const SEEARCH_URL = 'https://lrclib.net/api/search';
@@ -29,10 +30,23 @@ export interface LrcLibTrackResponse {
isrc: string;
lang: string;
name: string;
plainLyrics: string | null;
plainLyrics: null | string;
releaseDate: 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(
@@ -69,19 +83,6 @@ export async function getSearchResults(
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(
params: LyricSearchQuery,
): Promise<InternetProviderLyricResponse | null> {
+103 -44
View File
@@ -1,22 +1,19 @@
import axios, { AxiosResponse } from 'axios';
import { LyricSource } from '../../../../renderer/api/types';
import { orderSearchResults } from './shared';
import type {
import {
InternetProviderLyricResponse,
InternetProviderLyricSearchResponse,
LyricSearchQuery,
} from '/@/renderer/api/types';
LyricSource,
} from '.';
import { store } from '../settings';
import { orderSearchResults } from './shared';
const SEARCH_URL = 'https://music.163.com/api/search/get';
const LYRICS_URL = 'https://music.163.com/api/song/lyric';
// 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;
@@ -35,13 +32,13 @@ export interface Song {
mark: number;
mvid: number;
name: string;
rUrl: null;
rtype: number;
rUrl: null;
status: number;
transNames?: string[];
}
export interface Album {
interface Album {
artist: Artist;
copyrightId: number;
id: number;
@@ -54,7 +51,7 @@ export interface Album {
transNames?: string[];
}
export interface Artist {
interface Artist {
albumSize: number;
alias: any[];
fansGroup: null;
@@ -67,6 +64,35 @@ export interface Artist {
trans: null;
}
interface NetEaseResponse {
code: number;
result: Result;
}
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',
tv: '-1',
},
});
} catch (e) {
console.error('NetEase lyrics request got an error!', e);
return null;
}
const enableTranslation = store.get('enableNeteaseTranslation', false) as boolean;
const originalLrc = result.data.lrc?.lyric;
if (!enableTranslation) {
return originalLrc || null;
}
const translatedLrc = result.data.tlyric?.lyric;
return mergeLyrics(originalLrc, translatedLrc);
}
export async function getSearchResults(
params: LyricSearchQuery,
): Promise<InternetProviderLyricSearchResponse[] | null> {
@@ -110,38 +136,6 @@ export async function getSearchResults(
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(
params: LyricSearchQuery,
): Promise<InternetProviderLyricResponse | null> {
@@ -165,3 +159,68 @@ export async function query(
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;
}
function mergeLyrics(original: string | undefined, translated: string | undefined): null | string {
if (!original) {
return null;
}
if (!translated) {
return original;
}
const lrcLineRegex = /\[(\d{2}:\d{2}\.\d{2,3})\](.*)/;
const translatedMap = new Map<string, string>();
// Parse the translated LRC and store it in a Map for efficient timestamp-based lookups.
translated.split('\n').forEach((line) => {
const match = line.match(lrcLineRegex);
if (match) {
const timestamp = match[1];
const text = match[2].trim();
if (text) {
translatedMap.set(timestamp, text);
}
}
});
if (translatedMap.size === 0) {
return original;
}
// Iterate through each line of the original LRC. If a translation exists for
// the same timestamp, insert it as a new, fully-formatted LRC line.
const finalLines = original.split('\n').flatMap((line) => {
const match = line.match(lrcLineRegex);
if (match) {
const timestamp = match[1];
const translatedText = translatedMap.get(timestamp);
if (translatedText) {
// Return an array containing both the original line and the new translated line.
// flatMap will flatten this into the final array of lines.
const translatedLine = `[${timestamp}]${translatedText}`;
return [line, translatedLine];
}
}
// If no match or no translation is found, return only the original line.
return [line];
});
return finalLines.join('\n');
}
+2 -1
View File
@@ -1,8 +1,9 @@
import Fuse from 'fuse.js';
import {
InternetProviderLyricSearchResponse,
LyricSearchQuery,
} from '../../../../renderer/api/types';
} from '/@/shared/types/domain-types';
export const orderSearchResults = (args: {
params: LyricSearchQuery;
+27 -25
View File
@@ -1,10 +1,11 @@
import console from 'console';
import { rm } from 'fs/promises';
import { pid } from 'node:process';
import { app, ipcMain } from 'electron';
import { rm } from 'fs/promises';
import uniq from 'lodash/uniq';
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 { store } from '../settings';
@@ -86,7 +87,7 @@ const createMpv = async (data: {
extraParameters?: string[];
properties?: Record<string, any>;
}): Promise<MpvAPI> => {
const { extraParameters, properties, binaryPath } = data;
const { binaryPath, extraParameters, properties } = data;
const params = uniq([...DEFAULT_MPV_PARAMETERS(extraParameters), ...(extraParameters || [])]);
@@ -174,7 +175,7 @@ ipcMain.on('player-set-properties', async (_event, data: Record<string, any>) =>
} else {
getMpvInstance()?.setMultipleProperties(data);
}
} catch (err: NodeMpvError | any) {
} catch (err: any | NodeMpvError) {
mpvLog({ action: `Failed to set properties: ${JSON.stringify(data)}` }, err);
}
});
@@ -199,7 +200,7 @@ ipcMain.handle(
mpvInstance = await createMpv(data);
mpvLog({ action: 'Restarted mpv', toast: 'success' });
setAudioPlayerFallback(false);
} catch (err: NodeMpvError | any) {
} catch (err: any | NodeMpvError) {
mpvLog({ action: 'Failed to restart mpv, falling back to web player' }, err);
setAudioPlayerFallback(true);
}
@@ -215,7 +216,7 @@ ipcMain.handle(
});
mpvInstance = await createMpv(data);
setAudioPlayerFallback(false);
} catch (err: NodeMpvError | any) {
} catch (err: any | NodeMpvError) {
mpvLog({ action: 'Failed to initialize mpv, falling back to web player' }, err);
setAudioPlayerFallback(true);
}
@@ -226,7 +227,7 @@ ipcMain.on('player-quit', async () => {
try {
await getMpvInstance()?.stop();
await quit();
} catch (err: NodeMpvError | any) {
} catch (err: any | NodeMpvError) {
mpvLog({ action: 'Failed to quit mpv' }, err);
} finally {
mpvInstance = null;
@@ -245,7 +246,7 @@ ipcMain.handle('player-clean-up', async () => {
ipcMain.on('player-start', async () => {
try {
await getMpvInstance()?.play();
} catch (err: NodeMpvError | any) {
} catch (err: any | NodeMpvError) {
mpvLog({ action: 'Failed to start mpv playback' }, err);
}
});
@@ -254,7 +255,7 @@ ipcMain.on('player-start', async () => {
ipcMain.on('player-play', async () => {
try {
await getMpvInstance()?.play();
} catch (err: NodeMpvError | any) {
} catch (err: any | NodeMpvError) {
mpvLog({ action: 'Failed to start mpv playback' }, err);
}
});
@@ -263,7 +264,7 @@ ipcMain.on('player-play', async () => {
ipcMain.on('player-pause', async () => {
try {
await getMpvInstance()?.pause();
} catch (err: NodeMpvError | any) {
} catch (err: any | NodeMpvError) {
mpvLog({ action: 'Failed to pause mpv playback' }, err);
}
});
@@ -272,7 +273,7 @@ ipcMain.on('player-pause', async () => {
ipcMain.on('player-stop', async () => {
try {
await getMpvInstance()?.stop();
} catch (err: NodeMpvError | any) {
} catch (err: any | NodeMpvError) {
mpvLog({ action: 'Failed to stop mpv playback' }, err);
}
});
@@ -281,7 +282,7 @@ ipcMain.on('player-stop', async () => {
ipcMain.on('player-next', async () => {
try {
await getMpvInstance()?.next();
} catch (err: NodeMpvError | any) {
} catch (err: any | NodeMpvError) {
mpvLog({ action: 'Failed to go to next track' }, err);
}
});
@@ -290,7 +291,7 @@ ipcMain.on('player-next', async () => {
ipcMain.on('player-previous', async () => {
try {
await getMpvInstance()?.prev();
} catch (err: NodeMpvError | any) {
} catch (err: any | NodeMpvError) {
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) => {
try {
await getMpvInstance()?.seek(time);
} catch (err: NodeMpvError | any) {
} catch (err: any | NodeMpvError) {
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) => {
try {
await getMpvInstance()?.goToPosition(time);
} catch (err: NodeMpvError | any) {
} catch (err: any | NodeMpvError) {
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()?.pause();
return;
} catch (err: NodeMpvError | any) {
} catch (err: any | NodeMpvError) {
mpvLog({ action: `Failed to clear play queue` }, err);
}
}
@@ -329,7 +330,8 @@ ipcMain.on('player-set-queue', async (_event, current?: string, next?: string, p
if (current) {
try {
await getMpvInstance()?.load(current, 'replace');
} catch (error) {
} catch (error: any | NodeMpvError) {
mpvLog({ action: `Failed to load current song` }, error);
await getMpvInstance()?.play();
}
@@ -344,7 +346,7 @@ ipcMain.on('player-set-queue', async (_event, current?: string, next?: string, p
// Only force play if pause is explicitly false
await getMpvInstance()?.play();
}
} catch (err: NodeMpvError | any) {
} catch (err: any | NodeMpvError) {
mpvLog({ action: `Failed to set play queue` }, err);
}
});
@@ -365,7 +367,7 @@ ipcMain.on('player-set-queue-next', async (_event, url?: string) => {
if (url) {
await getMpvInstance()?.load(url, 'append');
}
} catch (err: NodeMpvError | any) {
} catch (err: any | NodeMpvError) {
mpvLog({ action: `Failed to set play queue` }, err);
}
});
@@ -385,7 +387,7 @@ ipcMain.on('player-auto-next', async (_event, url?: string) => {
if (url) {
await getMpvInstance()?.load(url, 'append');
}
} catch (err: NodeMpvError | any) {
} catch (err: any | NodeMpvError) {
mpvLog({ action: `Failed to load next song` }, err);
}
});
@@ -398,7 +400,7 @@ ipcMain.on('player-volume', async (_event, value: number) => {
}
await getMpvInstance()?.volume(value);
} catch (err: NodeMpvError | any) {
} catch (err: any | NodeMpvError) {
mpvLog({ action: `Failed to set volume to ${value}` }, err);
}
});
@@ -407,7 +409,7 @@ ipcMain.on('player-volume', async (_event, value: number) => {
ipcMain.on('player-mute', async (_event, mute: boolean) => {
try {
await getMpvInstance()?.mute(mute);
} catch (err: NodeMpvError | any) {
} catch (err: any | NodeMpvError) {
mpvLog({ action: `Failed to set mute status` }, err);
}
});
@@ -415,7 +417,7 @@ ipcMain.on('player-mute', async (_event, mute: boolean) => {
ipcMain.handle('player-get-time', async (): Promise<number | undefined> => {
try {
return getMpvInstance()?.getTimePosition();
} catch (err: NodeMpvError | any) {
} catch (err: any | NodeMpvError) {
mpvLog({ action: `Failed to get current time` }, err);
return 0;
}
@@ -442,7 +444,7 @@ app.on('before-quit', async (event) => {
event.preventDefault();
await getMpvInstance()?.stop();
await quit();
} catch (err: NodeMpvError | any) {
} catch (err: any | NodeMpvError) {
mpvLog({ action: `Failed to cleanly before-quit` }, err);
} finally {
mpvState = MpvState.DONE;
+1 -1
View File
@@ -1,5 +1,5 @@
/* eslint-disable promise/always-return */
import { BrowserWindow, globalShortcut, systemPreferences } from 'electron';
import { isMacOS } from '../../../utils';
import { store } from '../settings';
+101 -101
View File
@@ -1,23 +1,36 @@
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 { app, ipcMain } from 'electron';
import { Server as WsServer, WebSocketServer, WebSocket } from 'ws';
import { promises, Stats } from 'fs';
import { readFile } from 'fs/promises';
import { createServer, IncomingMessage, Server, ServerResponse } from 'http';
import { join } from 'path';
import { WebSocket, WebSocketServer, Server as WsServer } from 'ws';
import { deflate, gzip } from 'zlib';
import manifest from './manifest.json';
import { ClientEvent, ServerEvent } from '../../../../remote/types';
import { PlayerRepeat, PlayerStatus, SongState } from '../../../../renderer/types';
import { getMainWindow } from '../../../main';
import { isLinux } from '../../../utils';
import type { QueueSong } from '/@/renderer/api/types';
import { getMainWindow } from '/@/main/index';
import { isLinux } from '/@/main/utils';
import { QueueSong } from '/@/shared/types/domain-types';
import { ClientEvent, ServerEvent } from '/@/shared/types/remote-types';
import { PlayerRepeat, PlayerStatus, SongState } from '/@/shared/types/types';
let mprisPlayer: any | undefined;
if (isLinux()) {
// eslint-disable-next-line global-require
mprisPlayer = require('../../linux/mpris').mprisPlayer;
async function initMpris() {
if (isLinux()) {
const mpris = await import('../../linux/mpris');
mprisPlayer = mpris.mprisPlayer;
}
}
initMpris();
interface MimeType {
css: string;
html: string;
ico: string;
js: string;
}
interface RemoteConfig {
@@ -27,21 +40,13 @@ interface RemoteConfig {
username: string;
}
interface MimeType {
css: string;
html: string;
ico: string;
js: string;
}
declare class StatefulWebSocket extends WebSocket {
alive: boolean;
auth: boolean;
}
let server: Server | undefined;
let wsServer: WsServer<typeof StatefulWebSocket> | undefined;
let wsServer: undefined | WsServer<typeof StatefulWebSocket>;
const settings: RemoteConfig = {
enabled: false,
@@ -54,14 +59,6 @@ type SendData = ServerEvent & {
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 {
if (wsServer) {
for (const client of wsServer.clients) {
@@ -70,7 +67,15 @@ function broadcast(message: ServerEvent): void {
}
}
const shutdownServer = () => {
function send({ client, data, event }: SendData): void {
if (client.readyState === WebSocket.OPEN) {
if (client.alive && client.auth) {
client.send(JSON.stringify({ data, event }));
}
}
}
export const shutdownServer = () => {
if (wsServer) {
wsServer.clients.forEach((client) => client.close(4000));
wsServer.close();
@@ -121,21 +126,17 @@ const getEncoding = (encoding: string | string[]): Encoding => {
const cache = new Map<string, Map<Encoding, [number, Buffer]>>();
function setOk(
res: ServerResponse,
mtimeMs: number,
extension: keyof MimeType,
encoding: Encoding,
data?: Buffer,
) {
res.statusCode = data ? 200 : 304;
function authorize(req: IncomingMessage): boolean {
if (settings.username || settings.password) {
// https://stackoverflow.com/questions/23616371/basic-http-authentication-with-node-and-express-4
res.setHeader('Content-Type', MIME_TYPES[extension]);
res.setHeader('ETag', `"${mtimeMs}"`);
res.setHeader('Cache-Control', 'public');
const authorization = req.headers.authorization?.split(' ')[1] || '';
const [login, password] = Buffer.from(authorization, 'base64').toString().split(':');
if (encoding !== 'none') res.setHeader('Content-Encoding', encoding);
res.end(data);
return login === settings.username && password === settings.password;
}
return true;
}
async function serveFile(
@@ -147,7 +148,7 @@ async function serveFile(
const fileName = `${file}.${extension}`;
const path = app.isPackaged
? join(__dirname, '../remote', fileName)
: join(__dirname, '../../../../../.erb/dll', fileName);
: join(__dirname, '../../out/remote', fileName);
let stats: Stats;
@@ -252,17 +253,21 @@ async function serveFile(
return Promise.resolve();
}
function authorize(req: IncomingMessage): boolean {
if (settings.username || settings.password) {
// https://stackoverflow.com/questions/23616371/basic-http-authentication-with-node-and-express-4
function setOk(
res: ServerResponse,
mtimeMs: number,
extension: keyof MimeType,
encoding: Encoding,
data?: Buffer,
) {
res.statusCode = data ? 200 : 304;
const authorization = req.headers.authorization?.split(' ')[1] || '';
const [login, password] = Buffer.from(authorization, 'base64').toString().split(':');
res.setHeader('Content-Type', MIME_TYPES[extension]);
res.setHeader('ETag', `"${mtimeMs}"`);
res.setHeader('Cache-Control', 'public');
return login === settings.username && password === settings.password;
}
return true;
if (encoding !== 'none') res.setHeader('Content-Encoding', encoding);
res.end(data);
}
const enableServer = (config: RemoteConfig): Promise<void> => {
@@ -286,28 +291,28 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
await serveFile(req, 'index', 'html', res);
break;
}
case '/credentials': {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end(req.headers.authorization);
break;
}
case '/favicon.ico': {
await serveFile(req, 'favicon', 'ico', res);
break;
}
case '/remote.css': {
await serveFile(req, 'remote', 'css', res);
break;
}
case '/remote.js': {
await serveFile(req, 'remote', 'js', res);
break;
}
case '/manifest.json': {
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(manifest));
break;
}
case '/credentials': {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end(req.headers.authorization);
case '/remote.css': {
await serveFile(req, 'remote', 'css', res);
break;
}
case '/remote.js': {
await serveFile(req, 'remote', 'js', res);
break;
}
default: {
@@ -371,6 +376,21 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
}
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': {
getMainWindow()?.webContents.send('renderer-player-pause');
break;
@@ -379,10 +399,6 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
getMainWindow()?.webContents.send('renderer-player-play');
break;
}
case 'next': {
getMainWindow()?.webContents.send('renderer-player-next');
break;
}
case 'previous': {
getMainWindow()?.webContents.send('renderer-player-previous');
break;
@@ -421,6 +437,17 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
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': {
getMainWindow()?.webContents.send('renderer-player-toggle-repeat');
break;
@@ -450,28 +477,6 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
}
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': {
const { position } = json;
if (mprisPlayer) {
@@ -631,15 +636,10 @@ ipcMain.on('update-volume', (_event, volume: number) => {
if (mprisPlayer) {
mprisPlayer.on('loopStatus', (event: string) => {
const repeat =
event === 'Playlist'
? PlayerRepeat.ALL
: event === 'Track'
? PlayerRepeat.ONE
: PlayerRepeat.NONE;
const repeat = event === 'Playlist' ? 'all' : event === 'Track' ? 'one' : 'none';
currentState.repeat = repeat;
broadcast({ data: repeat, event: 'repeat' });
currentState.repeat = repeat as PlayerRepeat;
broadcast({ data: repeat, event: 'repeat' } as ServerEvent);
});
mprisPlayer.on('shuffle', (shuffle: boolean) => {
+13 -3
View File
@@ -1,6 +1,7 @@
import { ipcMain, nativeTheme, safeStorage } from 'electron';
import type { TitleTheme } from '/@/shared/types/types';
import { dialog, ipcMain, nativeTheme, OpenDialogOptions, safeStorage } from 'electron';
import Store from 'electron-store';
import type { TitleTheme } from '/@/renderer/types';
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);
});
ipcMain.handle('password-get', (_event, server: string): string | null => {
ipcMain.handle('password-get', (_event, server: string): null | string => {
if (safeStorage.isEncryptionAvailable()) {
const servers = store.get('server') as Record<string, string> | undefined;
@@ -54,3 +55,12 @@ ipcMain.on('theme-set', (_event, theme: TitleTheme) => {
store.set('theme', theme);
nativeTheme.themeSource = theme;
});
ipcMain.handle('open-file-selector', async (_event, options: OpenDialogOptions) => {
const result = await dialog.showOpenDialog({
...options,
properties: ['openFile'],
});
return result.filePaths[0] || null;
});
+1 -3
View File
@@ -1,4 +1,2 @@
import './core';
// eslint-disable-next-line import/no-dynamic-require
require(`./${process.platform}`);
import(`./${process.platform}`);
+5 -4
View File
@@ -1,8 +1,9 @@
import { ipcMain } from 'electron';
import Player from 'mpris-service';
import { PlayerRepeat, PlayerStatus } from '../../../renderer/types';
import { getMainWindow } from '../../main';
import { QueueSong } from '/@/renderer/api/types';
import { getMainWindow } from '/@/main/index';
import { QueueSong } from '/@/shared/types/domain-types';
import { PlayerRepeat, PlayerStatus } from '/@/shared/types/types';
const mprisPlayer = Player({
identity: 'Feishin',
@@ -124,8 +125,8 @@ ipcMain.on('update-playback', (_event, status: PlayerStatus) => {
const REPEAT_TO_MPRIS: Record<PlayerRepeat, string> = {
[PlayerRepeat.ALL]: 'Playlist',
[PlayerRepeat.ONE]: 'Track',
[PlayerRepeat.NONE]: 'None',
[PlayerRepeat.ONE]: 'Track',
};
ipcMain.on('update-repeat', (_event, arg: PlayerRepeat) => {
+76 -56
View File
@@ -1,51 +1,42 @@
/* eslint global-require: off, no-console: off, promise/always-return: off */
/**
* 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 { is } from '@electron-toolkit/utils';
import {
app,
BrowserWindow,
shell,
ipcMain,
BrowserWindowConstructorOptions,
globalShortcut,
Tray,
ipcMain,
Menu,
nativeImage,
nativeTheme,
BrowserWindowConstructorOptions,
protocol,
net,
protocol,
Rectangle,
screen,
shell,
Tray,
} from 'electron';
import electronLocalShortcut from 'electron-localshortcut';
import log from 'electron-log/main';
import { autoUpdater } from 'electron-updater';
import { access, constants, readFile, writeFile } from 'fs';
import path, { join } from 'path';
import { deflate, inflate } from 'zlib';
import { disableMediaKeys, enableMediaKeys } from './features/core/player/media-keys';
import { store } from './features/core/settings/index';
import { shutdownServer } from './features/core/remote';
import { store } from './features/core/settings';
import MenuBuilder from './menu';
import {
autoUpdaterLogInterface,
createLog,
hotkeyToElectronAccelerator,
isLinux,
isMacOS,
isWindows,
resolveHtmlPath,
createLog,
autoUpdaterLogInterface,
} from './utils';
import './features';
import type { TitleTheme } from '/@/renderer/types';
declare module 'node-mpv';
import { TitleTheme } from '/@/shared/types/types';
export default class AppUpdater {
constructor() {
@@ -72,34 +63,51 @@ if (isLinux() && !process.argv.some((a) => a.startsWith('--password-store='))) {
}
let mainWindow: BrowserWindow | null = null;
let tray: Tray | null = null;
let tray: null | Tray = null;
let exitFromTray = false;
let forceQuit = false;
if (process.env.NODE_ENV === 'production') {
const sourceMapSupport = require('source-map-support');
sourceMapSupport.install();
import('source-map-support').then((sourceMapSupport) => {
sourceMapSupport.install();
});
}
const isDevelopment = process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true';
if (isDevelopment) {
require('electron-debug')();
import('electron-debug').then((electronDebug) => {
electronDebug.default();
});
}
const installExtensions = async () => {
const installer = require('electron-devtools-installer');
const forceDownload = !!process.env.UPGRADE_EXTENSIONS;
const extensions = ['REACT_DEVELOPER_TOOLS', 'REDUX_DEVTOOLS'];
import('electron-devtools-installer').then((installer) => {
const forceDownload = !!process.env.UPGRADE_EXTENSIONS;
const extensions = ['REACT_DEVELOPER_TOOLS', 'REDUX_DEVTOOLS'];
return installer
.default(
extensions.map((name) => installer[name]),
forceDownload,
)
.catch(console.log);
return installer
.default(
extensions.map((name) => installer[name]),
forceDownload,
)
.then((installedExtensions) => {
createLog({
message: `Installed extension: ${installedExtensions}`,
type: 'info',
});
})
.catch(console.error);
});
};
const userDataPath = app.getPath('userData');
if (isDevelopment) {
const devUserDataPath = `${userDataPath}-dev`;
app.setPath('userData', devUserDataPath);
}
const RESOURCES_PATH = app.isPackaged
? path.join(process.resourcesPath, 'assets')
: path.join(__dirname, '../../assets');
@@ -117,7 +125,7 @@ export const sendToastToRenderer = ({
type,
}: {
message: string;
type: 'success' | 'error' | 'warning' | 'info';
type: 'error' | 'info' | 'success' | 'warning';
}) => {
getMainWindow()?.webContents.send('toast-from-main', {
message,
@@ -208,7 +216,7 @@ const createTray = () => {
tray.setContextMenu(contextMenu);
};
const createWindow = async (first = true) => {
async function createWindow(first = true): Promise<void> {
if (isDevelopment) {
await installExtensions().catch(console.log);
}
@@ -233,6 +241,7 @@ const createWindow = async (first = true) => {
},
};
// Create the browser window.
mainWindow = new BrowserWindow({
autoHideMenuBar: true,
frame: false,
@@ -247,9 +256,8 @@ const createWindow = async (first = true) => {
contextIsolation: true,
devTools: true,
nodeIntegration: true,
preload: app.isPackaged
? path.join(__dirname, 'preload.js')
: path.join(__dirname, '../../.erb/dll/preload.js'),
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
webSecurity: !store.get('ignore_cors'),
},
width: 1440,
@@ -303,6 +311,7 @@ const createWindow = async (first = true) => {
});
ipcMain.on('window-quit', () => {
shutdownServer();
mainWindow?.close();
app.exit();
});
@@ -374,11 +383,11 @@ const createWindow = async (first = true) => {
enableMediaKeys(mainWindow);
}
mainWindow.loadURL(resolveHtmlPath('index.html'));
const startWindowMinimized = store.get('window_start_minimized', false) as boolean;
mainWindow.on('ready-to-show', () => {
// mainWindow.show()
if (!mainWindow) {
throw new Error('"mainWindow" is not defined');
}
@@ -457,7 +466,7 @@ const createWindow = async (first = true) => {
}
});
mainWindow.on('minimize', (event: any) => {
(mainWindow as any).on('minimize', (event: any) => {
if (store.get('window_minimize_to_tray') === true) {
event.preventDefault();
mainWindow?.hide();
@@ -484,13 +493,25 @@ const createWindow = async (first = true) => {
});
if (store.get('disable_auto_updates') !== true) {
// eslint-disable-next-line
new AppUpdater();
}
const theme = store.get('theme') as TitleTheme | undefined;
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');
@@ -519,6 +540,8 @@ enum BindingActions {
}
const HOTKEY_ACTIONS: Record<BindingActions, () => void> = {
[BindingActions.GLOBAL_SEARCH]: () => {},
[BindingActions.LOCAL_SEARCH]: () => {},
[BindingActions.MUTE]: () => getMainWindow()?.webContents.send('renderer-player-volume-mute'),
[BindingActions.NEXT]: () => getMainWindow()?.webContents.send('renderer-player-next'),
[BindingActions.PAUSE]: () => getMainWindow()?.webContents.send('renderer-player-pause'),
@@ -533,16 +556,14 @@ const HOTKEY_ACTIONS: Record<BindingActions, () => void> = {
[BindingActions.SKIP_FORWARD]: () =>
getMainWindow()?.webContents.send('renderer-player-skip-forward'),
[BindingActions.STOP]: () => getMainWindow()?.webContents.send('renderer-player-stop'),
[BindingActions.TOGGLE_FULLSCREEN_PLAYER]: () => {},
[BindingActions.TOGGLE_QUEUE]: () => {},
[BindingActions.TOGGLE_REPEAT]: () =>
getMainWindow()?.webContents.send('renderer-player-toggle-repeat'),
[BindingActions.VOLUME_UP]: () =>
getMainWindow()?.webContents.send('renderer-player-volume-up'),
[BindingActions.VOLUME_DOWN]: () =>
getMainWindow()?.webContents.send('renderer-player-volume-down'),
[BindingActions.GLOBAL_SEARCH]: () => {},
[BindingActions.LOCAL_SEARCH]: () => {},
[BindingActions.TOGGLE_QUEUE]: () => {},
[BindingActions.TOGGLE_FULLSCREEN_PLAYER]: () => {},
[BindingActions.VOLUME_UP]: () =>
getMainWindow()?.webContents.send('renderer-player-volume-up'),
};
ipcMain.on(
@@ -585,7 +606,7 @@ ipcMain.on(
_event,
data: {
message: string;
type: 'debug' | 'verbose' | 'success' | 'error' | 'warning' | 'info';
type: 'debug' | 'error' | 'info' | 'success' | 'verbose' | 'warning';
},
) => {
createLog(data);
@@ -597,7 +618,6 @@ app.on('window-all-closed', () => {
// Respect the OSX convention of having the application in memory even
// after all windows have been closed
if (isMacOS()) {
ipcMain.removeHandler('window-clear-cache');
mainWindow = null;
} else {
app.quit();
@@ -613,7 +633,7 @@ const FONT_HEADERS = [
'font/woff2',
];
const singleInstance = app.requestSingleInstanceLock();
const singleInstance = isDevelopment ? true : app.requestSingleInstanceLock();
if (!singleInstance) {
app.quit();
+32 -32
View File
@@ -1,4 +1,4 @@
import { app, Menu, shell, BrowserWindow, MenuItemConstructorOptions } from 'electron';
import { app, BrowserWindow, Menu, MenuItemConstructorOptions, shell } from 'electron';
interface DarwinMenuItemConstructorOptions extends MenuItemConstructorOptions {
selector?: string;
@@ -12,37 +12,6 @@ export default class MenuBuilder {
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[] {
const subMenuAbout: DarwinMenuItemConstructorOptions = {
label: 'Electron',
@@ -276,4 +245,35 @@ export default class MenuBuilder {
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 });
});
}
}
-23
View File
@@ -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,
});
+2 -3
View File
@@ -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 process from 'process';
import { URL } from 'url';
import log from 'electron-log/main';
export let resolveHtmlPath: (htmlFileName: string) => string;
@@ -76,7 +75,7 @@ const logColor = {
export const createLog = (data: {
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]}`);
};
+15
View File
@@ -0,0 +1,15 @@
import { ElectronAPI } from '@electron-toolkit/preload';
import { PreloadApi } from './index';
declare global {
interface Window {
api: PreloadApi;
electron: ElectronAPI;
queryLocalFonts?: () => Promise<Font[]>;
SERVER_LOCK?: boolean;
SERVER_NAME?: string;
SERVER_TYPE?: ServerType;
SERVER_URL?: string;
}
}
+45
View 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,13 @@
import { IpcRendererEvent, ipcRenderer, webFrame } from 'electron';
import { ipcRenderer, IpcRendererEvent, OpenDialogOptions, webFrame } from 'electron';
import Store from 'electron-store';
import { toServerType, type TitleTheme } from '/@/renderer/types';
import { TitleTheme } from '/@/shared/types/types';
const store = new Store();
const set = (
property: string,
value: string | Record<string, unknown> | boolean | string[] | undefined,
value: boolean | Record<string, unknown> | string | string[] | undefined,
) => {
if (value === undefined) {
store.delete(property);
@@ -32,7 +33,7 @@ const disableMediaKeys = () => {
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);
};
@@ -56,6 +57,24 @@ const themeSet = (theme: TitleTheme): void => {
ipcRenderer.send('theme-set', theme);
};
const openFileSelector = async (options?: OpenDialogOptions) => {
const result = await ipcRenderer.invoke('open-file-selector', options);
return result;
};
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 env = {
@@ -73,6 +92,7 @@ export const localSettings = {
env,
fontError,
get,
openFileSelector,
passwordGet,
passwordRemove,
passwordSet,
@@ -1,11 +1,13 @@
import { ipcRenderer } from 'electron';
import {
InternetProviderLyricSearchResponse,
LyricGetQuery,
LyricSearchQuery,
LyricSource,
QueueSong,
} from '/@/renderer/api/types';
} from '../main/features/core/lyrics';
import { QueueSong } from '/@/shared/types/domain-types';
const getRemoteLyricsBySong = (song: QueueSong) => {
const result = ipcRenderer.invoke('lyric-by-song', song);
@@ -1,5 +1,6 @@
import { IpcRendererEvent, ipcRenderer } from 'electron';
import type { PlayerRepeat } from '/@/renderer/types';
import { ipcRenderer, IpcRendererEvent } from 'electron';
import { PlayerRepeat } from '/@/shared/types/types';
const updatePosition = (timeSec: number) => {
ipcRenderer.send('mpris-update-position', timeSec);
@@ -1,5 +1,6 @@
import { ipcRenderer, IpcRendererEvent } from 'electron';
import { PlayerData } from '/@/renderer/store';
import { PlayerData } from '/@/shared/types/domain-types';
const initialize = (data: { extraParameters?: string[]; properties?: Record<string, any> }) => {
return ipcRenderer.invoke('player-initialize', data);
@@ -187,8 +188,8 @@ export const mpvPlayerListener = {
rendererNext,
rendererPause,
rendererPlay,
rendererPlayPause,
rendererPlayerFallback,
rendererPlayPause,
rendererPrevious,
rendererQuit,
rendererSkipBackward,
@@ -1,6 +1,7 @@
import { IpcRendererEvent, ipcRenderer } from 'electron';
import { QueueSong } from '/@/renderer/api/types';
import { PlayerStatus } from '/@/renderer/types';
import { ipcRenderer, IpcRendererEvent } from 'electron';
import { QueueSong } from '/@/shared/types/domain-types';
import { PlayerStatus } from '/@/shared/types/types';
const requestFavorite = (
cb: (
@@ -29,12 +30,12 @@ const requestVolume = (cb: (event: IpcRendererEvent, data: { volume: number }) =
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);
return result;
};
const setRemotePort = (port: number): Promise<string | null> => {
const setRemotePort = (port: number): Promise<null | string> => {
const result = ipcRenderer.invoke('remote-port', port);
return result;
};
@@ -56,7 +57,7 @@ const updateSetting = (
port: number,
username: string,
password: string,
): Promise<string | null> => {
): Promise<null | string> => {
return ipcRenderer.invoke('remote-settings', enabled, port, username, password);
};
@@ -1,6 +1,6 @@
import { IpcRendererEvent, ipcRenderer } from 'electron';
import { isMacOS, isWindows, isLinux } from '../utils';
import { PlayerState } from '/@/renderer/store';
import { ipcRenderer, IpcRendererEvent } from 'electron';
import { isLinux, isMacOS, isWindows } from '../main/utils';
const saveQueue = (data: Record<string, any>) => {
ipcRenderer.send('player-save-queue', data);
@@ -18,7 +18,7 @@ const onSaveQueue = (cb: (event: IpcRendererEvent) => void) => {
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);
};
@@ -29,7 +29,7 @@ const playerErrorListener = (cb: (event: IpcRendererEvent, data: { code: number
const mainMessageListener = (
cb: (
event: IpcRendererEvent,
data: { message: string; type: 'success' | 'error' | 'warning' | 'info' },
data: { message: string; type: 'error' | 'info' | 'success' | 'warning' },
) => void,
) => {
ipcRenderer.on('toast-from-main', cb);
@@ -40,7 +40,7 @@ const logger = (
event: IpcRendererEvent,
data: {
message: string;
type: 'debug' | 'verbose' | 'error' | 'warning' | 'info';
type: 'debug' | 'error' | 'info' | 'verbose' | 'warning';
},
) => void,
) => {
+14 -66
View File
@@ -1,8 +1,15 @@
import { useEffect } from 'react';
import { MantineProvider } from '@mantine/core';
import './styles/global.scss';
import { useIsDark, useReconnect } from '/@/remote/store';
import '@mantine/core/styles.css';
import '@mantine/notifications/styles.css';
import '/@/shared/styles/global.css';
import { useEffect } from 'react';
import { Shell } from '/@/remote/components/shell';
import { useIsDark, useReconnect } from '/@/remote/store';
import { useAppTheme } from '/@/renderer/themes/use-app-theme';
import { AppTheme } from '/@/shared/themes/app-theme-types';
export const App = () => {
const isDark = useIsDark();
@@ -12,71 +19,12 @@ export const App = () => {
reconnect();
}, [reconnect]);
const { mode, theme } = useAppTheme(isDark ? AppTheme.DEFAULT_DARK : AppTheme.DEFAULT_LIGHT);
return (
<MantineProvider
withGlobalStyles
withNormalizeCSS
theme={{
colorScheme: isDark ? 'dark' : 'light',
components: {
AppShell: {
styles: {
body: {
height: '100vh',
overflow: 'scroll',
},
},
},
Modal: {
styles: {
body: {
background: 'var(--modal-bg)',
height: '100vh',
},
close: { marginRight: '0.5rem' },
content: { borderRadius: '5px' },
header: {
background: 'var(--modal-header-bg)',
paddingBottom: '1rem',
},
title: { fontSize: 'medium', fontWeight: 500 },
},
},
},
defaultRadius: 'xs',
dir: 'ltr',
focusRing: 'auto',
focusRingStyles: {
inputStyles: () => ({
border: '1px solid var(--primary-color)',
}),
resetStyles: () => ({ outline: 'none' }),
styles: () => ({
outline: '1px solid var(--primary-color)',
outlineOffset: '-1px',
}),
},
fontFamily: 'var(--content-font-family)',
fontSizes: {
lg: '1.1rem',
md: '1rem',
sm: '0.9rem',
xl: '1.5rem',
xs: '0.8rem',
},
headings: {
fontFamily: 'var(--content-font-family)',
fontWeight: 700,
},
other: {},
spacing: {
lg: '2rem',
md: '1rem',
sm: '0.5rem',
xl: '4rem',
xs: '0rem',
},
}}
defaultColorScheme={mode}
theme={theme}
>
<Shell />
</MantineProvider>
@@ -1,20 +1,21 @@
import { CiImageOff, CiImageOn } from 'react-icons/ci';
import { RemoteButton } from '/@/remote/components/buttons/remote-button';
import { useShowImage, useToggleShowImage } from '/@/remote/store';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
export const ImageButton = () => {
const showImage = useShowImage();
const toggleImage = useToggleShowImage();
return (
<RemoteButton
mr={5}
size="xl"
tooltip={showImage ? 'Hide Image' : 'Show Image'}
variant="default"
<ActionIcon
onClick={() => toggleImage()}
tooltip={{
label: showImage ? 'Hide Image' : 'Show Image',
}}
variant="default"
>
{showImage ? <CiImageOff size={30} /> : <CiImageOn size={30} />}
</RemoteButton>
</ActionIcon>
);
};
@@ -1,21 +1,24 @@
import { RemoteButton } from '/@/remote/components/buttons/remote-button';
import { useConnected, useReconnect } from '/@/remote/store';
import { RiRestartLine } from 'react-icons/ri';
import { useConnected, useReconnect } from '/@/remote/store';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
export const ReconnectButton = () => {
const connected = useConnected();
const reconnect = useReconnect();
return (
<RemoteButton
$active={!connected}
mr={5}
size="xl"
tooltip={connected ? 'Reconnect' : 'Not connected. Reconnect.'}
variant="default"
<ActionIcon
onClick={() => reconnect()}
tooltip={{
label: connected ? 'Reconnect' : 'Not connected. Reconnect.',
}}
variant="default"
>
<RiRestartLine size={30} />
</RemoteButton>
<RiRestartLine
color={connected ? 'var(--theme-colors-primary)' : 'var(--theme-colors-foreground)'}
size={30}
/>
</ActionIcon>
);
};

Some files were not shown because too many files have changed in this diff Show More