├── .bitmap ├── .editorconfig ├── .erb ├── configs │ ├── .eslintrc │ ├── webpack.config.base.ts │ ├── webpack.config.eslint.ts │ ├── webpack.config.main.prod.ts │ ├── webpack.config.renderer.dev.dll.ts │ ├── webpack.config.renderer.dev.ts │ ├── webpack.config.renderer.prod.ts │ └── webpack.paths.ts ├── img │ ├── erb-banner.svg │ └── erb-logo.png ├── mocks │ └── fileMock.js └── scripts │ ├── .eslintrc │ ├── check-build-exists.ts │ ├── check-native-dep.js │ ├── check-node-env.js │ ├── check-port-in-use.js │ ├── clean.js │ ├── delete-source-maps.js │ ├── electron-rebuild.js │ ├── link-modules.ts │ └── notarize.js ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── 1-Bug_report_app.md │ ├── 2-Bug_report_dev.md │ ├── 3-Question.md │ └── 4-Feature_request.md ├── config.yml ├── stale.yml └── workflows │ ├── codeql-analysis.yml │ ├── publish.yml │ └── test.yml ├── .gitignore ├── .husky └── pre-commit ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── LICENSE ├── README.md ├── TRANSLATING.md ├── assets ├── assets.d.ts ├── data │ └── tachiyomi-model.proto ├── entitlements.mac.plist ├── icons │ ├── app.png │ ├── app_default │ │ ├── 1024x1024.png │ │ ├── 128x128.png │ │ ├── 16x16.png │ │ ├── 24x24.png │ │ ├── 256x256.png │ │ ├── 32x32.png │ │ ├── 48x48.png │ │ ├── 512x512.png │ │ ├── 64x64.png │ │ ├── 96x96.png │ │ ├── icon.icns │ │ ├── icon.ico │ │ ├── icon.png │ │ └── icon.svg │ ├── icon.icns │ ├── icon.ico │ ├── icon.png │ ├── icon.svg │ ├── login │ │ ├── anilist │ │ │ ├── opaque.png │ │ │ └── transparent.png │ │ └── myanimelist │ │ │ ├── opaque.png │ │ │ └── transparent.png │ └── main │ │ ├── 1024x1024.png │ │ ├── 128x128.png │ │ ├── 16x16.png │ │ ├── 24x24.png │ │ ├── 256x256.png │ │ ├── 32x32.png │ │ ├── 48x48.png │ │ ├── 512x512.png │ │ ├── 64x64.png │ │ └── 96x96.png └── images │ └── nocover_dark.png ├── package-lock.json ├── package.json ├── patches └── node-polyfill-webpack-plugin+1.1.4.patch ├── release └── app │ ├── package-lock.json │ └── package.json ├── src ├── __tests__ │ └── App.test.tsx ├── index.d.ts ├── main │ ├── main.ts │ ├── menu.ts │ ├── preload.js │ ├── sources │ │ ├── handler.ts │ │ └── static │ │ │ └── base.ts │ ├── util.ts │ └── util │ │ ├── cache.ts │ │ ├── manga.ts │ │ ├── mangaupdate.ts │ │ ├── misc.ts │ │ ├── read.ts │ │ ├── reader.ts │ │ ├── rpc.ts │ │ ├── settings.ts │ │ ├── source.ts │ │ └── theme.ts ├── renderer │ ├── App.tsx │ ├── components │ │ ├── button.tsx │ │ ├── chapter.tsx │ │ ├── chaptermodal.tsx │ │ ├── context │ │ │ └── reader.tsx │ │ ├── defer.tsx │ │ ├── dialog.tsx │ │ ├── filter.tsx │ │ ├── filtersettings.tsx │ │ ├── lightbar.tsx │ │ ├── loading.tsx │ │ ├── loginitem.tsx │ │ ├── mangaitem.tsx │ │ ├── readerbutton.tsx │ │ ├── search.tsx │ │ ├── select.tsx │ │ ├── settings │ │ │ ├── downloadlocation.tsx │ │ │ ├── filterslider.tsx │ │ │ ├── themebutton.tsx │ │ │ └── themeswitch.tsx │ │ ├── settingsmodal.tsx │ │ ├── shortpagination.tsx │ │ ├── switch.tsx │ │ ├── tabs.tsx │ │ ├── tag.tsx │ │ ├── textfield.tsx │ │ ├── topbar.tsx │ │ ├── trackeritem.tsx │ │ └── trackermodal.tsx │ ├── css │ │ └── App.css │ ├── index.ejs │ ├── index.tsx │ ├── pages │ │ ├── 404.tsx │ │ ├── library.tsx │ │ ├── login.tsx │ │ ├── reader.tsx │ │ ├── search.tsx │ │ ├── settings.tsx │ │ ├── sources.tsx │ │ └── view.tsx │ └── util │ │ ├── auxiliary.ts │ │ ├── func.tsx │ │ ├── hook │ │ ├── useevent.ts │ │ ├── useforceupdate.ts │ │ ├── usekeyboard.ts │ │ ├── usemounteffect.ts │ │ ├── useonscreen.ts │ │ ├── usequery.ts │ │ └── usethrottle.ts │ │ ├── search.ts │ │ └── tracker │ │ └── tracker.ts └── shared │ ├── intl.ts │ ├── locale │ └── en.json │ ├── theme │ └── default │ │ ├── dark │ │ └── colors.json │ │ ├── light │ │ └── colors.json │ │ └── metadata.json │ └── util.ts └── tsconfig.json /.bitmap: -------------------------------------------------------------------------------- 1 | /* THIS IS A BIT-AUTO-GENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. */ 2 | 3 | /** 4 | * The Bitmap file is an auto generated file used by Bit to track all your Bit components. It maps the component to a folder in your file system. 5 | * This file should be committed to VCS(version control). 6 | * Components are listed using their component ID (https://harmony-docs.bit.dev/aspects/component/#component-id). 7 | * If you want to delete components you can use the "bit remove " command. 8 | * See the docs (https://harmony-docs.bit.dev/building-with-bit/removing-components) for more information, or use "bit remove --help". 9 | */ 10 | 11 | { 12 | "$schema-version": "14.9.0" 13 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.erb/configs/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": "off", 4 | "global-require": "off", 5 | "import/no-dynamic-require": "off" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.base.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Base webpack config used across other specific configs 3 | */ 4 | 5 | import webpack from 'webpack'; 6 | import webpackPaths from './webpack.paths'; 7 | // import NodePolyFillPlugin from 'node-polyfill-webpack-plugin'; 8 | import { dependencies as externals } from '../../release/app/package.json'; 9 | 10 | const configuration: webpack.Configuration = { 11 | externals: [...Object.keys(externals || {})], 12 | 13 | stats: 'errors-only', 14 | 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.[jt]sx?$/, 19 | exclude: /node_modules/, 20 | use: { 21 | loader: 'ts-loader', 22 | options: { 23 | // Remove this line to enable type checking in webpack builds 24 | transpileOnly: true, 25 | }, 26 | }, 27 | }, 28 | { 29 | test: /\.[jt]sx?$/, 30 | loader: 'esbuild-loader', 31 | options: { 32 | loader: 'tsx', // Or 'ts' if you don't need tsx 33 | target: 'esnext', 34 | }, 35 | }, 36 | { 37 | test: /\.[jt]sx?$/, 38 | exclude: /(node_modules|bower_components)/, 39 | use: { 40 | loader: 'swc-loader', 41 | options: { 42 | jsc: { 43 | parser: { 44 | syntax: 'typescript', 45 | }, 46 | }, 47 | }, 48 | }, 49 | }, 50 | { 51 | test: /\.node$/, 52 | use: { 53 | loader: 'node-loader', 54 | }, 55 | }, 56 | ], 57 | }, 58 | 59 | output: { 60 | path: webpackPaths.srcPath, 61 | // https://github.com/webpack/webpack/issues/1114 62 | library: { 63 | type: 'commonjs2', 64 | }, 65 | }, 66 | 67 | /** 68 | * Determine the array of extensions that should be used to resolve modules. 69 | */ 70 | resolve: { 71 | extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'], 72 | modules: [webpackPaths.srcPath, 'node_modules'], 73 | alias: { 74 | process: 'process/browser.js', 75 | }, 76 | fallback: {}, 77 | }, 78 | 79 | plugins: [ 80 | new webpack.EnvironmentPlugin({ 81 | NODE_ENV: 'production', 82 | }), 83 | new webpack.ProvidePlugin({ 84 | React: 'react', 85 | ReactDOM: 'react-dom', 86 | }), 87 | ], 88 | }; 89 | 90 | export default configuration; 91 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.eslint.ts: -------------------------------------------------------------------------------- 1 | /* eslint import/no-unresolved: off, import/no-self-import: off */ 2 | 3 | module.exports = require('./webpack.config.renderer.dev').default; 4 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.main.prod.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Webpack config for production electron main process 3 | */ 4 | 5 | import path from 'path'; 6 | import webpack from 'webpack'; 7 | import { merge } from 'webpack-merge'; 8 | import TerserPlugin from 'terser-webpack-plugin'; 9 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 10 | import baseConfig from './webpack.config.base'; 11 | import webpackPaths from './webpack.paths'; 12 | import checkNodeEnv from '../scripts/check-node-env'; 13 | import deleteSourceMaps from '../scripts/delete-source-maps'; 14 | 15 | checkNodeEnv('production'); 16 | deleteSourceMaps(); 17 | 18 | const devtoolsConfig = 19 | process.env.DEBUG_PROD === 'true' 20 | ? { 21 | devtool: 'source-map', 22 | } 23 | : {}; 24 | 25 | const configuration: webpack.Configuration = { 26 | ...devtoolsConfig, 27 | 28 | mode: 'production', 29 | 30 | target: 'electron-main', 31 | 32 | entry: { 33 | main: path.join(webpackPaths.srcMainPath, 'main.ts'), 34 | preload: path.join(webpackPaths.srcMainPath, 'preload.js'), 35 | }, 36 | 37 | output: { 38 | path: webpackPaths.distMainPath, 39 | filename: '[name].js', 40 | }, 41 | 42 | optimization: { 43 | minimizer: [ 44 | new TerserPlugin({ 45 | parallel: true, 46 | }), 47 | ], 48 | }, 49 | 50 | plugins: [ 51 | new BundleAnalyzerPlugin({ 52 | analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled', 53 | }), 54 | 55 | /** 56 | * Create global constants which can be configured at compile time. 57 | * 58 | * Useful for allowing different behaviour between development builds and 59 | * release builds 60 | * 61 | * NODE_ENV should be production so that modules do not perform certain 62 | * development checks 63 | */ 64 | new webpack.EnvironmentPlugin({ 65 | NODE_ENV: 'production', 66 | DEBUG_PROD: false, 67 | START_MINIMIZED: false, 68 | }), 69 | ], 70 | 71 | /** 72 | * Disables webpack processing of __dirname and __filename. 73 | * If you run the bundle in node.js it falls back to these values of node.js. 74 | * https://github.com/webpack/webpack/issues/2010 75 | */ 76 | node: { 77 | __dirname: false, 78 | __filename: false, 79 | }, 80 | }; 81 | 82 | export default merge(baseConfig, configuration); 83 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.renderer.dev.dll.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Builds the DLL for development electron renderer process 3 | */ 4 | 5 | import webpack from 'webpack'; 6 | import path from 'path'; 7 | import { merge } from 'webpack-merge'; 8 | import baseConfig from './webpack.config.base'; 9 | import webpackPaths from './webpack.paths'; 10 | import { dependencies } from '../../package.json'; 11 | import checkNodeEnv from '../scripts/check-node-env'; 12 | 13 | checkNodeEnv('development'); 14 | 15 | const dist = webpackPaths.dllPath; 16 | 17 | const configuration: webpack.Configuration = { 18 | context: webpackPaths.rootPath, 19 | 20 | devtool: 'eval', 21 | 22 | mode: 'development', 23 | 24 | target: 'electron-renderer', 25 | 26 | externals: ['fsevents', 'crypto-browserify'], 27 | 28 | /** 29 | * Use `module` from `webpack.config.renderer.dev.js` 30 | */ 31 | module: require('./webpack.config.renderer.dev').default.module, 32 | 33 | entry: { 34 | renderer: Object.keys(dependencies || {}), 35 | }, 36 | 37 | output: { 38 | path: dist, 39 | filename: '[name].dev.dll.js', 40 | library: { 41 | name: 'renderer', 42 | type: 'var', 43 | }, 44 | }, 45 | 46 | plugins: [ 47 | new webpack.DllPlugin({ 48 | path: path.join(dist, '[name].json'), 49 | name: '[name]', 50 | }), 51 | 52 | /** 53 | * Create global constants which can be configured at compile time. 54 | * 55 | * Useful for allowing different behaviour between development builds and 56 | * release builds 57 | * 58 | * NODE_ENV should be production so that modules do not perform certain 59 | * development checks 60 | */ 61 | new webpack.EnvironmentPlugin({ 62 | NODE_ENV: 'development', 63 | }), 64 | 65 | new webpack.LoaderOptionsPlugin({ 66 | debug: true, 67 | options: { 68 | context: webpackPaths.srcPath, 69 | output: { 70 | path: webpackPaths.dllPath, 71 | }, 72 | }, 73 | }), 74 | ], 75 | }; 76 | 77 | export default merge(baseConfig, configuration); 78 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.renderer.dev.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import webpack from 'webpack'; 4 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 5 | // import NodePolyFillPlugin from 'node-polyfill-webpack-plugin'; 6 | import chalk from 'chalk'; 7 | import { merge } from 'webpack-merge'; 8 | import { spawn, execSync } from 'child_process'; 9 | import baseConfig from './webpack.config.base'; 10 | import webpackPaths from './webpack.paths'; 11 | import checkNodeEnv from '../scripts/check-node-env'; 12 | import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin'; 13 | 14 | // When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's 15 | // at the dev webpack config is not accidentally run in a production environment 16 | if (process.env.NODE_ENV === 'production') { 17 | checkNodeEnv('development'); 18 | } 19 | 20 | const port = process.env.PORT || 4123; 21 | const manifest = path.resolve(webpackPaths.dllPath, 'renderer.json'); 22 | const requiredByDLLConfig = module.parent!.filename.includes( 23 | 'webpack.config.renderer.dev.dll' 24 | ); 25 | 26 | /** 27 | * Warn if the DLL is not built 28 | */ 29 | if ( 30 | !requiredByDLLConfig && 31 | !(fs.existsSync(webpackPaths.dllPath) && fs.existsSync(manifest)) 32 | ) { 33 | console.log( 34 | chalk.black.bgYellow.bold( 35 | 'The DLL files are missing. Sit back while we build them for you with "npm run build-dll"' 36 | ) 37 | ); 38 | execSync('npm run postinstall'); 39 | } 40 | 41 | const configuration: webpack.Configuration = { 42 | devtool: 'inline-source-map', 43 | 44 | mode: 'development', 45 | 46 | target: 'electron-renderer', 47 | 48 | entry: [ 49 | `webpack-dev-server/client?http://localhost:${port}/dist`, 50 | 'webpack/hot/only-dev-server', 51 | path.join(webpackPaths.srcRendererPath, 'index.tsx'), 52 | ], 53 | 54 | output: { 55 | path: webpackPaths.distRendererPath, 56 | publicPath: '/', 57 | filename: 'renderer.dev.js', 58 | }, 59 | 60 | module: { 61 | rules: [ 62 | { 63 | test: /\.s?css$/, 64 | use: [ 65 | 'style-loader', 66 | { 67 | loader: 'css-loader', 68 | options: { 69 | modules: true, 70 | sourceMap: true, 71 | importLoaders: 1, 72 | }, 73 | }, 74 | 'sass-loader', 75 | ], 76 | include: /\.module\.s?(c|a)ss$/, 77 | }, 78 | { 79 | test: /\.s?css$/, 80 | use: ['style-loader', 'css-loader', 'sass-loader'], 81 | exclude: /\.module\.s?(c|a)ss$/, 82 | }, 83 | // Fonts 84 | { 85 | test: /\.(woff|woff2|eot|ttf|otf)$/i, 86 | type: 'asset/resource', 87 | }, 88 | // Images 89 | { 90 | test: /\.(png|svg|jpg|jpeg|gif)$/i, 91 | type: 'asset/resource', 92 | }, 93 | ], 94 | }, 95 | plugins: [ 96 | ...(requiredByDLLConfig 97 | ? [] 98 | : [ 99 | new webpack.DllReferencePlugin({ 100 | context: webpackPaths.dllPath, 101 | manifest: require(manifest), 102 | sourceType: 'var', 103 | }), 104 | ]), 105 | 106 | new webpack.NoEmitOnErrorsPlugin(), 107 | 108 | /** 109 | * Create global constants which can be configured at compile time. 110 | * 111 | * Useful for allowing different behaviour between development builds and 112 | * release builds 113 | * 114 | * NODE_ENV should be production so that modules do not perform certain 115 | * development checks 116 | * 117 | * By default, use 'development' as NODE_ENV. This can be overriden with 118 | * 'staging', for example, by changing the ENV variables in the npm scripts 119 | */ 120 | new webpack.EnvironmentPlugin({ 121 | NODE_ENV: 'development', 122 | }), 123 | 124 | new webpack.LoaderOptionsPlugin({ 125 | debug: true, 126 | }), 127 | 128 | new ReactRefreshWebpackPlugin(), 129 | 130 | new HtmlWebpackPlugin({ 131 | filename: path.join('index.html'), 132 | template: path.join(webpackPaths.srcRendererPath, 'index.ejs'), 133 | minify: { 134 | collapseWhitespace: true, 135 | removeAttributeQuotes: true, 136 | removeComments: true, 137 | }, 138 | isBrowser: false, 139 | env: process.env.NODE_ENV, 140 | isDevelopment: process.env.NODE_ENV !== 'production', 141 | nodeModules: webpackPaths.appNodeModulesPath, 142 | }), 143 | 144 | // new NodePolyFillPlugin(), 145 | ], 146 | 147 | node: { 148 | __dirname: false, 149 | __filename: false, 150 | }, 151 | 152 | // @ts-ignore 153 | devServer: { 154 | port, 155 | compress: true, 156 | hot: true, 157 | headers: { 'Access-Control-Allow-Origin': '*' }, 158 | static: { 159 | publicPath: '/', 160 | }, 161 | historyApiFallback: { 162 | disableDotRule: true, 163 | verbose: true, 164 | }, 165 | onBeforeSetupMiddleware() { 166 | console.log('Starting Main Process...'); 167 | spawn('npm', ['run', 'start:main'], { 168 | shell: true, 169 | env: process.env, 170 | stdio: 'inherit', 171 | }) 172 | .on('close', (code: number) => process.exit(code!)) 173 | .on('error', (spawnError) => console.error(spawnError)); 174 | }, 175 | }, 176 | }; 177 | 178 | export default merge(baseConfig, configuration); 179 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.renderer.prod.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Build config for electron renderer process 3 | */ 4 | 5 | import path from 'path'; 6 | import webpack from 'webpack'; 7 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 8 | import MiniCssExtractPlugin from 'mini-css-extract-plugin'; 9 | // import NodePolyFillPlugin from 'node-polyfill-webpack-plugin'; 10 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 11 | import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'; 12 | import { merge } from 'webpack-merge'; 13 | import TerserPlugin from 'terser-webpack-plugin'; 14 | import baseConfig from './webpack.config.base'; 15 | import webpackPaths from './webpack.paths'; 16 | import checkNodeEnv from '../scripts/check-node-env'; 17 | import deleteSourceMaps from '../scripts/delete-source-maps'; 18 | 19 | checkNodeEnv('production'); 20 | deleteSourceMaps(); 21 | 22 | const devtoolsConfig = 23 | process.env.DEBUG_PROD === 'true' 24 | ? { 25 | devtool: 'source-map', 26 | } 27 | : {}; 28 | 29 | const configuration: webpack.Configuration = { 30 | ...devtoolsConfig, 31 | 32 | mode: 'production', 33 | 34 | target: 'electron-renderer', 35 | 36 | entry: [ 37 | 'core-js', 38 | 'regenerator-runtime/runtime', 39 | path.join(webpackPaths.srcRendererPath, 'index.tsx'), 40 | ], 41 | 42 | output: { 43 | path: webpackPaths.distRendererPath, 44 | publicPath: './', 45 | filename: 'renderer.js', 46 | }, 47 | 48 | module: { 49 | rules: [ 50 | { 51 | test: /\.s?(a|c)ss$/, 52 | use: [ 53 | MiniCssExtractPlugin.loader, 54 | { 55 | loader: 'css-loader', 56 | options: { 57 | modules: true, 58 | sourceMap: true, 59 | importLoaders: 1, 60 | }, 61 | }, 62 | 'sass-loader', 63 | ], 64 | include: /\.module\.s?(c|a)ss$/, 65 | }, 66 | { 67 | test: /\.s?(a|c)ss$/, 68 | use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'], 69 | exclude: /\.module\.s?(c|a)ss$/, 70 | }, 71 | // Fonts 72 | { 73 | test: /\.(woff|woff2|eot|ttf|otf)$/i, 74 | type: 'asset/resource', 75 | }, 76 | // Images 77 | { 78 | test: /\.(png|svg|jpg|jpeg|gif)$/i, 79 | type: 'asset/resource', 80 | }, 81 | ], 82 | }, 83 | 84 | optimization: { 85 | minimize: true, 86 | minimizer: [ 87 | new TerserPlugin({ 88 | parallel: true, 89 | }), 90 | new CssMinimizerPlugin(), 91 | ], 92 | }, 93 | 94 | plugins: [ 95 | /** 96 | * Create global constants which can be configured at compile time. 97 | * 98 | * Useful for allowing different behaviour between development builds and 99 | * release builds 100 | * 101 | * NODE_ENV should be production so that modules do not perform certain 102 | * development checks 103 | */ 104 | new webpack.EnvironmentPlugin({ 105 | NODE_ENV: 'production', 106 | DEBUG_PROD: false, 107 | }), 108 | 109 | new MiniCssExtractPlugin({ 110 | filename: 'style.css', 111 | }), 112 | 113 | new BundleAnalyzerPlugin({ 114 | analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled', 115 | }), 116 | 117 | new HtmlWebpackPlugin({ 118 | filename: 'index.html', 119 | template: path.join(webpackPaths.srcRendererPath, 'index.ejs'), 120 | minify: { 121 | collapseWhitespace: true, 122 | removeAttributeQuotes: true, 123 | removeComments: true, 124 | }, 125 | isBrowser: false, 126 | isDevelopment: process.env.NODE_ENV !== 'production', 127 | }), 128 | 129 | // new NodePolyFillPlugin(), 130 | ], 131 | }; 132 | 133 | export default merge(baseConfig, configuration); 134 | -------------------------------------------------------------------------------- /.erb/configs/webpack.paths.ts: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const rootPath = path.join(__dirname, '../..'); 4 | 5 | const dllPath = path.join(__dirname, '../dll'); 6 | 7 | const srcPath = path.join(rootPath, 'src'); 8 | const srcMainPath = path.join(srcPath, 'main'); 9 | const srcRendererPath = path.join(srcPath, 'renderer'); 10 | 11 | const releasePath = path.join(rootPath, 'release'); 12 | const appPath = path.join(releasePath, 'app'); 13 | const appPackagePath = path.join(appPath, 'package.json'); 14 | const appNodeModulesPath = path.join(appPath, 'node_modules'); 15 | const srcNodeModulesPath = path.join(srcPath, 'node_modules'); 16 | 17 | const distPath = path.join(appPath, 'dist'); 18 | const distMainPath = path.join(distPath, 'main'); 19 | const distRendererPath = path.join(distPath, 'renderer'); 20 | 21 | const buildPath = path.join(releasePath, 'build'); 22 | 23 | export default { 24 | rootPath, 25 | dllPath, 26 | srcPath, 27 | srcMainPath, 28 | srcRendererPath, 29 | releasePath, 30 | appPath, 31 | appPackagePath, 32 | appNodeModulesPath, 33 | srcNodeModulesPath, 34 | distPath, 35 | distMainPath, 36 | distRendererPath, 37 | buildPath, 38 | }; 39 | -------------------------------------------------------------------------------- /.erb/img/erb-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nowaaru/suwariyomi/e07cc3481fb7cdcca680c5f4bb5c341bde48977a/.erb/img/erb-logo.png -------------------------------------------------------------------------------- /.erb/mocks/fileMock.js: -------------------------------------------------------------------------------- 1 | export default 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /.erb/scripts/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": "off", 4 | "global-require": "off", 5 | "import/no-dynamic-require": "off", 6 | "import/no-extraneous-dependencies": "off" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.erb/scripts/check-build-exists.ts: -------------------------------------------------------------------------------- 1 | // Check if the renderer and main bundles are built 2 | import path from 'path'; 3 | import chalk from 'chalk'; 4 | import fs from 'fs'; 5 | import webpackPaths from '../configs/webpack.paths'; 6 | 7 | const mainPath = path.join(webpackPaths.distMainPath, 'main.js'); 8 | const rendererPath = path.join(webpackPaths.distRendererPath, 'renderer.js'); 9 | 10 | if (!fs.existsSync(mainPath)) { 11 | throw new Error( 12 | chalk.whiteBright.bgRed.bold( 13 | 'The main process is not built yet. Build it by running "npm run build:main"' 14 | ) 15 | ); 16 | } 17 | 18 | if (!fs.existsSync(rendererPath)) { 19 | throw new Error( 20 | chalk.whiteBright.bgRed.bold( 21 | 'The renderer process is not built yet. Build it by running "npm run build:renderer"' 22 | ) 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /.erb/scripts/check-native-dep.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import chalk from 'chalk'; 3 | import { execSync } from 'child_process'; 4 | import { dependencies } from '../../package.json'; 5 | 6 | if (dependencies) { 7 | const dependenciesKeys = Object.keys(dependencies); 8 | const nativeDeps = fs 9 | .readdirSync('node_modules') 10 | .filter((folder) => fs.existsSync(`node_modules/${folder}/binding.gyp`)); 11 | if (nativeDeps.length === 0) { 12 | process.exit(0); 13 | } 14 | try { 15 | // Find the reason for why the dependency is installed. If it is installed 16 | // because of a devDependency then that is okay. Warn when it is installed 17 | // because of a dependency 18 | const { dependencies: dependenciesObject } = JSON.parse( 19 | execSync(`npm ls ${nativeDeps.join(' ')} --json`).toString() 20 | ); 21 | const rootDependencies = Object.keys(dependenciesObject); 22 | const filteredRootDependencies = rootDependencies.filter((rootDependency) => 23 | dependenciesKeys.includes(rootDependency) 24 | ); 25 | if (filteredRootDependencies.length > 0) { 26 | const plural = filteredRootDependencies.length > 1; 27 | console.log(` 28 | ${chalk.whiteBright.bgYellow.bold( 29 | 'Webpack does not work with native dependencies.' 30 | )} 31 | ${chalk.bold(filteredRootDependencies.join(', '))} ${ 32 | plural ? 'are native dependencies' : 'is a native dependency' 33 | } and should be installed inside of the "./release/app" folder. 34 | First, uninstall the packages from "./package.json": 35 | ${chalk.whiteBright.bgGreen.bold('npm uninstall your-package')} 36 | ${chalk.bold( 37 | 'Then, instead of installing the package to the root "./package.json":' 38 | )} 39 | ${chalk.whiteBright.bgRed.bold('npm install your-package')} 40 | ${chalk.bold('Install the package to "./release/app/package.json"')} 41 | ${chalk.whiteBright.bgGreen.bold('cd ./release/app && npm install your-package')} 42 | Read more about native dependencies at: 43 | ${chalk.bold( 44 | 'https://electron-react-boilerplate.js.org/docs/adding-dependencies/#module-structure' 45 | )} 46 | `); 47 | process.exit(1); 48 | } 49 | } catch (e) { 50 | console.log('Native dependencies could not be checked'); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /.erb/scripts/check-node-env.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | export default function checkNodeEnv(expectedEnv) { 4 | if (!expectedEnv) { 5 | throw new Error('"expectedEnv" not set'); 6 | } 7 | 8 | if (process.env.NODE_ENV !== expectedEnv) { 9 | console.log( 10 | chalk.whiteBright.bgRed.bold( 11 | `"process.env.NODE_ENV" must be "${expectedEnv}" to use this webpack config` 12 | ) 13 | ); 14 | process.exit(2); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.erb/scripts/check-port-in-use.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import detectPort from 'detect-port'; 3 | 4 | const port = process.env.PORT || '4123'; 5 | detectPort(port, (err, availablePort) => { 6 | if (port !== String(availablePort)) { 7 | throw new Error( 8 | chalk.whiteBright.bgRed.bold( 9 | `Port "${port}" on "localhost" is already in use. Please use another port. ex: PORT=4123 npm start` 10 | ) 11 | ); 12 | } else { 13 | process.exit(0); 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /.erb/scripts/clean.js: -------------------------------------------------------------------------------- 1 | import rimraf from 'rimraf'; 2 | import webpackPaths from '../configs/webpack.paths.ts'; 3 | import process from 'process'; 4 | 5 | const args = process.argv.slice(2); 6 | const commandMap = { 7 | dist: webpackPaths.distPath, 8 | release: webpackPaths.releasePath, 9 | dll: webpackPaths.dllPath, 10 | }; 11 | 12 | args.forEach((x) => { 13 | const pathToRemove = commandMap[x]; 14 | if (pathToRemove !== undefined) { 15 | rimraf.sync(pathToRemove); 16 | } 17 | }); 18 | -------------------------------------------------------------------------------- /.erb/scripts/delete-source-maps.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import rimraf from 'rimraf'; 3 | import webpackPaths from '../configs/webpack.paths'; 4 | 5 | export default function deleteSourceMaps() { 6 | rimraf.sync(path.join(webpackPaths.distMainPath, '*.js.map')); 7 | rimraf.sync(path.join(webpackPaths.distRendererPath, '*.js.map')); 8 | } 9 | -------------------------------------------------------------------------------- /.erb/scripts/electron-rebuild.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { execSync } from 'child_process'; 3 | import fs from 'fs'; 4 | import { dependencies } from '../../release/app/package.json'; 5 | import webpackPaths from '../configs/webpack.paths'; 6 | 7 | if ( 8 | Object.keys(dependencies || {}).length > 0 && 9 | fs.existsSync(webpackPaths.appNodeModulesPath) 10 | ) { 11 | const electronRebuildCmd = 12 | '../../node_modules/.bin/electron-rebuild --parallel --force --types prod,dev,optional --module-dir .'; 13 | const cmd = 14 | process.platform === 'win32' 15 | ? electronRebuildCmd.replace(/\//g, '\\') 16 | : electronRebuildCmd; 17 | execSync(cmd, { 18 | cwd: webpackPaths.appPath, 19 | stdio: 'inherit', 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /.erb/scripts/link-modules.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import webpackPaths from '../configs/webpack.paths'; 3 | 4 | const srcNodeModulesPath = webpackPaths.srcNodeModulesPath; 5 | const appNodeModulesPath = webpackPaths.appNodeModulesPath 6 | 7 | if (!fs.existsSync(srcNodeModulesPath) && fs.existsSync(appNodeModulesPath)) { 8 | fs.symlinkSync(appNodeModulesPath, srcNodeModulesPath, 'junction'); 9 | } 10 | -------------------------------------------------------------------------------- /.erb/scripts/notarize.js: -------------------------------------------------------------------------------- 1 | const { notarize } = require('electron-notarize'); 2 | const { build } = require('../../package.json'); 3 | 4 | exports.default = async function notarizeMacos(context) { 5 | const { electronPlatformName, appOutDir } = context; 6 | if (electronPlatformName !== 'darwin') { 7 | return; 8 | } 9 | 10 | if (process.env.CI !== "true") { 11 | console.warn('Skipping notarizing step. Packaging is not running in CI'); 12 | return; 13 | } 14 | 15 | if (!('APPLE_ID' in process.env && 'APPLE_ID_PASS' in process.env)) { 16 | console.warn('Skipping notarizing step. APPLE_ID and APPLE_ID_PASS env variables must be set'); 17 | return; 18 | } 19 | 20 | const appName = context.packager.appInfo.productFilename; 21 | 22 | await notarize({ 23 | appBundleId: build.appId, 24 | appPath: `${appOutDir}/${appName}.app`, 25 | appleId: process.env.APPLE_ID, 26 | appleIdPassword: process.env.APPLE_ID_PASS, 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Coverage directory used by tools like istanbul 11 | coverage 12 | .eslintcache 13 | 14 | # Dependency directory 15 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 16 | node_modules 17 | 18 | # OSX 19 | .DS_Store 20 | 21 | release/app/dist 22 | release/build 23 | .erb/dll 24 | 25 | .idea 26 | npm-debug.log.* 27 | *.css.d.ts 28 | *.sass.d.ts 29 | *.scss.d.ts 30 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'erb', 3 | rules: { 4 | // A temporary hack related to IDE not resolving correct package.json 5 | 'import/no-extraneous-dependencies': 'off', 6 | // Since React 17 and typescript 4.1 you can safely disable the rule 7 | 'react/react-in-jsx-scope': 'off', 8 | 'react/prop-types': 'off', 9 | 'no-plusplus': 'off', 10 | 'no-nested-ternary': 'off', 11 | 'no-console': 'off', 12 | 'no-return-assign': 'off', 13 | 'no-underscore-dangle': 'off', 14 | 'class-methods-use-this': 'off', 15 | 'no-else-return': 'off', 16 | 'promise/always-return': 'off', 17 | 'consistent-return': 'off', 18 | '@typescript-eslint/no-explicit-any': 'off', 19 | '@typescript-eslint/no-non-null-assertion': 'off', 20 | '@typescript-eslint/ban-ts-comment': 'off', 21 | }, 22 | parserOptions: { 23 | ecmaVersion: 2020, 24 | sourceType: 'module', 25 | project: './tsconfig.json', 26 | tsconfigRootDir: __dirname, 27 | createDefaultProgram: true, 28 | }, 29 | settings: { 30 | 'prettier/prettier': [ 31 | 'error', 32 | { 33 | endOfLine: 'auto', 34 | }, 35 | ], 36 | 'import/resolver': { 37 | // See https://github.com/benmosher/eslint-plugin-import/issues/1396#issuecomment-575727774 for line below 38 | node: {}, 39 | webpack: { 40 | config: require.resolve('./.erb/configs/webpack.config.eslint.ts'), 41 | }, 42 | typescript: {}, 43 | }, 44 | 'import/parsers': { 45 | '@typescript-eslint/parser': ['.ts', '.tsx'], 46 | }, 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | *.exe binary 3 | *.png binary 4 | *.jpg binary 5 | *.jpeg binary 6 | *.ico binary 7 | *.icns binary 8 | *.eot binary 9 | *.otf binary 10 | *.ttf binary 11 | *.woff binary 12 | *.woff2 binary 13 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | # github: [electron-react-boilerplate, amilajack] 4 | # patreon: amilajack 5 | # open_collective: electron-react-boilerplate-594 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1-Bug_report_app.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report (in-app) 3 | about: You're having technical issues when running the application. 🐞 4 | labels: 'bug-app' 5 | --- 6 | 7 | 8 | 9 | ## Prerequisites 10 | 11 | 12 | 13 | - [ ] If there is an error generated from this, the error is included in this report. 14 | - [ ] If this is a visual error, screenshots are present. 15 | - [ ] Regardless of the error, logs are attached to this report. 16 | 17 | ## Expected Behavior 18 | 19 | 20 | 21 | ## Current Behavior 22 | 23 | 24 | 25 | ## Steps to Reproduce 26 | 27 | 28 | 29 | 1. 30 | 31 | 2. 32 | 33 | 3. 34 | 35 | 4. 36 | 37 | ## Possible Solution (Not obligatory) 38 | 39 | 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2-Bug_report_dev.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report (in-dev) 3 | about: You're having technical issues when working on the application. 🐞 4 | labels: 'bug-dev' 5 | --- 6 | 7 | 8 | 9 | 10 | ## Prerequisites 11 | 12 | 13 | 14 | - [ ] If there is an error generated from this, the error is included in this report. 15 | - [ ] Regardless of the error, logs are attached to this report. 16 | 17 | ## Expected Behavior 18 | 19 | 20 | 21 | ## Current Behavior 22 | 23 | 24 | 25 | ## Steps to Reproduce 26 | 27 | 28 | 29 | 30 | 1. 31 | 32 | 2. 33 | 34 | 3. 35 | 36 | 4. 37 | 38 | ## Possible Solution (Not obligatory) 39 | 40 | 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/3-Question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a question.❓ 4 | labels: 'question' 5 | --- 6 | 7 | ## Summary 8 | 9 | 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/4-Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: You want something added to the reader. 🎉 4 | labels: 'enhancement' 5 | --- 6 | -------------------------------------------------------------------------------- /.github/config.yml: -------------------------------------------------------------------------------- 1 | requiredHeaders: 2 | - Prerequisites 3 | - Expected Behavior 4 | - Current Behavior 5 | - Possible Solution 6 | - Your Environment 7 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - discussion 8 | - security 9 | # Label to use when marking an issue as stale 10 | staleLabel: wontfix 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '39 19 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v1 71 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: [workflow_dispatch] 4 | 5 | jobs: 6 | publish: 7 | # To enable auto publishing to github, update your electron publisher 8 | # config in package.json > "build" and remove the conditional below 9 | # if: ${{ github.repository_owner == 'electron-react-boilerplate' }} 10 | 11 | runs-on: ${{ matrix.os }} 12 | 13 | strategy: 14 | matrix: 15 | os: [macos-latest] 16 | 17 | steps: 18 | - name: Checkout git repo 19 | uses: actions/checkout@v1 20 | 21 | - name: Install Node and NPM 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: 16 25 | cache: 'npm' 26 | 27 | - name: Install dependencies 28 | run: | 29 | npm install 30 | 31 | - name: Publish releases 32 | env: 33 | # These values are used for auto updates signing 34 | APPLE_ID: ${{ secrets.APPLE_ID }} 35 | APPLE_ID_PASS: ${{ secrets.APPLE_ID_PASS }} 36 | CSC_LINK: ${{ secrets.CSC_LINK }} 37 | CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} 38 | # This is used for uploading release assets to github 39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | run: | 41 | npm run postinstall 42 | npm run build 43 | npm exec electron-builder -- --publish always --win --mac --linux 44 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | release: 7 | runs-on: ${{ matrix.os }} 8 | 9 | strategy: 10 | matrix: 11 | os: [macos-latest, windows-latest, ubuntu-latest] 12 | 13 | steps: 14 | - name: Check out Git repository 15 | uses: actions/checkout@v1 16 | 17 | - name: Install Node.js and NPM 18 | uses: actions/setup-node@v2 19 | with: 20 | node-version: 16 21 | cache: npm 22 | 23 | - name: npm install 24 | run: | 25 | npm install 26 | 27 | - name: npm test 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | run: | 31 | npm run package 32 | npm run lint 33 | npm exec tsc 34 | npm test 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Coverage directory used by tools like istanbul 11 | coverage 12 | .eslintcache 13 | 14 | # Dependency directory 15 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 16 | node_modules 17 | _node_modules 18 | 19 | # OSX 20 | .DS_Store 21 | 22 | release/app/dist 23 | release/build 24 | .erb/dll 25 | 26 | .idea 27 | npm-debug.log.* 28 | *.css.d.ts 29 | *.sass.d.ts 30 | *.scss.d.ts 31 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint", "EditorConfig.EditorConfig"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Electron: Main", 6 | "type": "node", 7 | "request": "launch", 8 | "protocol": "inspector", 9 | "runtimeExecutable": "npm", 10 | "runtimeArgs": [ 11 | "run start:main --inspect=5858 --remote-debugging-port=9223" 12 | ], 13 | "preLaunchTask": "Start Webpack Dev" 14 | }, 15 | { 16 | "name": "Electron: Renderer", 17 | "type": "chrome", 18 | "request": "attach", 19 | "port": 9223, 20 | "webRoot": "${workspaceFolder}", 21 | "timeout": 15000 22 | } 23 | ], 24 | "compounds": [ 25 | { 26 | "name": "Electron: All", 27 | "configurations": ["Electron: Main", "Electron: Renderer"] 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | ".eslintrc": "jsonc", 4 | ".prettierrc": "jsonc", 5 | ".eslintignore": "ignore" 6 | }, 7 | 8 | "javascript.validate.enable": false, 9 | "javascript.format.enable": false, 10 | "typescript.format.enable": false, 11 | 12 | "search.exclude": { 13 | ".git": true, 14 | ".eslintcache": true, 15 | ".erb/dll": true, 16 | "release/{build,app/dist}": true, 17 | "node_modules": true, 18 | "npm-debug.log.*": true, 19 | "test/**/__snapshots__": true, 20 | "package-lock.json": true, 21 | "*.{css,sass,scss}.d.ts": true 22 | }, 23 | "terminal.integrated.cwd": "./" 24 | } 25 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "label": "Start Webpack Dev", 7 | "script": "start:renderer", 8 | "options": { 9 | "cwd": "${workspaceFolder}" 10 | }, 11 | "isBackground": true, 12 | "problemMatcher": { 13 | "owner": "custom", 14 | "pattern": { 15 | "regexp": "____________" 16 | }, 17 | "background": { 18 | "activeOnStart": true, 19 | "beginsPattern": "Compiling\\.\\.\\.$", 20 | "endsPattern": "(Compiled successfully|Failed to compile)\\.$" 21 | } 22 | } 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present Electron React Boilerplate 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # yeah this got archived 2 | ## don't ever use electron-react-boilerplate 3 | ### ever. 4 | #### please. 5 | 6 |

座り読み

7 | 8 |
9 | old stuff 10 | | issues | forks | stars | license | codefactor | share | 11 | | --------------------------------------------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | 12 | | | | | | CodeFactor | | 13 | 14 | similarly to [Tachiyomi](https://github.com/tachiyomiorg/tachiyomi), suwariyomi is a free and open-source manga reader for windows, mac, and linux. 15 |
16 | 17 | this application is **_incomplete._** some features are bound to change; and some could be removed **entirely.** 18 | 19 |

downloads

20 | 21 | downloads are available [here](https://github.com/Nowaaru/suwariyomi/releases). 22 | 23 | found a bug or problem? make an issue! 24 | 25 |

features

26 | 27 | user features: 28 | 29 | - online reading from one source. it is mangadex. 30 | - tracker support: ~~[MyAnimeList](https://www.myanimelist.com) and~~ [AniList](https://anilist.co). more soon to come! 31 | - image filtering with alpha, red, green, blue and blend modes. 32 | - very pretty UI 33 | - very nice UX 34 | - very responsive! (laymans: faaaast) 35 | - lots of settings for you to customize your app just right 36 | - ability copy pages to the clipboard when reading via the context menu 37 | - ability to download individual pages when reading via the context menu 38 | - its all in dark mode 39 | 40 | developer features: 41 | 42 | - modular sources 43 | - very segmented for easy understanding 44 | - the code is pretty 45 | - made in react 46 | - easy-to-make sources 47 | - utilizes Aphrodite for styling 48 | - utilizes Material UI for ui design 49 | 50 | planned features: 51 | 52 | - online reading from **all the sources.** 53 | - support **Kitsu**, **Shikimori**, and **Bangumi** similarly to Tachiyomi 54 | - library organization via categories 55 | - custom themes; hopefully a theme repository! 56 | - plugins to further extend user experience 57 | - downloading 58 | 59 | in short, i aim to accomplish what Tachiyomi can do and then some. 60 | 61 |

styling

62 |
work in progress!
63 | all pages have a series of dummy stylings that allow you to change the general format of the page: 64 | these stylings look something among the lines of 65 | 66 | - `...`Container 67 | - `...`Inner 68 | 69 | with enough experience, this alone gives you enough experience 70 | to create an entirely different look and feel of the app, 71 | you could even say that you're essentially making your own 72 | application at that rate! 73 | 74 | you can also style already-existing elements if you so 75 | desire. anything that i style is what you can style! 76 | 77 |

"how do i contribute?"

78 | as you know, doing everything alone all the time is unrealistic. 79 | suwariyomi endeavors to be a community-driven application; those who 80 | want something to be implemented can implement it themselves with ease and in order to generate that ease, people have to point out flaws in 81 | the application; so get to making issues and pull requests! 82 |

83 | 84 |

if you are comitting...

85 | follow these guidelines! 86 |
(some things might not be applicable)
87 | 88 | - please make sure that your code is **clear and concise.** 89 | - if you disable linter rules, give an explanation why! 90 | it doesn't need to be an essay, but a short reason, i.e. '`Module is typed incorrectly.`' is preferable. 91 | - if your code can be **abstracted into a component**, please do so! 92 | - a page should be a list of components. if a page itself 93 | is starting to look like a component, maybe you should start to do some splicing! 94 | - use **comments** when it gets rough! 95 | - sometimes you'd have the occasional nested ternary or a snippet of code that simply looks weird or might go against standard. if something might be hard to understand to an intermediate or novice programmer, you probably want to **use a comment**. 96 | - when updating pages or implementing components, give everything a style! 97 | - this is to ensure that everything can be styled to the user's liking in case we make a design choice that might not be for everyone's liking. 98 | - if you are commenting or proposing changes to a PR, be **respectful** and clearly state your gripe. 99 | 100 | - being rude or passive-aggressive does nothing to help; all it does is brew unwanted argument. if you are unable to successfully follow this guideline, **you will be blocked from collaborating in this repository.** be kind! 101 |
102 | 103 |

if you are making an issue...

104 | follow these guidelines! 105 |
(some things might not be applicable)
106 | 107 | - make sure your issue is in the right spot! 108 | - if you're looking to report an issue or suggest a feature for an extension, we don't do that here! you should instead check the **extensions repository** instead of looking here. 109 | - tag your issue correctly! 110 | - this allows us to focus on specific things at specific times and allows us to designate varying severities and other auxiliary tags to help production. 111 | - if you're making a feature request, be **clear and concise**; as if you're coding. 112 | - we don't want to misinterpret what you say and close your issue despite it being a valid or necessary addition to the app. **be clear!** 113 | - if you're reporting a bug, once again, be **clear and concise**. 114 | - give us as much information as possible, **treat us as if we're babies**! if we can't reproduce something after a handful of attempts, we'll mark it as **`invalid`** and **close it**. to reduce room for error, tell us everything! 115 | - if you have a clue as to why this might be happening, **let us know**! it doesn't matter whether you're wrong or not, it would at least narrow down what the issue could possibly be. everyone is knowledgeable! 😁 116 | 117 |

how to build

118 | clone the repository and install the dependencies: 119 | 120 | ``` 121 | git clone --depth 1 --branch main https://github.com/Nowaaru/suwariyomi.git 122 | ``` 123 | 124 | navigate into your new folder (if you didn't clone in the current directory) and run `npm install`. 125 | 126 | ``` 127 | cd 128 | npm install 129 | ``` 130 | 131 | to test the program, run `npm start`. 132 | if you have a port conflict, change the port in `.erb/scripts/check-port-in-use.js` (or figure out command line args in node) 133 | 134 | ###### powered by [Tachiyomi](https://github.com/tachiyomiorg/tachiyomi), [TachiyomiJ2K](https://github.com/Jays2Kings/tachiyomiJ2K), [electron](https://github.com/electron/electron), [electron-react-boilerplate](https://github.com/electron-react-boilerplate), [Material UI](https://mui.com/), and [react](https://github.com/facebook/react). thank you. 135 | 136 | -------------------------------------------------------------------------------- /TRANSLATING.md: -------------------------------------------------------------------------------- 1 | ## You want to help translate Suwariyomi? That's great! 2 | 3 | #### Translating this application is easier than one might think. 4 | 5 | --- 6 | 7 | 1. First, you'll have to clone the repository (see README.md). 8 | 2. After cloning the repository, make your way to `src/shared/locale`. This folder holds all of the languages. 9 | 3. Clone the base `en.json` and rename it to your target language's corresponding [`ISO 639-1 Code`](https://www.loc.gov/standards/iso639-2/php/code_list.php). ISO 639-2 works as well, but ISO 639-1 is preferred. 10 | 4. In the "`$meta`" field, change the `name` field to the full name of your target language. This is used for the dropdown label in Settings. 11 | 5. After making your translations by changing the text in the string, add your language in `src/shared/intl.ts` on the `mainTranslator` definition. 12 | 6. Do the same for the `locale` schema enum in `src/main/util/settings.ts`. 13 | 7. Commit your changes and make a pull request! 14 | 15 | Thank you for your help! 16 | -------------------------------------------------------------------------------- /assets/assets.d.ts: -------------------------------------------------------------------------------- 1 | type Styles = Record; 2 | 3 | declare module '*.svg' { 4 | const content: string; 5 | export default content; 6 | } 7 | 8 | declare module '*.png' { 9 | const content: string; 10 | export default content; 11 | } 12 | 13 | declare module '*.jpg' { 14 | const content: string; 15 | export default content; 16 | } 17 | 18 | declare module '*.scss' { 19 | const content: Styles; 20 | export default content; 21 | } 22 | 23 | declare module '*.sass' { 24 | const content: Styles; 25 | export default content; 26 | } 27 | 28 | declare module '*.css' { 29 | const content: Styles; 30 | export default content; 31 | } 32 | -------------------------------------------------------------------------------- /assets/data/tachiyomi-model.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message Backup { 4 | // @ProtoNumber(1) val backupManga: List 5 | repeated BackupManga backupManga = 1; 6 | // @ProtoNumber(2) var backupCategories: List = 7 | repeated BackupCategory backupCategories = 2; 8 | // @ProtoNumber(101) var backupSources: List = 9 | repeated BackupSource backupSources = 101; 10 | } 11 | 12 | message BackupCategory { 13 | // @ProtoNumber(1) var name: String 14 | required string name = 1; 15 | // @ProtoNumber(2) var order: Int = 16 | optional int32 order = 2; 17 | // @ProtoNumber(100) var flags: Int = 18 | optional int32 flags = 100; 19 | } 20 | 21 | message BackupChapter { 22 | // @ProtoNumber(1) var url: String 23 | required string url = 1; 24 | // @ProtoNumber(2) var name: String 25 | required string name = 2; 26 | // @ProtoNumber(3) var scanlator: String? 27 | optional string scanlator = 3; 28 | // @ProtoNumber(4) var read: Boolean = 29 | optional bool read = 4; 30 | // @ProtoNumber(5) var bookmark: Boolean = 31 | optional bool bookmark = 5; 32 | // @ProtoNumber(6) var lastPageRead: Int = 33 | optional int32 lastPageRead = 6; 34 | // @ProtoNumber(7) var dateFetch: Long = 35 | optional int64 dateFetch = 7; 36 | // @ProtoNumber(8) var dateUpload: Long = 37 | optional int64 dateUpload = 8; 38 | // @ProtoNumber(9) var chapterNumber: Float = 39 | optional float chapterNumber = 9; 40 | // @ProtoNumber(10) var sourceOrder: Int = 41 | optional int32 sourceOrder = 10; 42 | } 43 | 44 | message BackupHistory { 45 | // @ProtoNumber(1) var url: String 46 | required string url = 1; 47 | // @ProtoNumber(2) var lastRead: Long 48 | required int64 lastRead = 2; 49 | } 50 | 51 | message BackupManga { 52 | // @ProtoNumber(1) var source: Long 53 | required int64 source = 1; 54 | // @ProtoNumber(2) var url: String 55 | required string url = 2; 56 | // @ProtoNumber(3) var title: String = 57 | optional string title = 3; 58 | // @ProtoNumber(4) var artist: String? 59 | optional string artist = 4; 60 | // @ProtoNumber(5) var author: String? 61 | optional string author = 5; 62 | // @ProtoNumber(6) var description: String? 63 | optional string description = 6; 64 | // @ProtoNumber(7) var genre: List = 65 | repeated string genre = 7; 66 | // @ProtoNumber(8) var status: Int = 67 | optional int32 status = 8; 68 | // @ProtoNumber(9) var thumbnailUrl: String? 69 | optional string thumbnailUrl = 9; 70 | // @ProtoNumber(13) var dateAdded: Long = 71 | optional int64 dateAdded = 13; 72 | // @ProtoNumber(14) var viewer: Int = 73 | optional int32 viewer = 14; 74 | // @ProtoNumber(16) var chapters: List = 75 | repeated BackupChapter chapters = 16; 76 | // @ProtoNumber(17) var categories: List = 77 | repeated int32 categories = 17; 78 | // @ProtoNumber(18) var tracking: List = 79 | repeated BackupTracking tracking = 18; 80 | // @ProtoNumber(100) var favorite: Boolean = 81 | optional bool favorite = 100; 82 | // @ProtoNumber(101) var chapterFlags: Int = 83 | optional int32 chapterFlags = 101; 84 | // @ProtoNumber(103) var viewer_flags: Int? 85 | optional int32 viewer_flags = 103; 86 | // @ProtoNumber(104) var history: List = 87 | repeated BackupHistory history = 104; 88 | } 89 | 90 | message BackupSource { 91 | // @ProtoNumber(1) var name: String = 92 | optional string name = 1; 93 | // @ProtoNumber(2) var sourceId: Long 94 | required int64 sourceId = 2; 95 | } 96 | 97 | message BackupTracking { 98 | // @ProtoNumber(1) var syncId: Int 99 | required int32 syncId = 1; 100 | // @ProtoNumber(2) var libraryId: Long 101 | required int64 libraryId = 2; 102 | // @ProtoNumber(3) var mediaId: Int = 103 | optional int32 mediaId = 3; 104 | // @ProtoNumber(4) var trackingUrl: String = 105 | optional string trackingUrl = 4; 106 | // @ProtoNumber(5) var title: String = 107 | optional string title = 5; 108 | // @ProtoNumber(6) var lastChapterRead: Float = 109 | optional float lastChapterRead = 6; 110 | // @ProtoNumber(7) var totalChapters: Int = 111 | optional int32 totalChapters = 7; 112 | // @ProtoNumber(8) var score: Float = 113 | optional float score = 8; 114 | // @ProtoNumber(9) var status: Int = 115 | optional int32 status = 9; 116 | // @ProtoNumber(10) var startedReadingDate: Long = 117 | optional int64 startedReadingDate = 10; 118 | // @ProtoNumber(11) var finishedReadingDate: Long = 119 | optional int64 finishedReadingDate = 11; 120 | } 121 | -------------------------------------------------------------------------------- /assets/entitlements.mac.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-unsigned-executable-memory 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /assets/icons/app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nowaaru/suwariyomi/e07cc3481fb7cdcca680c5f4bb5c341bde48977a/assets/icons/app.png -------------------------------------------------------------------------------- /assets/icons/app_default/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nowaaru/suwariyomi/e07cc3481fb7cdcca680c5f4bb5c341bde48977a/assets/icons/app_default/1024x1024.png -------------------------------------------------------------------------------- /assets/icons/app_default/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nowaaru/suwariyomi/e07cc3481fb7cdcca680c5f4bb5c341bde48977a/assets/icons/app_default/128x128.png -------------------------------------------------------------------------------- /assets/icons/app_default/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nowaaru/suwariyomi/e07cc3481fb7cdcca680c5f4bb5c341bde48977a/assets/icons/app_default/16x16.png -------------------------------------------------------------------------------- /assets/icons/app_default/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nowaaru/suwariyomi/e07cc3481fb7cdcca680c5f4bb5c341bde48977a/assets/icons/app_default/24x24.png -------------------------------------------------------------------------------- /assets/icons/app_default/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nowaaru/suwariyomi/e07cc3481fb7cdcca680c5f4bb5c341bde48977a/assets/icons/app_default/256x256.png -------------------------------------------------------------------------------- /assets/icons/app_default/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nowaaru/suwariyomi/e07cc3481fb7cdcca680c5f4bb5c341bde48977a/assets/icons/app_default/32x32.png -------------------------------------------------------------------------------- /assets/icons/app_default/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nowaaru/suwariyomi/e07cc3481fb7cdcca680c5f4bb5c341bde48977a/assets/icons/app_default/48x48.png -------------------------------------------------------------------------------- /assets/icons/app_default/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nowaaru/suwariyomi/e07cc3481fb7cdcca680c5f4bb5c341bde48977a/assets/icons/app_default/512x512.png -------------------------------------------------------------------------------- /assets/icons/app_default/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nowaaru/suwariyomi/e07cc3481fb7cdcca680c5f4bb5c341bde48977a/assets/icons/app_default/64x64.png -------------------------------------------------------------------------------- /assets/icons/app_default/96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nowaaru/suwariyomi/e07cc3481fb7cdcca680c5f4bb5c341bde48977a/assets/icons/app_default/96x96.png -------------------------------------------------------------------------------- /assets/icons/app_default/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nowaaru/suwariyomi/e07cc3481fb7cdcca680c5f4bb5c341bde48977a/assets/icons/app_default/icon.icns -------------------------------------------------------------------------------- /assets/icons/app_default/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nowaaru/suwariyomi/e07cc3481fb7cdcca680c5f4bb5c341bde48977a/assets/icons/app_default/icon.ico -------------------------------------------------------------------------------- /assets/icons/app_default/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nowaaru/suwariyomi/e07cc3481fb7cdcca680c5f4bb5c341bde48977a/assets/icons/app_default/icon.png -------------------------------------------------------------------------------- /assets/icons/app_default/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /assets/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nowaaru/suwariyomi/e07cc3481fb7cdcca680c5f4bb5c341bde48977a/assets/icons/icon.icns -------------------------------------------------------------------------------- /assets/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nowaaru/suwariyomi/e07cc3481fb7cdcca680c5f4bb5c341bde48977a/assets/icons/icon.ico -------------------------------------------------------------------------------- /assets/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nowaaru/suwariyomi/e07cc3481fb7cdcca680c5f4bb5c341bde48977a/assets/icons/icon.png -------------------------------------------------------------------------------- /assets/icons/login/anilist/opaque.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nowaaru/suwariyomi/e07cc3481fb7cdcca680c5f4bb5c341bde48977a/assets/icons/login/anilist/opaque.png -------------------------------------------------------------------------------- /assets/icons/login/anilist/transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nowaaru/suwariyomi/e07cc3481fb7cdcca680c5f4bb5c341bde48977a/assets/icons/login/anilist/transparent.png -------------------------------------------------------------------------------- /assets/icons/login/myanimelist/opaque.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nowaaru/suwariyomi/e07cc3481fb7cdcca680c5f4bb5c341bde48977a/assets/icons/login/myanimelist/opaque.png -------------------------------------------------------------------------------- /assets/icons/login/myanimelist/transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nowaaru/suwariyomi/e07cc3481fb7cdcca680c5f4bb5c341bde48977a/assets/icons/login/myanimelist/transparent.png -------------------------------------------------------------------------------- /assets/icons/main/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nowaaru/suwariyomi/e07cc3481fb7cdcca680c5f4bb5c341bde48977a/assets/icons/main/1024x1024.png -------------------------------------------------------------------------------- /assets/icons/main/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nowaaru/suwariyomi/e07cc3481fb7cdcca680c5f4bb5c341bde48977a/assets/icons/main/128x128.png -------------------------------------------------------------------------------- /assets/icons/main/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nowaaru/suwariyomi/e07cc3481fb7cdcca680c5f4bb5c341bde48977a/assets/icons/main/16x16.png -------------------------------------------------------------------------------- /assets/icons/main/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nowaaru/suwariyomi/e07cc3481fb7cdcca680c5f4bb5c341bde48977a/assets/icons/main/24x24.png -------------------------------------------------------------------------------- /assets/icons/main/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nowaaru/suwariyomi/e07cc3481fb7cdcca680c5f4bb5c341bde48977a/assets/icons/main/256x256.png -------------------------------------------------------------------------------- /assets/icons/main/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nowaaru/suwariyomi/e07cc3481fb7cdcca680c5f4bb5c341bde48977a/assets/icons/main/32x32.png -------------------------------------------------------------------------------- /assets/icons/main/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nowaaru/suwariyomi/e07cc3481fb7cdcca680c5f4bb5c341bde48977a/assets/icons/main/48x48.png -------------------------------------------------------------------------------- /assets/icons/main/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nowaaru/suwariyomi/e07cc3481fb7cdcca680c5f4bb5c341bde48977a/assets/icons/main/512x512.png -------------------------------------------------------------------------------- /assets/icons/main/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nowaaru/suwariyomi/e07cc3481fb7cdcca680c5f4bb5c341bde48977a/assets/icons/main/64x64.png -------------------------------------------------------------------------------- /assets/icons/main/96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nowaaru/suwariyomi/e07cc3481fb7cdcca680c5f4bb5c341bde48977a/assets/icons/main/96x96.png -------------------------------------------------------------------------------- /assets/images/nocover_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nowaaru/suwariyomi/e07cc3481fb7cdcca680c5f4bb5c341bde48977a/assets/images/nocover_dark.png -------------------------------------------------------------------------------- /patches/node-polyfill-webpack-plugin+1.1.4.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/node-polyfill-webpack-plugin/index.js b/node_modules/node-polyfill-webpack-plugin/index.js 2 | index 51c6810..25694ac 100644 3 | --- a/node_modules/node-polyfill-webpack-plugin/index.js 4 | +++ b/node_modules/node-polyfill-webpack-plugin/index.js 5 | @@ -16,7 +16,7 @@ module.exports = class NodePolyfillPlugin { 6 | compiler.options.plugins.push(new ProvidePlugin(excludeObjectKeys({ 7 | Buffer: [require.resolve("buffer/"), "Buffer"], 8 | console: require.resolve("console-browserify"), 9 | - process: require.resolve("process/browser") 10 | + process: require.resolve("process/browser.js") 11 | }, this.options.excludeAliases))) 12 | 13 | compiler.options.resolve.fallback = { 14 | @@ -33,7 +33,7 @@ module.exports = class NodePolyfillPlugin { 15 | os: require.resolve("os-browserify/browser"), 16 | path: require.resolve("path-browserify"), 17 | punycode: require.resolve("punycode/"), 18 | - process: require.resolve("process/browser"), 19 | + process: require.resolve("process/browser.js"), 20 | querystring: require.resolve("querystring-es3"), 21 | stream: require.resolve("stream-browserify"), 22 | /* eslint-disable camelcase */ 23 | -------------------------------------------------------------------------------- /release/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "suwariyomi", 3 | "version": "0.14.1", 4 | "description": "Manga reader made with electron-react-boilerplate. Inspired by tachiyomi.", 5 | "main": "./dist/main/main.js", 6 | "author": { 7 | "name": "Nowaaru", 8 | "email": "zackyboy35@gmail.com", 9 | "url": "https://nowaaru.github.io" 10 | }, 11 | "scripts": { 12 | "electron-rebuild": "node -r ts-node/register ../../.erb/scripts/electron-rebuild.js", 13 | "link-modules": "node -r ts-node/register ../../.erb/scripts/link-modules.ts", 14 | "postinstall": "npm run electron-rebuild && npm run link-modules" 15 | }, 16 | "dependencies": { 17 | "discord-rpc": "^4.0.1", 18 | "enmap": "^5.8.7" 19 | }, 20 | "license": "MIT" 21 | } 22 | -------------------------------------------------------------------------------- /src/__tests__/App.test.tsx: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | import { render } from '@testing-library/react'; 3 | import App from '../renderer/App'; 4 | 5 | describe('App', () => { 6 | it('should render', () => { 7 | expect(render()).toBeTruthy(); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | // Allow images to be imported 2 | declare module '*.png' { 3 | const value: any; 4 | export = value; 5 | } 6 | 7 | declare module '*.proto' { 8 | const value: any; 9 | export = value; 10 | } 11 | -------------------------------------------------------------------------------- /src/main/menu.ts: -------------------------------------------------------------------------------- 1 | import { 2 | app, 3 | Menu, 4 | shell, 5 | BrowserWindow, 6 | MenuItemConstructorOptions, 7 | } from 'electron'; 8 | 9 | interface DarwinMenuItemConstructorOptions extends MenuItemConstructorOptions { 10 | selector?: string; 11 | submenu?: DarwinMenuItemConstructorOptions[] | Menu; 12 | } 13 | 14 | const isInDebug = 15 | true || 16 | process.env.NODE_ENV === 'development' || 17 | process.env.DEBUG_PROD === 'true'; 18 | export default class MenuBuilder { 19 | mainWindow: BrowserWindow; 20 | 21 | constructor(mainWindow: BrowserWindow) { 22 | this.mainWindow = mainWindow; 23 | } 24 | 25 | buildMenu(): Menu { 26 | if (isInDebug) { 27 | this.setupDevelopmentEnvironment(); 28 | } 29 | 30 | const template = 31 | process.platform === 'darwin' 32 | ? this.buildDarwinTemplate() 33 | : this.buildDefaultTemplate(); 34 | 35 | const menu = Menu.buildFromTemplate(template); 36 | Menu.setApplicationMenu(menu); 37 | 38 | return menu; 39 | } 40 | 41 | setupDevelopmentEnvironment(): void { 42 | // I'll keep this here for now, but I don't think it's necessary. 43 | // In case we need to do something with it later. 44 | /* 45 | 46 | this.mainWindow.webContents.on('context-menu', (_, props) => { 47 | const { x, y } = props; 48 | Menu.buildFromTemplate([ 49 | { 50 | label: 'Inspect element', 51 | click: () => { 52 | this.mainWindow.webContents.inspectElement(x, y); 53 | }, 54 | }, 55 | ]).popup({ window: this.mainWindow }); 56 | }); 57 | 58 | */ 59 | } 60 | 61 | buildDarwinTemplate(): MenuItemConstructorOptions[] { 62 | const subMenuAbout: DarwinMenuItemConstructorOptions = { 63 | label: 'Electron', 64 | submenu: [ 65 | { 66 | label: 'About ElectronReact', 67 | selector: 'orderFrontStandardAboutPanel:', 68 | }, 69 | { type: 'separator' }, 70 | { label: 'Services', submenu: [] }, 71 | { type: 'separator' }, 72 | { 73 | label: 'Hide ElectronReact', 74 | accelerator: 'Command+H', 75 | selector: 'hide:', 76 | }, 77 | { 78 | label: 'Hide Others', 79 | accelerator: 'Command+Shift+H', 80 | selector: 'hideOtherApplications:', 81 | }, 82 | { label: 'Show All', selector: 'unhideAllApplications:' }, 83 | { type: 'separator' }, 84 | { 85 | label: 'Quit', 86 | accelerator: 'Command+Q', 87 | click: () => { 88 | app.quit(); 89 | }, 90 | }, 91 | ], 92 | }; 93 | const subMenuEdit: DarwinMenuItemConstructorOptions = { 94 | label: 'Edit', 95 | submenu: [ 96 | { label: 'Undo', accelerator: 'Command+Z', selector: 'undo:' }, 97 | { label: 'Redo', accelerator: 'Shift+Command+Z', selector: 'redo:' }, 98 | { type: 'separator' }, 99 | { label: 'Cut', accelerator: 'Command+X', selector: 'cut:' }, 100 | { label: 'Copy', accelerator: 'Command+C', selector: 'copy:' }, 101 | { label: 'Paste', accelerator: 'Command+V', selector: 'paste:' }, 102 | { 103 | label: 'Select All', 104 | accelerator: 'Command+A', 105 | selector: 'selectAll:', 106 | }, 107 | ], 108 | }; 109 | const subMenuViewDev: MenuItemConstructorOptions = { 110 | label: 'View', 111 | submenu: [ 112 | { 113 | label: 'Reload', 114 | accelerator: 'Command+R', 115 | click: () => { 116 | this.mainWindow.webContents.reload(); 117 | }, 118 | }, 119 | { 120 | label: 'Toggle Full Screen', 121 | accelerator: 'Ctrl+Command+F', 122 | click: () => { 123 | this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen()); 124 | }, 125 | }, 126 | { 127 | label: 'Toggle Developer Tools', 128 | accelerator: 'Alt+Command+I', 129 | click: () => { 130 | this.mainWindow.webContents.toggleDevTools(); 131 | }, 132 | }, 133 | ], 134 | }; 135 | const subMenuViewProd: MenuItemConstructorOptions = { 136 | label: 'View', 137 | submenu: [ 138 | { 139 | label: 'Toggle Full Screen', 140 | accelerator: 'Ctrl+Command+F', 141 | click: () => { 142 | this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen()); 143 | }, 144 | }, 145 | ], 146 | }; 147 | const subMenuWindow: DarwinMenuItemConstructorOptions = { 148 | label: 'Window', 149 | submenu: [ 150 | { 151 | label: 'Minimize', 152 | accelerator: 'Command+M', 153 | selector: 'performMiniaturize:', 154 | }, 155 | { label: 'Close', accelerator: 'Command+W', selector: 'performClose:' }, 156 | { type: 'separator' }, 157 | { label: 'Bring All to Front', selector: 'arrangeInFront:' }, 158 | ], 159 | }; 160 | const subMenuHelp: MenuItemConstructorOptions = { 161 | label: 'Help', 162 | submenu: [ 163 | { 164 | label: 'Learn More', 165 | click() { 166 | shell.openExternal('https://electronjs.org'); 167 | }, 168 | }, 169 | { 170 | label: 'Documentation', 171 | click() { 172 | shell.openExternal( 173 | 'https://github.com/electron/electron/tree/main/docs#readme' 174 | ); 175 | }, 176 | }, 177 | { 178 | label: 'Community Discussions', 179 | click() { 180 | shell.openExternal('https://www.electronjs.org/community'); 181 | }, 182 | }, 183 | { 184 | label: 'Search Issues', 185 | click() { 186 | shell.openExternal('https://github.com/electron/electron/issues'); 187 | }, 188 | }, 189 | ], 190 | }; 191 | 192 | const subMenuView = isInDebug ? subMenuViewDev : subMenuViewProd; 193 | 194 | return [subMenuAbout, subMenuEdit, subMenuView, subMenuWindow, subMenuHelp]; 195 | } 196 | 197 | buildDefaultTemplate() { 198 | const templateDefault = [ 199 | { 200 | label: '&File', 201 | submenu: [ 202 | { 203 | label: '&Open', 204 | accelerator: 'Ctrl+O', 205 | }, 206 | { 207 | label: '&Close', 208 | accelerator: 'Ctrl+W', 209 | click: () => { 210 | this.mainWindow.close(); 211 | }, 212 | }, 213 | ], 214 | }, 215 | { 216 | label: '&View', 217 | submenu: isInDebug 218 | ? [ 219 | { 220 | label: '&Reload', 221 | accelerator: 'Ctrl+R', 222 | click: () => { 223 | this.mainWindow.webContents.reload(); 224 | }, 225 | }, 226 | { 227 | label: 'Toggle &Full Screen', 228 | accelerator: 'F11', 229 | click: () => { 230 | this.mainWindow.setFullScreen( 231 | !this.mainWindow.isFullScreen() 232 | ); 233 | }, 234 | }, 235 | { 236 | label: 'Toggle &Developer Tools', 237 | accelerator: 'Alt+Ctrl+I', 238 | click: () => { 239 | this.mainWindow.webContents.toggleDevTools(); 240 | }, 241 | }, 242 | ] 243 | : [ 244 | { 245 | label: 'Toggle &Full Screen', 246 | accelerator: 'F11', 247 | click: () => { 248 | this.mainWindow.setFullScreen( 249 | !this.mainWindow.isFullScreen() 250 | ); 251 | }, 252 | }, 253 | ], 254 | }, 255 | ]; 256 | 257 | return templateDefault; 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /src/main/sources/handler.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable new-cap */ 2 | /* eslint-disable global-require */ 3 | /* eslint-disable import/no-dynamic-require */ 4 | import path from 'path'; 5 | import { getSourceFiles, getSourceDirectory } from '../util'; 6 | import { getMainRequire } from '../../shared/util'; 7 | import SourceBase from './static/base'; 8 | 9 | const requireFunc = getMainRequire(); 10 | 11 | // I actually like the export class filled with no class but actual structures; so we'll use that. 12 | // I just came back to this comment and I have absolutely *no* clue what that was supposed to mean. 13 | 14 | export default class Handler { 15 | public static getSource(sourceName: string): SourceBase { 16 | const isRenderer = typeof window !== 'undefined'; 17 | const fileSources = isRenderer 18 | ? window.electron.util.getSourceFiles() 19 | : getSourceFiles(); 20 | const fileSourceDirectory = isRenderer 21 | ? window.electron.util.getSourceDirectory() 22 | : getSourceDirectory(); 23 | 24 | const foundSource = fileSources.find( 25 | (sourcePath) => sourcePath.toLowerCase() === sourceName.toLowerCase() 26 | ); 27 | 28 | return foundSource 29 | ? new (requireFunc( 30 | path.join(fileSourceDirectory, foundSource, 'main.js') 31 | ))() 32 | : null; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/sources/static/base.ts: -------------------------------------------------------------------------------- 1 | import type { Manga, Chapter, FullManga } from '../../util/manga'; 2 | 3 | export type SearchFilters = { 4 | query: string; 5 | results: number; 6 | offset: number; 7 | }; 8 | 9 | type SearchFilterFieldBase = { 10 | noDisplay?: boolean; 11 | accordion?: boolean; 12 | writeTo: string; 13 | }; 14 | 15 | export type Checkable = { 16 | display: string; 17 | value: string; 18 | }; 19 | 20 | export type Selectable = { 21 | label: string; 22 | value: string; 23 | }; 24 | 25 | export type SearchFilterFieldTypeCheckbox = SearchFilterFieldBase & { 26 | fieldType: 'checkbox'; 27 | choices: Checkable[]; 28 | }; 29 | 30 | export type SearchFilterFieldTypeCheckbox3 = SearchFilterFieldBase & { 31 | fieldType: 'checkbox3'; 32 | // The field to write to when the "disallowed" state is enabled 33 | disallowedWriteTo: string; 34 | choices: Checkable[]; 35 | }; 36 | 37 | export type SearchFilterFieldTypeSelect = SearchFilterFieldBase & { 38 | fieldType: 'select'; 39 | choices: Selectable[]; 40 | }; 41 | 42 | export type SearchFilterFieldTypeRadio = SearchFilterFieldBase & { 43 | fieldType: 'radio'; 44 | isHorizontal?: boolean; 45 | choices: Selectable[]; 46 | }; 47 | 48 | export type SearchFilterFieldTypes = { 49 | [categoryName: string]: 50 | | SearchFilterFieldTypeCheckbox 51 | | SearchFilterFieldTypeCheckbox3 52 | | SearchFilterFieldTypeSelect 53 | | SearchFilterFieldTypeRadio; 54 | }; 55 | 56 | export default abstract class SourceBase { 57 | protected abstract _sourceName: string; 58 | 59 | public _metadata: { 60 | isNSFW: boolean; 61 | } = { 62 | isNSFW: false, 63 | }; 64 | 65 | public getName(): typeof SourceBase.prototype._sourceName { 66 | return this._sourceName; 67 | } 68 | 69 | protected _icon: string = ''; 70 | 71 | public getIcon(): string { 72 | return this._icon; 73 | } 74 | 75 | protected _canDownload: boolean = true; 76 | 77 | public get canDownload(): boolean { 78 | return this._canDownload; 79 | } 80 | 81 | public download: ( 82 | location: string, 83 | manga: string, 84 | chapter: string 85 | ) => Promise = async () => { 86 | return false; 87 | }; 88 | 89 | public async getItemCount(): Promise { 90 | return 0; 91 | } 92 | 93 | public abstract IDFromURL( 94 | url: string, 95 | search?: 'chapter' | 'manga' 96 | ): Promise; 97 | 98 | protected abstract Tags: Promise< 99 | { 100 | tagName: string; 101 | tagID: string; 102 | }[] 103 | >; 104 | 105 | public abstract tagColours: { [tagName: string]: string }; 106 | 107 | public abstract tagColors?: typeof SourceBase.prototype.tagColours; // Just for the people who spell `colour` wrong :) 108 | 109 | protected abstract _locale: string; 110 | 111 | protected abstract _locales: Array<{ 112 | id: string; 113 | name: string; 114 | }>; 115 | 116 | protected abstract searchFilters: any; 117 | 118 | protected abstract searchFilterFieldTypes: SearchFilterFieldTypes; 119 | 120 | public setLocale(locale: string): void { 121 | this._locale = locale; 122 | } 123 | 124 | public getLocale(): string { 125 | return this._locale; 126 | } 127 | 128 | public getLocales(): Array<{ 129 | id: string; 130 | name: string; 131 | }> { 132 | return [...this._locales]; 133 | } 134 | 135 | public setFilters( 136 | searchFilters: typeof SourceBase.prototype.searchFilters 137 | ): void { 138 | this.searchFilters = searchFilters; 139 | } 140 | 141 | public getFilters(): typeof SourceBase.prototype.searchFilters { 142 | return { ...this.searchFilters }; 143 | } 144 | 145 | public getFieldTypes(): SearchFilterFieldTypes { 146 | return { ...this.searchFilterFieldTypes }; 147 | } 148 | 149 | public abstract getManga( 150 | mangaID: string, 151 | doFull: boolean 152 | ): Promise; 153 | 154 | public abstract getMangas( 155 | mangaIDs: string[], 156 | doFull: boolean 157 | ): Promise[]>; 158 | 159 | public abstract getUrl(mangaID: string): string; 160 | 161 | public abstract getChapters(mangaID: string): Promise; 162 | 163 | public abstract serialize( 164 | mangaItem: any, 165 | doFull: boolean 166 | ): Promise | Promise; 167 | 168 | public abstract getPages(chapterId: string): Promise; 169 | 170 | public abstract serializeChapters(chapters: any[]): Promise; 171 | 172 | public abstract getAuthors(mangaID: any): Promise; 173 | 174 | public abstract search(): Promise; 175 | } 176 | -------------------------------------------------------------------------------- /src/main/util.ts: -------------------------------------------------------------------------------- 1 | /* eslint import/prefer-default-export: off, import/no-mutable-exports: off */ 2 | import { URL } from 'url'; 3 | import { app } from 'electron'; 4 | import path from 'path'; 5 | import fs from 'fs'; 6 | 7 | // Create folders for themes, locales, and plugins. 8 | export function createFolders() { 9 | const folders = ['themes', 'locales', 'plugins', 'sources'].filter( 10 | (x) => !fs.existsSync(path.normalize(path.join(app.getPath('userData'), x))) 11 | ); 12 | folders.forEach((x) => { 13 | fs.mkdirSync(path.normalize(path.join(app.getPath('userData'), x))); 14 | }); 15 | } 16 | 17 | export const getSourceDirectory = (): string => 18 | path.resolve(path.join(app.getPath('userData'), 'sources')); 19 | 20 | export function getSourceFiles(): string[] { 21 | const sources = fs.readdirSync(getSourceDirectory()); 22 | return sources; 23 | } 24 | 25 | export let resolveHtmlPath: (htmlFileName: string) => string; 26 | if (process.env.NODE_ENV === 'development') { 27 | const port = process.env.PORT || 4123; 28 | resolveHtmlPath = (htmlFileName: string) => { 29 | const url = new URL(`http://localhost:${port}`); 30 | url.pathname = htmlFileName; 31 | return url.href; 32 | }; 33 | } else { 34 | resolveHtmlPath = (htmlFileName: string) => { 35 | return `file://${path.resolve(__dirname, '../renderer/', htmlFileName)}`; 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /src/main/util/cache.ts: -------------------------------------------------------------------------------- 1 | import Enmap from 'enmap'; 2 | 3 | const CACHE = new Enmap(); 4 | export default class Cache { 5 | public static async flush(): Promise { 6 | CACHE.deleteAll(); 7 | } 8 | 9 | public static async get(key: string): Promise { 10 | return CACHE.get(key); 11 | } 12 | 13 | public static async set(key: string, value: any): Promise { 14 | CACHE.set(key, value); 15 | } 16 | 17 | public static async has(key: string): Promise { 18 | return CACHE.has(key); 19 | } 20 | 21 | public static async delete(...keys: string[]): Promise { 22 | CACHE.evict(keys); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/util/misc.ts: -------------------------------------------------------------------------------- 1 | // TODO: this is supposed to be used for general information like sorting methods or whatever goodnight 2 | // will be using enmaps to store data since electron-store can't serialize some datatypes 3 | // this will NOT be exposed through the IPCRenderer because sendSync has to serialize the data, 4 | // and again: some datatypes cannot be serialized. 5 | 6 | // This should seldom be used for anything related to the renderer. 7 | // This preferably should be used for plugins and other things. 8 | 9 | import Enmap from 'enmap'; 10 | import { info } from 'electron-log'; 11 | import { app } from 'electron'; 12 | 13 | const generalEnmap = new Enmap>({ 14 | name: 'misc', 15 | dataDir: app?.getPath('userData') ?? window.electron.util.getUserDataPath(), 16 | }); 17 | 18 | info('misc.ts loaded'); 19 | export default class { 20 | static get(key: string): Record | undefined { 21 | return generalEnmap.get(key); 22 | } 23 | 24 | static set(key: string, value: Record): void { 25 | generalEnmap.set(key, value); 26 | } 27 | 28 | static has(key: string): boolean { 29 | return generalEnmap.has(key); 30 | } 31 | 32 | static delete(key: string): void { 33 | generalEnmap.delete(key); 34 | } 35 | 36 | static flush(): void { 37 | generalEnmap.clear(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/util/read.ts: -------------------------------------------------------------------------------- 1 | import Enmap from 'enmap'; 2 | import { app } from 'electron'; 3 | 4 | export type ReadDatabaseValue = { 5 | [chapterID: string]: { 6 | isBookmarked: boolean; 7 | pageCount: number; 8 | currentPage: number; 9 | lastRead: Date | undefined; 10 | timeElapsed: number; 11 | mangaid?: string; 12 | }; 13 | }; 14 | const ReadDatabase = new Enmap< 15 | string, // Source ID 16 | ReadDatabaseValue 17 | >({ 18 | name: 'read', 19 | dataDir: app.getPath('userData'), 20 | }); 21 | 22 | export default class ReadDB { 23 | public static async flush(): Promise { 24 | ReadDatabase.deleteAll(); 25 | } 26 | 27 | public static async get( 28 | sourceID: string 29 | ): Promise { 30 | return ReadDatabase.get(sourceID); 31 | } 32 | 33 | public static async set( 34 | sourceID: string, 35 | chapterID: string, 36 | pageCount: number, 37 | currentPage: number, 38 | lastRead: Date | undefined, 39 | timeElapsed: number, 40 | isBookmarked: boolean, 41 | mangaid?: string 42 | ): Promise { 43 | const read = await ReadDB.get(sourceID); 44 | if (!read) { 45 | ReadDatabase.set(sourceID, { 46 | [chapterID]: { 47 | isBookmarked, 48 | pageCount, 49 | currentPage, 50 | lastRead, 51 | timeElapsed, 52 | mangaid, 53 | }, 54 | }); 55 | } else { 56 | read[chapterID] = { 57 | pageCount, 58 | currentPage, 59 | lastRead, 60 | isBookmarked, 61 | timeElapsed, 62 | mangaid, 63 | }; 64 | ReadDatabase.set(sourceID, read); 65 | } 66 | } 67 | 68 | public static async deleteEntry( 69 | sourceID: string, 70 | chapterID: string 71 | ): Promise { 72 | const read = await ReadDB.get(sourceID); 73 | if (read) { 74 | delete read[chapterID]; 75 | ReadDatabase.set(sourceID, read); 76 | } 77 | } 78 | 79 | public static async deleteSource(sourceID: string): Promise { 80 | ReadDatabase.delete(sourceID); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/util/reader.ts: -------------------------------------------------------------------------------- 1 | // Reader options for individual manga. 2 | import Enmap from 'enmap'; 3 | import { app } from 'electron'; 4 | 5 | const readerEnmap = new Enmap< 6 | string, 7 | { [mangaID: string]: { [overriddenSetting: string]: any } } 8 | >({ 9 | name: 'reader', 10 | dataDir: app.getPath('userData'), 11 | }); 12 | 13 | export default class { 14 | static getMangaSettings(sourceID: string, mangaID: string) { 15 | return readerEnmap.get(sourceID)?.[mangaID]; 16 | } 17 | 18 | static setMangaSettings( 19 | sourceID: string, 20 | mangaID: string, 21 | overrides: { [setting: string]: any } 22 | ) { 23 | const mangaSettings = readerEnmap.get(sourceID) || {}; 24 | mangaSettings[mangaID] = overrides; 25 | readerEnmap.set(sourceID, mangaSettings); 26 | } 27 | 28 | static flush() { 29 | readerEnmap.deleteAll(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/util/rpc.ts: -------------------------------------------------------------------------------- 1 | import { throttle } from 'lodash'; 2 | import DiscordRPC, { Presence } from 'discord-rpc'; 3 | 4 | // These are the hooks that are used to listen to events from main.ts. 5 | // Should be present in every page component. 6 | 7 | const RPCId = '956049974263689278'; 8 | DiscordRPC.register(RPCId); 9 | export const RPCClient = new DiscordRPC.Client({ transport: 'ipc' }); 10 | 11 | let currentRPC: Presence; 12 | let RPCEnabled = true; 13 | let isInitialized = false; 14 | export const updateRichPresence = throttle((data: Presence) => { 15 | currentRPC = data; 16 | }, 15000); 17 | 18 | setInterval(() => { 19 | if (RPCEnabled) RPCClient.setActivity(currentRPC ?? {}); 20 | }, 15000); 21 | 22 | export const toggleRPC = (enabled: boolean) => { 23 | RPCEnabled = enabled; 24 | if (!RPCEnabled) { 25 | RPCClient.clearActivity(); 26 | } else RPCClient.setActivity(currentRPC); 27 | }; 28 | 29 | export default async () => { 30 | if (isInitialized) return; 31 | isInitialized = true; 32 | return RPCClient.login({ clientId: RPCId }).catch((e) => { 33 | console.error('Failed to initialize RPC client.', e); 34 | toggleRPC(false); 35 | }); 36 | }; 37 | -------------------------------------------------------------------------------- /src/main/util/source.ts: -------------------------------------------------------------------------------- 1 | // Settings for each source 2 | -------------------------------------------------------------------------------- /src/main/util/theme.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-classes-per-file */ 2 | import path from 'path'; 3 | import { existsSync } from 'fs'; 4 | import { getMainRequire } from '../../shared/util'; 5 | 6 | // default theme imports 7 | import defaultThemeMetadata from '../../shared/theme/default/metadata.json'; 8 | import defaultThemeDarkColors from '../../shared/theme/default/dark/colors.json'; 9 | import defaultThemeLightColors from '../../shared/theme/default/light/colors.json'; 10 | 11 | const mainRequire = getMainRequire(); 12 | const userData = window.electron.util.getUserDataPath(); 13 | 14 | class Theme { 15 | constructor(public themeName: string, public variant: 'light' | 'dark') { 16 | this.isDefault = themeName.toLowerCase() === 'default'; 17 | 18 | const allThemes = window.electron.util.themes; 19 | const theme = allThemes[themeName.toLowerCase()]; 20 | 21 | const variantPath = this.isDefault 22 | ? 'default' 23 | : path.join(theme.location, variant); 24 | 25 | if ( 26 | !existsSync(userData) || 27 | ((!existsSync(variantPath) || !theme) && !this.isDefault) 28 | ) { 29 | // this can 100% loop forever if the default theme is broken so be wary! :) 30 | 31 | return new Theme('default', variant); 32 | } 33 | 34 | this.isDefault = themeName === 'default'; 35 | this.baseThemePath = themeName; 36 | this.variantPath = variantPath; 37 | this.metadata = theme?.metadata ?? defaultThemeMetadata; 38 | } 39 | 40 | public metadata!: Record; 41 | 42 | private baseThemePath!: string; 43 | 44 | private isDefault: boolean; 45 | 46 | private variantPath!: string; 47 | 48 | public getComponentStyle = (componentName: string) => { 49 | if (this.isDefault) return {}; 50 | 51 | const componentFile = path.join( 52 | this.variantPath, 53 | 'components', 54 | `${componentName}.json` 55 | ); 56 | 57 | if (!existsSync(componentFile)) { 58 | return {}; 59 | } 60 | 61 | try { 62 | delete mainRequire.cache[mainRequire.resolve(componentFile)]; 63 | } catch (e) { 64 | window.electron.log.warn(e); 65 | } 66 | return mainRequire(componentFile); 67 | }; 68 | 69 | public getPageStyle = (pageName: string) => { 70 | if (this.isDefault) return {}; 71 | const pageFile = path.join(this.variantPath, 'pages', `${pageName}.json`); 72 | if (!existsSync(pageFile)) { 73 | return {}; 74 | } 75 | 76 | try { 77 | delete mainRequire.cache[mainRequire.resolve(pageFile)]; 78 | } catch (e) { 79 | window.electron.log.warn(e); 80 | } 81 | return mainRequire(pageFile); 82 | }; 83 | 84 | public getColors = (): 85 | | { 86 | accent: string; 87 | accent2: string; 88 | accentSpecial: string; 89 | 90 | background: string; 91 | backgroundDark: string; 92 | backgroundLight: string; 93 | backgroundTransparent: string; 94 | 95 | white: string; 96 | black: string; 97 | 98 | textLight: string; 99 | textDark: string; 100 | 101 | tag: string; 102 | tagText: string; 103 | } 104 | | Record => { 105 | const correspondingDefault = { 106 | dark: defaultThemeDarkColors, 107 | light: defaultThemeLightColors, 108 | }[this.variant]; 109 | if (!this.isDefault) { 110 | const colorsFile = path.join(this.variantPath, 'colors.json'); 111 | 112 | if (!existsSync(colorsFile)) { 113 | return correspondingDefault; 114 | } 115 | 116 | try { 117 | delete mainRequire.cache[mainRequire.resolve(colorsFile)]; 118 | } catch (e) { 119 | window.electron.log.warn(e); 120 | } 121 | return { ...correspondingDefault, ...mainRequire(colorsFile) }; 122 | } 123 | 124 | return correspondingDefault; 125 | }; 126 | } 127 | 128 | export default Theme; 129 | -------------------------------------------------------------------------------- /src/renderer/App.tsx: -------------------------------------------------------------------------------- 1 | import './css/App.css'; 2 | import { MemoryRouter as Router, Routes, Route } from 'react-router-dom'; 3 | import { StyleSheet, css } from 'aphrodite'; 4 | import { ipcRenderer } from 'electron'; 5 | 6 | import Topbar from './components/topbar'; 7 | import Library from './pages/library'; 8 | import Login from './pages/login'; 9 | import Search from './pages/search'; 10 | import NotFound from './pages/404'; 11 | import View from './pages/view'; 12 | import Reader from './pages/reader'; 13 | import Settings from './pages/settings'; 14 | import Sources from './pages/sources'; 15 | import Theme from '../main/util/theme'; 16 | 17 | const { theme, themeStyleDark, themeStyleLight } = 18 | window.electron.settings.getAll().appearance; 19 | 20 | const currentTheme = new Theme( 21 | theme === 'dark' ? themeStyleDark : themeStyleLight, 22 | theme as 'dark' | 'light' 23 | ); 24 | 25 | const themeColors = currentTheme.getColors(); 26 | const pageStyle = currentTheme.getPageStyle('main'); 27 | 28 | const styles = StyleSheet.create({ 29 | root: { 30 | height: 'calc(100% - 32px)', 31 | width: '100%', 32 | overflow: 'hidden', 33 | position: 'absolute', 34 | bottom: 0, 35 | }, 36 | main: { 37 | height: '101%', 38 | width: '100%', 39 | backgroundColor: themeColors.background, 40 | zIndex: 1, 41 | position: 'absolute', 42 | }, 43 | ...pageStyle, 44 | }) as any; 45 | 46 | ipcRenderer.on('download-source-error', (_, src, msg) => 47 | window.electron.log.error(`DL Error for Source ${src}:`, msg) 48 | ); 49 | 50 | ipcRenderer.on('download-source-success', (_, src, msg) => 51 | window.electron.log.info(`DL Response for Source ${src}:`, msg) 52 | ); 53 | 54 | export default function App() { 55 | window.electron.log.info('App.tsx: Rendering App'); 56 | return ( 57 |
58 | 59 |
60 | 61 | 62 | } /> 63 | } /> 64 | } /> 65 | } /> 66 | } /> 67 | } /> 68 | } /> 69 | } /> 70 | } /> 71 | 72 | 73 |
74 |
75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /src/renderer/components/button.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-props-no-spreading */ 2 | import { ButtonProps, Button as MuiButton, Tooltip } from '@mui/material'; 3 | import { StyleSheet, css } from 'aphrodite'; 4 | import Theme from '../../main/util/theme'; 5 | 6 | const { theme, themeStyleDark, themeStyleLight } = 7 | window.electron.settings.getAll().appearance; 8 | 9 | const currentTheme = new Theme( 10 | theme === 'dark' ? themeStyleDark : themeStyleLight, 11 | theme as 'dark' | 'light' 12 | ); 13 | 14 | const themeColors = currentTheme.getColors(); 15 | const componentStyle = currentTheme.getComponentStyle('trackeritem'); 16 | 17 | const styles = StyleSheet.create({ 18 | buttonContainer: { 19 | width: 'fit-content', 20 | display: 'flex', 21 | flexDirection: 'column', 22 | justifyContent: 'center', 23 | alignItems: 'center', 24 | boxSizing: 'border-box', 25 | padding: '8px', 26 | }, 27 | 28 | button: { 29 | display: 'flex', 30 | fontWeight: 'bold', 31 | color: themeColors.accent, 32 | minWidth: '80px', 33 | }, 34 | 35 | ...componentStyle, 36 | }) as any; 37 | 38 | const Button = ( 39 | props: ButtonProps & { 40 | tooltipTitle?: string; 41 | label?: string; 42 | tooltipPlacement?: 'top' | 'bottom'; 43 | } 44 | ) => { 45 | const { tooltipTitle, tooltipPlacement, label, onClick, ...rest } = props; 46 | return ( 47 |
48 | 49 | {})} 61 | > 62 | {label ?? 'Button'} 63 | 64 | 65 |
66 | ); 67 | }; 68 | 69 | Button.defaultProps = { 70 | tooltipTitle: '', 71 | tooltipPlacement: 'top', 72 | label: 'Button', 73 | }; 74 | 75 | export default Button; 76 | -------------------------------------------------------------------------------- /src/renderer/components/chaptermodal.tsx: -------------------------------------------------------------------------------- 1 | import { Dialog, DialogTitle, DialogContent, Button } from '@mui/material'; 2 | import { StyleSheet, css } from 'aphrodite'; 3 | import { useRef, useState, useEffect } from 'react'; 4 | 5 | import { 6 | Chapter as DatabaseChapter, 7 | Manga as DatabaseManga, 8 | } from '../../main/util/manga'; 9 | 10 | import Loading from './loading'; 11 | import Chapter from './chapter'; 12 | import Handler from '../../main/sources/handler'; 13 | 14 | import { sortChapters } from '../util/func'; 15 | import { useTranslation } from '../../shared/intl'; 16 | import Theme from '../../main/util/theme'; 17 | 18 | const { theme, themeStyleDark, themeStyleLight } = 19 | window.electron.settings.getAll().appearance; 20 | 21 | const currentTheme = new Theme( 22 | theme === 'dark' ? themeStyleDark : themeStyleLight, 23 | theme as 'dark' | 'light' 24 | ); 25 | 26 | const themeColors = currentTheme.getColors(); 27 | const componentStyle = currentTheme.getComponentStyle('chaptermodal'); 28 | 29 | const stylesObject = { 30 | dialog: {}, 31 | selected: { 32 | border: `4px solid ${themeColors.accent}`, 33 | }, 34 | 35 | chapterModalDialog: { background: 'transparent' }, 36 | 37 | chapterModalDialogTitle: { 38 | backgroundColor: themeColors.background, 39 | color: themeColors.textLight, 40 | }, 41 | 42 | chapterModalDialogContent: { 43 | backgroundColor: themeColors.background, 44 | '::-webkit-scrollbar': { 45 | width: '8px', 46 | }, 47 | '::-webkit-scrollbar-thumb': { 48 | background: themeColors.white, 49 | ':hover': { 50 | background: themeColors.accent, 51 | }, 52 | }, 53 | }, 54 | 55 | chapterModalDialogChapterItem: {}, 56 | 57 | chapterModalDialogLoadingCircle: {}, 58 | 59 | chapterModalDialogErrorContainer: {}, 60 | 61 | chapterModalDialogErrorRetryButton: {}, 62 | 63 | ...componentStyle, 64 | }; 65 | 66 | const styles = StyleSheet.create(stylesObject) as any; 67 | 68 | const ChapterModal = ({ 69 | onChange, 70 | onClose, 71 | 72 | chapters, 73 | current, 74 | source, 75 | manga, 76 | open, 77 | }: { 78 | onChange: (chapterId: string) => void; 79 | onClose?: () => void; 80 | 81 | chapters: DatabaseChapter[]; 82 | current: string; 83 | source: string; 84 | manga: string; 85 | open: boolean; 86 | }) => { 87 | const Chapters = useRef(window.electron.read.get(source)); 88 | const [mangaObject, setManga] = useState< 89 | DatabaseManga | Record 90 | >(); 91 | const [errorOccured, setErrorOccured] = useState(false); 92 | const { t } = useTranslation(); 93 | 94 | // If manga is not in cache, pull from source 95 | useEffect(() => { 96 | if (!mangaObject || errorOccured) { 97 | const cacheManga = window.electron.library.getCachedManga(source, manga); 98 | if (cacheManga) return setManga(cacheManga); 99 | 100 | const foundSource = window.electron.util 101 | .getSourceFiles() 102 | .map(Handler.getSource) 103 | .filter((x) => x.getName().toLowerCase() === source.toLowerCase()); 104 | 105 | if (foundSource.length === 0) { 106 | window.electron.log.error(t('chaptermodal_error_source')); 107 | return setErrorOccured(true); 108 | } 109 | const sourceObject = foundSource[0]; 110 | sourceObject 111 | .getManga(manga, false) 112 | .then((fullManga) => { 113 | return setManga(fullManga); 114 | }) 115 | .catch((e) => { 116 | window.electron.log.error(e); 117 | setErrorOccured(true); 118 | }); 119 | } 120 | 121 | return undefined; 122 | }, [mangaObject, source, manga, errorOccured, t]); 123 | 124 | return ( 125 | 130 | 131 | Chapters 132 | 133 | 134 | {errorOccured ? ( 135 |
136 |

137 | {t('chaptermodal_error_load')} 138 | 147 |

148 |
149 | ) : ( 150 |
151 | {mangaObject ? ( 152 |
153 | {sortChapters(chapters).map((chapter) => { 154 | return ( 155 | 168 | ); 169 | })} 170 |
171 | ) : ( 172 |
173 | 176 |
177 | )} 178 |
179 | )} 180 |
181 |
182 | ); 183 | }; 184 | 185 | ChapterModal.defaultProps = { 186 | onClose: () => {}, 187 | }; 188 | 189 | export default ChapterModal; 190 | -------------------------------------------------------------------------------- /src/renderer/components/context/reader.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/destructuring-assignment */ 2 | /* eslint-disable react/jsx-props-no-spreading */ 3 | import { 4 | Menu, 5 | MenuItem, 6 | MenuProps, 7 | Divider, 8 | ListItemIcon, 9 | ListItemText, 10 | } from '@mui/material'; 11 | 12 | import { css, StyleSheet } from 'aphrodite'; 13 | 14 | import ShareIcon from '@mui/icons-material/Share'; 15 | import ContentCopyIcon from '@mui/icons-material/ContentCopy'; 16 | import FileDownloadIcon from '@mui/icons-material/FileDownload'; 17 | import SkipNextIcon from '@mui/icons-material/SkipNext'; 18 | import SkipPreviousIcon from '@mui/icons-material/SkipPrevious'; 19 | import NavigateNextIcon from '@mui/icons-material/NavigateNext'; 20 | import NavigateBeforeIcon from '@mui/icons-material/NavigateBefore'; 21 | import SendIcon from '@mui/icons-material/Send'; 22 | 23 | import Theme from '../../../main/util/theme'; 24 | import { useTranslation } from '../../../shared/intl'; 25 | 26 | const { theme, themeStyleDark, themeStyleLight } = 27 | window.electron.settings.getAll().appearance; 28 | 29 | const currentTheme = new Theme( 30 | theme === 'dark' ? themeStyleDark : themeStyleLight, 31 | theme as 'dark' | 'light' 32 | ); 33 | 34 | const themeColors = currentTheme.getColors(); 35 | const componentStyle = currentTheme.getComponentStyle('reader'); 36 | 37 | const contextMenuInner = { 38 | backgroundColor: themeColors.background, 39 | }; 40 | 41 | const contextDividerLine = { 42 | borderTop: `${themeColors.white.substring(0, 7)}22`, 43 | }; 44 | 45 | const styles = StyleSheet.create({ 46 | contextMenu: { 47 | zIndex: 32000, 48 | }, 49 | contextMenuFont: { 50 | fontSize: '0.8rem', 51 | }, 52 | contextMenuIcon: { 53 | color: themeColors.accent, 54 | }, 55 | 56 | contextDivider: { 57 | fontFamily: 'Poppins', 58 | color: 'white', 59 | }, 60 | 61 | contextItem: { 62 | color: 'white', 63 | }, 64 | 65 | // Stlyes used with MUI's `sx` fields. 66 | contextMenuInner, 67 | contextDividerLine, 68 | ...componentStyle, 69 | }) as any; 70 | 71 | type UniversalChildren = JSX.Element[] | JSX.Element | string; 72 | const ElementDivider = ({ children }: { children: UniversalChildren }) => ( 73 | 84 | {children} 85 | 86 | ); 87 | 88 | const ReaderMenu = ( 89 | props: Exclude< 90 | MenuProps & { 91 | onItemClick?: (itemClicked: string) => void; 92 | }, 93 | 'className' 94 | > 95 | ) => { 96 | const { t } = useTranslation(); 97 | 98 | const onItemClick = props.onItemClick!; // Will be present due to defaultProps. 99 | const dataSet: Record< 100 | string, 101 | Array<{ op: string; icon: JSX.Element; text: string }> 102 | > = { 103 | File: [ 104 | { 105 | op: 'clipboard', 106 | icon: , 107 | text: t('menu_reader_file_clipboard'), 108 | }, 109 | { 110 | op: 'save', 111 | icon: , 112 | text: t('menu_reader_file_save'), 113 | }, 114 | ], 115 | Pages: [ 116 | { 117 | op: 'nextpage', 118 | icon: , 119 | text: t('menu_reader_pages_nextpage'), 120 | }, 121 | { 122 | op: 'prevpage', 123 | icon: , 124 | text: t('menu_reader_pages_prevpage'), 125 | }, 126 | ], 127 | Chapters: [ 128 | { 129 | op: 'nextchap', 130 | icon: , 131 | text: t('menu_reader_chapters_nextchap'), 132 | }, 133 | { 134 | op: 'prevchap', 135 | icon: , 136 | text: t('menu_reader_chapters_prevchap'), 137 | }, 138 | ], 139 | Misc: [ 140 | { 141 | op: 'sharepage', 142 | icon: , 143 | text: t('menu_reader_misc_sharepage'), 144 | }, 145 | { 146 | op: 'sharechapter', 147 | icon: , 148 | text: t('menu_reader_misc_sharechapter'), 149 | }, 150 | ], 151 | }; 152 | 153 | return ( 154 | 162 | {Object.keys(dataSet).map((key) => { 163 | return ( 164 |
165 | {key} 166 | {dataSet[key].map((item) => { 167 | return ( 168 | onItemClick(item.op)}> 169 | 170 | {item.icon} 171 | 172 | 176 | 177 | ); 178 | })} 179 |
180 | ); 181 | })} 182 |
183 | ); 184 | }; 185 | 186 | ReaderMenu.defaultProps = { 187 | onItemClick: () => {}, 188 | }; 189 | 190 | export default ReaderMenu; 191 | -------------------------------------------------------------------------------- /src/renderer/components/defer.tsx: -------------------------------------------------------------------------------- 1 | // https://itnext.io/improving-slow-mounts-in-react-apps-cff5117696dc 2 | import { useState, Children, useEffect, useMemo, ReactElement } from 'react'; 3 | 4 | const Defer = ({ 5 | chunkSize, 6 | children, 7 | }: { 8 | chunkSize: number; 9 | children: JSX.Element[]; 10 | }) => { 11 | const [renderedItemsCount, setRenderedItemsCount] = useState(chunkSize); 12 | const childrenArray = useMemo(() => Children.toArray(children), [children]); 13 | 14 | useEffect(() => { 15 | if (renderedItemsCount < childrenArray.length) { 16 | window.requestIdleCallback( 17 | () => { 18 | setRenderedItemsCount( 19 | Math.min(renderedItemsCount + chunkSize, childrenArray.length) 20 | ); 21 | }, 22 | { timeout: 200 } 23 | ); 24 | } 25 | }, [renderedItemsCount, childrenArray.length, chunkSize]); 26 | 27 | return childrenArray.slice( 28 | 0, 29 | renderedItemsCount 30 | ) as unknown as ReactElement[]; 31 | }; 32 | 33 | export default Defer; 34 | -------------------------------------------------------------------------------- /src/renderer/components/dialog.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-props-no-spreading */ 2 | import { 3 | Dialog as MuiDialog, 4 | DialogContent, 5 | DialogTitle, 6 | DialogProps as MuiDialogProps, 7 | DialogActions, 8 | } from '@mui/material'; 9 | import { StyleSheet, css } from 'aphrodite'; 10 | import { omit } from 'lodash'; 11 | import Theme from '../../main/util/theme'; 12 | 13 | const { theme, themeStyleDark, themeStyleLight } = 14 | window.electron.settings.getAll().appearance; 15 | 16 | const currentTheme = new Theme( 17 | theme === 'dark' ? themeStyleDark : themeStyleLight, 18 | theme as 'dark' | 'light' 19 | ); 20 | 21 | const themeColors = currentTheme.getColors(); 22 | const componentStyle = currentTheme.getComponentStyle('trackeritem'); 23 | 24 | type DialogProps = MuiDialogProps & 25 | Pick, 'children' | 'open'> & { 26 | rawcontent?: boolean; 27 | title?: string; 28 | actions?: React.ReactNode; 29 | }; 30 | 31 | const stylesObject = { 32 | dialog: {}, 33 | selected: { 34 | border: `4px solid ${themeColors.accent}`, 35 | }, 36 | 37 | modalDialog: { background: 'transparent' }, 38 | 39 | modalDialogTitle: { 40 | backgroundColor: themeColors.background, 41 | color: themeColors.textLight, 42 | }, 43 | 44 | modalDialogContent: { 45 | backgroundColor: themeColors.background, 46 | '::-webkit-scrollbar': { 47 | width: '8px', 48 | }, 49 | '::-webkit-scrollbar-thumb': { 50 | background: themeColors.white, 51 | ':hover': { 52 | background: themeColors.accent, 53 | }, 54 | }, 55 | }, 56 | 57 | modalDialogActions: { 58 | background: themeColors.accent, 59 | }, 60 | 61 | ...componentStyle, 62 | }; 63 | 64 | const styles = StyleSheet.create(stylesObject) as any; 65 | const Dialog = (props: DialogProps) => { 66 | const { children, rawcontent, actions, title } = props; 67 | return ( 68 | 69 | {rawcontent ? ( 70 | children 71 | ) : ( 72 | <> 73 | 74 | {title} 75 | 76 | 77 | {children} 78 | 79 | {actions ? ( 80 | 81 | {actions} 82 | 83 | ) : null} 84 | 85 | )} 86 | 87 | ); 88 | }; 89 | 90 | Dialog.defaultProps = { 91 | rawcontent: false, 92 | title: 'Modal', 93 | actions: null, 94 | }; 95 | 96 | export default Dialog; 97 | -------------------------------------------------------------------------------- /src/renderer/components/filter.tsx: -------------------------------------------------------------------------------- 1 | import { StyleSheet, css } from 'aphrodite'; 2 | import { 3 | AppBar, 4 | Toolbar, 5 | IconButton, 6 | Typography, 7 | useScrollTrigger, 8 | } from '@mui/material'; 9 | 10 | import FilterListIcon from '@mui/icons-material/FilterList'; 11 | import PropTypes from 'prop-types'; 12 | import React, { useState } from 'react'; 13 | import Theme from '../../main/util/theme'; 14 | 15 | const { theme, themeStyleDark, themeStyleLight } = 16 | window.electron.settings.getAll().appearance; 17 | 18 | const currentTheme = new Theme( 19 | theme === 'dark' ? themeStyleDark : themeStyleLight, 20 | theme as 'dark' | 'light' 21 | ); 22 | 23 | const themeColors = currentTheme.getColors(); 24 | const componentStyle = currentTheme.getComponentStyle('filter'); 25 | 26 | const styles = StyleSheet.create({ 27 | appBarContainer: { 28 | position: 'absolute', 29 | width: 'fit-content', 30 | height: 'fit-content', 31 | bottom: '15px', 32 | right: '25px', 33 | overflow: 'hidden', 34 | }, 35 | 36 | appBar: { 37 | position: 'relative', 38 | backgroundColor: themeColors.backgroundDark, 39 | color: themeColors.textLight, 40 | width: '128px', 41 | height: 'fit-content', 42 | borderRadius: '5%', 43 | transition: 44 | 'width 0.2s ease-in-out, height 0.2s ease-in-out, border-radius 0.1s ease-in-out', 45 | }, 46 | 47 | small: { 48 | transition: 49 | 'width 0.2s ease-in-out, height 0.2s ease-in-out, border-radius 0.3s ease-in-out', 50 | 51 | borderRadius: '50%', 52 | width: '48px', 53 | height: '48px', 54 | }, 55 | 56 | buttonSmall: { 57 | display: 'flex', 58 | alignItems: 'center', 59 | justifyContent: 'center', 60 | alignContent: 'center', 61 | }, 62 | 63 | filterIcon: { 64 | color: themeColors.accent, 65 | transition: 'color 0.2s ease-in-out, transform 0.2s ease-in-out', 66 | ':hover': { 67 | color: themeColors.textLight, 68 | }, 69 | }, 70 | 71 | filterIconDisabled: { 72 | color: '#7C7C7C', 73 | ':hover': { 74 | color: '#7C7C7C', 75 | }, 76 | }, 77 | 78 | toolbarsmall: { 79 | display: 'flex', 80 | alignItems: 'center', 81 | justifyContent: 'center', 82 | alignContent: 'center', 83 | padding: '0px', 84 | }, 85 | 86 | buttonWrapper: { 87 | cursor: 'pointer', 88 | padding: '0', 89 | border: 'none', 90 | background: 'none', 91 | outline: 'none', 92 | }, 93 | 94 | ...componentStyle, 95 | }) as any; 96 | 97 | type FilterProps = { 98 | onClick: () => void; 99 | disabled?: boolean; 100 | scrollTarget: Node | Window; 101 | }; 102 | 103 | const FilterButton = ({ onClick, scrollTarget, disabled }: FilterProps) => { 104 | const didScroll = useScrollTrigger({ 105 | disableHysteresis: false, 106 | target: scrollTarget, 107 | threshold: 100, 108 | }); 109 | 110 | const [didHover, setHoverState] = useState(false); 111 | const displaySmall = didScroll && !didHover; 112 | 113 | return ( 114 |
115 | 153 |
154 | ); 155 | }; 156 | 157 | FilterButton.propTypes = { 158 | disabled: PropTypes.bool.isRequired, 159 | }; 160 | 161 | export default FilterButton; 162 | -------------------------------------------------------------------------------- /src/renderer/components/lightbar.tsx: -------------------------------------------------------------------------------- 1 | import { StyleSheet, css } from 'aphrodite'; 2 | import { useState } from 'react'; 3 | import Theme from '../../main/util/theme'; 4 | 5 | const { theme, themeStyleDark, themeStyleLight } = 6 | window.electron.settings.getAll().appearance; 7 | 8 | const currentTheme = new Theme( 9 | theme === 'dark' ? themeStyleDark : themeStyleLight, 10 | theme as 'dark' | 'light' 11 | ); 12 | 13 | const themeColors = currentTheme.getColors(); 14 | const componentStyleLightbar = currentTheme.getComponentStyle('lightbar'); 15 | const componentStyleLightbarItem = 16 | currentTheme.getComponentStyle('lightbaritem'); 17 | 18 | export const lightbarStyle = StyleSheet.create({ 19 | lightbar: {}, 20 | 21 | vertical: { 22 | display: 'inline-flex', 23 | flexDirection: 'column', 24 | height: '100%', 25 | width: '100%', 26 | }, 27 | horizontal: { 28 | display: 'inline-flex', 29 | flexDirection: 'row', 30 | height: '100%', 31 | width: '100%', 32 | }, 33 | left: { 34 | left: 0, 35 | top: 0, 36 | borderRadius: '0 5px 5px 0px', 37 | }, 38 | right: { 39 | position: 'absolute', 40 | right: 0, 41 | top: 0, 42 | borderRadius: '0px 5px', 43 | }, 44 | bottom: { 45 | position: 'absolute', 46 | bottom: 0, 47 | left: 0, 48 | borderRadius: '0px 0px 5px 5px', 49 | }, 50 | lightbarItem: { 51 | border: 'none', 52 | background: 'none', 53 | cursor: 'pointer', 54 | flexGrow: 1, 55 | }, 56 | lightbarItemBeforeHorizontal: { 57 | margin: '0px 1px', 58 | }, 59 | before: {}, 60 | lightbarItemBeforeVertical: { 61 | margin: '1% 0px 1% 0px', 62 | }, 63 | ...componentStyleLightbar, 64 | }) as any; 65 | 66 | const SidebarItem = ({ 67 | pageValue = 1 as number, 68 | pageDisplay = String(pageValue) as string, 69 | doublePage = false as boolean, 70 | isSelected = false as boolean, 71 | isVertical = false as boolean, 72 | isRight = false as boolean, 73 | isTooSmall = false as boolean, 74 | isSmall = false as boolean, 75 | forceShow = false as boolean, 76 | onClick = (() => {}) as (pageNumber: number) => void, 77 | }) => { 78 | const Colour = `221,4,38`; 79 | const selectedStylesheet = StyleSheet.create({ 80 | itemGradient: { 81 | backgroundImage: isSelected 82 | ? `linear-gradient(to ${ 83 | isVertical ? (isRight ? 'right' : 'left') : 'bottom' 84 | }, rgba(${ 85 | isSelected ? '255,255,255' : '0,0,0' 86 | },0) 0%,rgba(255,255,255,0.25) 100%)` 87 | : 'rgba(0,0,0,255)', 88 | zIndex: 1, 89 | transition: 90 | 'opacity 0.2s ease-in-out, top 0.2s ease-in-out, left 0.2s ease-in-out, right 0.2s ease-in-out, bottom 0.2s ease-in-out', 91 | position: 'relative', 92 | top: !isVertical 93 | ? isSelected 94 | ? '-8px' 95 | : forceShow 96 | ? '0px' 97 | : '12px' 98 | : '0px', 99 | left: isVertical // why am i such an awful programmer? 100 | ? isSelected 101 | ? isRight 102 | ? '-8px' 103 | : '8px' 104 | : forceShow 105 | ? '0px' 106 | : isRight 107 | ? '12px' 108 | : '-12px' 109 | : '0px', 110 | opacity: isSelected ? 1 : forceShow ? 1 : 0, 111 | '::before': { 112 | content: `""`, // this makes sure that if there are too many pages, the text is not shown 113 | position: 'absolute', 114 | top: 0, 115 | left: 0, 116 | width: '100%', 117 | height: '100%', 118 | opacity: 0, 119 | transition: 120 | 'top 0.5s ease-in-out, left 0.5s ease-in-out, right 0.5s ease-in-out, bottom 0.5s ease-in-out, opacity 0.5s ease-in-out', 121 | zIndex: -1, 122 | backgroundImage: `linear-gradient(to ${ 123 | isVertical ? (isRight ? 'right' : 'left') : 'bottom' 124 | }, rgba(0,0,0,0) 0%,rgba(${Colour},0.25) 100%)`, 125 | }, 126 | ':hover': { 127 | transform: 'scale(1.05), translateY(-15px)', 128 | [isVertical ? (isRight ? 'left' : 'right') : 'top']: '-8px', 129 | '::before': { 130 | opacity: 1, 131 | }, 132 | }, 133 | }, 134 | bar: { 135 | height: isVertical ? '100%' : '3px', 136 | width: isVertical ? '3px' : '100%', 137 | position: 'absolute', 138 | backgroundColor: isSelected ? themeColors.accent : themeColors.white, 139 | [`${isVertical ? (isRight ? 'right' : 'left') : 'bottom'}`]: isVertical 140 | ? '0%' 141 | : '8%', 142 | [`${isVertical ? 'top' : ''}`]: 0, 143 | }, 144 | pageNumber: { 145 | top: isVertical ? '0' : '50%', 146 | left: doublePage ? '0' : isVertical && !isRight ? '0' : '50%', 147 | bottom: 0, 148 | right: isVertical && isRight ? '0' : '50%', 149 | [isVertical ? 'height' : 'width']: doublePage ? '100%' : 'unset', 150 | [`${isVertical ? (isRight ? 'right' : 'left') : 'bottom'}`]: '65%', 151 | textAlign: isVertical ? (isRight ? 'right' : 'left') : 'center', 152 | position: 'absolute', 153 | verticalAlign: 'middle', 154 | fontSize: '0.625em', 155 | fontFamily: '"Roboto", sans-serif', 156 | fontWeight: 'bolder', 157 | display: 'inline-flex', 158 | alignItems: 'center', 159 | justifyContent: 'center', 160 | userFocus: 'none', 161 | userInput: 'none', 162 | 'user-select': 'none', 163 | color: themeColors.textLight, 164 | }, 165 | ...componentStyleLightbarItem, 166 | }) as any; 167 | const showText = !isTooSmall ? ( 168 | {pageDisplay} 169 | ) : null; 170 | return ( 171 | 193 | ); 194 | }; 195 | 196 | const Lightbar = ({ 197 | disabled = false, 198 | isVertical = false, 199 | isRight = false, 200 | Page = 1, 201 | outOf = 16, 202 | isRightToLeft = true, 203 | doublePageDisplay = false, 204 | onItemClick = (() => {}) as (pageNumber: number) => void, 205 | }) => { 206 | const [isHover, setHover] = useState(false); 207 | const pageCalculation = 208 | outOf / (document.getElementById('root') as HTMLElement).offsetWidth; 209 | 210 | const items = []; 211 | for (let i = 0; i < outOf; doublePageDisplay ? (i += 2) : i++) { 212 | const iteration = isRightToLeft 213 | ? outOf - i - (outOf % 2 === 0 && doublePageDisplay ? 2 : 1) 214 | : i; 215 | // If doublePageDisplay is true and the page count is an even number, then 216 | items.push( 217 | = 1 / 32} 228 | isTooSmall={pageCalculation >= 1 / 8} 229 | isSelected={Page - 1 === iteration} 230 | forceShow={isHover} 231 | isVertical={isVertical} 232 | isRight={isRight} 233 | onClick={onItemClick} 234 | key={i} 235 | /> 236 | ); 237 | } 238 | 239 | const containerSpecificStylesheet = StyleSheet.create({ 240 | horizontal: { 241 | backgroundImage: `linear-gradient(to top, rgba(0,0,0,0.45) 65%,rgba(0,0,0,0) 100%)`, 242 | }, 243 | verticalL: { 244 | backgroundImage: `linear-gradient(to left, rgba(0,0,0,0) 0%,rgba(0,0,0,0.35) 100%)`, 245 | }, 246 | verticalR: { 247 | backgroundImage: `linear-gradient(to right, rgba(0,0,0,0) 0%,rgba(0,0,0,0.35) 100%)`, 248 | }, 249 | container: { 250 | position: 'absolute', 251 | width: isVertical ? '5%' : '100%', 252 | top: isVertical ? '0' : 'unset', 253 | [isVertical ? (isRight ? 'right' : 'left') : 'bottom']: '0', 254 | height: isVertical ? '95%' : '5%', 255 | marginTop: isVertical ? '1%' : '0%', 256 | display: 'flex', 257 | alignItems: 'center', 258 | zIndex: 1290, 259 | }, 260 | }); 261 | 262 | const containerData = isVertical 263 | ? isRight 264 | ? containerSpecificStylesheet.verticalR 265 | : containerSpecificStylesheet.verticalL 266 | : containerSpecificStylesheet.horizontal; 267 | 268 | return disabled ? null : ( 269 |
{ 272 | e.preventDefault(); 273 | e.stopPropagation(); 274 | }} 275 | > 276 |
{ 288 | setHover(true); 289 | }} 290 | onMouseLeave={() => { 291 | setHover(false); 292 | }} 293 | > 294 | {items} 295 |
296 |
297 | ); 298 | }; 299 | 300 | export default Lightbar; 301 | -------------------------------------------------------------------------------- /src/renderer/components/loading.tsx: -------------------------------------------------------------------------------- 1 | import { CircularProgress, Backdrop } from '@mui/material'; 2 | 3 | const LoadingModal = ({ className }: { className?: string }) => { 4 | return ( 5 | 6 | 7 | 8 | ); 9 | }; 10 | 11 | LoadingModal.defaultProps = { 12 | className: '', 13 | }; 14 | 15 | export default LoadingModal; 16 | -------------------------------------------------------------------------------- /src/renderer/components/loginitem.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/sort-comp */ 2 | import { useCallback, useMemo, useState } from 'react'; 3 | import { Tooltip } from '@mui/material'; 4 | import { StyleSheet, css } from 'aphrodite'; 5 | import { SupportedTrackers, getTracker } from '../util/tracker/tracker'; 6 | import Theme from '../../main/util/theme'; 7 | 8 | const { theme, themeStyleDark, themeStyleLight } = 9 | window.electron.settings.getAll().appearance; 10 | 11 | const currentTheme = new Theme( 12 | theme === 'dark' ? themeStyleDark : themeStyleLight, 13 | theme as 'dark' | 'light' 14 | ); 15 | 16 | const componentStyle = currentTheme.getComponentStyle('loginitem'); 17 | const AniListIntegrationHandler = async () => { 18 | window.electron.auth 19 | .generateAuthenticationWindow( 20 | { 21 | width: 400, 22 | height: 600, 23 | center: true, 24 | maximizable: false, 25 | minimizable: false, 26 | resizable: false, 27 | title: 'AniList Login', 28 | darkTheme: true, 29 | backgroundColor: '#111', 30 | webPreferences: { 31 | contextIsolation: true, 32 | nodeIntegration: false, 33 | nodeIntegrationInWorker: false, 34 | nodeIntegrationInSubFrames: false, 35 | }, 36 | }, 37 | 'https://anilist.co/api/v2/oauth/authorize?client_id=7246&response_type=token' 38 | ) 39 | .then((returnData) => { 40 | if (!returnData) return false; 41 | returnData.expires_in = Date.now() + returnData.expires_in * 1000; 42 | 43 | const previousAuthorization = 44 | window.electron.store.get('authorization') || {}; 45 | 46 | previousAuthorization.anilist = returnData; 47 | window.electron.store.set('authorization', previousAuthorization); 48 | 49 | return true; 50 | }) 51 | .catch(console.error); 52 | }; 53 | 54 | const MyAnimeListIntegrationHandler = async () => { 55 | const Authentication = {} as any; 56 | const PKCE = await window.electron.auth.generatePKCE(); 57 | const authorizationURL = Authentication.getOAuthUrl(PKCE.code_challenge); 58 | return window.electron.auth 59 | .generateAuthenticationWindow( 60 | { 61 | width: 400, 62 | height: 600, 63 | center: true, 64 | maximizable: false, 65 | minimizable: false, 66 | resizable: false, 67 | title: 'MyAnimeList Login', 68 | darkTheme: true, 69 | backgroundColor: '#111', 70 | webPreferences: { contextIsolation: false }, 71 | }, 72 | authorizationURL 73 | ) 74 | .then((returnData) => { 75 | if (!returnData) return false; 76 | const previousAuthorization = window.electron.store.get('authorization'); 77 | previousAuthorization.myanimelist = returnData; 78 | 79 | window.electron.store.set('authorization', previousAuthorization); 80 | return true; 81 | }); 82 | }; 83 | 84 | const styles = StyleSheet.create({ 85 | greyedOut: { 86 | filter: 'grayscale(0.25) brightness(25%)', 87 | }, 88 | loginItem: { 89 | transition: 'filter 1s linear', 90 | display: 'flex', 91 | width: '64px', 92 | height: '64px', 93 | background: 'rgb(14, 14, 14)', 94 | border: '2px solid rgb(14, 14, 14)', 95 | borderRadius: '100%', 96 | marginTop: '10px', 97 | marginRight: '10px', 98 | marginBottom: '10px', 99 | cursor: 'pointer', 100 | 101 | flexDirection: 'column', 102 | justifyContent: 'center', 103 | alignItems: 'center', 104 | }, 105 | img: { 106 | maxWidth: '100%', 107 | maxHeight: '100%', 108 | }, 109 | ...componentStyle, 110 | }) as any; 111 | 112 | type ComponentProps = { 113 | title?: string; 114 | authenticator: SupportedTrackers; 115 | trackedtitle?: string; 116 | onAuth?: () => void; 117 | onDeauth?: () => void; 118 | }; 119 | 120 | const Authenticators = { 121 | AniList: AniListIntegrationHandler, 122 | MyAnimeList: MyAnimeListIntegrationHandler, 123 | }; 124 | 125 | const LoginItem = ({ 126 | title, 127 | authenticator, 128 | trackedtitle, 129 | onAuth, 130 | onDeauth, 131 | }: ComponentProps) => { 132 | const [isAuthenticated, setIsAuthenticated] = useState( 133 | window.electron.auth.checkAuthenticated(authenticator) 134 | ); 135 | 136 | const Tracker = useMemo(() => getTracker(authenticator), [authenticator]); 137 | const handleClick = useCallback(async () => { 138 | await Authenticators[authenticator](); 139 | return true; 140 | }, [authenticator]); 141 | 142 | if (!Tracker) 143 | throw new Error(`Tracker support for ${authenticator} does not exist.`); 144 | const displayTitle = (isAuthenticated ? trackedtitle : title) ?? ''; 145 | return ( 146 | 147 | 171 | 172 | ); 173 | }; 174 | 175 | LoginItem.defaultProps = { 176 | onAuth: () => {}, 177 | onDeauth: () => {}, 178 | trackedtitle: '', 179 | title: '', 180 | }; 181 | 182 | export default LoginItem; 183 | -------------------------------------------------------------------------------- /src/renderer/components/readerbutton.tsx: -------------------------------------------------------------------------------- 1 | const ReaderButton = ({ 2 | className, 3 | divClassName, 4 | 5 | disabled, 6 | onClick, 7 | onMouseMove, 8 | onMouseLeave, 9 | onWheelCapture, 10 | clickIcon, 11 | }: { 12 | className?: string; 13 | divClassName?: string; 14 | 15 | disabled?: boolean; 16 | onClick?: VoidFunction; 17 | onMouseMove?: VoidFunction; 18 | onMouseLeave?: VoidFunction; 19 | onWheelCapture?: (event: React.WheelEvent) => void; 20 | clickIcon?: JSX.Element; 21 | }) => { 22 | return !disabled ? ( 23 |
{}} 27 | onMouseMoveCapture={onMouseMove} 28 | onMouseLeave={onMouseLeave} 29 | role="button" 30 | onWheelCapture={onWheelCapture} 31 | tabIndex={-1} 32 | > 33 |
{clickIcon}
34 |
35 | ) : null; 36 | }; 37 | 38 | ReaderButton.defaultProps = { 39 | className: '', 40 | divClassName: '', 41 | disabled: false, 42 | onClick: () => {}, 43 | onMouseMove: () => {}, 44 | onMouseLeave: () => {}, 45 | onWheelCapture: () => {}, 46 | clickIcon:
, 47 | }; 48 | 49 | export default ReaderButton; 50 | -------------------------------------------------------------------------------- /src/renderer/components/search.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-props-no-spreading */ 2 | import { TextField, TextFieldProps } from '@mui/material'; 3 | import { StyleSheet, css } from 'aphrodite'; 4 | import Theme from '../../main/util/theme'; 5 | 6 | const { theme, themeStyleDark, themeStyleLight } = 7 | window.electron.settings.getAll().appearance; 8 | 9 | const currentTheme = new Theme( 10 | theme === 'dark' ? themeStyleDark : themeStyleLight, 11 | theme as 'dark' | 'light' 12 | ); 13 | 14 | const themeColors = currentTheme.getColors(); 15 | const componentStyle = currentTheme.getComponentStyle('search'); 16 | 17 | const searchStyles = StyleSheet.create({ 18 | searchbarContainer: { 19 | position: 'fixed', 20 | bottom: 15, 21 | left: 10, 22 | width: 'fit-content', 23 | height: 'fit-content', 24 | display: 'flex', 25 | alignItems: 'center', 26 | padding: '8px', 27 | justifyContent: 'center', 28 | border: '1px solid transparent', 29 | zIndex: 260, 30 | }, 31 | searchbarContainerInner: { 32 | backgroundColor: themeColors.textLight, 33 | borderRadius: '80%', 34 | padding: '8px', 35 | width: '52px', 36 | height: '52px', 37 | opacity: 0.2, 38 | transition: 39 | 'width 0.2s ease-out, opacity 0.2s ease-in-out, border-radius 0s ease-in-out', 40 | ':focus-within': { 41 | opacity: 1, 42 | width: 'fit-content', 43 | borderRadius: '4px', 44 | }, 45 | ':hover': { 46 | opacity: 0.8, 47 | }, 48 | }, 49 | searchbar: { 50 | width: '64px', 51 | minWidth: '64px', 52 | height: '100%', 53 | transition: 'width 0.2s ease-in-out', 54 | opacity: 0, 55 | ':focus-within': { 56 | width: '600px', 57 | minWidth: '300px', 58 | opacity: 1, 59 | }, 60 | }, 61 | 62 | ...componentStyle, 63 | }) as any; 64 | 65 | type NewSearchProps = Omit; 66 | const SearchBar = (props: TextFieldProps & NewSearchProps) => { 67 | // Copy props to be able to mutate them 68 | const { ...newProps } = props; 69 | const { className } = newProps; 70 | newProps.className = `${css(searchStyles.searchbar)}${ 71 | className ? ` ${className}` : '' 72 | }`; 73 | 74 | return ( 75 |
76 |
77 | 78 |
79 |
80 | ); 81 | }; 82 | 83 | export default SearchBar; 84 | -------------------------------------------------------------------------------- /src/renderer/components/select.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-props-no-spreading */ 2 | /* eslint-disable react/destructuring-assignment */ 3 | import { Typography, Select as MaterialSelect, MenuItem } from '@mui/material'; 4 | import type { SelectProps as MaterialSelectProps } from '@mui/material/Select'; 5 | 6 | import { omit } from 'lodash'; 7 | import { StyleSheet, css } from 'aphrodite'; 8 | import { useTranslation } from '../../shared/intl'; 9 | import Theme from '../../main/util/theme'; 10 | 11 | const { theme, themeStyleDark, themeStyleLight } = 12 | window.electron.settings.getAll().appearance; 13 | 14 | const currentTheme = new Theme( 15 | theme === 'dark' ? themeStyleDark : themeStyleLight, 16 | theme as 'dark' | 'light' 17 | ); 18 | 19 | const themeColors = currentTheme.getColors(); 20 | const componentStyle = currentTheme.getComponentStyle('select'); 21 | 22 | const stylesObject = { 23 | selected: { 24 | color: themeColors.textLight, 25 | }, 26 | 27 | legend: { 28 | opacity: 0, 29 | transition: 'opacity 0.2s ease-in-out 0.25s', 30 | visibility: 'hidden', 31 | }, 32 | 33 | selectedLegend: { 34 | opacity: 1, 35 | position: 'relative', 36 | top: '-5px', 37 | visibility: 'visible', 38 | color: themeColors.textLight, 39 | }, 40 | 41 | icon: { 42 | color: themeColors.textLight, 43 | }, 44 | 45 | iconSelected: { 46 | color: themeColors.accent, 47 | }, 48 | 49 | focused: { 50 | borderColor: themeColors.accent, 51 | }, 52 | 53 | ...componentStyle, 54 | }; 55 | 56 | // @ts-ignore Aphrodite Sucks: Part 2 57 | const styles = StyleSheet.create(stylesObject) as any; 58 | 59 | const Select = ( 60 | props: Exclude< 61 | Exclude, 'children'>, 62 | 'renderValue' 63 | > & { 64 | values: { 65 | // OptionValue: OptionLabel 66 | [optionValue: string]: string; 67 | }; 68 | } 69 | ) => { 70 | const { value, values, sx, defaultValue } = props; 71 | const { t } = useTranslation(); 72 | if ( 73 | !Object.keys(values).find( 74 | (optionValue) => optionValue === (value ?? defaultValue) 75 | ) 76 | ) 77 | throw new Error( 78 | t('select_error', { 79 | value: String(value ?? defaultValue), 80 | }) 81 | ); 82 | 83 | return ( 84 | { 103 | const displayValue = values[(selected ?? defaultValue) as string]; 104 | return ( 105 | 106 | {displayValue} 107 | 108 | ); 109 | }} 110 | > 111 | {Object.keys(values).map((valuesIndex) => ( 112 | 113 | {values[valuesIndex]} 114 | 115 | ))} 116 | 117 | ); 118 | }; 119 | 120 | export default Select; 121 | -------------------------------------------------------------------------------- /src/renderer/components/settings/downloadlocation.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Box, Typography, ButtonProps, Tooltip } from '@mui/material'; 2 | import { StyleSheet, css } from 'aphrodite'; 3 | import { settingsStylesObject } from '../../util/func'; 4 | import { useTranslation } from '../../../shared/intl'; 5 | import type { Schema } from '../../util/auxiliary'; 6 | import Theme from '../../../main/util/theme'; 7 | 8 | const { theme, themeStyleDark, themeStyleLight } = 9 | window.electron.settings.getAll().appearance; 10 | 11 | const currentTheme = new Theme( 12 | theme === 'dark' ? themeStyleDark : themeStyleLight, 13 | theme as 'dark' | 'light' 14 | ); 15 | 16 | const themeColors = currentTheme.getColors(); 17 | const componentStyle = currentTheme.getComponentStyle('downloadlocation'); 18 | 19 | const styles = StyleSheet.create({ 20 | ...(settingsStylesObject as any), 21 | settingsButton: { 22 | border: `1px solid ${themeColors.accent}`, 23 | color: themeColors.accent, 24 | minWidth: '150px', 25 | width: 'fit-content', 26 | height: '42px', 27 | ':hover': { 28 | backgroundColor: themeColors.accent, 29 | color: themeColors.textLight, 30 | fontWeight: 'bold', 31 | }, 32 | }, 33 | ...componentStyle, 34 | }) as any; 35 | 36 | const DownloadLocation = ( 37 | props: ButtonProps & { 38 | schema: Schema; 39 | setting: string; 40 | onChange: (location: string) => void; 41 | } 42 | ) => { 43 | const { setting, schema, onChange } = props; 44 | const { t } = useTranslation(); 45 | 46 | return ( 47 | 48 | 49 | {schema.label} 50 | 51 | {schema.description} 52 | 53 | 54 | 55 | 56 | 73 | 74 | 75 | ); 76 | }; 77 | 78 | export default DownloadLocation; 79 | -------------------------------------------------------------------------------- /src/renderer/components/settings/filterslider.tsx: -------------------------------------------------------------------------------- 1 | import { StyleSheet, css } from 'aphrodite'; 2 | import { Slider, SliderProps, Box, Typography } from '@mui/material'; 3 | import { noop, clamp } from 'lodash'; 4 | 5 | import type { DefaultSettings } from '../../../main/util/settings'; 6 | import { settingsStylesObject, hexToRgb } from '../../util/func'; 7 | import type { Schema } from '../../util/auxiliary'; 8 | import { useTranslation } from '../../../shared/intl'; 9 | import Theme from '../../../main/util/theme'; 10 | 11 | const { theme, themeStyleDark, themeStyleLight } = 12 | window.electron.settings.getAll().appearance; 13 | 14 | const currentTheme = new Theme( 15 | theme === 'dark' ? themeStyleDark : themeStyleLight, 16 | theme as 'dark' | 'light' 17 | ); 18 | 19 | const themeColors = currentTheme.getColors(); 20 | const componentStyle = currentTheme.getComponentStyle('filterslider'); 21 | 22 | const stylesObject = { 23 | sliderObject: { 24 | width: '50%', 25 | left: '15px', 26 | }, 27 | sliderRail: { 28 | backgroundColor: `${themeColors.white.substring(0, 7)}22`, 29 | }, 30 | sliderHead: { 31 | color: 'white', 32 | }, 33 | ...settingsStylesObject, 34 | ...componentStyle, 35 | }; 36 | 37 | export const generateSliderStyles = ( 38 | headHexColour: string, 39 | sliderColor: string 40 | ) => ({ 41 | '&.MuiSlider-root': stylesObject.sliderHead, 42 | '&.MuiSlider-root span.MuiSlider-thumb': stylesObject.sliderHead, 43 | '&.MuiSlider-root span.MuiSlider-thumb:hover': { 44 | boxShadow: `0px 0px 0px 8px ${headHexColour}`, 45 | }, 46 | '&.MuiSlider-root span.MuiSlider-thumb.Mui-active': { 47 | boxShadow: `0px 0px 0px 8px ${headHexColour}`, 48 | }, 49 | '&.MuiSlider-root span.MuiSlider-thumb.Mui-focusVisaible': { 50 | boxShadow: `0px 0px 0px 8px ${headHexColour}`, 51 | }, 52 | '&.MuiSlider-root span.Mui-active': stylesObject.sliderHead, 53 | '&.MuiSlider-root .MuiSlider-rail': stylesObject.sliderRail, 54 | '&.MuiSlider-root .MuiSlider-track': { 55 | backgroundColor: sliderColor, 56 | color: sliderColor, 57 | }, 58 | }); 59 | 60 | const styles = StyleSheet.create(stylesObject as any) as any; 61 | const FilterSlider = ( 62 | sliderProps: SliderProps & { 63 | schema: Schema; 64 | settings: DefaultSettings; 65 | setting: number; 66 | } 67 | ) => { 68 | const { t } = useTranslation(); 69 | const { onChange = noop, schema, setting: value, settings } = sliderProps; 70 | const clampedValue = clamp(value, 0, 255); 71 | const colorConstantHex = `${themeColors.accent.substring(0, 7)}22`; // Strip off opacity that a themer might have added to their accent color 72 | const colorConstant = hexToRgb(themeColors.accent); 73 | const isEnabled: boolean = (settings.reader as DefaultSettings['reader']) 74 | .useCustomColorFilter; 75 | if (!colorConstant) throw new Error(t('colorslider_error')); 76 | 77 | const sliderColor = `rgba(${(clampedValue / 255) * colorConstant.r}, ${ 78 | (clampedValue / 255) * colorConstant.g 79 | }, ${(clampedValue / 255) * colorConstant.b}, 1)`; 80 | return isEnabled ? ( 81 | 82 | 83 | {schema.label} 84 | 85 | {schema.description} 86 | 87 | 88 | 89 | onChange(newValue)} 97 | /> 98 | 99 | ) : null; 100 | }; 101 | 102 | export default FilterSlider; 103 | -------------------------------------------------------------------------------- /src/renderer/components/settings/themeswitch.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-props-no-spreading */ 2 | 3 | import { Box, Typography, SwitchProps } from '@mui/material'; 4 | 5 | import { StyleSheet, css } from 'aphrodite'; 6 | import { noop } from 'lodash'; 7 | import React from 'react'; 8 | import ModeNightIcon from '@mui/icons-material/ModeNight'; 9 | import ModeDayIcon from '@mui/icons-material/LightMode'; 10 | 11 | import { settingsStylesObject } from '../../util/func'; 12 | import type { Schema } from '../../util/auxiliary'; 13 | import Switch from '../switch'; 14 | import Theme from '../../../main/util/theme'; 15 | import { useTranslation } from '../../../shared/intl'; 16 | 17 | const { theme, themeStyleDark, themeStyleLight } = 18 | window.electron.settings.getAll().appearance; 19 | 20 | const currentTheme = new Theme( 21 | theme === 'dark' ? themeStyleDark : themeStyleLight, 22 | theme as 'dark' | 'light' 23 | ); 24 | const componentStyle = currentTheme.getComponentStyle('themeswitch'); 25 | const styles = StyleSheet.create({ 26 | switchContainer: { 27 | display: 'flex', 28 | flexDirection: 'row', 29 | }, 30 | switchIcon: { 31 | color: 'white', 32 | transition: 'color 0.2s ease-in-out', 33 | position: 'relative', 34 | top: '7px', 35 | }, 36 | dayIcon: { 37 | marginLeft: '-4px', 38 | }, 39 | nightIcon: { 40 | marginRight: '-4px', 41 | }, 42 | iconOff: { 43 | color: 'transparent', 44 | }, 45 | ...settingsStylesObject, 46 | ...componentStyle, 47 | } as any) as any; 48 | 49 | const ThemeSwitch = ( 50 | switchProps: SwitchProps & { 51 | schema: Schema; 52 | setting: string; 53 | } 54 | ) => { 55 | const { t } = useTranslation(); 56 | const { onChange = noop, schema, setting } = switchProps; 57 | const checked = setting === 'light'; 58 | 59 | return ( 60 | 61 | 62 | {schema.label} 63 | 64 | {schema.description} 65 | 66 | 67 | 74 | onChange(checked ? 'dark' : 'light')} 77 | tooltipOff={t('themeswitch_darkmode')} 78 | tooltipOn={t('themeswitch_lightmode')} 79 | /> 80 | 87 | 88 | ); 89 | }; 90 | 91 | export default ThemeSwitch; 92 | -------------------------------------------------------------------------------- /src/renderer/components/switch.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-props-no-spreading */ 2 | import { 3 | Switch as MaterialSwitch, 4 | SwitchProps as MaterialSwitchProps, 5 | Tooltip, 6 | } from '@mui/material'; 7 | import { omit } from 'lodash'; 8 | import Theme from '../../main/util/theme'; 9 | 10 | const { theme, themeStyleDark, themeStyleLight } = 11 | window.electron.settings.getAll().appearance; 12 | 13 | const currentTheme = new Theme( 14 | theme === 'dark' ? themeStyleDark : themeStyleLight, 15 | theme as 'dark' | 'light' 16 | ); 17 | 18 | const themeColors = currentTheme.getColors(); 19 | const componentStyle = currentTheme.getComponentStyle('switch'); 20 | 21 | const stylesObject = { 22 | switchTrackOn: { 23 | opacity: 0.6, 24 | backgroundColor: themeColors.accent, 25 | }, 26 | 27 | switchTrackOff: { 28 | opacity: 0.6, 29 | backgroundColor: themeColors.textLight, 30 | }, 31 | 32 | switchBase: { 33 | color: themeColors.textLight, 34 | }, 35 | 36 | switchThumb: {}, 37 | 38 | switchThumbOn: { 39 | color: themeColors.textLight, 40 | backgroundColor: themeColors.accent, 41 | }, 42 | 43 | switchHoverOn: { 44 | backgroundColor: `${themeColors.accent.substring(0, 7)}22`, 45 | }, 46 | 47 | switchHover: { 48 | backgroundColor: 49 | themeColors.white.length === 2 50 | ? themeColors.white 51 | : `${themeColors.white}11`, 52 | }, 53 | 54 | ...componentStyle, 55 | }; 56 | 57 | const Switch = ( 58 | props: MaterialSwitchProps & 59 | Pick, 'checked'> & { 60 | tooltipOn?: string; 61 | tooltipOff?: string; 62 | } 63 | ) => { 64 | const { checked, onChange, tooltipOn, tooltipOff } = props; 65 | return ( 66 | 67 | {})} 70 | sx={{ 71 | // Material UI is pain, part.. like, eight trillion? 72 | '&.MuiSwitch-root .MuiSwitch-switchBase': stylesObject.switchBase, 73 | '&.MuiSwitch-root .MuiSwitch-switchBase:hover': 74 | stylesObject.switchHover, 75 | '&.MuiSwitch-root .MuiSwitch-switchBase.Mui-checked:hover': 76 | stylesObject.switchHoverOn, 77 | '&.MuiSwitch-root span.MuiSwitch-track': 78 | (checked 79 | ? stylesObject.switchTrackOn 80 | : stylesObject.switchTrackOff) ?? {}, 81 | '& .MuiButtonBase-root.MuiSwitch-switchBase .MuiSwitch-thumb': 82 | stylesObject.switchThumb, 83 | '& .MuiButtonBase-root.MuiSwitch-switchBase.Mui-checked .MuiSwitch-thumb': 84 | stylesObject.switchThumbOn, 85 | }} 86 | /> 87 | 88 | ); 89 | }; 90 | 91 | Switch.defaultProps = { 92 | tooltipOn: 'On', 93 | tooltipOff: 'Off', 94 | }; 95 | 96 | export default Switch; 97 | -------------------------------------------------------------------------------- /src/renderer/components/tabs.tsx: -------------------------------------------------------------------------------- 1 | import { Tabs as MuiTabs, Tab } from '@mui/material'; 2 | import { 3 | useCallback, 4 | useState, 5 | JSXElementConstructor, 6 | ReactElement, 7 | } from 'react'; 8 | import { StyleSheet, css } from 'aphrodite'; 9 | 10 | import Theme from '../../main/util/theme'; 11 | 12 | const { theme, themeStyleDark, themeStyleLight } = 13 | window.electron.settings.getAll().appearance; 14 | 15 | const currentTheme = new Theme( 16 | theme === 'dark' ? themeStyleDark : themeStyleLight, 17 | theme as 'dark' | 'light' 18 | ); 19 | 20 | const themeColors = currentTheme.getColors(); 21 | const componentStyle = currentTheme.getComponentStyle('tabs'); 22 | 23 | const styles = StyleSheet.create({ 24 | tabs: { 25 | marginBottom: '12px', 26 | }, 27 | 28 | tab: { 29 | color: themeColors.textLight, 30 | marginRight: '12px', 31 | padding: '6px', 32 | boxSizing: 'border-box', 33 | width: '165px', 34 | height: '36px', 35 | borderRight: 1, 36 | borderColor: themeColors.accent, 37 | }, 38 | 39 | ...componentStyle, 40 | }) as any; 41 | 42 | type Element = ReactElement>; 43 | type TabsProps = { 44 | tabs: Array<{ 45 | label: string; 46 | icon?: Element; 47 | }>; 48 | onChange?: (index: number) => void; 49 | selectedIndex?: number; 50 | }; 51 | 52 | const Tabs = ({ tabs, onChange, selectedIndex: defaultIndex }: TabsProps) => { 53 | const [selectedIndex, setSelectedIndex] = useState(defaultIndex ?? 0); 54 | 55 | const handleChange = useCallback( 56 | (event: React.ChangeEvent, index: number) => { 57 | setSelectedIndex(index); 58 | onChange?.(index); 59 | }, 60 | [setSelectedIndex, onChange] 61 | ); 62 | 63 | return ( 64 | 75 | {tabs.map((tab) => ( 76 | 82 | ))} 83 | 84 | ); 85 | }; 86 | 87 | Tabs.defaultProps = { 88 | selectedIndex: 0, 89 | onChange: () => {}, 90 | }; 91 | 92 | export default Tabs; 93 | -------------------------------------------------------------------------------- /src/renderer/components/tag.tsx: -------------------------------------------------------------------------------- 1 | import { StyleSheet, css } from 'aphrodite'; 2 | import Theme from '../../main/util/theme'; 3 | 4 | const { theme, themeStyleDark, themeStyleLight } = 5 | window.electron.settings.getAll().appearance; 6 | 7 | const currentTheme = new Theme( 8 | theme === 'dark' ? themeStyleDark : themeStyleLight, 9 | theme as 'dark' | 'light' 10 | ); 11 | 12 | const themeColors = currentTheme.getColors(); 13 | const componentStyle = currentTheme.getComponentStyle('tag'); 14 | 15 | type TagColour = string; 16 | type TagProps = { 17 | name: string; 18 | color?: TagColour; 19 | }; 20 | 21 | const styles = StyleSheet.create({ 22 | tag: { 23 | display: 'inline-block', 24 | padding: '2px 5px', 25 | margin: '0px', 26 | marginRight: '10px', 27 | marginBottom: '5px', 28 | borderRadius: '5px', 29 | backgroundColor: themeColors.tag ?? '#272727', 30 | color: themeColors.textLight, 31 | fontFamily: '"Poppins", "Roboto", "Helvetica", "Arial", sans-serif', 32 | }, 33 | }); 34 | const Tag = ({ name, color }: TagProps) => { 35 | /* 36 | Later Functionality: 37 | When the Tag is clicked, the App will automatically start a search query for that source and tag. 38 | */ 39 | 40 | return ( 41 |
48 | {name} 49 |
50 | ); 51 | }; 52 | 53 | Tag.defaultProps = { 54 | color: '', 55 | }; 56 | 57 | export default Tag; 58 | -------------------------------------------------------------------------------- /src/renderer/components/textfield.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-props-no-spreading */ 2 | import { 3 | TextField as MaterialTextField, 4 | TextFieldProps as MaterialTextFieldProps, 5 | } from '@mui/material'; 6 | 7 | import Theme from '../../main/util/theme'; 8 | 9 | const { theme, themeStyleDark, themeStyleLight } = 10 | window.electron.settings.getAll().appearance; 11 | 12 | const currentTheme = new Theme( 13 | theme === 'dark' ? themeStyleDark : themeStyleLight, 14 | theme as 'dark' | 'light' 15 | ); 16 | 17 | const themeColors = currentTheme.getColors(); 18 | const componentStyle = currentTheme.getComponentStyle('textfield'); 19 | 20 | const TextField = (props: MaterialTextFieldProps) => { 21 | const { sx } = props; 22 | const textFieldRoot = `&.MuiTextField-root`; 23 | const textFieldInput = `${textFieldRoot} .MuiInputBase-input`; 24 | const textFieldSetFocused = `${textFieldRoot} .MuiOutlinedInput-root.Mui-focused fieldset`; 25 | 26 | const textFieldLabelFocused = `${textFieldRoot} label.MuiInputLabel-root.Mui-focused`; 27 | const textFieldLabel = `${textFieldRoot} label.MuiInputLabel-root`; 28 | 29 | return ( 30 | 52 | ); 53 | }; 54 | 55 | export default TextField; 56 | -------------------------------------------------------------------------------- /src/renderer/components/topbar.tsx: -------------------------------------------------------------------------------- 1 | import { StyleSheet, css } from 'aphrodite'; 2 | import { useState, useEffect, useCallback } from 'react'; 3 | import type { IpcRendererEvent } from 'electron/renderer'; 4 | import icon from '../../../assets/icons/main/32x32.png'; 5 | 6 | import Theme from '../../main/util/theme'; 7 | 8 | const { theme, themeStyleDark, themeStyleLight } = 9 | window.electron.settings.getAll().appearance; 10 | 11 | const currentTheme = new Theme( 12 | theme === 'dark' ? themeStyleDark : themeStyleLight, 13 | theme as 'dark' | 'light' 14 | ); 15 | 16 | const colors = currentTheme.getColors(); 17 | const componentStyle = currentTheme.getComponentStyle('topbar'); 18 | 19 | export const Styling = StyleSheet.create({ 20 | button: { 21 | width: '16px', 22 | height: '16px', 23 | margin: '8px', 24 | borderRadius: '50%', 25 | zIndex: 260, 26 | border: 'none', 27 | background: 'none', 28 | padding: 0, 29 | '-webkit-app-region': 'no-drag', 30 | }, 31 | 32 | topbar: { 33 | background: `linear-gradient(to bottom, ${colors.backgroundDark} 60%, transparent 100%)`, 34 | position: 'fixed', 35 | height: '48px', 36 | width: '100%', 37 | '-webkit-app-region': 'drag', 38 | zIndex: Number.MAX_SAFE_INTEGER, 39 | paddingLeft: '8px', 40 | }, 41 | 42 | icon: { 43 | position: 'absolute', 44 | zIndex: 261, 45 | userSelect: 'none', 46 | '::after': { 47 | content: '"SUWARIYOMI"', 48 | fontFamily: "'Bebas Neue', cursive", 49 | verticalAlign: 'text-bottom', 50 | color: 'white', 51 | }, 52 | }, 53 | close: { 54 | backgroundColor: '#A51A1A', 55 | }, 56 | minimize: { 57 | backgroundColor: '#B4AA1D', 58 | }, 59 | maximize: { 60 | backgroundColor: '#1AAA1A', 61 | }, 62 | buttonContainer: { 63 | position: 'absolute', 64 | display: 'flex', 65 | alignItems: 'center', 66 | float: 'right', 67 | width: '128px', 68 | height: 'inherit', 69 | right: 0, 70 | }, 71 | inner: { 72 | position: 'relative', 73 | width: '100%', 74 | height: 'inherit', 75 | }, 76 | buttonContainerInner: { 77 | display: 'inline-flex', 78 | alignItems: 'center', 79 | justifyContent: 'center', 80 | width: 'inherit', 81 | height: 'inherit', 82 | }, 83 | 84 | ...componentStyle, 85 | }) as any; 86 | 87 | const Topbar = () => { 88 | const { ipcRenderer } = window.electron; 89 | const doExit = () => ipcRenderer.exit(); 90 | const doMinimize = () => ipcRenderer.minimize(); 91 | const doMaximize = () => ipcRenderer.maximize(); 92 | const [doHide, setHide] = useState(false); 93 | 94 | const handleFullscreen = useCallback( 95 | (_: IpcRendererEvent, isVisible: boolean) => setHide(isVisible), 96 | [setHide] 97 | ); 98 | 99 | useEffect(() => { 100 | ipcRenderer.on('fullscreen-toggle', handleFullscreen); 101 | 102 | return () => { 103 | ipcRenderer.off('fullscreen-toggle', handleFullscreen); 104 | }; 105 | }, [setHide, ipcRenderer, handleFullscreen]); 106 | 107 | return doHide ? null : ( 108 |
109 |
110 | Icon 111 |
112 |
113 |
114 |
115 |
134 |
135 |
136 |
137 | ); 138 | }; 139 | 140 | export default Topbar; 141 | -------------------------------------------------------------------------------- /src/renderer/components/trackeritem.tsx: -------------------------------------------------------------------------------- 1 | import { StyleSheet, css } from 'aphrodite'; 2 | import { Box } from '@mui/material'; 3 | import { useState } from 'react'; 4 | import sanitizeHtml from 'sanitize-html'; 5 | 6 | import Theme from '../../main/util/theme'; 7 | import { useTranslation } from '../../shared/intl'; 8 | import { Media } from '../util/tracker/tracker'; 9 | 10 | const { theme, themeStyleDark, themeStyleLight } = 11 | window.electron.settings.getAll().appearance; 12 | 13 | const currentTheme = new Theme( 14 | theme === 'dark' ? themeStyleDark : themeStyleLight, 15 | theme as 'dark' | 'light' 16 | ); 17 | 18 | const themeColors = currentTheme.getColors(); 19 | const componentStyle = currentTheme.getComponentStyle('trackeritem'); 20 | 21 | const styles = StyleSheet.create({ 22 | chosen: { 23 | backgroundColor: `${themeColors.accent.substring(0, 7)}33`, 24 | }, 25 | container: { 26 | display: 'flex', 27 | flexDirection: 'row', 28 | width: '500px', 29 | height: 'fit-content', 30 | cursor: 'pointer', 31 | marginBottom: '32px', 32 | transition: 'background-color 0.2s ease-in-out', 33 | borderRadius: '5px', 34 | }, 35 | coverImage: { 36 | maxWidth: '100%', 37 | maxHeight: '100%', 38 | borderRadius: '5px', 39 | }, 40 | coverImageContainer: { 41 | width: '100px', 42 | height: '144px', 43 | }, 44 | containerContentText: { 45 | display: 'flex', 46 | flexDirection: 'column', 47 | marginLeft: '8px', 48 | }, 49 | containerContentMeta: { 50 | display: 'flex', 51 | fontFamily: 'PT Sans Narrow', 52 | color: '#9e9e9e', 53 | }, 54 | containerContentDescription: { 55 | color: '#AeAeAe', 56 | marginTop: '8px', 57 | fontSize: '0.8em', 58 | maxWidth: '300px', 59 | fontFamily: 'Open Sans, sans-serif', 60 | }, 61 | containerContentTitle: { 62 | color: '#fff', 63 | fontFamily: 'Poppins', 64 | fontSize: '1.3em', 65 | fontWeight: 400, 66 | }, 67 | 68 | ...componentStyle, 69 | }) as any; 70 | 71 | const TrackerItem = ({ 72 | id, 73 | onClick, 74 | chosen, 75 | media, 76 | }: { 77 | id: string | number; 78 | media: Media; 79 | chosen?: boolean; 80 | onClick?: () => void; 81 | }) => { 82 | const coverImage = 83 | media.covers?.medium ?? media.covers?.large ?? media.covers?.extraLarge; 84 | 85 | const { t } = useTranslation(); 86 | const descriptionTrimLength = 200; 87 | const [isBackgroundIlluminated, setIllumination] = useState(chosen!); 88 | return ( 89 | setIllumination(true)} 95 | onMouseLeave={() => setIllumination(chosen!)} 96 | onClick={onClick} 97 | key={id} 98 | > 99 | {coverImage ? ( 100 |
101 | 102 |
103 | ) : null} 104 |
105 | 106 | {media.title?.userPreferred ?? 107 | media.title?.romaji ?? 108 | media.title?.english ?? 109 | media.title?.native ?? 110 | t('notitle')} 111 | 112 | {media.chapters ?? media.volumes ? ( 113 | 114 | {media.volumes 115 | ? media.volumes === 1 116 | ? t('countv') 117 | : t('countvs') 118 | : null} 119 | {media.volumes ? t('comma') : ''} 120 | {media.chapters 121 | ? t(media.chapters === 1 ? 'countc' : 'countcs') 122 | : null} 123 | 124 | ) : null} 125 | 126 | {sanitizeHtml(media.description ?? t('trackeritem_nodesc')).substring( 127 | 0, 128 | descriptionTrimLength 129 | )} 130 | {(media.description?.length ?? 0) > descriptionTrimLength 131 | ? '...' 132 | : null} 133 | 134 |
135 |
136 | ); 137 | }; 138 | 139 | TrackerItem.defaultProps = { 140 | chosen: false, 141 | onClick: () => {}, 142 | }; 143 | 144 | export default TrackerItem; 145 | -------------------------------------------------------------------------------- /src/renderer/css/App.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=PT+Sans+Narrow&display=swap'); 2 | @import url('https://fonts.googleapis.com/css2?family=Licorice&display=swap'); 3 | @import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap'); 4 | @import url('https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300;0,500;0,600;0,700;0,800;1,300;1,400;1,500;1,600;1,700;1,800&display=swap'); 5 | @import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&display=swap'); 6 | 7 | html { 8 | width: 100vw !important; 9 | height: 100vh !important; 10 | min-width: 727px; 11 | min-height: 825px; 12 | } 13 | 14 | h1 { 15 | color: #fff; 16 | font-size: 2em; 17 | font-weight: bold; 18 | margin: 0; 19 | padding: 0; 20 | font-family: 'PT Sans Narrow', sans-serif; 21 | text-align: center; 22 | } 23 | 24 | body { 25 | width: 100% !important; 26 | height: 100% !important; 27 | overflow: hidden !important; 28 | position: absolute !important; 29 | min-width: 727px; 30 | min-height: 825px; 31 | margin: 0 !important; 32 | } 33 | 34 | button { 35 | padding: 0 !important; 36 | } 37 | 38 | input::-webkit-outer-spin-button, 39 | input::-webkit-inner-spin-button { 40 | -webkit-appearance: none; 41 | margin: 0; 42 | } 43 | -------------------------------------------------------------------------------- /src/renderer/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Suwariyomi 6 | 9 | 12 | 15 | 16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /src/renderer/index.tsx: -------------------------------------------------------------------------------- 1 | import { functions } from 'electron-log'; 2 | import { render } from 'react-dom'; 3 | import { Presence } from 'discord-rpc'; 4 | import { IpcRendererEvent } from 'electron'; 5 | import { LibrarySources, FullManga, LibraryManga } from '../main/util/manga'; 6 | import { ReadDatabaseValue } from '../main/util/read'; 7 | import type { DefaultSettings } from '../main/util/settings'; 8 | import App from './App'; 9 | 10 | import '../shared/intl'; 11 | 12 | type SetSignature = ( 13 | sourceName: string, 14 | chapterId: string, 15 | pageCount: number, 16 | currentPage: number, 17 | lastRead: Date | undefined, 18 | timeElapsed: number, 19 | isBookmarked: boolean, 20 | mangaId: string 21 | ) => void; 22 | 23 | export type SourceMetadata = { 24 | name: string; // The name of the source. 25 | version: string; // The version of the source. 26 | icon: string; // The icon for the source. 27 | nsfw: boolean; // Whether the source can distribute NSFW content. 28 | zip: string; // The name of the zip file in the dist/zip branch. 29 | lang: string; // The language of the source. 30 | path?: string; // The path to the source. 31 | }; 32 | 33 | export type ThemeType = { 34 | location: string; 35 | metadata: { 36 | name: string; 37 | }; 38 | colors: Record<'light' | 'dark', Record>; 39 | }; 40 | 41 | declare global { 42 | interface Window { 43 | electron: { 44 | log: typeof functions; 45 | rpc: { 46 | updateRPC: (presence: Presence) => void; 47 | toggleRPC: (rpcEnabled: boolean) => void; 48 | }; 49 | download: { 50 | getDownloadsPath: () => string; 51 | downloadSource: (sourceZip: string) => Promise; 52 | removeSource: (sourceData: SourceMetadata) => Promise; 53 | }; 54 | util: { 55 | showOpenDialog: ( 56 | options: Electron.OpenDialogOptions 57 | ) => Promise; 58 | downloadImage: ( 59 | url: string, 60 | payload: { 61 | filename: string; 62 | mangaid: string; 63 | manganame: string; 64 | chapternumber: number; 65 | sourceid: string; 66 | } 67 | ) => Promise; 68 | getSourceFiles: () => string[]; 69 | getSourceMetadata: (sourceId?: string) => SourceMetadata[]; 70 | getSourceCatalogue: () => SourceMetadata[]; 71 | getUserDataPath: () => string; 72 | getDownloadsPath: () => string; 73 | getSourceDirectory: () => string; 74 | openInBrowser: (url: string) => void; 75 | get appVersion(): string; 76 | get themes(): Record; 77 | }; 78 | reader: { 79 | getMangaSettings: ( 80 | sourceName: string, 81 | mangaID: string 82 | ) => Partial; 83 | setMangaSettings: ( 84 | sourceName: string, 85 | mangaID: string, 86 | settings: Partial 87 | ) => void; 88 | flush: () => void; 89 | }; 90 | library: { 91 | cycle: { 92 | getUpdateQueue: () => Array[]; 93 | addToUpdateQueue: ( 94 | ...mangaObjects: ( 95 | | LibraryManga 96 | | FullManga 97 | | { MangaID: string; SourceID: string } 98 | )[] 99 | ) => void; 100 | updateSource: (sourceID: string) => boolean; 101 | forceUpdateCycle: () => void; 102 | isUpdating: () => boolean; 103 | flushUpdateQueue: () => void; 104 | getUpdatingSources: () => string[]; 105 | get processedTotal(): number; 106 | get isBusy(): boolean; 107 | }; 108 | getSources: () => LibrarySources; 109 | flush: () => void; 110 | addMangaToLibrary: (sourceName: string, mangaId: string) => void; 111 | removeMangaFromLibrary: (sourceName: string, mangaId: string) => void; 112 | getLibraryMangas: (sourceName: string) => string[]; 113 | addMangasToCache: (...fullManga: (FullManga | LibraryManga)[]) => void; 114 | removeMangaFromCache: ( 115 | sourceName: string, 116 | ...mangaIds: string[] 117 | ) => void; 118 | getCachedManga: ( 119 | sourceName: string, 120 | mangaId: string 121 | ) => FullManga | LibraryManga | undefined; 122 | getCachedMangas: (sourceName: string) => FullManga[]; 123 | getAllCachedMangas: () => FullManga[]; 124 | }; 125 | misc: { 126 | flush: () => void; 127 | }; 128 | read: { 129 | get: (sourceName: string) => ReadDatabaseValue; 130 | set: SetSignature; 131 | setSync: SetSignature; 132 | deleteEntry: (sourceName: string, chapterId: string) => void; 133 | deleteSource: (sourceName: string) => void; 134 | flush: () => void; 135 | }; 136 | theme: { 137 | get: (themeName: string) => Record; 138 | getAll: () => Record; 139 | }; 140 | cache: { 141 | get: (key: string) => any; 142 | set: (key: string, value: any) => void; 143 | has: (key: string) => boolean; 144 | delete: (...keys: string[]) => void; 145 | flush: () => void; 146 | }; 147 | auth: { 148 | generateAuthenticationWindow: ( 149 | windowData: { [key: string]: any }, 150 | targetLocation: string 151 | ) => Promise<{ access_token: string; expires_in: number }>; 152 | generatePKCE: () => { 153 | code_challenge: string; 154 | code_verifier: string; 155 | }; 156 | getAuthentication: (specificLogin: string) => string; 157 | checkAuthenticated: (specificLogin?: string) => boolean; 158 | setAuthenticated: ( 159 | specificLogin: string, 160 | access_token: string, 161 | expires_in: number 162 | ) => boolean; 163 | deleteAuthenticated: (specificLogin?: string) => boolean; 164 | }; 165 | ipcRenderer: { 166 | minimize: () => void; 167 | maximize: () => void; 168 | exit: () => void; 169 | on: ( 170 | channel: string, 171 | func: (event: IpcRendererEvent, ...args: any[]) => void 172 | ) => void; 173 | off: ( 174 | channel: string, 175 | func: (event: IpcRendererEvent, ...args: any[]) => void 176 | ) => void; 177 | once: ( 178 | channel: string, 179 | func: (event: IpcRendererEvent, ...args: any[]) => void 180 | ) => void; 181 | }; 182 | store: { 183 | get: (key: string) => any; 184 | set: (key: string, value: any) => void; 185 | flush: () => void; 186 | }; 187 | settings: { 188 | get: (key: keyof DefaultSettings) => DefaultSettings[typeof key]; 189 | getAll: () => DefaultSettings; 190 | set: (key: keyof DefaultSettings, value: Record) => void; 191 | overwrite: (settings: Record) => void; 192 | flush: () => void; 193 | }; 194 | }; 195 | } 196 | } 197 | 198 | // Setup authorization defaults 199 | if (!window.electron.store.get('authorization')) 200 | window.electron.store.set('authorization', { 201 | myanimelist: { 202 | access_token: null, 203 | expires: null, 204 | }, 205 | anilist: { 206 | access_token: null, 207 | expires: null, 208 | }, 209 | }); 210 | 211 | render(, document.body); 212 | -------------------------------------------------------------------------------- /src/renderer/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import { StyleSheet, css } from 'aphrodite'; 2 | import { Link } from 'react-router-dom'; 3 | import Particles from 'react-tsparticles'; 4 | import Theme from '../../main/util/theme'; 5 | import { useTranslation } from '../../shared/intl'; 6 | 7 | const { theme, themeStyleDark, themeStyleLight } = 8 | window.electron.settings.getAll().appearance; 9 | 10 | const currentTheme = new Theme( 11 | theme === 'dark' ? themeStyleDark : themeStyleLight, 12 | theme as 'dark' | 'light' 13 | ); 14 | 15 | const themeColors = currentTheme.getColors(); 16 | const pageStyle = currentTheme.getPageStyle('404'); 17 | 18 | const StylesNotFound = StyleSheet.create({ 19 | container: { 20 | display: 'flex', 21 | flexDirection: 'column', 22 | alignItems: 'center', 23 | justifyContent: 'center', 24 | height: '100%', 25 | width: '100%', 26 | fontFamily: '"Roboto", sans-serif', 27 | }, 28 | title: { 29 | color: themeColors.textLight, 30 | fontSize: '48px', 31 | fontWeight: 'bold', 32 | marginBottom: '-15px', 33 | fontFamily: 'Poppins', 34 | zIndex: 3, 35 | textShadow: `0 0 10px ${themeColors.white}`, 36 | }, 37 | subtitle: { 38 | color: themeColors.textLight, 39 | fontSize: '24px', 40 | fontWeight: 'bold', 41 | marginBottom: '-10px', 42 | zIndex: 3, 43 | fontFamily: '"Open Sans", Roboto, Poppins, sans-serif', 44 | }, 45 | text: { 46 | color: themeColors.textLight, 47 | fontSize: '16px', 48 | fontWeight: 'bold', 49 | marginBottom: '10px', 50 | zIndex: 3, 51 | fontFamily: '"Open Sans", Roboto, Poppins, sans-serif', 52 | }, 53 | backgroundDim: { 54 | position: 'absolute', 55 | top: 0, 56 | left: 0, 57 | width: '100%', 58 | height: '100%', 59 | backgroundColor: themeColors.textDark, 60 | opacity: 0.8, 61 | zIndex: 2, 62 | }, 63 | link: { 64 | color: themeColors.accent, 65 | letterSpacing: 'unset', 66 | transition: 'all 0.3s ease-in-out', 67 | ':hover': { 68 | color: themeColors.accentSpecial, 69 | letterSpacing: '1px', 70 | }, 71 | }, 72 | 73 | ...pageStyle, 74 | }) as any; 75 | 76 | const Page404 = () => { 77 | const { t, a } = useTranslation(); 78 | return ( 79 |
80 | 152 |
153 |
159 |

