├── .editorconfig ├── .erb ├── configs │ ├── .eslintrc │ ├── webpack.config.base.ts │ ├── webpack.config.eslint.ts │ ├── webpack.config.main.prod.ts │ ├── webpack.config.preload.dev.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 │ └── palette-sponsor-banner.svg ├── 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 ├── ISSUE_TEMPLATE │ ├── 1-Bug_report.md │ ├── 2-Question.md │ └── 3-Feature_request.md ├── config.yml ├── stale.yml └── workflows │ ├── codeql-analysis.yml │ ├── publish.yml │ └── test.yml ├── .gitignore ├── .npmrc ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── README.zh.md ├── assets ├── assets.d.ts ├── entitlements.mac.plist ├── icon.png ├── icon.svg └── icons │ ├── 1024x1024.png │ ├── 128x128.png │ ├── 16x16.png │ ├── 24x24.png │ ├── 256x256.png │ ├── 32x32.png │ ├── 48x48.png │ ├── 512x512.png │ ├── 64x64.png │ ├── icon.icns │ └── icon.ico ├── docs ├── terrible-tabs.png └── webbox.png ├── package.json ├── pnpm-lock.yaml ├── release └── app │ ├── .release-it.json │ ├── package-lock.json │ └── package.json ├── src ├── __tests__ │ └── App.test.tsx ├── main │ ├── main.ts │ ├── menu.ts │ ├── preload.ts │ ├── subWindow.ts │ ├── util.ts │ └── webviewManager.ts └── renderer │ ├── App.tsx │ ├── assets │ └── web-page.svg │ ├── components │ ├── AddWebsiteBtn.tsx │ ├── ClearWebsiteBtn.tsx │ ├── SideTree.tsx │ └── main │ │ ├── WebContent.tsx │ │ ├── WebInvalidUrl.tsx │ │ ├── WebPlaceholder.tsx │ │ └── WebviewRender.tsx │ ├── index.ejs │ ├── index.tsx │ ├── preload.d.ts │ ├── store │ └── tree.ts │ └── utils │ ├── dom.ts │ ├── hooks │ └── useEditValue.ts │ └── index.ts └── tsconfig.json /.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 TsconfigPathsPlugins from 'tsconfig-paths-webpack-plugin'; 7 | import webpackPaths from './webpack.paths'; 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 | compilerOptions: { 26 | module: 'esnext', 27 | }, 28 | }, 29 | }, 30 | }, 31 | ], 32 | }, 33 | 34 | output: { 35 | path: webpackPaths.srcPath, 36 | // https://github.com/webpack/webpack/issues/1114 37 | library: { 38 | type: 'commonjs2', 39 | }, 40 | }, 41 | 42 | /** 43 | * Determine the array of extensions that should be used to resolve modules. 44 | */ 45 | resolve: { 46 | extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'], 47 | modules: [webpackPaths.srcPath, 'node_modules'], 48 | // There is no need to add aliases here, the paths in tsconfig get mirrored 49 | plugins: [ 50 | // new TsconfigPathsPlugins() 51 | ], 52 | }, 53 | 54 | plugins: [ 55 | new webpack.EnvironmentPlugin({ 56 | NODE_ENV: 'production', 57 | }), 58 | ], 59 | }; 60 | 61 | export default configuration; 62 | -------------------------------------------------------------------------------- /.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 configuration: webpack.Configuration = { 19 | devtool: 'source-map', 20 | 21 | mode: 'production', 22 | 23 | target: 'electron-main', 24 | 25 | entry: { 26 | main: path.join(webpackPaths.srcMainPath, 'main.ts'), 27 | preload: path.join(webpackPaths.srcMainPath, 'preload.ts'), 28 | }, 29 | 30 | output: { 31 | path: webpackPaths.distMainPath, 32 | filename: '[name].js', 33 | library: { 34 | type: 'umd', 35 | }, 36 | }, 37 | 38 | optimization: { 39 | minimizer: [ 40 | new TerserPlugin({ 41 | parallel: true, 42 | }), 43 | ], 44 | }, 45 | 46 | plugins: [ 47 | new BundleAnalyzerPlugin({ 48 | analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled', 49 | analyzerPort: 8888, 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: 'production', 63 | DEBUG_PROD: false, 64 | START_MINIMIZED: false, 65 | }), 66 | 67 | new webpack.DefinePlugin({ 68 | 'process.type': '"browser"', 69 | }), 70 | ], 71 | 72 | /** 73 | * Disables webpack processing of __dirname and __filename. 74 | * If you run the bundle in node.js it falls back to these values of node.js. 75 | * https://github.com/webpack/webpack/issues/2010 76 | */ 77 | node: { 78 | __dirname: false, 79 | __filename: false, 80 | }, 81 | }; 82 | 83 | export default merge(baseConfig, configuration); 84 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.preload.dev.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import webpack from 'webpack'; 3 | import { merge } from 'webpack-merge'; 4 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 5 | import baseConfig from './webpack.config.base'; 6 | import webpackPaths from './webpack.paths'; 7 | import checkNodeEnv from '../scripts/check-node-env'; 8 | 9 | // When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's 10 | // at the dev webpack config is not accidentally run in a production environment 11 | if (process.env.NODE_ENV === 'production') { 12 | checkNodeEnv('development'); 13 | } 14 | 15 | const configuration: webpack.Configuration = { 16 | devtool: 'inline-source-map', 17 | 18 | mode: 'development', 19 | 20 | target: 'electron-preload', 21 | 22 | entry: path.join(webpackPaths.srcMainPath, 'preload.ts'), 23 | 24 | output: { 25 | path: webpackPaths.dllPath, 26 | filename: 'preload.js', 27 | library: { 28 | type: 'umd', 29 | }, 30 | }, 31 | 32 | plugins: [ 33 | new BundleAnalyzerPlugin({ 34 | analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled', 35 | }), 36 | 37 | /** 38 | * Create global constants which can be configured at compile time. 39 | * 40 | * Useful for allowing different behaviour between development builds and 41 | * release builds 42 | * 43 | * NODE_ENV should be production so that modules do not perform certain 44 | * development checks 45 | * 46 | * By default, use 'development' as NODE_ENV. This can be overriden with 47 | * 'staging', for example, by changing the ENV variables in the npm scripts 48 | */ 49 | new webpack.EnvironmentPlugin({ 50 | NODE_ENV: 'development', 51 | }), 52 | 53 | new webpack.LoaderOptionsPlugin({ 54 | debug: true, 55 | }), 56 | ], 57 | 58 | /** 59 | * Disables webpack processing of __dirname and __filename. 60 | * If you run the bundle in node.js it falls back to these values of node.js. 61 | * https://github.com/webpack/webpack/issues/2010 62 | */ 63 | node: { 64 | __dirname: false, 65 | __filename: false, 66 | }, 67 | 68 | watch: true, 69 | }; 70 | 71 | export default merge(baseConfig, configuration); 72 | -------------------------------------------------------------------------------- /.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 'webpack-dev-server'; 2 | import path from 'path'; 3 | import fs from 'fs'; 4 | import webpack from 'webpack'; 5 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 6 | import chalk from 'chalk'; 7 | import { merge } from 'webpack-merge'; 8 | import { execSync, spawn } from 'child_process'; 9 | import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin'; 10 | import baseConfig from './webpack.config.base'; 11 | import webpackPaths from './webpack.paths'; 12 | import checkNodeEnv from '../scripts/check-node-env'; 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 || 1212; 21 | const manifest = path.resolve(webpackPaths.dllPath, 'renderer.json'); 22 | const skipDLLs = 23 | module.parent?.filename.includes('webpack.config.renderer.dev.dll') || 24 | module.parent?.filename.includes('webpack.config.eslint'); 25 | 26 | /** 27 | * Warn if the DLL is not built 28 | */ 29 | if ( 30 | !skipDLLs && 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: ['web', '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 | library: { 59 | type: 'umd', 60 | }, 61 | }, 62 | 63 | module: { 64 | rules: [ 65 | { 66 | test: /\.s?(c|a)ss$/, 67 | use: [ 68 | 'style-loader', 69 | { 70 | loader: 'css-loader', 71 | options: { 72 | modules: true, 73 | sourceMap: true, 74 | importLoaders: 1, 75 | }, 76 | }, 77 | 'sass-loader', 78 | ], 79 | include: /\.module\.s?(c|a)ss$/, 80 | }, 81 | { 82 | test: /\.s?css$/, 83 | use: ['style-loader', 'css-loader', 'sass-loader'], 84 | exclude: /\.module\.s?(c|a)ss$/, 85 | }, 86 | // Fonts 87 | { 88 | test: /\.(woff|woff2|eot|ttf|otf)$/i, 89 | type: 'asset/resource', 90 | }, 91 | // Images 92 | { 93 | test: /\.(png|jpg|jpeg|gif)$/i, 94 | type: 'asset/resource', 95 | }, 96 | // SVG 97 | { 98 | test: /\.svg$/, 99 | use: [ 100 | { 101 | loader: '@svgr/webpack', 102 | options: { 103 | prettier: false, 104 | svgo: false, 105 | svgoConfig: { 106 | plugins: [{ removeViewBox: false }], 107 | }, 108 | titleProp: true, 109 | ref: true, 110 | }, 111 | }, 112 | 'file-loader', 113 | ], 114 | }, 115 | ], 116 | }, 117 | plugins: [ 118 | ...(skipDLLs 119 | ? [] 120 | : [ 121 | new webpack.DllReferencePlugin({ 122 | context: webpackPaths.dllPath, 123 | manifest: require(manifest), 124 | sourceType: 'var', 125 | }), 126 | ]), 127 | 128 | new webpack.NoEmitOnErrorsPlugin(), 129 | 130 | /** 131 | * Create global constants which can be configured at compile time. 132 | * 133 | * Useful for allowing different behaviour between development builds and 134 | * release builds 135 | * 136 | * NODE_ENV should be production so that modules do not perform certain 137 | * development checks 138 | * 139 | * By default, use 'development' as NODE_ENV. This can be overriden with 140 | * 'staging', for example, by changing the ENV variables in the npm scripts 141 | */ 142 | new webpack.EnvironmentPlugin({ 143 | NODE_ENV: 'development', 144 | }), 145 | 146 | new webpack.LoaderOptionsPlugin({ 147 | debug: true, 148 | }), 149 | 150 | new ReactRefreshWebpackPlugin(), 151 | 152 | new HtmlWebpackPlugin({ 153 | filename: path.join('index.html'), 154 | template: path.join(webpackPaths.srcRendererPath, 'index.ejs'), 155 | minify: { 156 | collapseWhitespace: true, 157 | removeAttributeQuotes: true, 158 | removeComments: true, 159 | }, 160 | isBrowser: false, 161 | env: process.env.NODE_ENV, 162 | isDevelopment: process.env.NODE_ENV !== 'production', 163 | nodeModules: webpackPaths.appNodeModulesPath, 164 | }), 165 | ], 166 | 167 | node: { 168 | __dirname: false, 169 | __filename: false, 170 | }, 171 | 172 | devServer: { 173 | port, 174 | compress: true, 175 | hot: true, 176 | headers: { 'Access-Control-Allow-Origin': '*' }, 177 | static: { 178 | publicPath: '/', 179 | }, 180 | historyApiFallback: { 181 | verbose: true, 182 | }, 183 | setupMiddlewares(middlewares) { 184 | console.log('Starting preload.js builder...'); 185 | const preloadProcess = spawn('npm', ['run', 'start:preload'], { 186 | shell: true, 187 | stdio: 'inherit', 188 | }) 189 | .on('close', (code: number) => process.exit(code!)) 190 | .on('error', (spawnError) => console.error(spawnError)); 191 | 192 | console.log('Starting Main Process...'); 193 | let args = ['run', 'start:main']; 194 | if (process.env.MAIN_ARGS) { 195 | args = args.concat( 196 | ['--', ...process.env.MAIN_ARGS.matchAll(/"[^"]+"|[^\s"]+/g)].flat() 197 | ); 198 | } 199 | spawn('npm', args, { 200 | shell: true, 201 | stdio: 'inherit', 202 | }) 203 | .on('close', (code: number) => { 204 | preloadProcess.kill(); 205 | process.exit(code!); 206 | }) 207 | .on('error', (spawnError) => console.error(spawnError)); 208 | return middlewares; 209 | }, 210 | }, 211 | }; 212 | 213 | export default merge(baseConfig, configuration); 214 | -------------------------------------------------------------------------------- /.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 { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 10 | import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'; 11 | import { merge } from 'webpack-merge'; 12 | import TerserPlugin from 'terser-webpack-plugin'; 13 | import baseConfig from './webpack.config.base'; 14 | import webpackPaths from './webpack.paths'; 15 | import checkNodeEnv from '../scripts/check-node-env'; 16 | import deleteSourceMaps from '../scripts/delete-source-maps'; 17 | 18 | checkNodeEnv('production'); 19 | deleteSourceMaps(); 20 | 21 | const configuration: webpack.Configuration = { 22 | devtool: 'source-map', 23 | 24 | mode: 'production', 25 | 26 | target: ['web', 'electron-renderer'], 27 | 28 | entry: [path.join(webpackPaths.srcRendererPath, 'index.tsx')], 29 | 30 | output: { 31 | path: webpackPaths.distRendererPath, 32 | publicPath: './', 33 | filename: 'renderer.js', 34 | library: { 35 | type: 'umd', 36 | }, 37 | }, 38 | 39 | module: { 40 | rules: [ 41 | { 42 | test: /\.s?(a|c)ss$/, 43 | use: [ 44 | MiniCssExtractPlugin.loader, 45 | { 46 | loader: 'css-loader', 47 | options: { 48 | modules: true, 49 | sourceMap: true, 50 | importLoaders: 1, 51 | }, 52 | }, 53 | 'sass-loader', 54 | ], 55 | include: /\.module\.s?(c|a)ss$/, 56 | }, 57 | { 58 | test: /\.s?(a|c)ss$/, 59 | use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'], 60 | exclude: /\.module\.s?(c|a)ss$/, 61 | }, 62 | // Fonts 63 | { 64 | test: /\.(woff|woff2|eot|ttf|otf)$/i, 65 | type: 'asset/resource', 66 | }, 67 | // Images 68 | { 69 | test: /\.(png|jpg|jpeg|gif)$/i, 70 | type: 'asset/resource', 71 | }, 72 | // SVG 73 | { 74 | test: /\.svg$/, 75 | use: [ 76 | { 77 | loader: '@svgr/webpack', 78 | options: { 79 | prettier: false, 80 | svgo: false, 81 | svgoConfig: { 82 | plugins: [{ removeViewBox: false }], 83 | }, 84 | titleProp: true, 85 | ref: true, 86 | }, 87 | }, 88 | 'file-loader', 89 | ], 90 | }, 91 | ], 92 | }, 93 | 94 | optimization: { 95 | minimize: true, 96 | minimizer: [ 97 | new TerserPlugin({ 98 | parallel: true, 99 | }), 100 | new CssMinimizerPlugin(), 101 | ], 102 | }, 103 | 104 | plugins: [ 105 | /** 106 | * Create global constants which can be configured at compile time. 107 | * 108 | * Useful for allowing different behaviour between development builds and 109 | * release builds 110 | * 111 | * NODE_ENV should be production so that modules do not perform certain 112 | * development checks 113 | */ 114 | new webpack.EnvironmentPlugin({ 115 | NODE_ENV: 'production', 116 | DEBUG_PROD: false, 117 | }), 118 | 119 | new MiniCssExtractPlugin({ 120 | filename: 'style.css', 121 | }), 122 | 123 | new BundleAnalyzerPlugin({ 124 | analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled', 125 | analyzerPort: 8889, 126 | }), 127 | 128 | new HtmlWebpackPlugin({ 129 | filename: 'index.html', 130 | template: path.join(webpackPaths.srcRendererPath, 'index.ejs'), 131 | minify: { 132 | collapseWhitespace: true, 133 | removeAttributeQuotes: true, 134 | removeComments: true, 135 | }, 136 | isBrowser: false, 137 | isDevelopment: process.env.NODE_ENV !== 'production', 138 | }), 139 | 140 | new webpack.DefinePlugin({ 141 | 'process.type': '"renderer"', 142 | }), 143 | ], 144 | }; 145 | 146 | export default merge(baseConfig, configuration); 147 | -------------------------------------------------------------------------------- /.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-banner.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 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /.erb/img/erb-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msgbyte/webbox/0fda84774651396f2b46c54e20bd5b774c1a5349/.erb/img/erb-logo.png -------------------------------------------------------------------------------- /.erb/img/palette-sponsor-banner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.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( 42 | 'cd ./release/app && npm install your-package' 43 | )} 44 | Read more about native dependencies at: 45 | ${chalk.bold( 46 | 'https://electron-react-boilerplate.js.org/docs/adding-dependencies/#module-structure' 47 | )} 48 | `); 49 | process.exit(1); 50 | } 51 | } catch (e) { 52 | console.log('Native dependencies could not be checked'); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /.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 || '1212'; 5 | 6 | detectPort(port, (err, availablePort) => { 7 | if (port !== String(availablePort)) { 8 | throw new Error( 9 | chalk.whiteBright.bgRed.bold( 10 | `Port "${port}" on "localhost" is already in use. Please use another port. ex: PORT=4343 npm start` 11 | ) 12 | ); 13 | } else { 14 | process.exit(0); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /.erb/scripts/clean.js: -------------------------------------------------------------------------------- 1 | import rimraf from 'rimraf'; 2 | import fs from 'fs'; 3 | import webpackPaths from '../configs/webpack.paths'; 4 | 5 | const foldersToRemove = [ 6 | webpackPaths.distPath, 7 | webpackPaths.buildPath, 8 | webpackPaths.dllPath, 9 | ]; 10 | 11 | foldersToRemove.forEach((folder) => { 12 | if (fs.existsSync(folder)) rimraf.sync(folder); 13 | }); 14 | -------------------------------------------------------------------------------- /.erb/scripts/delete-source-maps.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import rimraf from 'rimraf'; 4 | import webpackPaths from '../configs/webpack.paths'; 5 | 6 | export default function deleteSourceMaps() { 7 | if (fs.existsSync(webpackPaths.distMainPath)) 8 | rimraf.sync(path.join(webpackPaths.distMainPath, '*.js.map')); 9 | if (fs.existsSync(webpackPaths.distRendererPath)) 10 | rimraf.sync(path.join(webpackPaths.distRendererPath, '*.js.map')); 11 | } 12 | -------------------------------------------------------------------------------- /.erb/scripts/electron-rebuild.js: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | import fs from 'fs'; 3 | import { dependencies } from '../../release/app/package.json'; 4 | import webpackPaths from '../configs/webpack.paths'; 5 | 6 | if ( 7 | Object.keys(dependencies || {}).length > 0 && 8 | fs.existsSync(webpackPaths.appNodeModulesPath) 9 | ) { 10 | const electronRebuildCmd = 11 | '../../node_modules/.bin/electron-rebuild --force --types prod,dev,optional --module-dir .'; 12 | const cmd = 13 | process.platform === 'win32' 14 | ? electronRebuildCmd.replace(/\//g, '\\') 15 | : electronRebuildCmd; 16 | execSync(cmd, { 17 | cwd: webpackPaths.appPath, 18 | stdio: 'inherit', 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /.erb/scripts/link-modules.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import webpackPaths from '../configs/webpack.paths'; 3 | 4 | const { srcNodeModulesPath } = webpackPaths; 5 | const { appNodeModulesPath } = webpackPaths; 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( 17 | 'Skipping notarizing step. APPLE_ID and APPLE_ID_PASS env variables must be set' 18 | ); 19 | return; 20 | } 21 | 22 | const appName = context.packager.appInfo.productFilename; 23 | 24 | await notarize({ 25 | appBundleId: build.appId, 26 | appPath: `${appOutDir}/${appName}.app`, 27 | appleId: process.env.APPLE_ID, 28 | appleIdPassword: process.env.APPLE_ID_PASS, 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /.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 | 31 | # eslint ignores hidden directories by default: 32 | # https://github.com/eslint/eslint/issues/8429 33 | !.erb 34 | -------------------------------------------------------------------------------- /.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 | 'react/react-in-jsx-scope': 'off', 7 | 'react/jsx-filename-extension': 'off', 8 | 'import/extensions': 'off', 9 | 'import/no-unresolved': 'off', 10 | 'import/no-import-module-exports': 'off', 11 | 'import/prefer-default-export': 'off', 12 | 'no-use-before-define': 'off', 13 | }, 14 | parserOptions: { 15 | ecmaVersion: 2020, 16 | sourceType: 'module', 17 | project: './tsconfig.json', 18 | tsconfigRootDir: __dirname, 19 | createDefaultProgram: true, 20 | }, 21 | settings: { 22 | 'import/resolver': { 23 | // See https://github.com/benmosher/eslint-plugin-import/issues/1396#issuecomment-575727774 for line below 24 | node: {}, 25 | webpack: { 26 | config: require.resolve('./.erb/configs/webpack.config.eslint.ts'), 27 | }, 28 | typescript: {}, 29 | }, 30 | 'import/parsers': { 31 | '@typescript-eslint/parser': ['.ts', '.tsx'], 32 | }, 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /.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/ISSUE_TEMPLATE/1-Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: You're having technical issues. 🐞 4 | labels: 'bug' 5 | --- 6 | 7 | 8 | 9 | ## Prerequisites 10 | 11 | 12 | 13 | - [ ] Using npm 14 | - [ ] Using an up-to-date [`main` branch](https://github.com/electron-react-boilerplate/electron-react-boilerplate/tree/main) 15 | - [ ] Using latest version of devtools. [Check the docs for how to update](https://electron-react-boilerplate.js.org/docs/dev-tools/) 16 | - [ ] Tried solutions mentioned in [#400](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/400) 17 | - [ ] For issue in production release, add devtools output of `DEBUG_PROD=true npm run build && npm start` 18 | 19 | ## Expected Behavior 20 | 21 | 22 | 23 | ## Current Behavior 24 | 25 | 26 | 27 | ## Steps to Reproduce 28 | 29 | 30 | 31 | 32 | 1. 33 | 34 | 2. 35 | 36 | 3. 37 | 38 | 4. 39 | 40 | ## Possible Solution (Not obligatory) 41 | 42 | 43 | 44 | ## Context 45 | 46 | 47 | 48 | 49 | 50 | ## Your Environment 51 | 52 | 53 | 54 | - Node version : 55 | - electron-react-boilerplate version or branch : 56 | - Operating System and version : 57 | - Link to your project : 58 | 59 | 68 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2-Question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a question.❓ 4 | labels: 'question' 5 | --- 6 | 7 | ## Summary 8 | 9 | 10 | 11 | 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/3-Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: You want something added to the boilerplate. 🎉 4 | labels: 'enhancement' 5 | --- 6 | 7 | 16 | -------------------------------------------------------------------------------- /.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: '44 16 * * 4' 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://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 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 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | 9 | jobs: 10 | publish: 11 | # To enable auto publishing to github, update your electron publisher 12 | # config in package.json > "build" and remove the conditional below 13 | runs-on: ${{ matrix.os }} 14 | 15 | strategy: 16 | matrix: 17 | os: [macos-latest] 18 | 19 | steps: 20 | - name: Checkout git repo 21 | uses: actions/checkout@v3 22 | 23 | - uses: pnpm/action-setup@v2.0.1 24 | with: 25 | version: latest 26 | 27 | - name: Install Node and NPM 28 | uses: actions/setup-node@v3 29 | with: 30 | node-version: 16 31 | cache: 'pnpm' 32 | 33 | - name: Install and build 34 | run: | 35 | pnpm install 36 | pnpm run build 37 | 38 | - name: Publish releases 39 | env: 40 | # # These values are used for auto updates signing 41 | # APPLE_ID: ${{ secrets.APPLE_ID }} 42 | # APPLE_ID_PASS: ${{ secrets.APPLE_ID_PASS }} 43 | # CSC_LINK: ${{ secrets.CSC_LINK }} 44 | # CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} 45 | # This is used for uploading release assets to github 46 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 47 | run: | 48 | pnpm electron-builder --publish always --win --mac --linux 49 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 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@v3 16 | 17 | - uses: pnpm/action-setup@v2.0.1 18 | with: 19 | version: latest 20 | 21 | - name: Install Node and NPM 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: 16 25 | cache: 'pnpm' 26 | 27 | - name: pnpm install 28 | run: | 29 | pnpm install 30 | 31 | - name: npm test 32 | run: | 33 | pnpm tsc --noEmit 34 | -------------------------------------------------------------------------------- /.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 | 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 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # https://npmmirror.com/ 2 | registry = https://registry.npmmirror.com 3 | strict-peer-dependencies = false 4 | electron_mirror = https://npmmirror.com/mirrors/electron/ 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": ["run", "start"], 11 | "env": { 12 | "MAIN_ARGS": "--inspect=5858 --remote-debugging-port=9223" 13 | } 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 | "eslint.validate": [ 9 | "javascript", 10 | "javascriptreact", 11 | "html", 12 | "typescriptreact" 13 | ], 14 | 15 | "javascript.validate.enable": false, 16 | "javascript.format.enable": false, 17 | "typescript.format.enable": false, 18 | 19 | "search.exclude": { 20 | ".git": true, 21 | ".eslintcache": true, 22 | ".erb/dll": true, 23 | "release/{build,app/dist}": true, 24 | "node_modules": true, 25 | "npm-debug.log.*": true, 26 | "test/**/__snapshots__": true, 27 | "package-lock.json": true, 28 | "*.{css,sass,scss}.d.ts": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # 1.0.0 (2023-03-16) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * change all webview view bound to avoid under view display on resize. ([f07a25a](https://github.com/msgbyte/webbox/commit/f07a25ada9ab8a070e3ccb08eaee356847104cd3)) 9 | * fix display in transparent background website ([bda1943](https://github.com/msgbyte/webbox/commit/bda19432ba15cf4e2a55b62fbe4317c804f2df17)) 10 | * fix scroll height in more tree items ([89b9673](https://github.com/msgbyte/webbox/commit/89b96733aab0ca2e84cb676d5f2aad52f34e18db)) 11 | * fix y offset in window ([05b3fb4](https://github.com/msgbyte/webbox/commit/05b3fb4d80dfa30cccaa49cb64f46ab76c4c90f5)) 12 | 13 | 14 | ### Features 15 | 16 | * add batch webview update ([b5f4d35](https://github.com/msgbyte/webbox/commit/b5f4d356d4d5ed95bef9251ed7ef6ad819a065d8)) 17 | * add context menu into webview ([cd424a5](https://github.com/msgbyte/webbox/commit/cd424a59ab26c71e3d153e70fd58dbb8e47fc0d8)) 18 | * add header and change url ([2b314d7](https://github.com/msgbyte/webbox/commit/2b314d75939637114b9a9d3643d4f3f2395e3749)) 19 | * add hide all webview ([13fc093](https://github.com/msgbyte/webbox/commit/13fc093f4a38c178bd19ca859d2c4cd8a7401d7a)) 20 | * add key and url check ([48a0f36](https://github.com/msgbyte/webbox/commit/48a0f36ebddf25f0e7f43e4bfc142013b901187e)) 21 | * add package config for appid, icons, version etc. ([c2b14e1](https://github.com/msgbyte/webbox/commit/c2b14e14fa038f81ec3f761bc0950558eda3bd25)) 22 | * add tree data persist ([251fa62](https://github.com/msgbyte/webbox/commit/251fa62cd9335b36c91819ef0795b20bc95255cb)) 23 | * add unmount-webview ([d8840b4](https://github.com/msgbyte/webbox/commit/d8840b4da41e8ffca30b5ef81d4429df33568a6c)) 24 | * add webcontent no select placeholder ([cce31b0](https://github.com/msgbyte/webbox/commit/cce31b0017592f88f57423947ff8a090e0ebc93c)) 25 | * add website btn and tree drop operate ([e694d92](https://github.com/msgbyte/webbox/commit/e694d929bbc1f096001e668a1d0322988b0bdd17)) 26 | * add webview render ([aa30586](https://github.com/msgbyte/webbox/commit/aa30586a54e0116781e968e84849a75b34d8669d)) 27 | * allow snap tree list ([7f921aa](https://github.com/msgbyte/webbox/commit/7f921aafaec326a7ded81099264f383b161aef99)) 28 | * clear website and delete tree node ([476e4a7](https://github.com/msgbyte/webbox/commit/476e4a785237bc9a1ffa997dba0a7b962e999920)) 29 | * record tree expend keys ([8195e81](https://github.com/msgbyte/webbox/commit/8195e812e548ea0959dc65fd14f540b3fc3b7b79)) 30 | * 基本结构与树操作 ([94bbacd](https://github.com/msgbyte/webbox/commit/94bbacdf26ad40d48d8866a783635bdadeb3b66b)) 31 | 32 | 33 | ### Performance Improvements 34 | 35 | * optimize fullscreen experience in mac ([82c0ec8](https://github.com/msgbyte/webbox/commit/82c0ec888ac43e90bb150b52f0a6d33184a3dc7e)) -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at electronreactboilerplate@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /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 | # Webbox 2 | 3 | > Organize your favorite websites 4 | 5 | [简体中文](./README.zh.md) 6 | 7 | `Bookmarks` + `Tabs` + `Tree Organize` 8 | 9 | ## Motivation 10 | 11 | As web applications become more and more developed, there are various tabs in my work. Resident and temporary tabs are mixed together, and web pages with different needs are intertwined. 12 | 13 | Taking me personally as an example, I personally often need 3 to 5 personal websites, and work-related websites, including design drafts, various online documents, project management, application publishing, code management, etc., need 5 to 10 tabs. 14 | 15 | And more and more temporary tabs, so my current routine is to open three different browser windows, each creating 20-30 different tabs. 16 | 17 | Managing tabs has become a rigid requirement with the increasing reliance on online tools. The tab management method of conventional browsers can no longer meet the more complex tab management mode. 18 | 19 | ![](./docs/terrible-tabs.png) 20 | 21 | ## Feature 22 | 23 | - Organize your web pages with a hierarchical tree! Simply drag and drop to categorize your pages 24 | - Clean up uncommonly used pages in time to reduce unnecessary memory usage! Many times all you need is an entry that can be found later 25 | - Split your browser space, split long-term and temporary through'webbox ', 26 | - Record your organization and expansion mode to ensure that you can get back to work as soon as possible every time. No need for the browser to remember the last open state - more and more tabs will only make it difficult for your browser to move forward 27 | 28 | ![](./docs/webbox.png) 29 | 30 | It's time to return to a refreshing state! 31 | 32 | ## Try it Now! 33 | 34 | No need to compile, download now! 35 | 36 | In [here](https://github.com/msgbyte/webbox/releases) can directly obtain the pre-compiled executable file. 37 | -------------------------------------------------------------------------------- /README.zh.md: -------------------------------------------------------------------------------- 1 | # Webbox 2 | 3 | > 组织你的常用网站 4 | 5 | `收藏夹` + `标签页` + `树状管理` 6 | 7 | ## 动机 8 | 9 | 随着web应用的越来越发达,在我的工作中充斥着各种各样的标签页。常驻的与临时的标签页混杂在一起,不同需求的网页交织在一起。 10 | 11 | 以我个人为例,我个人常需要3~5个个人网站,工作相关的网站,包括设计稿、各种在线文档、项目管理、应用发布、代码管理等需要常开5~10个标签页。 12 | 13 | 以及更多更多临时的标签页,让我目前常规的常规现状是开启三个不同的浏览器窗口,每个窗口都创建着20~30个不等的标签页。 14 | 15 | 管理标签页随着对在线工具愈发的依赖变成了一个刚需的要求。而常规浏览器的标签页管理方式已经无法满足更加复杂的标签页管理模式了。 16 | 17 | ![](./docs/terrible-tabs.png) 18 | 19 | ## 特性 20 | 21 | - 通过层级树来组织你的网页! 将你的页面分门别类,简单拖动就可以 22 | - 及时清理不常用的页面,减少不必要的内存占用!很多时候你需要的仅仅是一个后续能找到的入口 23 | - 分割你的浏览器空间,通过 `webbox` 将长久的和临时的分割, 24 | - 记录你的组织与展开模式,确保每一次都能最快回到工作状态。不再需要浏览器记忆上一次的打开状态 —— 越来越多的标签页只会让你的浏览器难以前行 25 | 26 | ![](./docs/webbox.png) 27 | 28 | 是时候回归清爽的状态了! 29 | 30 | ## 立即使用 31 | 32 | 无需编译,立即下载! 33 | 34 | 在[这里](https://github.com/msgbyte/webbox/releases)可以直接获得已经预编译好的可执行文件. 35 | -------------------------------------------------------------------------------- /assets/assets.d.ts: -------------------------------------------------------------------------------- 1 | type Styles = Record; 2 | 3 | declare module '*.svg' { 4 | import React = require('react'); 5 | 6 | export const ReactComponent: React.FC>; 7 | 8 | const content: string; 9 | export default content; 10 | } 11 | 12 | declare module '*.png' { 13 | const content: string; 14 | export default content; 15 | } 16 | 17 | declare module '*.jpg' { 18 | const content: string; 19 | export default content; 20 | } 21 | 22 | declare module '*.scss' { 23 | const content: Styles; 24 | export default content; 25 | } 26 | 27 | declare module '*.sass' { 28 | const content: Styles; 29 | export default content; 30 | } 31 | 32 | declare module '*.css' { 33 | const content: Styles; 34 | export default content; 35 | } 36 | -------------------------------------------------------------------------------- /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/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msgbyte/webbox/0fda84774651396f2b46c54e20bd5b774c1a5349/assets/icon.png -------------------------------------------------------------------------------- /assets/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/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msgbyte/webbox/0fda84774651396f2b46c54e20bd5b774c1a5349/assets/icons/1024x1024.png -------------------------------------------------------------------------------- /assets/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msgbyte/webbox/0fda84774651396f2b46c54e20bd5b774c1a5349/assets/icons/128x128.png -------------------------------------------------------------------------------- /assets/icons/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msgbyte/webbox/0fda84774651396f2b46c54e20bd5b774c1a5349/assets/icons/16x16.png -------------------------------------------------------------------------------- /assets/icons/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msgbyte/webbox/0fda84774651396f2b46c54e20bd5b774c1a5349/assets/icons/24x24.png -------------------------------------------------------------------------------- /assets/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msgbyte/webbox/0fda84774651396f2b46c54e20bd5b774c1a5349/assets/icons/256x256.png -------------------------------------------------------------------------------- /assets/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msgbyte/webbox/0fda84774651396f2b46c54e20bd5b774c1a5349/assets/icons/32x32.png -------------------------------------------------------------------------------- /assets/icons/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msgbyte/webbox/0fda84774651396f2b46c54e20bd5b774c1a5349/assets/icons/48x48.png -------------------------------------------------------------------------------- /assets/icons/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msgbyte/webbox/0fda84774651396f2b46c54e20bd5b774c1a5349/assets/icons/512x512.png -------------------------------------------------------------------------------- /assets/icons/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msgbyte/webbox/0fda84774651396f2b46c54e20bd5b774c1a5349/assets/icons/64x64.png -------------------------------------------------------------------------------- /assets/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msgbyte/webbox/0fda84774651396f2b46c54e20bd5b774c1a5349/assets/icons/icon.icns -------------------------------------------------------------------------------- /assets/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msgbyte/webbox/0fda84774651396f2b46c54e20bd5b774c1a5349/assets/icons/icon.ico -------------------------------------------------------------------------------- /docs/terrible-tabs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msgbyte/webbox/0fda84774651396f2b46c54e20bd5b774c1a5349/docs/terrible-tabs.png -------------------------------------------------------------------------------- /docs/webbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msgbyte/webbox/0fda84774651396f2b46c54e20bd5b774c1a5349/docs/webbox.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Organize your favorite websites", 3 | "keywords": [ 4 | "webbox", 5 | "electron", 6 | "boilerplate", 7 | "react", 8 | "typescript", 9 | "ts", 10 | "sass", 11 | "webpack", 12 | "hot", 13 | "reload" 14 | ], 15 | "homepage": "https://github.com/msgbyte/webbox#readme", 16 | "bugs": { 17 | "url": "https://github.com/msgbyte/webbox/issues" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/msgbyte/webbox.git" 22 | }, 23 | "license": "MIT", 24 | "author": { 25 | "name": "moonrailgun", 26 | "email": "moonrailgun@gmail.com", 27 | "url": "http://moonrailgun.com/" 28 | }, 29 | "main": "./src/main/main.ts", 30 | "scripts": { 31 | "build": "concurrently \"npm run build:main\" \"npm run build:renderer\"", 32 | "build:main": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.main.prod.ts", 33 | "build:renderer": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.renderer.prod.ts", 34 | "postinstall": "ts-node .erb/scripts/check-native-dep.js && electron-builder install-app-deps && cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.renderer.dev.dll.ts", 35 | "lint": "cross-env NODE_ENV=development eslint . --ext .js,.jsx,.ts,.tsx", 36 | "package": "ts-node ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never", 37 | "rebuild": "electron-rebuild --parallel --types prod,dev,optional --module-dir release/app", 38 | "dev": "pnpm run start", 39 | "start": "ts-node ./.erb/scripts/check-port-in-use.js && npm run start:renderer", 40 | "start:main": "cross-env NODE_ENV=development electronmon -r ts-node/register/transpile-only .", 41 | "start:preload": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.preload.dev.ts", 42 | "start:renderer": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./.erb/configs/webpack.config.renderer.dev.ts", 43 | "create:icons": "electron-icon-builder --input=./assets/icon.png --output=./assets -f", 44 | "release": "cd release/app && release-it --no-increment", 45 | "test": "jest" 46 | }, 47 | "browserslist": [], 48 | "prettier": { 49 | "singleQuote": true, 50 | "overrides": [ 51 | { 52 | "files": [ 53 | ".prettierrc", 54 | ".eslintrc" 55 | ], 56 | "options": { 57 | "parser": "json" 58 | } 59 | } 60 | ] 61 | }, 62 | "jest": { 63 | "moduleDirectories": [ 64 | "node_modules", 65 | "release/app/node_modules", 66 | "src" 67 | ], 68 | "moduleFileExtensions": [ 69 | "js", 70 | "jsx", 71 | "ts", 72 | "tsx", 73 | "json" 74 | ], 75 | "moduleNameMapper": { 76 | "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/.erb/mocks/fileMock.js", 77 | "\\.(css|less|sass|scss)$": "identity-obj-proxy" 78 | }, 79 | "setupFiles": [ 80 | "./.erb/scripts/check-build-exists.ts" 81 | ], 82 | "testEnvironment": "jsdom", 83 | "testEnvironmentOptions": { 84 | "url": "http://localhost/" 85 | }, 86 | "testPathIgnorePatterns": [ 87 | "release/app/dist", 88 | ".erb/dll" 89 | ], 90 | "transform": { 91 | "\\.(ts|tsx|js|jsx)$": "ts-jest" 92 | } 93 | }, 94 | "dependencies": { 95 | "@arco-design/web-react": "^2.44.1", 96 | "ahooks": "^3.7.5", 97 | "allotment": "^1.18.0", 98 | "electron-context-menu": "^3.6.1", 99 | "electron-debug": "^3.2.0", 100 | "electron-log": "^4.4.8", 101 | "electron-updater": "^5.3.0", 102 | "immer": "^9.0.19", 103 | "lodash": "^4.17.21", 104 | "nanoid": "^4.0.1", 105 | "react": "^18.2.0", 106 | "react-dom": "^18.2.0", 107 | "react-is": "^18.2.0", 108 | "react-router-dom": "^6.8.0", 109 | "source-map-support": "^0.5.21", 110 | "styled-components": "^5.3.6", 111 | "url-regex": "^5.0.0", 112 | "zustand": "^4.3.2" 113 | }, 114 | "devDependencies": { 115 | "@electron/notarize": "^1.2.3", 116 | "@electron/rebuild": "^3.2.10", 117 | "@pmmmwh/react-refresh-webpack-plugin": "^0.5.10", 118 | "@release-it/conventional-changelog": "^5.1.1", 119 | "@svgr/webpack": "^6.5.1", 120 | "@teamsupercell/typings-for-css-modules-loader": "^2.5.2", 121 | "@testing-library/jest-dom": "^5.16.5", 122 | "@testing-library/react": "^13.4.0", 123 | "@types/jest": "^29.4.0", 124 | "@types/lodash": "^4.14.191", 125 | "@types/node": "18.11.19", 126 | "@types/react": "^18.0.27", 127 | "@types/react-dom": "^18.0.10", 128 | "@types/react-test-renderer": "^18.0.0", 129 | "@types/styled-components": "^5.1.26", 130 | "@types/terser-webpack-plugin": "^5.0.4", 131 | "@types/webpack-bundle-analyzer": "^4.6.0", 132 | "@typescript-eslint/eslint-plugin": "^5.50.0", 133 | "@typescript-eslint/parser": "^5.50.0", 134 | "browserslist-config-erb": "^0.0.3", 135 | "chalk": "^4.1.2", 136 | "concurrently": "^7.6.0", 137 | "core-js": "^3.27.2", 138 | "cross-env": "^7.0.3", 139 | "css-loader": "^6.7.3", 140 | "css-minimizer-webpack-plugin": "^4.2.2", 141 | "detect-port": "^1.5.1", 142 | "electron": "^22.2.0", 143 | "electron-builder": "^23.6.0", 144 | "electron-devtools-installer": "^3.2.0", 145 | "electronmon": "^2.0.2", 146 | "eslint": "^8.33.0", 147 | "eslint-config-airbnb-base": "^15.0.0", 148 | "eslint-config-erb": "^4.0.5", 149 | "eslint-import-resolver-typescript": "^3.5.3", 150 | "eslint-import-resolver-webpack": "^0.13.2", 151 | "eslint-plugin-compat": "^4.0.2", 152 | "eslint-plugin-import": "^2.27.5", 153 | "eslint-plugin-jest": "^27.2.1", 154 | "eslint-plugin-jsx-a11y": "^6.7.1", 155 | "eslint-plugin-promise": "^6.1.1", 156 | "eslint-plugin-react": "^7.32.2", 157 | "eslint-plugin-react-hooks": "^4.6.0", 158 | "file-loader": "^6.2.0", 159 | "html-webpack-plugin": "^5.5.0", 160 | "identity-obj-proxy": "^3.0.0", 161 | "jest": "^29.4.1", 162 | "jest-environment-jsdom": "^29.4.1", 163 | "mini-css-extract-plugin": "^2.7.2", 164 | "prettier": "^2.8.3", 165 | "react-refresh": "^0.14.0", 166 | "react-test-renderer": "^18.2.0", 167 | "release-it": "^15.8.0", 168 | "rimraf": "^4.1.2", 169 | "sass": "^1.58.0", 170 | "sass-loader": "^13.2.0", 171 | "style-loader": "^3.3.1", 172 | "terser-webpack-plugin": "^5.3.6", 173 | "ts-jest": "^29.0.5", 174 | "ts-loader": "^9.4.2", 175 | "ts-node": "^10.9.1", 176 | "tsconfig-paths-webpack-plugin": "^4.0.0", 177 | "typescript": "^4.9.5", 178 | "url-loader": "^4.1.1", 179 | "webpack": "^5.75.0", 180 | "webpack-bundle-analyzer": "^4.7.0", 181 | "webpack-cli": "^5.0.1", 182 | "webpack-dev-server": "^4.11.1", 183 | "webpack-merge": "^5.8.0" 184 | }, 185 | "build": { 186 | "productName": "Webbox", 187 | "appId": "com.msgbyte.webbox", 188 | "asar": true, 189 | "asarUnpack": "**\\*.{node,dll}", 190 | "files": [ 191 | "dist", 192 | "node_modules", 193 | "package.json" 194 | ], 195 | "afterSign": ".erb/scripts/notarize.js", 196 | "mac": { 197 | "target": { 198 | "target": "default", 199 | "arch": [ 200 | "arm64", 201 | "x64" 202 | ] 203 | }, 204 | "type": "distribution", 205 | "hardenedRuntime": true, 206 | "entitlements": "assets/entitlements.mac.plist", 207 | "entitlementsInherit": "assets/entitlements.mac.plist", 208 | "gatekeeperAssess": false 209 | }, 210 | "dmg": { 211 | "contents": [ 212 | { 213 | "x": 130, 214 | "y": 220 215 | }, 216 | { 217 | "x": 410, 218 | "y": 220, 219 | "type": "link", 220 | "path": "/Applications" 221 | } 222 | ] 223 | }, 224 | "win": { 225 | "target": [ 226 | "nsis" 227 | ] 228 | }, 229 | "linux": { 230 | "target": [ 231 | "AppImage" 232 | ], 233 | "category": "Development" 234 | }, 235 | "directories": { 236 | "app": "release/app", 237 | "buildResources": "assets", 238 | "output": "release/build" 239 | }, 240 | "extraResources": [ 241 | "./assets/**" 242 | ], 243 | "publish": { 244 | "provider": "github", 245 | "owner": "msgbyte", 246 | "repo": "webbox" 247 | } 248 | }, 249 | "devEngines": { 250 | "node": ">=14.x", 251 | "pnpm": ">=7.x" 252 | }, 253 | "electronmon": { 254 | "patterns": [ 255 | "!**/**", 256 | "src/main/**" 257 | ], 258 | "logLevel": "quiet" 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /release/app/.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "release": true 4 | }, 5 | "git": { 6 | "commitMessage": "chore: release v${version}" 7 | }, 8 | "npm": { 9 | "publish": false 10 | }, 11 | "hooks": { 12 | "after:bump": "echo Version Upgrade Success. checkout more in CHANGELOG" 13 | }, 14 | "plugins": { 15 | "@release-it/conventional-changelog": { 16 | "preset": "angular", 17 | "infile": "../../CHANGELOG.md" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /release/app/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webbox", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "webbox", 9 | "version": "1.0.0", 10 | "hasInstallScript": true, 11 | "license": "MIT" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /release/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webbox", 3 | "version": "1.1.0", 4 | "description": "Organize your favorite websites", 5 | "license": "MIT", 6 | "author": { 7 | "name": "moonrailgun", 8 | "email": "moonrailgun@gmail.com", 9 | "url": "http://moonrailgun.com/" 10 | }, 11 | "main": "./dist/main/main.js", 12 | "scripts": { 13 | "rebuild": "node -r ts-node/register ../../.erb/scripts/electron-rebuild.js", 14 | "postinstall": "npm run rebuild && npm run link-modules", 15 | "link-modules": "node -r ts-node/register ../../.erb/scripts/link-modules.ts" 16 | }, 17 | "dependencies": {} 18 | } 19 | -------------------------------------------------------------------------------- /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/main/main.ts: -------------------------------------------------------------------------------- 1 | /* eslint global-require: off, no-console: off, promise/always-return: off */ 2 | 3 | /** 4 | * This module executes inside of electron's main process. You can start 5 | * electron renderer process from here and communicate with the other processes 6 | * through IPC. 7 | * 8 | * When running `npm run build` or `npm run build:main`, this file is compiled to 9 | * `./src/main.js` using webpack. This gives us some performance wins. 10 | */ 11 | import path from 'path'; 12 | import { app, BrowserWindow, shell, ipcMain } from 'electron'; 13 | import { autoUpdater } from 'electron-updater'; 14 | import log from 'electron-log'; 15 | import { MenuBuilder } from './menu'; 16 | import { resolveHtmlPath } from './util'; 17 | import { initWebviewManager } from './webviewManager'; 18 | 19 | class AppUpdater { 20 | constructor() { 21 | log.transports.file.level = 'info'; 22 | autoUpdater.logger = log; 23 | autoUpdater.checkForUpdatesAndNotify(); 24 | } 25 | } 26 | 27 | let mainWindow: BrowserWindow | null = null; 28 | 29 | ipcMain.on('ipc-example', async (event, arg) => { 30 | const msgTemplate = (pingPong: string) => `IPC test: ${pingPong}`; 31 | console.log(msgTemplate(arg)); 32 | event.reply('ipc-example', msgTemplate('pong')); 33 | }); 34 | 35 | if (process.env.NODE_ENV === 'production') { 36 | const sourceMapSupport = require('source-map-support'); 37 | sourceMapSupport.install(); 38 | } 39 | 40 | const isDebug = 41 | process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true'; 42 | 43 | if (isDebug) { 44 | require('electron-debug')(); 45 | } 46 | 47 | const installExtensions = async () => { 48 | const installer = require('electron-devtools-installer'); 49 | const forceDownload = !!process.env.UPGRADE_EXTENSIONS; 50 | const extensions = ['REACT_DEVELOPER_TOOLS']; 51 | 52 | return installer 53 | .default( 54 | extensions.map((name) => installer[name]), 55 | forceDownload 56 | ) 57 | .catch(console.log); 58 | }; 59 | 60 | const createWindow = async () => { 61 | if (isDebug) { 62 | await installExtensions(); 63 | } 64 | 65 | const RESOURCES_PATH = app.isPackaged 66 | ? path.join(process.resourcesPath, 'assets') 67 | : path.join(__dirname, '../../assets'); 68 | 69 | const getAssetPath = (...paths: string[]): string => { 70 | return path.join(RESOURCES_PATH, ...paths); 71 | }; 72 | 73 | mainWindow = new BrowserWindow({ 74 | show: false, 75 | width: 1024, 76 | height: 728, 77 | minWidth: 800, 78 | minHeight: 600, 79 | icon: getAssetPath('icon.png'), 80 | webPreferences: { 81 | preload: app.isPackaged 82 | ? path.join(__dirname, 'preload.js') 83 | : path.join(__dirname, '../../.erb/dll/preload.js'), 84 | }, 85 | }); 86 | 87 | mainWindow.loadURL(resolveHtmlPath('index.html')); 88 | 89 | mainWindow.on('ready-to-show', () => { 90 | if (!mainWindow) { 91 | throw new Error('"mainWindow" is not defined'); 92 | } 93 | if (process.env.START_MINIMIZED) { 94 | mainWindow.minimize(); 95 | } else { 96 | mainWindow.show(); 97 | } 98 | }); 99 | 100 | mainWindow.on('closed', () => { 101 | mainWindow = null; 102 | }); 103 | 104 | const menuBuilder = new MenuBuilder(mainWindow); 105 | menuBuilder.buildMenu(); 106 | 107 | // Open urls in the user's browser 108 | mainWindow.webContents.setWindowOpenHandler((edata) => { 109 | shell.openExternal(edata.url); 110 | return { action: 'deny' }; 111 | }); 112 | 113 | // Remove this if your app does not use auto updates 114 | // eslint-disable-next-line 115 | new AppUpdater(); 116 | 117 | initWebviewManager(mainWindow); 118 | }; 119 | 120 | /** 121 | * Add event listeners... 122 | */ 123 | 124 | app.on('window-all-closed', () => { 125 | // Respect the OSX convention of having the application in memory even 126 | // after all windows have been closed 127 | if (process.platform !== 'darwin') { 128 | app.quit(); 129 | } 130 | }); 131 | 132 | app 133 | .whenReady() 134 | .then(() => { 135 | createWindow(); 136 | app.on('activate', () => { 137 | // On macOS it's common to re-create a window in the app when the 138 | // dock icon is clicked and there are no other windows open. 139 | if (mainWindow === null) createWindow(); 140 | }); 141 | }) 142 | .catch(console.log); 143 | -------------------------------------------------------------------------------- /src/main/menu.ts: -------------------------------------------------------------------------------- 1 | import { 2 | app, 3 | Menu, 4 | shell, 5 | BrowserWindow, 6 | MenuItemConstructorOptions, 7 | BrowserView, 8 | clipboard, 9 | } from 'electron'; 10 | import contextMenu from 'electron-context-menu'; 11 | import _compact from 'lodash/compact'; 12 | 13 | interface DarwinMenuItemConstructorOptions extends MenuItemConstructorOptions { 14 | selector?: string; 15 | submenu?: DarwinMenuItemConstructorOptions[] | Menu; 16 | } 17 | 18 | export class MenuBuilder { 19 | mainWindow: BrowserWindow; 20 | 21 | allowClose: boolean; 22 | 23 | constructor(mainWindow: BrowserWindow, allowClose = false) { 24 | this.mainWindow = mainWindow; 25 | this.allowClose = allowClose; 26 | } 27 | 28 | buildMenu(): Menu { 29 | if ( 30 | process.env.NODE_ENV === 'development' || 31 | process.env.DEBUG_PROD === 'true' 32 | ) { 33 | this.setupDevelopmentEnvironment(); 34 | } 35 | 36 | const template = 37 | process.platform === 'darwin' 38 | ? this.buildDarwinTemplate() 39 | : this.buildDefaultTemplate(); 40 | 41 | const menu = Menu.buildFromTemplate(template); 42 | Menu.setApplicationMenu(menu); 43 | 44 | return menu; 45 | } 46 | 47 | setupDevelopmentEnvironment(): void { 48 | this.mainWindow.webContents.on('context-menu', (_, props) => { 49 | const { x, y } = props; 50 | 51 | Menu.buildFromTemplate([ 52 | { 53 | label: 'Inspect element', 54 | click: () => { 55 | this.mainWindow.webContents.inspectElement(x, y); 56 | }, 57 | }, 58 | ]).popup({ window: this.mainWindow }); 59 | }); 60 | } 61 | 62 | buildDarwinTemplate(): MenuItemConstructorOptions[] { 63 | const subMenuAbout: DarwinMenuItemConstructorOptions = { 64 | label: 'Electron', 65 | submenu: [ 66 | { 67 | label: 'About ElectronReact', 68 | selector: 'orderFrontStandardAboutPanel:', 69 | }, 70 | { type: 'separator' }, 71 | { label: 'Services', submenu: [] }, 72 | { type: 'separator' }, 73 | { 74 | label: 'Hide ElectronReact', 75 | accelerator: 'Command+H', 76 | selector: 'hide:', 77 | }, 78 | { 79 | label: 'Hide Others', 80 | accelerator: 'Command+Shift+H', 81 | selector: 'hideOtherApplications:', 82 | }, 83 | { label: 'Show All', selector: 'unhideAllApplications:' }, 84 | { type: 'separator' }, 85 | { 86 | label: 'Quit', 87 | accelerator: 'Command+Q', 88 | click: () => { 89 | app.quit(); 90 | }, 91 | }, 92 | ], 93 | }; 94 | const subMenuEdit: DarwinMenuItemConstructorOptions = { 95 | label: 'Edit', 96 | submenu: [ 97 | { label: 'Undo', accelerator: 'Command+Z', selector: 'undo:' }, 98 | { label: 'Redo', accelerator: 'Shift+Command+Z', selector: 'redo:' }, 99 | { type: 'separator' }, 100 | { label: 'Cut', accelerator: 'Command+X', selector: 'cut:' }, 101 | { label: 'Copy', accelerator: 'Command+C', selector: 'copy:' }, 102 | { label: 'Paste', accelerator: 'Command+V', selector: 'paste:' }, 103 | { 104 | label: 'Select All', 105 | accelerator: 'Command+A', 106 | selector: 'selectAll:', 107 | }, 108 | ], 109 | }; 110 | const subMenuViewDev: MenuItemConstructorOptions = { 111 | label: 'View', 112 | submenu: [ 113 | { 114 | label: 'Reload', 115 | accelerator: 'Command+R', 116 | click: () => { 117 | this.mainWindow.webContents.reload(); 118 | }, 119 | }, 120 | { 121 | label: 'Toggle Full Screen', 122 | accelerator: 'Ctrl+Command+F', 123 | click: () => { 124 | this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen()); 125 | }, 126 | }, 127 | { 128 | label: 'Toggle Developer Tools', 129 | accelerator: 'Alt+Command+I', 130 | click: () => { 131 | this.mainWindow.webContents.toggleDevTools(); 132 | }, 133 | }, 134 | ], 135 | }; 136 | const subMenuViewProd: MenuItemConstructorOptions = { 137 | label: 'View', 138 | submenu: [ 139 | { 140 | label: 'Toggle Full Screen', 141 | accelerator: 'Ctrl+Command+F', 142 | click: () => { 143 | this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen()); 144 | }, 145 | }, 146 | ], 147 | }; 148 | const subMenuWindow: DarwinMenuItemConstructorOptions = { 149 | label: 'Window', 150 | submenu: _compact([ 151 | { 152 | label: 'Minimize', 153 | accelerator: 'Command+M', 154 | selector: 'performMiniaturize:', 155 | }, 156 | this.allowClose 157 | ? { 158 | label: 'Close', 159 | accelerator: 'Command+W', 160 | selector: 'performClose:', 161 | } 162 | : null, 163 | { type: 'separator' }, 164 | { label: 'Bring All to Front', selector: 'arrangeInFront:' }, 165 | ]), 166 | }; 167 | const subMenuHelp: MenuItemConstructorOptions = { 168 | label: 'Help', 169 | submenu: [ 170 | { 171 | label: 'Learn More', 172 | click() { 173 | shell.openExternal('https://electronjs.org'); 174 | }, 175 | }, 176 | { 177 | label: 'Documentation', 178 | click() { 179 | shell.openExternal( 180 | 'https://github.com/electron/electron/tree/main/docs#readme' 181 | ); 182 | }, 183 | }, 184 | { 185 | label: 'Community Discussions', 186 | click() { 187 | shell.openExternal('https://www.electronjs.org/community'); 188 | }, 189 | }, 190 | { 191 | label: 'Search Issues', 192 | click() { 193 | shell.openExternal('https://github.com/electron/electron/issues'); 194 | }, 195 | }, 196 | ], 197 | }; 198 | 199 | const subMenuView = 200 | process.env.NODE_ENV === 'development' || 201 | process.env.DEBUG_PROD === 'true' 202 | ? subMenuViewDev 203 | : subMenuViewProd; 204 | 205 | return [subMenuAbout, subMenuEdit, subMenuView, subMenuWindow, subMenuHelp]; 206 | } 207 | 208 | buildDefaultTemplate() { 209 | const templateDefault = [ 210 | { 211 | label: '&File', 212 | submenu: _compact([ 213 | { 214 | label: '&Open', 215 | accelerator: 'Ctrl+O', 216 | }, 217 | this.allowClose 218 | ? { 219 | label: '&Close', 220 | accelerator: 'Ctrl+W', 221 | click: () => { 222 | this.mainWindow.close(); 223 | }, 224 | } 225 | : null, 226 | ]), 227 | }, 228 | { 229 | label: '&View', 230 | submenu: 231 | process.env.NODE_ENV === 'development' || 232 | process.env.DEBUG_PROD === 'true' 233 | ? [ 234 | { 235 | label: '&Reload', 236 | accelerator: 'Ctrl+R', 237 | click: () => { 238 | this.mainWindow.webContents.reload(); 239 | }, 240 | }, 241 | { 242 | label: 'Toggle &Full Screen', 243 | accelerator: 'F11', 244 | click: () => { 245 | this.mainWindow.setFullScreen( 246 | !this.mainWindow.isFullScreen() 247 | ); 248 | }, 249 | }, 250 | { 251 | label: 'Toggle &Developer Tools', 252 | accelerator: 'Alt+Ctrl+I', 253 | click: () => { 254 | this.mainWindow.webContents.toggleDevTools(); 255 | }, 256 | }, 257 | ] 258 | : [ 259 | { 260 | label: 'Toggle &Full Screen', 261 | accelerator: 'F11', 262 | click: () => { 263 | this.mainWindow.setFullScreen( 264 | !this.mainWindow.isFullScreen() 265 | ); 266 | }, 267 | }, 268 | ], 269 | }, 270 | { 271 | label: 'Help', 272 | submenu: [ 273 | { 274 | label: 'Learn More', 275 | click() { 276 | shell.openExternal('https://electronjs.org'); 277 | }, 278 | }, 279 | { 280 | label: 'Documentation', 281 | click() { 282 | shell.openExternal( 283 | 'https://github.com/electron/electron/tree/main/docs#readme' 284 | ); 285 | }, 286 | }, 287 | { 288 | label: 'Community Discussions', 289 | click() { 290 | shell.openExternal('https://www.electronjs.org/community'); 291 | }, 292 | }, 293 | { 294 | label: 'Search Issues', 295 | click() { 296 | shell.openExternal('https://github.com/electron/electron/issues'); 297 | }, 298 | }, 299 | ], 300 | }, 301 | ]; 302 | 303 | return templateDefault; 304 | } 305 | } 306 | 307 | /** 308 | * Build Context Menu 309 | */ 310 | export function buildContextMenu(window: BrowserWindow | BrowserView) { 311 | // context menu in browser view 312 | contextMenu({ 313 | window, 314 | showLearnSpelling: false, 315 | showLookUpSelection: false, 316 | showSearchWithGoogle: false, 317 | showInspectElement: true, 318 | prepend: (defaultActions, parameters, browserWindow) => { 319 | return [ 320 | { 321 | label: 'Reload', 322 | visible: 323 | browserWindow instanceof BrowserWindow || 324 | browserWindow instanceof BrowserView, 325 | click: () => { 326 | if ( 327 | browserWindow instanceof BrowserWindow || 328 | browserWindow instanceof BrowserView 329 | ) { 330 | browserWindow.webContents.reload(); 331 | } 332 | }, 333 | }, 334 | { 335 | label: 'Copy Current Page Url', 336 | visible: 337 | browserWindow instanceof BrowserWindow || 338 | browserWindow instanceof BrowserView, 339 | click: () => { 340 | if ( 341 | browserWindow instanceof BrowserWindow || 342 | browserWindow instanceof BrowserView 343 | ) { 344 | clipboard.writeText(browserWindow.webContents.getURL()); 345 | } 346 | }, 347 | }, 348 | ]; 349 | }, 350 | }); 351 | } 352 | -------------------------------------------------------------------------------- /src/main/preload.ts: -------------------------------------------------------------------------------- 1 | // Disable no-unused-vars, broken for spread args 2 | /* eslint no-unused-vars: off */ 3 | import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron'; 4 | 5 | export type Channels = 6 | | 'mount-webview' 7 | | 'unmount-webview' 8 | | 'update-webview-rect' 9 | | 'hide-all-webview' 10 | | 'clear-all-webview'; 11 | 12 | const electronHandler = { 13 | ipcRenderer: { 14 | sendMessage(channel: Channels, ...args: unknown[]) { 15 | ipcRenderer.send(channel, ...args); 16 | }, 17 | on(channel: Channels, func: (...args: unknown[]) => void) { 18 | const subscription = (_event: IpcRendererEvent, ...args: unknown[]) => 19 | func(...args); 20 | ipcRenderer.on(channel, subscription); 21 | 22 | return () => { 23 | ipcRenderer.removeListener(channel, subscription); 24 | }; 25 | }, 26 | once(channel: Channels, func: (...args: unknown[]) => void) { 27 | ipcRenderer.once(channel, (_event, ...args) => func(...args)); 28 | }, 29 | }, 30 | }; 31 | 32 | contextBridge.exposeInMainWorld('electron', electronHandler); 33 | 34 | export type ElectronHandler = typeof electronHandler; 35 | -------------------------------------------------------------------------------- /src/main/subWindow.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow, HandlerDetails } from 'electron'; 2 | import { MenuBuilder } from './menu'; 3 | 4 | export function subWindowOpenHandler(details: HandlerDetails) { 5 | createSubWindow(details.url); 6 | 7 | return { 8 | action: 'deny' as const, 9 | }; 10 | } 11 | 12 | let subWinSize: { width: number; height: number } | null = null; 13 | 14 | /** 15 | * For the handling of opening sub-windows via url in the webview 16 | */ 17 | function createSubWindow(url: string) { 18 | const win = new BrowserWindow({ 19 | width: subWinSize?.width ?? 1024, 20 | height: subWinSize?.height ?? 728, 21 | minWidth: 800, 22 | minHeight: 600, 23 | webPreferences: { 24 | devTools: false, 25 | nodeIntegration: false, 26 | }, 27 | }); 28 | win.webContents.setWindowOpenHandler(subWindowOpenHandler); 29 | win.loadURL(url); 30 | win.on('resize', () => { 31 | const { width, height } = win.getBounds(); 32 | subWinSize = { 33 | width, 34 | height, 35 | }; 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /src/main/util.ts: -------------------------------------------------------------------------------- 1 | /* eslint import/prefer-default-export: off */ 2 | import { URL } from 'url'; 3 | import path from 'path'; 4 | 5 | export function resolveHtmlPath(htmlFileName: string) { 6 | if (process.env.NODE_ENV === 'development') { 7 | const port = process.env.PORT || 1212; 8 | const url = new URL(`http://localhost:${port}`); 9 | url.pathname = htmlFileName; 10 | return url.href; 11 | } 12 | return `file://${path.resolve(__dirname, '../renderer/', htmlFileName)}`; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/webviewManager.ts: -------------------------------------------------------------------------------- 1 | import { BrowserView, BrowserWindow, ipcMain, Rectangle } from 'electron'; 2 | import os from 'os'; 3 | import log from 'electron-log'; 4 | import { buildContextMenu } from './menu'; 5 | import { subWindowOpenHandler } from './subWindow'; 6 | 7 | interface WebviewInfo { 8 | view: BrowserView; 9 | url: string; 10 | hidden: boolean; 11 | } 12 | 13 | const webviewMap = new Map(); 14 | 15 | /** 16 | * fix rect into correct size 17 | */ 18 | function fixRect(rect: Rectangle, isFullScreen: boolean): Rectangle { 19 | const xOffset = 1; 20 | const yOffset = !isFullScreen && os.platform() === 'darwin' ? 28 : 0; // add y axis offset in mac os if is not fullScreen 21 | 22 | return { 23 | x: Math.round(rect.x) + xOffset, 24 | y: Math.round(rect.y) + yOffset, 25 | width: Math.round(rect.width) - xOffset, 26 | height: Math.round(rect.height), 27 | }; 28 | } 29 | 30 | export function initWebviewManager(win: BrowserWindow) { 31 | ipcMain.on('mount-webview', (e, info) => { 32 | if (!win) { 33 | log.info('[mount-webview]', 'cannot get mainWindow'); 34 | return; 35 | } 36 | 37 | log.info('[mount-webview] info:', info); 38 | 39 | const { key, url } = info; 40 | if (!url) { 41 | return; 42 | } 43 | 44 | if (webviewMap.has(key)) { 45 | const webview = webviewMap.get(key)!; 46 | win.setTopBrowserView(webview.view); 47 | webview.view.setBounds(fixRect(info.rect, win.isFullScreen())); 48 | if (webview.url !== url) { 49 | // url has been change. 50 | webview.view.webContents.loadURL(url); 51 | } 52 | return; 53 | } 54 | 55 | hideAllWebview(); 56 | const view = new BrowserView({ 57 | webPreferences: { 58 | nodeIntegration: false, 59 | }, 60 | }); 61 | buildContextMenu(view); 62 | view.setBackgroundColor('#fff'); 63 | view.setBounds(fixRect(info.rect, win.isFullScreen())); 64 | view.webContents.loadURL(url); 65 | view.webContents.setWindowOpenHandler(subWindowOpenHandler); 66 | win.addBrowserView(view); 67 | webviewMap.set(key, { view, url, hidden: false }); 68 | }); 69 | 70 | ipcMain.on('unmount-webview', (e, info) => { 71 | if (!win) { 72 | log.info('[unmount-webview]', 'cannot get mainWindow'); 73 | return; 74 | } 75 | 76 | log.info('[unmount-webview] info:', info); 77 | 78 | const { key } = info; 79 | const webview = webviewMap.get(key); 80 | if (webview) { 81 | win.removeBrowserView(webview.view); 82 | webviewMap.delete(key); 83 | } 84 | }); 85 | 86 | ipcMain.on('update-webview-rect', (e, info) => { 87 | if (!win) { 88 | log.info('[update-webview-rect]', 'cannot get mainWindow'); 89 | return; 90 | } 91 | 92 | log.info('[update-webview-rect] info:', info); 93 | 94 | // Change All View to avoid under view display on resize. 95 | webviewMap.forEach((webview) => { 96 | webview.hidden = false; 97 | webview.view.setBounds(fixRect(info.rect, win.isFullScreen())); 98 | }); 99 | 100 | // Change Single View 101 | // const webview = webviewMap.get(info.key); 102 | // if (webview) { 103 | // webview.hidden = false; 104 | // webview.view.setBounds(fixRect(info.rect, win.isFullScreen())); 105 | // } 106 | }); 107 | 108 | ipcMain.on('hide-all-webview', () => { 109 | log.info('[hide-all-webview]'); 110 | 111 | hideAllWebview(); 112 | }); 113 | 114 | ipcMain.on('clear-all-webview', () => { 115 | if (!win) { 116 | log.info('[clear-all-webview]', 'cannot get mainWindow'); 117 | return; 118 | } 119 | 120 | log.info('[clear-all-webview]'); 121 | 122 | win.getBrowserViews().forEach((view) => { 123 | win.removeBrowserView(view); 124 | }); 125 | 126 | webviewMap.clear(); 127 | }); 128 | } 129 | 130 | const HIDDEN_OFFSET = 3000; 131 | 132 | /** 133 | * Show webview with remove offset in y 134 | */ 135 | function showWebView(webview: WebviewInfo) { 136 | if (webview.hidden === false) { 137 | return; 138 | } 139 | 140 | webview.hidden = false; 141 | const oldBounds = webview.view.getBounds(); 142 | webview.view.setBounds({ 143 | ...oldBounds, 144 | y: oldBounds.y - HIDDEN_OFFSET, 145 | }); 146 | } 147 | 148 | /** 149 | * Hide webview with append offset in y 150 | */ 151 | function hideWebView(webview: WebviewInfo) { 152 | if (webview.hidden === true) { 153 | return; 154 | } 155 | 156 | webview.hidden = true; 157 | const oldBounds = webview.view.getBounds(); 158 | webview.view.setBounds({ 159 | ...oldBounds, 160 | y: oldBounds.y + HIDDEN_OFFSET, 161 | }); 162 | } 163 | 164 | function hideAllWebview() { 165 | Array.from(webviewMap.values()).forEach((webview) => { 166 | hideWebView(webview); 167 | }); 168 | } 169 | -------------------------------------------------------------------------------- /src/renderer/App.tsx: -------------------------------------------------------------------------------- 1 | import { MemoryRouter as Router, Routes, Route } from 'react-router-dom'; 2 | import { Allotment } from 'allotment'; 3 | import { SideTree } from './components/SideTree'; 4 | import { WebContent } from './components/main/WebContent'; 5 | 6 | function Main() { 7 | return ( 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | ); 20 | } 21 | 22 | export default function App() { 23 | return ( 24 | 25 | 26 | } /> 27 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/renderer/assets/web-page.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/renderer/components/AddWebsiteBtn.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@arco-design/web-react'; 2 | import React from 'react'; 3 | import { generateDefaultNode, useTreeStore } from '../store/tree'; 4 | 5 | export const AddWebsiteBtn: React.FC = React.memo(() => { 6 | const { addTreeNode } = useTreeStore(); 7 | 8 | return ( 9 | 17 | ); 18 | }); 19 | AddWebsiteBtn.displayName = 'AddWebsiteBtn'; 20 | -------------------------------------------------------------------------------- /src/renderer/components/ClearWebsiteBtn.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@arco-design/web-react'; 2 | import { IconDelete } from '@arco-design/web-react/icon'; 3 | import React from 'react'; 4 | import { useTreeStore } from '../store/tree'; 5 | 6 | export const ClearWebsiteBtn: React.FC = React.memo(() => { 7 | const { setSelectedNode } = useTreeStore(); 8 | 9 | return ( 10 |