404

160 |

{t('404_subheader')}

161 |

162 | {a('404_subsubheader', [ 163 | undefined, 164 | , 165 | ])} 166 |

167 |
168 |
169 | ); 170 | }; 171 | 172 | export default Page404; 173 | -------------------------------------------------------------------------------- /src/renderer/pages/login.tsx: -------------------------------------------------------------------------------- 1 | import { StyleSheet, css } from 'aphrodite'; 2 | import { useNavigate } from 'react-router-dom'; 3 | 4 | import LoginItem from '../components/loginitem'; 5 | 6 | const onAuth = () => { 7 | const submitButton = document.getElementById('submit'); 8 | if (submitButton) submitButton.innerText = 'Continue'; 9 | }; 10 | 11 | const LoginMenu = () => { 12 | const { checkAuthenticated } = window.electron.auth; 13 | const navigate = useNavigate(); 14 | const styleSheet = StyleSheet.create({ 15 | buttonStateLogin: { 16 | top: '15%', 17 | }, 18 | invisible: { 19 | visibility: 'hidden', 20 | }, 21 | windowStateActive: { 22 | maxHeight: '85%', 23 | height: '300px', 24 | }, 25 | button: { 26 | width: '100%', 27 | height: '50px', 28 | background: 'rgb(14, 14, 14)', 29 | border: '2px solid rgb(14, 14, 14)', 30 | borderRadius: '8px', 31 | color: 'rgb(255, 255, 255)', 32 | fontSize: '1.2em', 33 | fontFamily: "'PT Sans Narrow', sans-serif", 34 | fontWeight: 'bold', 35 | marginTop: '10px', 36 | cursor: 'pointer', 37 | position: 'relative', 38 | top: 0, 39 | bottom: 0, 40 | left: 0, 41 | right: 0, 42 | }, 43 | }); 44 | if (window.electron.store.get('skipped_auth')) navigate('/library'); 45 | 46 | const submitButton = ( 47 | 59 | ); 60 | return ( 61 |
62 |
63 |
64 |

Login

65 |
66 |
67 |
68 |
69 | 70 | 71 |
72 |
73 | {submitButton} 74 |
75 |
76 |
77 |
78 | ); 79 | }; 80 | 81 | const Login = () => { 82 | return ( 83 |
84 | 85 |
86 |
87 | ); 88 | }; 89 | 90 | export default Login; 91 | -------------------------------------------------------------------------------- /src/renderer/util/func.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | import { Box, Typography } from '@mui/material'; 3 | import { StyleSheet, css } from 'aphrodite'; 4 | import dayjs from 'dayjs'; 5 | 6 | import Select from '../components/select'; 7 | import Switch from '../components/switch'; 8 | import Theme from '../../main/util/theme'; 9 | 10 | import type { Schema } from './auxiliary'; 11 | import { mainTranslator } from '../../shared/intl'; 12 | import { Chapter } from '../../main/util/manga'; 13 | 14 | const { theme, themeStyleDark, themeStyleLight } = 15 | window.electron.settings.getAll().appearance; 16 | 17 | const currentTheme = new Theme( 18 | theme === 'dark' ? themeStyleDark : themeStyleLight, 19 | theme as 'dark' | 'light' 20 | ); 21 | 22 | const themeColors = currentTheme.getColors(); 23 | const pageStyle = currentTheme.getPageStyle('func'); 24 | 25 | export const settingsStylesObject = { 26 | optionLabel: { 27 | fontFamily: '"Roboto", "Poppins", "Helvetica", "Arial", sans-serif', 28 | fontSize: '1.25rem', 29 | fontWeight: 'normal', 30 | lineHeight: '1.5', 31 | color: themeColors.textLight, 32 | verticalAlign: 'middle', 33 | minWidth: '80%', 34 | maxWidth: '80%', 35 | display: 'inline-flex', 36 | flexDirection: 'column', 37 | }, 38 | optionLabelDescription: { 39 | display: 'inline-block', 40 | fontFamily: '"Roboto", "Poppins", "Helvetica", "Arial", sans-serif', 41 | fontSize: '0.7rem', 42 | fontWeight: '200', 43 | marginTop: '4px', 44 | }, 45 | optionContainer: { 46 | '::after': { 47 | content: '""', 48 | display: 'block', 49 | height: '1px', 50 | // left to right white gradient 51 | background: 52 | 'linear-gradient(to left, rgba(255, 255, 255, 0), rgba(255, 255, 255, 1))', 53 | width: '80%', 54 | marginTop: '8px', 55 | marginBottom: '16px', 56 | }, 57 | position: 'relative', 58 | width: '100%', 59 | height: 'fit-content', 60 | overflowY: 'auto', 61 | }, 62 | ...pageStyle, 63 | }; 64 | 65 | export const hexToRgb = (hex: string) => { 66 | const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); 67 | return result 68 | ? { 69 | r: parseInt(result[1], 16), 70 | g: parseInt(result[2], 16), 71 | b: parseInt(result[3], 16), 72 | } 73 | : null; 74 | }; 75 | 76 | // @ts-ignore Aphrodite sucks, part eight undecillion. 77 | const styles = StyleSheet.create(settingsStylesObject) as any; 78 | 79 | export const sortChapters = (chapters: Chapter[], isDescending = true) => 80 | chapters.sort((a, b) => { 81 | // TODO: Remove repitition 82 | if (a.Chapter === b.Chapter) 83 | // Sort by date instead 84 | return ( 85 | (isDescending ? -1 : 1) * 86 | (new Date(a.PublishedAt).getTime() - new Date(b.PublishedAt).getTime()) 87 | ); 88 | 89 | return (isDescending ? -1 : 1) * (a.Chapter - b.Chapter); 90 | }); 91 | 92 | export const getReadUrl = ( 93 | mangaId: string, 94 | mangaName: string, 95 | sourceId: string, 96 | chapterId: string, 97 | page: number 98 | ) => 99 | `/read?id=${mangaId}&title=${mangaName}&source=${sourceId}&chapter=${chapterId}&page=${page}`; 100 | 101 | export const convertDateToFormatted = ( 102 | date: dayjs.Dayjs, 103 | format: 'MM/DD/YYYY' | 'DD/MM/YYYY' | 'YYYY/MM/DD' 104 | ) => 105 | ({ 106 | 'MM/DD/YYYY': date.format('MMMM Do YYYY'), 107 | 'DD/MM/YYYY': date.format('Do MMMM YYYY'), 108 | 'YYYY/MM/DD': date.format('YYYY MMMM Do'), 109 | }[format]); 110 | 111 | /** 112 | * 113 | * @param schemeItem {Schema} 114 | * @param currentValue {string} 115 | * @param onChange {(value: string) => void} 116 | * @param settingCagetory {string} 117 | * @param settingKey {string} 118 | * @returns {JSX.Element} 119 | */ 120 | export const generateSettings = ( 121 | schemeItem: Schema, 122 | currentValue: any, 123 | onChange: (value: any) => void, 124 | settingCategory?: string, 125 | settingKey?: string 126 | ): JSX.Element => { 127 | const { 128 | type, 129 | label, 130 | description, 131 | options, 132 | component: Component, 133 | default: defaultValue, 134 | } = schemeItem; 135 | 136 | const serializedOptions: { 137 | [optionValue: string]: string; 138 | } = {}; 139 | 140 | if (options) { 141 | (options as { label: string; value: string }[]).forEach((option) => { 142 | serializedOptions[option.value] = 143 | settingKey && settingCategory 144 | ? mainTranslator.translate( 145 | `settings_${settingCategory}_${settingKey}_${option.value}_label` 146 | ) 147 | : option.label; 148 | }); 149 | } 150 | 151 | // @ts-ignore Typescript is a godawful language. 152 | let elementToDisplay: JSX.Element | null = null; 153 | switch (type) { 154 | case 'select': 155 | elementToDisplay = ( 156 |