├── .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 │ └── test.yml ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── assets ├── assets.d.ts ├── autohotkey │ ├── AutoHotkey32.exe │ ├── JXON.ahk │ └── center-window-resize.ahk ├── entitlements.mac.plist ├── icon.icns ├── icon.ico ├── icon.png ├── icon.svg ├── icons-copy │ ├── dark │ │ ├── logo.png │ │ ├── open.png │ │ └── quit.png │ ├── light │ │ ├── logo.png │ │ ├── open.png │ │ └── quit.png │ └── logo.png └── icons │ ├── 1024x1024.png │ ├── 128x128.png │ ├── 16x16.png │ ├── 24x24.png │ ├── 256x256.png │ ├── 32x32.png │ ├── 48x48.png │ ├── 512x512.png │ ├── 64x64.png │ └── 96x96.png ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── release └── app │ ├── package-lock.json │ ├── package.json │ └── pnpm-lock.yaml ├── src ├── __mock__ │ └── electron.js ├── __tests__ │ └── App.test.tsx ├── constants │ └── defaultSettings.json ├── main │ ├── autohotkey.ts │ ├── main.ts │ ├── preload.ts │ ├── settings.ts │ ├── singleInstance.ts │ ├── tray.ts │ ├── util.ts │ └── window.ts └── renderer │ ├── App.css │ ├── App.tsx │ ├── components │ ├── About │ │ ├── About.css │ │ └── About.tsx │ ├── AppContainer │ │ ├── AppContainer.css │ │ └── AppContainer.tsx │ ├── Footer │ │ ├── Footer.css │ │ └── Footer.tsx │ └── tabs │ │ ├── TabsList │ │ ├── TabsList.css │ │ └── TabsList.tsx │ │ ├── TabsView │ │ ├── TabsView.css │ │ └── TabsView.tsx │ │ └── content │ │ ├── CenterTabContent │ │ ├── CenterTabContent.css │ │ └── CenterTabContent.tsx │ │ └── ResizeTabContent │ │ ├── ResizeTabContent.css │ │ └── ResizeTabContent.tsx │ ├── hooks │ └── useKeybindHandler.ts │ ├── index.ejs │ ├── index.tsx │ ├── preload.d.ts │ ├── providers │ ├── SettingsProvider.tsx │ └── TabsProvider.tsx │ └── utils.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: [new TsconfigPathsPlugins()], 50 | }, 51 | 52 | plugins: [ 53 | new webpack.EnvironmentPlugin({ 54 | NODE_ENV: 'production', 55 | }), 56 | ], 57 | }; 58 | 59 | export default configuration; 60 | -------------------------------------------------------------------------------- /.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: [new TerserPlugin(), new CssMinimizerPlugin()], 97 | }, 98 | 99 | plugins: [ 100 | /** 101 | * Create global constants which can be configured at compile time. 102 | * 103 | * Useful for allowing different behaviour between development builds and 104 | * release builds 105 | * 106 | * NODE_ENV should be production so that modules do not perform certain 107 | * development checks 108 | */ 109 | new webpack.EnvironmentPlugin({ 110 | NODE_ENV: 'production', 111 | DEBUG_PROD: false, 112 | }), 113 | 114 | new MiniCssExtractPlugin({ 115 | filename: 'style.css', 116 | }), 117 | 118 | new BundleAnalyzerPlugin({ 119 | analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled', 120 | analyzerPort: 8889, 121 | }), 122 | 123 | new HtmlWebpackPlugin({ 124 | filename: 'index.html', 125 | template: path.join(webpackPaths.srcRendererPath, 'index.ejs'), 126 | minify: { 127 | collapseWhitespace: true, 128 | removeAttributeQuotes: true, 129 | removeComments: true, 130 | }, 131 | isBrowser: false, 132 | isDevelopment: false, 133 | }), 134 | 135 | new webpack.DefinePlugin({ 136 | 'process.type': '"renderer"', 137 | }), 138 | ], 139 | }; 140 | 141 | export default merge(baseConfig, configuration); 142 | -------------------------------------------------------------------------------- /.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/Devail1/window-center-resize/fdef39c1d87c839b0a53085cfd567a1ffa692191/.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 { rimrafSync } 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)) rimrafSync(folder); 13 | }); 14 | -------------------------------------------------------------------------------- /.erb/scripts/delete-source-maps.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { rimrafSync } from 'rimraf'; 4 | import webpackPaths from '../configs/webpack.paths'; 5 | 6 | export default function deleteSourceMaps() { 7 | if (fs.existsSync(webpackPaths.distMainPath)) 8 | rimrafSync(path.join(webpackPaths.distMainPath, '*.js.map'), { 9 | glob: true, 10 | }); 11 | if (fs.existsSync(webpackPaths.distRendererPath)) 12 | rimrafSync(path.join(webpackPaths.distRendererPath, '*.js.map'), { 13 | glob: true, 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /.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 ( 16 | !('APPLE_ID' in process.env && 'APPLE_APP_SPECIFIC_PASSWORD' in process.env) 17 | ) { 18 | console.warn( 19 | 'Skipping notarizing step. APPLE_ID and APPLE_APP_SPECIFIC_PASSWORD env variables must be set', 20 | ); 21 | return; 22 | } 23 | 24 | const appName = context.packager.appInfo.productFilename; 25 | 26 | await notarize({ 27 | appBundleId: build.appId, 28 | appPath: `${appOutDir}/${appName}.app`, 29 | appleId: process.env.APPLE_ID, 30 | appleIdPassword: process.env.APPLE_APP_SPECIFIC_PASSWORD, 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /.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 | plugins: ['@typescript-eslint'], 4 | rules: { 5 | // A temporary hack related to IDE not resolving correct package.json 6 | 'import/no-extraneous-dependencies': 'off', 7 | 'react/react-in-jsx-scope': 'off', 8 | 'react/jsx-filename-extension': 'off', 9 | 'import/extensions': 'off', 10 | 'import/no-unresolved': 'off', 11 | 'import/no-import-module-exports': 'off', 12 | 'no-shadow': 'off', 13 | '@typescript-eslint/no-shadow': 'error', 14 | 'no-unused-vars': 'off', 15 | '@typescript-eslint/no-unused-vars': 'error', 16 | }, 17 | parserOptions: { 18 | ecmaVersion: 2022, 19 | sourceType: 'module', 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/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: [windows-latest] 12 | 13 | env: 14 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 15 | 16 | steps: 17 | - name: Check out Git repository 18 | uses: actions/checkout@v3 19 | 20 | - name: Install Node.js and NPM 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: 18 24 | cache: npm 25 | 26 | - name: Install dependencies 27 | run: | 28 | npm install 29 | 30 | - name: Run Prettier 31 | run: | 32 | npx prettier --write . 33 | 34 | - name: Lint code 35 | run: | 36 | npm run lint 37 | 38 | - name: Compile TypeScript 39 | run: | 40 | npm exec tsc 41 | 42 | - name: Package application 43 | run: | 44 | npm run package 45 | 46 | - name: Run tests 47 | run: | 48 | npm test 49 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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.1.0 2 | 3 | - Migrate to `css-minifier-webpack-plugin` 4 | 5 | # 2.0.1 6 | 7 | ## Fixes 8 | 9 | - Fix broken css linking in production build 10 | 11 | # 2.0.0 12 | 13 | ## Breaking Changes 14 | 15 | - drop redux 16 | - remove counter example app 17 | - simplify directory structure 18 | - move `dll` dir to `.erb` dir 19 | - fix icon/font import paths 20 | - migrate to `react-refresh` from `react-hot-loader` 21 | - migrate to webpack@5 22 | - migrate to electron@11 23 | - remove e2e tests and testcafe integration 24 | - rename `app` dir to more conventional `src` dir 25 | - rename `resources` dir to `assets` 26 | - simplify npm scripts 27 | - drop stylelint 28 | - simplify styling of boilerplate app 29 | - remove `START_HOT` env variable 30 | - notarize support 31 | - landing page boilerplate 32 | - docs updates 33 | - restore removed debugging support 34 | 35 | # 1.4.0 36 | 37 | - Migrate to `eslint-config-erb@2` 38 | - Rename `dev` npm script to `start` 39 | - GitHub Actions: only publish GitHub releases when on master branch 40 | 41 | # 1.3.1 42 | 43 | - Fix sass building bug ([#2540](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/2540)) 44 | - Fix CI bug related to E2E tests and network timeouts 45 | - Move automated dependency PRs to `next` ([#2554](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/2554)) 46 | - Bump dependencies to patch semver 47 | 48 | # 1.3.0 49 | 50 | - Fixes E2E tests ([#2516](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/2516)) 51 | - Fixes preload entrypoint ([#2503](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/2503)) 52 | - Downgrade to `electron@8` 53 | - Bump dependencies to latest semver 54 | 55 | # 1.2.0 56 | 57 | - Migrate to redux toolkit 58 | - Lazy load routes with react suspense 59 | - Drop support for azure-pipelines and use only github actions 60 | - Bump all deps to latest semver 61 | - Remove `test-e2e` script from tests (blocked on release of https://github.com/DevExpress/testcafe-browser-provider-electron/pull/65) 62 | - Swap `typed-css-modules-webpack-plugin` for `typings-for-css-modules-loader` 63 | - Use latest version of `eslint-config-erb` 64 | - Remove unnecessary file extensions from ts exclude 65 | - Add experimental support for vscode debugging 66 | - Revert https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/2365 as default for users, provide as opt in option 67 | 68 | # 1.1.0 69 | 70 | - Fix #2402 71 | - Simplify configs (https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/2406) 72 | 73 | # 1.0.0 74 | 75 | - Migrate to TypeScript from Flow ([#2363](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/2363)) 76 | - Use browserslist for `@babel/preset-env` targets ([#2368](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/2368)) 77 | - Use preload script, disable `nodeIntegration` in renderer process for [improved security](https://www.electronjs.org/docs/tutorial/security#2-do-not-enable-nodejs-integration-for-remote-content) ([#2365](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/2365)) 78 | - Add support for azure pipelines ([#2369](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/2369)) 79 | - Disable sourcemaps in production 80 | 81 | # 0.18.1 (2019.12.12) 82 | 83 | - Fix HMR env bug ([#2343](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/2343)) 84 | - Bump all deps to latest semver 85 | - Bump to `electron@7` 86 | 87 | # 0.18.0 (2019.11.19) 88 | 89 | - Bump electron to `electron@6` (`electron@7` introduces breaking changes to testcafe end to end tests) 90 | - Revert back to [two `package.json` structure](https://www.electron.build/tutorials/two-package-structure) 91 | - Bump all deps to latest semver 92 | 93 | # 0.17.1 (2018.11.20) 94 | 95 | - Fix `yarn test-e2e` and testcafe for single package.json structure 96 | - Fixes incorrect path in `yarn start` script 97 | - Bumped deps 98 | - Bump g++ in travis 99 | - Change clone arguments to clone only master 100 | - Change babel config to target current electron version 101 | 102 | For full change list, see https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/2021 103 | 104 | # 0.17.0 (2018.10.30) 105 | 106 | - upgraded to `babel@7` (thanks to @vikr01 🎉🎉🎉) 107 | - migrated from [two `package.json` structure](https://www.electron.build/tutorials/two-package-structure) (thanks to @HyperSprite!) 108 | - initial auto update support (experimental) 109 | - migrate from greenkeeper to [renovate](https://renovatebot.com) 110 | - added issue template 111 | - use `babel-preset-env` to target current electron version 112 | - add [opencollective](https://opencollective.com/electron-react-boilerplate-594) banner message display in postinstall script (help support ERB 🙏) 113 | - fix failing ci issues 114 | 115 | # 0.16.0 (2018.10.3) 116 | 117 | - removed unused dependencies 118 | - migrate from `react-redux-router` to `connect-react-router` 119 | - move webpack configs to `./webpack` dir 120 | - use `g++` on travis when testing linux 121 | - migrate from `spectron` to `testcafe` for e2e tests 122 | - add linting support for config styles 123 | - changed stylelint config 124 | - temporarily disabled flow in appveyor to make ci pass 125 | - added necessary infra to publish releases from ci 126 | 127 | # 0.15.0 (2018.8.25) 128 | 129 | - Performance: cache webpack uglify results 130 | - Feature: add start minimized feature 131 | - Feature: lint and fix styles with prettier and stylelint 132 | - Feature: add greenkeeper support 133 | 134 | # 0.14.0 (2018.5.24) 135 | 136 | - Improved CI timings 137 | - Migrated README commands to yarn from npm 138 | - Improved vscode config 139 | - Updated all dependencies to latest semver 140 | - Fix `electron-rebuild` script bug 141 | - Migrated to `mini-css-extract-plugin` from `extract-text-plugin` 142 | - Added `optimize-css-assets-webpack-plugin` 143 | - Run `prettier` on json, css, scss, and more filetypes 144 | 145 | # 0.13.3 (2018.5.24) 146 | 147 | - Add git precommit hook, when git commit will use `prettier` to format git add code 148 | - Add format code function in `lint-fix` npm script which can use `prettier` to format project js code 149 | 150 | # 0.13.2 (2018.1.31) 151 | 152 | - Hot Module Reload (HMR) fixes 153 | - Bumped all dependencies to latest semver 154 | - Prevent error propagation of `CheckNativeDeps` script 155 | 156 | # 0.13.1 (2018.1.13) 157 | 158 | - Hot Module Reload (HMR) fixes 159 | - Bumped all dependencies to latest semver 160 | - Fixed electron-rebuild script 161 | - Fixed tests scripts to run on all platforms 162 | - Skip redux logs in console in test ENV 163 | 164 | # 0.13.0 (2018.1.6) 165 | 166 | #### Additions 167 | 168 | - Add native dependencies check on postinstall 169 | - Updated all dependencies to latest semver 170 | 171 | # 0.12.0 (2017.7.8) 172 | 173 | #### Misc 174 | 175 | - Removed `babel-polyfill` 176 | - Renamed and alphabetized npm scripts 177 | 178 | #### Breaking 179 | 180 | - Changed node dev `__dirname` and `__filename` to node built in fn's (https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/1035) 181 | - Renamed `src/bundle.js` to `src/renderer.prod.js` for consistency 182 | - Renamed `dll/vendor.js` to `dll/renderer.dev.dll.js` for consistency 183 | 184 | #### Additions 185 | 186 | - Enable node_modules cache on CI 187 | 188 | # 0.11.2 (2017.5.1) 189 | 190 | Yay! Another patch release. This release mostly includes refactorings and router bug fixes. Huge thanks to @anthonyraymond! 191 | 192 | ⚠️ Windows electron builds are failing because of [this issue](https://github.com/electron/electron/issues/9321). This is not an issue with the boilerplate ⚠️ 193 | 194 | #### Breaking 195 | 196 | - **Renamed `./src/main.development.js` => `./src/main.{dev,prod}.js`:** [#963](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/963) 197 | 198 | #### Fixes 199 | 200 | - **Fixed reloading when not on `/` path:** [#958](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/958) [#949](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/949) 201 | 202 | #### Additions 203 | 204 | - **Added support for stylefmt:** [#960](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/960) 205 | 206 | # 0.11.1 (2017.4.23) 207 | 208 | You can now debug the production build with devtools like so: 209 | 210 | ``` 211 | DEBUG_PROD=true npm run package 212 | ``` 213 | 214 | 🎉🎉🎉 215 | 216 | #### Additions 217 | 218 | - **Added support for debugging production build:** [#fab245a](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/941/commits/fab245a077d02a09630f74270806c0c534a4ff95) 219 | 220 | #### Bug Fixes 221 | 222 | - **Fixed bug related to importing native dependencies:** [#933](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/933) 223 | 224 | #### Improvements 225 | 226 | - **Updated all deps to latest semver** 227 | 228 | # 0.11.0 (2017.4.19) 229 | 230 | Here's the most notable changes since `v0.10.0`. Its been about a year since a release has been pushed. Expect a new release to be published every 3-4 weeks. 231 | 232 | #### Breaking Changes 233 | 234 | - **Dropped support for node < 6** 235 | - **Refactored webpack config files** 236 | - **Migrate to two-package.json project structure** 237 | - **Updated all devDeps to latest semver** 238 | - **Migrated to Jest:** [#768](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/768) 239 | - **Migrated to `react-router@4`** 240 | - **Migrated to `electron-builder@4`** 241 | - **Migrated to `webpack@2`** 242 | - **Migrated to `react-hot-loader@3`** 243 | - **Changed default live reload server PORT to `1212` from `3000`** 244 | 245 | #### Additions 246 | 247 | - **Added support for Yarn:** [#451](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/451) 248 | - **Added support for Flow:** [#425](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/425) 249 | - **Added support for stylelint:** [#911](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/911) 250 | - **Added support for electron-builder:** [#876](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/876) 251 | - **Added optional support for SASS:** [#880](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/880) 252 | - **Added support for eslint-plugin-flowtype:** [#911](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/911) 253 | - **Added support for appveyor:** [#280](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/280) 254 | - **Added support for webpack dlls:** [#860](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/860) 255 | - **Route based code splitting:** [#884](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/884) 256 | - **Added support for Webpack Bundle Analyzer:** [#922](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/922) 257 | 258 | #### Improvements 259 | 260 | - **Parallelize renderer and main build processes when running `npm run build`** 261 | - **Dynamically generate electron app menu** 262 | - **Improved vscode integration:** [#856](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/856) 263 | 264 | #### Bug Fixes 265 | 266 | - **Fixed hot module replacement race condition bug:** [#917](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/917) [#920](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/920) 267 | 268 | # 0.10.0 (2016.4.18) 269 | 270 | #### Improvements 271 | 272 | - **Use Babel in main process with Webpack build:** [#201](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/201) 273 | - **Change targets to built-in support by webpack:** [#197](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/197) 274 | - **use es2015 syntax for webpack configs:** [#195](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/195) 275 | - **Open application when webcontent is loaded:** [#192](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/192) 276 | - **Upgraded dependencies** 277 | 278 | #### Bug fixed 279 | 280 | - **Fix `npm list electron-prebuilt` in package.js:** [#188](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/188) 281 | 282 | # 0.9.0 (2016.3.23) 283 | 284 | #### Improvements 285 | 286 | - **Added [redux-logger](https://github.com/fcomb/redux-logger)** 287 | - **Upgraded [react-router-redux](https://github.com/reactjs/react-router-redux) to v4** 288 | - **Upgraded dependencies** 289 | - **Added `npm run dev` command:** [#162](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/162) 290 | - **electron to v0.37.2** 291 | 292 | #### Breaking Changes 293 | 294 | - **css module as default:** [#154](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/154). 295 | - **set default NODE_ENV to production:** [#140](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/140) 296 | 297 | # 0.8.0 (2016.2.17) 298 | 299 | #### Bug fixed 300 | 301 | - **Fix lint errors** 302 | - **Fix Webpack publicPath for production builds**: [#119](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/119). 303 | - **package script now chooses correct OS icon extension** 304 | 305 | #### Improvements 306 | 307 | - **babel 6** 308 | - **Upgrade Dependencies** 309 | - **Enable CSS source maps** 310 | - **Add json-loader**: [#128](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/128). 311 | - **react-router 2.0 and react-router-redux 3.0** 312 | 313 | # 0.7.1 (2015.12.27) 314 | 315 | #### Bug fixed 316 | 317 | - **Fixed npm script on windows 10:** [#103](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/103). 318 | - **history and react-router version bump**: [#109](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/109), [#110](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/110). 319 | 320 | #### Improvements 321 | 322 | - **electron 0.36** 323 | 324 | # 0.7.0 (2015.12.16) 325 | 326 | #### Bug fixed 327 | 328 | - **Fixed process.env.NODE_ENV variable in webpack:** [#74](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/74). 329 | - **add missing object-assign**: [#76](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/76). 330 | - **packaging in npm@3:** [#77](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/77). 331 | - **compatibility in windows:** [#100](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/100). 332 | - **disable chrome debugger in production env:** [#102](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/102). 333 | 334 | #### Improvements 335 | 336 | - **redux** 337 | - **css-modules** 338 | - **upgrade to react-router 1.x** 339 | - **unit tests** 340 | - **e2e tests** 341 | - **travis-ci** 342 | - **upgrade to electron 0.35.x** 343 | - **use es2015** 344 | - **check dev engine for node and npm** 345 | 346 | # 0.6.5 (2015.11.7) 347 | 348 | #### Improvements 349 | 350 | - **Bump style-loader to 0.13** 351 | - **Bump css-loader to 0.22** 352 | 353 | # 0.6.4 (2015.10.27) 354 | 355 | #### Improvements 356 | 357 | - **Bump electron-debug to 0.3** 358 | 359 | # 0.6.3 (2015.10.26) 360 | 361 | #### Improvements 362 | 363 | - **Initialize ExtractTextPlugin once:** [#64](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/64). 364 | 365 | # 0.6.2 (2015.10.18) 366 | 367 | #### Bug fixed 368 | 369 | - **Babel plugins production env not be set properly:** [#57](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/57). 370 | 371 | # 0.6.1 (2015.10.17) 372 | 373 | #### Improvements 374 | 375 | - **Bump electron to v0.34.0** 376 | 377 | # 0.6.0 (2015.10.16) 378 | 379 | #### Breaking Changes 380 | 381 | - **From react-hot-loader to react-transform** 382 | 383 | # 0.5.2 (2015.10.15) 384 | 385 | #### Improvements 386 | 387 | - **Run tests with babel-register:** [#29](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/29). 388 | 389 | # 0.5.1 (2015.10.12) 390 | 391 | #### Bug fixed 392 | 393 | - **Fix #51:** use `path.join(__dirname` instead of `./`. 394 | 395 | # 0.5.0 (2015.10.11) 396 | 397 | #### Improvements 398 | 399 | - **Simplify webpack config** see [#50](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/50). 400 | 401 | #### Breaking Changes 402 | 403 | - **webpack configs** 404 | - **port changed:** changed default port from 2992 to 3000. 405 | - **npm scripts:** remove `start-dev` and `dev-server`. rename `hot-dev-server` to `hot-server`. 406 | 407 | # 0.4.3 (2015.9.22) 408 | 409 | #### Bug fixed 410 | 411 | - **Fix #45 zeromq crash:** bump version of `electron-prebuilt`. 412 | 413 | # 0.4.2 (2015.9.15) 414 | 415 | #### Bug fixed 416 | 417 | - **run start-hot breaks chrome refresh(CTRL+R) (#42)**: bump `electron-debug` to `0.2.1` 418 | 419 | # 0.4.1 (2015.9.11) 420 | 421 | #### Improvements 422 | 423 | - **use electron-prebuilt version for packaging (#33)** 424 | 425 | # 0.4.0 (2015.9.5) 426 | 427 | #### Improvements 428 | 429 | - **update dependencies** 430 | 431 | # 0.3.0 (2015.8.31) 432 | 433 | #### Improvements 434 | 435 | - **eslint-config-airbnb** 436 | 437 | # 0.2.10 (2015.8.27) 438 | 439 | #### Features 440 | 441 | - **custom placeholder icon** 442 | 443 | #### Improvements 444 | 445 | - **electron-renderer as target:** via [webpack-target-electron-renderer](https://github.com/chentsulin/webpack-target-electron-renderer) 446 | 447 | # 0.2.9 (2015.8.18) 448 | 449 | #### Bug fixed 450 | 451 | - **Fix hot-reload** 452 | 453 | # 0.2.8 (2015.8.13) 454 | 455 | #### Improvements 456 | 457 | - **bump electron-debug** 458 | - **babelrc** 459 | - **organize webpack scripts** 460 | 461 | # 0.2.7 (2015.7.9) 462 | 463 | #### Bug fixed 464 | 465 | - **defaultProps:** fix typos. 466 | 467 | # 0.2.6 (2015.7.3) 468 | 469 | #### Features 470 | 471 | - **menu** 472 | 473 | #### Bug fixed 474 | 475 | - **package.js:** include webpack build. 476 | 477 | # 0.2.5 (2015.7.1) 478 | 479 | #### Features 480 | 481 | - **NPM Script:** support multi-platform 482 | - **package:** `--all` option 483 | 484 | # 0.2.4 (2015.6.9) 485 | 486 | #### Bug fixed 487 | 488 | - **Eslint:** typo, [#17](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/17) and improve `.eslintrc` 489 | 490 | # 0.2.3 (2015.6.3) 491 | 492 | #### Features 493 | 494 | - **Package Version:** use latest release electron version as default 495 | - **Ignore Large peerDependencies** 496 | 497 | #### Bug fixed 498 | 499 | - **Npm Script:** typo, [#6](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/6) 500 | - **Missing css:** [#7](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/7) 501 | 502 | # 0.2.2 (2015.6.2) 503 | 504 | #### Features 505 | 506 | - **electron-debug** 507 | 508 | #### Bug fixed 509 | 510 | - **Webpack:** add `.json` and `.node` to extensions for imitating node require. 511 | - **Webpack:** set `node_modules` to externals for native module support. 512 | 513 | # 0.2.1 (2015.5.30) 514 | 515 | #### Bug fixed 516 | 517 | - **Webpack:** #1, change build target to `atom`. 518 | 519 | # 0.2.0 (2015.5.30) 520 | 521 | #### Features 522 | 523 | - **Ignore:** `test`, `tools`, `release` folder and devDependencies in `package.json`. 524 | - **Support asar** 525 | - **Support icon** 526 | 527 | # 0.1.0 (2015.5.27) 528 | 529 | #### Features 530 | 531 | - **Webpack:** babel, react-hot, ... 532 | - **Flux:** actions, api, components, containers, stores.. 533 | - **Package:** darwin (osx), linux and win32 (windows) platform. 534 | -------------------------------------------------------------------------------- /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 |
2 | Logo 3 |

Window Center & Resizer

4 | 5 |
6 | License 7 | GitHub release 8 | Download Count 9 | 10 | Known Vulnerabilities 11 | 12 |

The Open-Source Utility for Centering and Resizing Windows

13 |
14 | 15 |
16 | 17 | Center Window 18 | Resize Window 19 | 20 | ## Features 21 | Window Center & Resizer is a utility application for Windows that allows you to easily center and resize windows on your desktop using customizable keyboard shortcuts. This project is built upon the [Electron React Boilerplate](https://github.com/electron-react-boilerplate/electron-react-boilerplate), which provides a solid foundation for Electron and React development. 22 | 23 | - **Center Window**: Quickly center the active window on your screen. 24 | - **Resize Window**: Resize the active window to predefined sizes (small, medium, large) with customizable keyboard shortcuts. 25 | - **Customizable Keybinds**: Easily configure your preferred key combinations for centering and resizing windows. 26 | 27 | ## Installation 28 | 29 | To use Window Center & Resizer, [download](https://github.com/devail1/window-center-resize/releases/latest/download/Window-Center-Resize.exe) the latest release from the [GitHub repository](https://github.com/devail1/window-center-resize) and run the executable file. 30 | 31 | ## Usage 32 | 33 | 1. **Center Window**: Press the specified key combination to center the active window. 34 | 2. **Resize Window**: Press the specified key combination to resize the active window to small, medium, or large sizes. 35 | 3. **Customization**: Edit the `settings.json` file to customize keybinds and window sizes. 36 | 37 | ## Inspiration 38 | 39 | This project is inspired by the window centering helper freeware by [Kamil Szymborski](https://kamilszymborski.github.io/). Window Center & Resizer offers a modern approach to window management with additional features and extensive customization capabilities. 40 | 41 | ## Feature Roadmap 42 | - ~**Feature Enable/Disable**: Add the ability to enable or disable every feature in the app, such as centering and resizing.~ 43 | - **Label Sizes**: Provide an option to rename, add, or remove sizes for more flexibility in window resizing. 44 | - **Improved Settings UI**: Enhance the user interface for settings to make it more intuitive and user-friendly. 45 | 46 | ## Contributing 47 | 48 | Contributions are welcome! If you have any suggestions, bug reports, or feature requests, please open an issue on the GitHub repository or submit a pull request. 49 | 50 | ## License 51 | 52 | This project is licensed under the MIT License - see the [LICENSE](LICENSE.md) file for details. 53 | -------------------------------------------------------------------------------- /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/autohotkey/AutoHotkey32.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devail1/window-center-resize/fdef39c1d87c839b0a53085cfd567a1ffa692191/assets/autohotkey/AutoHotkey32.exe -------------------------------------------------------------------------------- /assets/autohotkey/JXON.ahk: -------------------------------------------------------------------------------- 1 | ;;;; AHK v2 2 | ; Example =================================================================================== 3 | ; =========================================================================================== 4 | 5 | ; Msgbox "The idea here is to create several nested arrays, save to text with jxon_dump(), and then reload the array with jxon_load(). The resulting array should be the same.`r`n`r`nThis is what this example shows." 6 | ; a := Map(), b := Map(), c := Map(), d := Map(), e := Map(), f := Map() ; Object() is more technically correct than {} but both will work. 7 | 8 | ; d["g"] := 1, d["h"] := 2, d["i"] := ["purple","pink","pippy red"] 9 | ; e["g"] := 1, e["h"] := 2, e["i"] := Map("1","test1","2","test2","3","test3") 10 | ; f["g"] := 1, f["h"] := 2, f["i"] := [1,2,Map("a",1.0009,"b",2.0003,"c",3.0001)] 11 | 12 | ; a["test1"] := "test11", a["d"] := d 13 | ; b["test3"] := "test33", b["e"] := e 14 | ; c["test5"] := "test55", c["f"] := f 15 | 16 | ; myObj := Map() 17 | ; myObj["a"] := a, myObj["b"] := b, myObj["c"] := c, myObj["test7"] := "test77", myObj["test8"] := "test88" 18 | 19 | ; g := ["blue","green","red"], myObj["h"] := g ; add linear array for testing 20 | 21 | ; q := Chr(34) 22 | ; textData2 := Jxon_dump(myObj,4) ; ===> convert array to JSON 23 | ; msgbox "JSON output text:`r`n===========================================`r`n(Should match second output.)`r`n`r`n" textData2 24 | 25 | ; newObj := Jxon_load(&textData2) ; ===> convert json back to array 26 | 27 | ; textData3 := Jxon_dump(newObj,4) ; ===> break down array into 2D layout again, should be identical 28 | ; msgbox "Second output text:`r`n===========================================`r`n(should be identical to first output)`r`n`r`n" textData3 29 | 30 | ; msgbox "textData2 = textData3: " ((textData2=textData3) ? "true" : "false") 31 | 32 | ; =========================================================================================== 33 | ; End Example ; ============================================================================= 34 | ; =========================================================================================== 35 | 36 | ; originally posted by user coco on AutoHotkey.com 37 | ; https://github.com/cocobelgica/AutoHotkey-JSON 38 | 39 | Jxon_Load(&src, args*) { 40 | key := "", is_key := false 41 | stack := [ tree := [] ] 42 | next := '"{[01234567890-tfn' 43 | pos := 0 44 | 45 | while ( (ch := SubStr(src, ++pos, 1)) != "" ) { 46 | if InStr(" `t`n`r", ch) 47 | continue 48 | if !InStr(next, ch, true) { 49 | testArr := StrSplit(SubStr(src, 1, pos), "`n") 50 | 51 | ln := testArr.Length 52 | col := pos - InStr(src, "`n",, -(StrLen(src)-pos+1)) 53 | 54 | msg := Format("{}: line {} col {} (char {})" 55 | , (next == "") ? ["Extra data", ch := SubStr(src, pos)][1] 56 | : (next == "'") ? "Unterminated string starting at" 57 | : (next == "\") ? "Invalid \escape" 58 | : (next == ":") ? "Expecting ':' delimiter" 59 | : (next == '"') ? "Expecting object key enclosed in double quotes" 60 | : (next == '"}') ? "Expecting object key enclosed in double quotes or object closing '}'" 61 | : (next == ",}") ? "Expecting ',' delimiter or object closing '}'" 62 | : (next == ",]") ? "Expecting ',' delimiter or array closing ']'" 63 | : [ "Expecting JSON value(string, number, [true, false, null], object or array)" 64 | , ch := SubStr(src, pos, (SubStr(src, pos)~="[\]\},\s]|$")-1) ][1] 65 | , ln, col, pos) 66 | 67 | throw Error(msg, -1, ch) 68 | } 69 | 70 | obj := stack[1] 71 | is_array := (obj is Array) 72 | 73 | if i := InStr("{[", ch) { ; start new object / map? 74 | val := (i = 1) ? Map() : Array() ; ahk v2 75 | 76 | is_array ? obj.Push(val) : obj[key] := val 77 | stack.InsertAt(1,val) 78 | 79 | next := '"' ((is_key := (ch == "{")) ? "}" : "{[]0123456789-tfn") 80 | } else if InStr("}]", ch) { 81 | stack.RemoveAt(1) 82 | next := (stack[1]==tree) ? "" : (stack[1] is Array) ? ",]" : ",}" 83 | } else if InStr(",:", ch) { 84 | is_key := (!is_array && ch == ",") 85 | next := is_key ? '"' : '"{[0123456789-tfn' 86 | } else { ; string | number | true | false | null 87 | if (ch == '"') { ; string 88 | i := pos 89 | while i := InStr(src, '"',, i+1) { 90 | val := StrReplace(SubStr(src, pos+1, i-pos-1), "\\", "\u005C") 91 | if (SubStr(val, -1) != "\") 92 | break 93 | } 94 | if !i ? (pos--, next := "'") : 0 95 | continue 96 | 97 | pos := i ; update pos 98 | 99 | val := StrReplace(val, "\/", "/") 100 | val := StrReplace(val, '\"', '"') 101 | , val := StrReplace(val, "\b", "`b") 102 | , val := StrReplace(val, "\f", "`f") 103 | , val := StrReplace(val, "\n", "`n") 104 | , val := StrReplace(val, "\r", "`r") 105 | , val := StrReplace(val, "\t", "`t") 106 | 107 | i := 0 108 | while i := InStr(val, "\",, i+1) { 109 | if (SubStr(val, i+1, 1) != "u") ? (pos -= StrLen(SubStr(val, i)), next := "\") : 0 110 | continue 2 111 | 112 | xxxx := Abs("0x" . SubStr(val, i+2, 4)) ; \uXXXX - JSON unicode escape sequence 113 | if (xxxx < 0x100) 114 | val := SubStr(val, 1, i-1) . Chr(xxxx) . SubStr(val, i+6) 115 | } 116 | 117 | if is_key { 118 | key := val, next := ":" 119 | continue 120 | } 121 | } else { ; number | true | false | null 122 | val := SubStr(src, pos, i := RegExMatch(src, "[\]\},\s]|$",, pos)-pos) 123 | 124 | if IsInteger(val) 125 | val += 0 126 | else if IsFloat(val) 127 | val += 0 128 | else if (val == "true" || val == "false") 129 | val := (val == "true") 130 | else if (val == "null") 131 | val := "" 132 | else if is_key { 133 | pos--, next := "#" 134 | continue 135 | } 136 | 137 | pos += i-1 138 | } 139 | 140 | is_array ? obj.Push(val) : obj[key] := val 141 | next := obj == tree ? "" : is_array ? ",]" : ",}" 142 | } 143 | } 144 | 145 | return tree[1] 146 | } 147 | 148 | Jxon_Dump(obj, indent:="", lvl:=1) { 149 | if IsObject(obj) { 150 | If !(obj is Array || obj is Map || obj is String || obj is Number) 151 | throw Error("Object type not supported.", -1, Format("", ObjPtr(obj))) 152 | 153 | if IsInteger(indent) 154 | { 155 | if (indent < 0) 156 | throw Error("Indent parameter must be a postive integer.", -1, indent) 157 | spaces := indent, indent := "" 158 | 159 | Loop spaces ; ===> changed 160 | indent .= " " 161 | } 162 | indt := "" 163 | 164 | Loop indent ? lvl : 0 165 | indt .= indent 166 | 167 | is_array := (obj is Array) 168 | 169 | lvl += 1, out := "" ; Make #Warn happy 170 | for k, v in obj { 171 | if IsObject(k) || (k == "") 172 | throw Error("Invalid object key.", -1, k ? Format("", ObjPtr(obj)) : "") 173 | 174 | if !is_array ;// key ; ObjGetCapacity([k], 1) 175 | out .= (ObjGetCapacity([k]) ? Jxon_Dump(k) : escape_str(k)) (indent ? ": " : ":") ; token + padding 176 | 177 | out .= Jxon_Dump(v, indent, lvl) ; value 178 | . ( indent ? ",`n" . indt : "," ) ; token + indent 179 | } 180 | 181 | if (out != "") { 182 | out := Trim(out, ",`n" . indent) 183 | if (indent != "") 184 | out := "`n" . indt . out . "`n" . SubStr(indt, StrLen(indent)+1) 185 | } 186 | 187 | return is_array ? "[" . out . "]" : "{" . out . "}" 188 | 189 | } Else If (obj is Number) 190 | return obj 191 | 192 | Else ; String 193 | return escape_str(obj) 194 | 195 | escape_str(obj) { 196 | obj := StrReplace(obj,"\","\\") 197 | obj := StrReplace(obj,"`t","\t") 198 | obj := StrReplace(obj,"`r","\r") 199 | obj := StrReplace(obj,"`n","\n") 200 | obj := StrReplace(obj,"`b","\b") 201 | obj := StrReplace(obj,"`f","\f") 202 | obj := StrReplace(obj,"/","\/") 203 | obj := StrReplace(obj,'"','\"') 204 | 205 | return '"' obj '"' 206 | } 207 | } 208 | 209 | -------------------------------------------------------------------------------- /assets/autohotkey/center-window-resize.ahk: -------------------------------------------------------------------------------- 1 | #SingleInstance 2 | #NoTrayIcon 3 | #Include "%A_ScriptDir%\JXON.ahk" ; Include the JSON library 4 | 5 | Join(sep, params*) { 6 | str := '' 7 | for index, param in params 8 | str .= sep . param 9 | return SubStr(str, StrLen(sep) + 1) 10 | } 11 | 12 | hotkeysJsToHotHotkeys := Map("shift", "+", "ctrl", "^", "alt", "!", "meta", "#") 13 | 14 | ; Function to convert hotkeys-js keys to HotHotkeys syntax 15 | convertHotkeysJsToHotHotkeys(key) { 16 | keys := StrSplit(key, "+") ; Split key by "+" 17 | 18 | hotkeys := [] 19 | for each, k in keys { 20 | k := StrLower(Trim(k)) 21 | if (hotkeysJsToHotHotkeys.Has(k)) 22 | hotkeys.push(hotkeysJsToHotHotkeys.Get(k)) 23 | else 24 | hotkeys.push(StrUpper(k)) 25 | } 26 | return Join("", hotkeys*) 27 | } 28 | 29 | jsonFilePath := A_AppData . "\window-center-resize\settings.json" 30 | 31 | ; Read JSON file 32 | jsonContent := Fileread(jsonFilePath) 33 | 34 | ; Parse JSON content 35 | json := jxon_load(&jsonContent) 36 | 37 | centerWindowObj := json["centerWindow"] 38 | resizeWindowObj := json["resizeWindow"] 39 | global toggleSizes := resizeWindowObj["windowSizePercentages"] 40 | 41 | 42 | ; Extract values from JSON 43 | centerWindowKey := convertHotkeysJsToHotHotkeys(centerWindowObj["keybinding"]) 44 | resizeWindowKey := convertHotkeysJsToHotHotkeys(resizeWindowObj["keybinding"]) 45 | 46 | ; Define hotkeys if keybinding exists 47 | if (centerWindowKey != "") 48 | Hotkey(centerWindowKey, CenterWindow, "On") 49 | if (resizeWindowKey != "") 50 | Hotkey(resizeWindowKey, ResizeWindow, "On") 51 | 52 | 53 | CenterWindow(WinTitle) { 54 | hwnd := WinExist("A") ; Check if window exists 55 | 56 | if (hwnd) { 57 | mon := GetNearestMonitorInfo(hwnd) 58 | 59 | WinGetPos(&WinX, &WinY, &Width, &Height, hwnd) ; Get current window position and size 60 | 61 | NewX := mon.WALeft + (mon.WAWidth - Width) / 2 ; Calculate centered X position on current monitor 62 | NewY := mon.WATop + (mon.WAHeight - Height) / 2 ; Calculate centered Y position on current monitor 63 | 64 | WinMove(NewX, NewY, Width, Height, hwnd) ; Move window to the center 65 | } else { 66 | MsgBox("Window not found.") 67 | } 68 | } 69 | 70 | ResizeWindow(WinTitle) { 71 | static size := 1 72 | global toggleSizes 73 | 74 | size++ 75 | 76 | if (size > toggleSizes.Length) ; Reset size to 1 if it exceeds array length 77 | size := 1 78 | 79 | hwnd := WinExist("A") ; Get handle to active window 80 | 81 | if (hwnd) { 82 | CenterAndResizeWindow("A", toggleSizes[size]["width"], toggleSizes[size]["height"]) 83 | } else { 84 | MsgBox("Active window not found.") 85 | } 86 | } 87 | 88 | 89 | CenterAndResizeWindow(WinTitle, WidthPercentage, HeightPercentage) { 90 | hwnd := WinExist(WinTitle) ; Check if window exists 91 | 92 | if (hwnd) { 93 | ScreenWidth := A_ScreenWidth 94 | ScreenHeight := A_ScreenHeight 95 | mon := GetNearestMonitorInfo(hwnd) 96 | 97 | 98 | NewWidth := (ScreenWidth * WidthPercentage / 100) 99 | NewHeight := (ScreenHeight * HeightPercentage / 100) 100 | 101 | WinGetPos(&WinX, &WinY, &Width, &Height, hwnd) ; Get current window position and size 102 | 103 | NewX := mon.WALeft + (mon.WAWidth - NewWidth) / 2 ; Calculate centered X position on current monitor 104 | NewY := mon.WATop + (mon.WAHeight - NewHeight) / 2 ; Calculate centered Y position on current monitor 105 | 106 | WinMove(NewX, NewY, NewWidth, NewHeight, hwnd) ; Move and resize window 107 | } else { 108 | MsgBox("Window with title `"" WinTitle "`" not found.") 109 | } 110 | } 111 | 112 | 113 | GetNearestMonitorInfo(winTitle) { 114 | static MONITOR_DEFAULTTONEAREST := 0x00000002 115 | hwnd := WinExist(winTitle) 116 | hMonitor := DllCall("MonitorFromWindow", "ptr", hwnd, "uint", MONITOR_DEFAULTTONEAREST, "ptr") 117 | NumPut("uint", 104, MONITORINFOEX := Buffer(104)) 118 | if (DllCall("user32\GetMonitorInfo", "ptr", hMonitor, "ptr", MONITORINFOEX)) { 119 | Return { Handle: hMonitor 120 | , Name: Name := StrGet(MONITORINFOEX.ptr + 40, 32) 121 | , Number: RegExReplace(Name, ".*(\d+)$", "$1") 122 | , Left: L := NumGet(MONITORINFOEX, 4, "int") 123 | , Top: T := NumGet(MONITORINFOEX, 8, "int") 124 | , Right: R := NumGet(MONITORINFOEX, 12, "int") 125 | , Bottom: B := NumGet(MONITORINFOEX, 16, "int") 126 | , WALeft: WL := NumGet(MONITORINFOEX, 20, "int") 127 | , WATop: WT := NumGet(MONITORINFOEX, 24, "int") 128 | , WARight: WR := NumGet(MONITORINFOEX, 28, "int") 129 | , WABottom: WB := NumGet(MONITORINFOEX, 32, "int") 130 | , Width: Width := R - L 131 | , Height: Height := B - T 132 | , WAWidth: WR - WL 133 | , WAHeight: WB - WT 134 | , Primary: NumGet(MONITORINFOEX, 36, "uint") 135 | } 136 | } 137 | throw Error("GetMonitorInfo: " A_LastError, -1) 138 | } 139 | -------------------------------------------------------------------------------- /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.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devail1/window-center-resize/fdef39c1d87c839b0a53085cfd567a1ffa692191/assets/icon.icns -------------------------------------------------------------------------------- /assets/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devail1/window-center-resize/fdef39c1d87c839b0a53085cfd567a1ffa692191/assets/icon.ico -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devail1/window-center-resize/fdef39c1d87c839b0a53085cfd567a1ffa692191/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-copy/dark/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devail1/window-center-resize/fdef39c1d87c839b0a53085cfd567a1ffa692191/assets/icons-copy/dark/logo.png -------------------------------------------------------------------------------- /assets/icons-copy/dark/open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devail1/window-center-resize/fdef39c1d87c839b0a53085cfd567a1ffa692191/assets/icons-copy/dark/open.png -------------------------------------------------------------------------------- /assets/icons-copy/dark/quit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devail1/window-center-resize/fdef39c1d87c839b0a53085cfd567a1ffa692191/assets/icons-copy/dark/quit.png -------------------------------------------------------------------------------- /assets/icons-copy/light/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devail1/window-center-resize/fdef39c1d87c839b0a53085cfd567a1ffa692191/assets/icons-copy/light/logo.png -------------------------------------------------------------------------------- /assets/icons-copy/light/open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devail1/window-center-resize/fdef39c1d87c839b0a53085cfd567a1ffa692191/assets/icons-copy/light/open.png -------------------------------------------------------------------------------- /assets/icons-copy/light/quit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devail1/window-center-resize/fdef39c1d87c839b0a53085cfd567a1ffa692191/assets/icons-copy/light/quit.png -------------------------------------------------------------------------------- /assets/icons-copy/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devail1/window-center-resize/fdef39c1d87c839b0a53085cfd567a1ffa692191/assets/icons-copy/logo.png -------------------------------------------------------------------------------- /assets/icons/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devail1/window-center-resize/fdef39c1d87c839b0a53085cfd567a1ffa692191/assets/icons/1024x1024.png -------------------------------------------------------------------------------- /assets/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devail1/window-center-resize/fdef39c1d87c839b0a53085cfd567a1ffa692191/assets/icons/128x128.png -------------------------------------------------------------------------------- /assets/icons/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devail1/window-center-resize/fdef39c1d87c839b0a53085cfd567a1ffa692191/assets/icons/16x16.png -------------------------------------------------------------------------------- /assets/icons/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devail1/window-center-resize/fdef39c1d87c839b0a53085cfd567a1ffa692191/assets/icons/24x24.png -------------------------------------------------------------------------------- /assets/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devail1/window-center-resize/fdef39c1d87c839b0a53085cfd567a1ffa692191/assets/icons/256x256.png -------------------------------------------------------------------------------- /assets/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devail1/window-center-resize/fdef39c1d87c839b0a53085cfd567a1ffa692191/assets/icons/32x32.png -------------------------------------------------------------------------------- /assets/icons/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devail1/window-center-resize/fdef39c1d87c839b0a53085cfd567a1ffa692191/assets/icons/48x48.png -------------------------------------------------------------------------------- /assets/icons/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devail1/window-center-resize/fdef39c1d87c839b0a53085cfd567a1ffa692191/assets/icons/512x512.png -------------------------------------------------------------------------------- /assets/icons/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devail1/window-center-resize/fdef39c1d87c839b0a53085cfd567a1ffa692191/assets/icons/64x64.png -------------------------------------------------------------------------------- /assets/icons/96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devail1/window-center-resize/fdef39c1d87c839b0a53085cfd567a1ffa692191/assets/icons/96x96.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "window-center-resize", 3 | "version": "1.0.2", 4 | "description": "A utility app that allows you to easily center and resize windows on your desktop", 5 | "keywords": [ 6 | "electron", 7 | "boilerplate", 8 | "react", 9 | "typescript", 10 | "ts", 11 | "sass", 12 | "webpack", 13 | "hot", 14 | "reload", 15 | "window", 16 | "snapper", 17 | "resizer", 18 | "helper" 19 | ], 20 | "homepage": "https://github.com/devail1/window-center-resize#readme", 21 | "bugs": { 22 | "url": "https://github.com/devail1/window-center-resize/issues" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/devail1/window-center-resize.git" 27 | }, 28 | "license": "MIT", 29 | "author": "devail1", 30 | "main": "./src/main/main.ts", 31 | "scripts": { 32 | "build": "concurrently \"npm run build:main\" \"npm run build:renderer\"", 33 | "build:dll": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.renderer.dev.dll.ts", 34 | "build:main": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.main.prod.ts", 35 | "build:renderer": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.renderer.prod.ts", 36 | "postinstall": "ts-node .erb/scripts/check-native-dep.js && electron-builder install-app-deps && npm run build:dll", 37 | "lint": "cross-env NODE_ENV=development eslint . --ext .js,.jsx,.ts,.tsx", 38 | "package": "ts-node ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish always && npm run build:dll", 39 | "rebuild": "electron-rebuild --parallel --types prod,dev,optional --module-dir release/app", 40 | "start": "ts-node ./.erb/scripts/check-port-in-use.js && npm run start:renderer", 41 | "start:main": "cross-env NODE_ENV=development electronmon -r ts-node/register/transpile-only .", 42 | "start:preload": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.preload.dev.ts", 43 | "start:renderer": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./.erb/configs/webpack.config.renderer.dev.ts", 44 | "test": "jest" 45 | }, 46 | "browserslist": [], 47 | "prettier": { 48 | "singleQuote": true, 49 | "overrides": [ 50 | { 51 | "files": [ 52 | ".prettierrc", 53 | ".eslintrc" 54 | ], 55 | "options": { 56 | "parser": "json" 57 | } 58 | } 59 | ] 60 | }, 61 | "jest": { 62 | "moduleDirectories": [ 63 | "node_modules", 64 | "release/app/node_modules", 65 | "src" 66 | ], 67 | "moduleFileExtensions": [ 68 | "js", 69 | "jsx", 70 | "ts", 71 | "tsx", 72 | "json" 73 | ], 74 | "moduleNameMapper": { 75 | "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/.erb/mocks/fileMock.js", 76 | "\\.(css|less|sass|scss)$": "identity-obj-proxy", 77 | "^@/(.*)$": "/src/$1", 78 | "electron": "/src/__mock__/electron.js" 79 | }, 80 | "setupFiles": [ 81 | "./.erb/scripts/check-build-exists.ts" 82 | ], 83 | "testEnvironment": "jsdom", 84 | "testEnvironmentOptions": { 85 | "url": "http://localhost/" 86 | }, 87 | "testPathIgnorePatterns": [ 88 | "release/app/dist", 89 | ".erb/dll" 90 | ], 91 | "transform": { 92 | "\\.(ts|tsx|js|jsx)$": "ts-jest" 93 | }, 94 | "modulePathIgnorePatterns": [ 95 | "/release/" 96 | ] 97 | }, 98 | "dependencies": { 99 | "electron-debug": "^3.2.0", 100 | "electron-log": "^4.4.8", 101 | "electron-updater": "^6.1.4", 102 | "react": "^18.3.1", 103 | "react-dom": "^18.2.0", 104 | "react-hotkeys-hook": "^4.5.0", 105 | "react-router-dom": "^6.16.0" 106 | }, 107 | "devDependencies": { 108 | "@electron/notarize": "^2.1.0", 109 | "@electron/rebuild": "^3.3.0", 110 | "@pmmmwh/react-refresh-webpack-plugin": "^0.5.11", 111 | "@svgr/webpack": "^8.1.0", 112 | "@teamsupercell/typings-for-css-modules-loader": "^2.5.2", 113 | "@testing-library/jest-dom": "^6.1.3", 114 | "@testing-library/react": "^15.0.7", 115 | "@types/jest": "^29.5.5", 116 | "@types/node": "20.6.2", 117 | "@types/react": "^18.3.1", 118 | "@types/react-dom": "^18.2.7", 119 | "@types/react-test-renderer": "^18.0.1", 120 | "@types/source-map-support": "^0.5.10", 121 | "@types/terser-webpack-plugin": "^5.0.4", 122 | "@types/webpack-bundle-analyzer": "^4.6.0", 123 | "@typescript-eslint/eslint-plugin": "^6.7.0", 124 | "@typescript-eslint/parser": "^6.7.0", 125 | "browserslist-config-erb": "^0.0.3", 126 | "chalk": "^4.1.2", 127 | "concurrently": "^8.2.1", 128 | "core-js": "^3.32.2", 129 | "cross-env": "^7.0.3", 130 | "css-loader": "^6.8.1", 131 | "css-minimizer-webpack-plugin": "^5.0.1", 132 | "detect-port": "^1.5.1", 133 | "electron": "^26.2.1", 134 | "electron-builder": "^24.6.4", 135 | "electron-devtools-installer": "^3.2.0", 136 | "electronmon": "^2.0.2", 137 | "eslint": "^8.49.0", 138 | "eslint-config-airbnb-base": "^15.0.0", 139 | "eslint-config-erb": "^4.1.0-0", 140 | "eslint-import-resolver-typescript": "^3.6.0", 141 | "eslint-import-resolver-webpack": "^0.13.7", 142 | "eslint-plugin-compat": "^4.2.0", 143 | "eslint-plugin-import": "^2.28.1", 144 | "eslint-plugin-jest": "^27.4.0", 145 | "eslint-plugin-jsx-a11y": "^6.7.1", 146 | "eslint-plugin-promise": "^6.1.1", 147 | "eslint-plugin-react": "^7.33.2", 148 | "eslint-plugin-react-hooks": "^4.6.0", 149 | "file-loader": "^6.2.0", 150 | "html-webpack-plugin": "^5.5.3", 151 | "identity-obj-proxy": "^3.0.0", 152 | "jest": "^29.7.0", 153 | "jest-environment-jsdom": "^29.7.0", 154 | "mini-css-extract-plugin": "^2.7.6", 155 | "prettier": "^3.0.3", 156 | "react-refresh": "^0.14.0", 157 | "react-test-renderer": "^18.2.0", 158 | "rimraf": "^5.0.1", 159 | "sass": "^1.67.0", 160 | "sass-loader": "^13.3.2", 161 | "source-map-support": "^0.5.21", 162 | "style-loader": "^3.3.3", 163 | "terser-webpack-plugin": "^5.3.9", 164 | "ts-jest": "^29.1.1", 165 | "ts-loader": "^9.4.4", 166 | "ts-node": "^10.9.1", 167 | "tsconfig-paths-webpack-plugin": "^4.1.0", 168 | "typescript": "^5.2.2", 169 | "url-loader": "^4.1.1", 170 | "webpack": "^5.88.2", 171 | "webpack-bundle-analyzer": "^4.9.1", 172 | "webpack-cli": "^5.1.4", 173 | "webpack-dev-server": "^4.15.1", 174 | "webpack-merge": "^5.9.0" 175 | }, 176 | "build": { 177 | "productName": "Window Center Resize", 178 | "icon": "assets/icons-copy/logo.png", 179 | "asar": true, 180 | "asarUnpack": "**\\*.{node,dll,exe,ahk}", 181 | "files": [ 182 | "dist", 183 | "node_modules", 184 | "package.json" 185 | ], 186 | "win": { 187 | "target": "portable" 188 | }, 189 | "artifactName": "${productName}.${ext}", 190 | "directories": { 191 | "app": "release/app", 192 | "buildResources": "assets", 193 | "output": "release/build" 194 | }, 195 | "extraResources": [ 196 | "./assets/**/*" 197 | ], 198 | "publish": [ 199 | { 200 | "provider": "github", 201 | "owner": "devail1", 202 | "repo": "window-center-resize" 203 | } 204 | ] 205 | }, 206 | "devEngines": { 207 | "node": ">=18.x", 208 | "npm": ">=7.x" 209 | }, 210 | "electronmon": { 211 | "patterns": [ 212 | "!**/**", 213 | "src/main/**" 214 | ], 215 | "logLevel": "quiet" 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /release/app/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-react-boilerplate", 3 | "version": "4.6.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "electron-react-boilerplate", 9 | "version": "4.6.0", 10 | "hasInstallScript": true, 11 | "license": "MIT" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /release/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "window-center-resize", 3 | "version": "1.0.2", 4 | "description": "Window Center Resize", 5 | "license": "MIT", 6 | "author": { 7 | "name": "Devail1", 8 | "email": "liav.acc+window-center@gmail.com", 9 | "url": "https://github.com/devail1/window-center-resize" 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 | -------------------------------------------------------------------------------- /release/app/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | -------------------------------------------------------------------------------- /src/__mock__/electron.js: -------------------------------------------------------------------------------- 1 | const mockElectron = { 2 | ipcRenderer: { 3 | sendMessage: jest.fn(), 4 | on: jest.fn(), 5 | once: jest.fn(), 6 | invoke: jest.fn(), 7 | removeAllListeners: jest.fn(), 8 | }, 9 | }; 10 | 11 | export default mockElectron; 12 | -------------------------------------------------------------------------------- /src/__tests__/App.test.tsx: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | import { act, render, screen } from '@testing-library/react'; 3 | import App from '@/renderer/App'; 4 | import mockElectron from '@/__mock__/electron'; 5 | 6 | beforeAll(() => { 7 | global.window.electron = mockElectron; 8 | }); 9 | 10 | describe('App', () => { 11 | it('should render', async () => { 12 | await act(async () => { 13 | render(); 14 | }); 15 | 16 | expect(screen.getByText('Window Snapper & Resizer')).toBeInTheDocument(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/constants/defaultSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "centerWindow": { 3 | "keybinding": "ctrl + shift + c" 4 | }, 5 | "resizeWindow": { 6 | "keybinding": "f9", 7 | "windowSizePercentages": [ 8 | { "width": "50", "height": "50" }, 9 | { "width": "75", "height": "75" }, 10 | { "width": "90", "height": "90" } 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/autohotkey.ts: -------------------------------------------------------------------------------- 1 | /* eslint no-console: off */ 2 | 3 | import * as child from 'child_process'; 4 | import { app } from 'electron'; 5 | import path from 'path'; 6 | 7 | let autohotkeyProcess: child.ChildProcess | null = null; 8 | 9 | async function stopAutoHotkeyProcess() { 10 | if (autohotkeyProcess) { 11 | try { 12 | await autohotkeyProcess.kill(); 13 | } catch (error) { 14 | console.error('Error killing child process:', error); 15 | } 16 | autohotkeyProcess = null; 17 | } 18 | } 19 | 20 | async function startAutoHotkeyProcess() { 21 | if (autohotkeyProcess) { 22 | stopAutoHotkeyProcess(); 23 | } 24 | 25 | const { isPackaged } = app; 26 | 27 | const resourcesPath = isPackaged 28 | ? path.join(process.resourcesPath, 'assets', 'autohotkey') 29 | : path.join(__dirname, '../../assets', 'autohotkey'); 30 | 31 | const autohotkeyPath = path.join(resourcesPath, 'AutoHotkey32.exe'); 32 | const scriptPath = path.join(resourcesPath, 'center-window-resize.ahk'); 33 | 34 | try { 35 | autohotkeyProcess = child.spawn(autohotkeyPath, [scriptPath]); 36 | } catch (error) { 37 | console.error('Unexpected error spawning AutoHotkey:', error); 38 | } 39 | } 40 | 41 | function reloadAutoHotkey() { 42 | if (autohotkeyProcess) { 43 | stopAutoHotkeyProcess(); 44 | } 45 | startAutoHotkeyProcess(); 46 | } 47 | 48 | export { startAutoHotkeyProcess, stopAutoHotkeyProcess, reloadAutoHotkey }; 49 | -------------------------------------------------------------------------------- /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 { BrowserWindow, app, dialog, ipcMain } from 'electron'; 12 | import sourceMapSupport from 'source-map-support'; 13 | import debug from 'electron-debug'; 14 | import { autoUpdater } from 'electron-updater'; 15 | import log from 'electron-log'; 16 | import { startAutoHotkeyProcess, stopAutoHotkeyProcess } from './autohotkey'; 17 | import { createWindow } from './window'; 18 | import { 19 | resetSettings, 20 | loadSettings, 21 | saveCenterSettings, 22 | saveResizeSettings, 23 | closeWatcher, 24 | getSettings, 25 | } from './settings'; 26 | import createTrayMenu from './tray'; 27 | 28 | autoUpdater.logger = log; 29 | log.transports.file.level = 'info'; 30 | log.info('App starting...'); 31 | 32 | autoUpdater.on('checking-for-update', () => { 33 | log.info('Checking for update...'); 34 | }); 35 | 36 | autoUpdater.on('update-available', (info) => { 37 | log.info('Update available.', info); 38 | }); 39 | 40 | autoUpdater.on('update-not-available', (info) => { 41 | log.info('Update not available.', info); 42 | }); 43 | 44 | autoUpdater.on('error', (err) => { 45 | log.info('Error in auto-updater. ', err); 46 | }); 47 | 48 | autoUpdater.on('download-progress', (progressObj) => { 49 | let logMessage = `Download speed: ${progressObj.bytesPerSecond}`; 50 | logMessage = `${logMessage} - Downloaded ${progressObj.percent}%`; 51 | logMessage = `${logMessage} (${progressObj.transferred}/${progressObj.total})`; 52 | log.info(logMessage); 53 | }); 54 | 55 | autoUpdater.on('update-downloaded', (info) => { 56 | log.info('Update downloaded', info); 57 | // Show a dialog to the user to confirm restart 58 | dialog 59 | .showMessageBox({ 60 | type: 'info', 61 | title: 'Update Available', 62 | message: 63 | 'A new version is available. Do you want to restart the application to apply the updates now?', 64 | buttons: ['Restart', 'Later'], 65 | }) 66 | .then((result) => { 67 | const buttonIndex = result.response; 68 | if (buttonIndex === 0) { 69 | autoUpdater.quitAndInstall(); 70 | } 71 | }) 72 | .catch((error) => { 73 | console.error('Error in auto-updater. ', error); 74 | }); 75 | autoUpdater.quitAndInstall(); 76 | }); 77 | 78 | if (process.env.NODE_ENV === 'production') { 79 | sourceMapSupport.install(); 80 | } 81 | 82 | const isDebug = 83 | process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true'; 84 | 85 | if (isDebug) { 86 | debug(); 87 | } 88 | 89 | app 90 | .whenReady() 91 | .then(() => { 92 | createWindow(); 93 | createTrayMenu(); 94 | startAutoHotkeyProcess(); 95 | 96 | app.on('activate', () => { 97 | if (BrowserWindow.getAllWindows().length === 0) createWindow(); 98 | }); 99 | 100 | app.on('will-quit', () => { 101 | closeWatcher(); 102 | stopAutoHotkeyProcess(); 103 | }); 104 | 105 | // Check for updates and notify 106 | autoUpdater.checkForUpdatesAndNotify(); 107 | }) 108 | .catch((error) => { 109 | console.error('Error in app.whenReady:', error); 110 | }); 111 | 112 | ipcMain.handle('reset-settings', resetSettings); 113 | ipcMain.handle('load-settings', loadSettings); 114 | ipcMain.handle('get-settings', getSettings); 115 | ipcMain.handle('save-center-settings', saveCenterSettings); 116 | ipcMain.handle('save-resize-settings', saveResizeSettings); 117 | -------------------------------------------------------------------------------- /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 | | 'get-settings' 7 | | 'load-settings' 8 | | 'reset-settings' 9 | | 'save-center-settings' 10 | | 'save-resize-settings' 11 | | 'settings-updated'; 12 | 13 | const electronHandler = { 14 | ipcRenderer: { 15 | sendMessage(channel: Channels, ...args: unknown[]) { 16 | ipcRenderer.send(channel, ...args); 17 | }, 18 | on(channel: Channels, func: (...args: unknown[]) => void) { 19 | const subscription = (_event: IpcRendererEvent, ...args: unknown[]) => 20 | func(...args); 21 | ipcRenderer.on(channel, subscription); 22 | 23 | return () => { 24 | ipcRenderer.removeListener(channel, subscription); 25 | }; 26 | }, 27 | once(channel: Channels, func: (...args: unknown[]) => void) { 28 | ipcRenderer.once(channel, (_event, ...args) => func(...args)); 29 | }, 30 | invoke(channel: Channels, ...args: unknown[]) { 31 | return ipcRenderer.invoke(channel, ...args); 32 | }, 33 | removeAllListeners(channel: Channels) { 34 | ipcRenderer.removeAllListeners(channel); 35 | }, 36 | }, 37 | }; 38 | 39 | contextBridge.exposeInMainWorld('electron', electronHandler); 40 | 41 | export type ElectronHandler = typeof electronHandler; 42 | -------------------------------------------------------------------------------- /src/main/settings.ts: -------------------------------------------------------------------------------- 1 | /* eslint no-console: off */ 2 | 3 | import { promises as fs, existsSync, watch, FSWatcher } from 'fs'; 4 | import { join } from 'path'; 5 | import { app, IpcMainInvokeEvent } from 'electron'; 6 | import defaultSettings from '../constants/defaultSettings.json'; 7 | import { reloadAutoHotkey } from './autohotkey'; 8 | import { getMainWindow } from './window'; 9 | 10 | const settingsPath = join(app.getPath('userData'), 'settings.json'); 11 | 12 | let settingsWatcher: FSWatcher; 13 | 14 | export async function resetSettings() { 15 | const mainWindow = getMainWindow(); 16 | await fs.writeFile(settingsPath, JSON.stringify(defaultSettings)); 17 | reloadAutoHotkey(); 18 | mainWindow?.reload(); 19 | } 20 | 21 | export async function loadSettings() { 22 | try { 23 | if (!existsSync(settingsPath)) { 24 | await fs.writeFile( 25 | settingsPath, 26 | JSON.stringify(defaultSettings, null, 2), 27 | ); 28 | } 29 | reloadAutoHotkey(); 30 | } catch (err) { 31 | if (err instanceof Error) { 32 | console.error('Error loading settings:', err.message, err.stack); 33 | } else { 34 | console.error('Unknown error loading settings:', err); 35 | } 36 | } 37 | } 38 | 39 | export async function getSettings() { 40 | try { 41 | if (!existsSync(settingsPath)) { 42 | await fs.writeFile( 43 | settingsPath, 44 | JSON.stringify(defaultSettings, null, 2), 45 | ); 46 | } 47 | const settings = JSON.parse(await fs.readFile(settingsPath, 'utf8')); 48 | return settings; 49 | } catch (err) { 50 | if (err instanceof Error) { 51 | console.error('Error getting settings:', err.message, err.stack); 52 | } else { 53 | console.error('Unknown error getting settings:', err); 54 | } 55 | return null; 56 | } 57 | } 58 | 59 | function setupSettingsWatcher() { 60 | reloadAutoHotkey(); 61 | 62 | settingsWatcher = watch(settingsPath, () => { 63 | reloadAutoHotkey(); 64 | }); 65 | } 66 | 67 | export async function saveCenterSettings( 68 | event: IpcMainInvokeEvent, 69 | centerKeybind: string, 70 | ) { 71 | try { 72 | const rawSettings = await fs.readFile(settingsPath, 'utf8'); 73 | const settings = JSON.parse(rawSettings); 74 | settings.centerWindow.keybinding = centerKeybind; 75 | await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2)); 76 | 77 | setupSettingsWatcher(); 78 | } catch (err) { 79 | if (err instanceof Error) { 80 | console.error('Error saving center settings:', err.message, err.stack); 81 | } else { 82 | console.error('Unknown error saving center settings:', err); 83 | } 84 | } 85 | } 86 | 87 | export async function saveResizeSettings( 88 | event: IpcMainInvokeEvent, 89 | data: { 90 | keybinding: string; 91 | windowSizePercentages: { width: string; height: string }[]; 92 | }, 93 | ) { 94 | try { 95 | const rawSettings = await fs.readFile(settingsPath, 'utf8'); 96 | const settings = JSON.parse(rawSettings); 97 | settings.resizeWindow = { 98 | keybinding: data.keybinding, 99 | windowSizePercentages: data.windowSizePercentages, 100 | }; 101 | await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2)); 102 | setupSettingsWatcher(); 103 | } catch (err) { 104 | if (err instanceof Error) { 105 | console.error('Error saving resize settings:', err.message, err.stack); 106 | } else { 107 | console.error('Unknown error saving resize settings:', err); 108 | } 109 | } 110 | } 111 | 112 | export function closeWatcher() { 113 | if (settingsWatcher) { 114 | settingsWatcher.close(); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/main/singleInstance.ts: -------------------------------------------------------------------------------- 1 | /* eslint import/prefer-default-export: off */ 2 | import { app, BrowserWindow } from 'electron'; 3 | 4 | export function handleSingleInstance(mainWindow: BrowserWindow) { 5 | const gotTheLock = app.requestSingleInstanceLock(); 6 | 7 | if (!gotTheLock) { 8 | app.quit(); 9 | } else { 10 | app.on('second-instance', () => { 11 | if (mainWindow) { 12 | if (!mainWindow.isVisible()) { 13 | mainWindow.restore(); 14 | } 15 | mainWindow.focus(); 16 | } 17 | }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/tray.ts: -------------------------------------------------------------------------------- 1 | import { app, Menu, Notification, Tray } from 'electron'; 2 | import { capitalizeFirstLetterOfEachWord, getIconPath } from './util'; 3 | import { getSettings } from './settings'; 4 | import { getMainWindow } from './window'; 5 | 6 | let tray: Tray | null = null; 7 | 8 | const showNotification = async (): Promise => { 9 | const settings = await getSettings(); 10 | if (settings) { 11 | const { centerWindow, resizeWindow } = settings; 12 | 13 | const notification = new Notification({ 14 | title: 'Window Snapper', 15 | body: `Press ${capitalizeFirstLetterOfEachWord(centerWindow.keybinding)} to center the window. \nPress ${capitalizeFirstLetterOfEachWord(resizeWindow.keybinding)} to resize the window.`, 16 | silent: true, 17 | }); 18 | 19 | notification.on('click', () => { 20 | getMainWindow()?.show(); 21 | }); 22 | 23 | notification.show(); 24 | } 25 | }; 26 | 27 | const createTrayMenu = (): void => { 28 | tray = new Tray(getIconPath('logo')); 29 | 30 | const contextMenu = Menu.buildFromTemplate([ 31 | { 32 | label: 'Open', 33 | click: () => getMainWindow()?.show(), 34 | icon: getIconPath('open'), 35 | }, 36 | { 37 | label: 'Quit', 38 | click: () => app.quit(), 39 | icon: getIconPath('quit'), 40 | }, 41 | ]); 42 | 43 | tray.setToolTip('Window Center Resizer'); 44 | tray.setContextMenu(contextMenu); 45 | tray.on('click', () => getMainWindow()?.show()); 46 | 47 | showNotification(); 48 | }; 49 | 50 | export default createTrayMenu; 51 | -------------------------------------------------------------------------------- /src/main/util.ts: -------------------------------------------------------------------------------- 1 | /* eslint import/prefer-default-export: off */ 2 | import { URL } from 'url'; 3 | import path from 'path'; 4 | import { app, nativeTheme } from 'electron'; 5 | 6 | export function resolveHtmlPath(htmlFileName: string) { 7 | if (process.env.NODE_ENV === 'development') { 8 | const port = process.env.PORT || 1212; 9 | const url = new URL(`http://localhost:${port}`); 10 | url.pathname = htmlFileName; 11 | return url.href; 12 | } 13 | return `file://${path.resolve(__dirname, '../renderer/', htmlFileName)}`; 14 | } 15 | 16 | export function getIconPath(iconName: string) { 17 | const theme = nativeTheme.shouldUseDarkColors ? 'dark' : 'light'; 18 | 19 | const { isPackaged } = app; 20 | 21 | const resourcesPath = isPackaged 22 | ? path.join(process.resourcesPath, 'assets', 'icons-copy') 23 | : path.join(app.getAppPath(), 'assets', 'icons-copy'); 24 | 25 | const iconPath = path.join(resourcesPath, theme, `${iconName}.png`); 26 | 27 | return iconPath; 28 | } 29 | 30 | export function capitalizeFirstLetterOfEachWord(str: string): string { 31 | if (!str) return ''; 32 | return str 33 | .split(' ') 34 | .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) 35 | .join(' '); 36 | } 37 | -------------------------------------------------------------------------------- /src/main/window.ts: -------------------------------------------------------------------------------- 1 | /* eslint global-require: off, no-console: off */ 2 | 3 | import { app, BrowserWindow, shell } from 'electron'; 4 | import path from 'path'; 5 | import log from 'electron-log'; 6 | import { autoUpdater } from 'electron-updater'; 7 | import { getIconPath, resolveHtmlPath } from './util'; 8 | import { handleSingleInstance } from './singleInstance'; 9 | 10 | class AppUpdater { 11 | constructor() { 12 | log.transports.file.level = 'info'; 13 | autoUpdater.logger = log; 14 | autoUpdater.checkForUpdatesAndNotify(); 15 | } 16 | } 17 | 18 | const isDebug = 19 | process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true'; 20 | 21 | const installExtensions = async (): Promise => { 22 | const installer = require('electron-devtools-installer'); 23 | const forceDownload = !!process.env.UPGRADE_EXTENSIONS; 24 | const extensions = ['REACT_DEVELOPER_TOOLS']; 25 | 26 | return installer 27 | .default( 28 | extensions.map((name: string) => installer[name]), 29 | forceDownload, 30 | ) 31 | .catch(console.log); 32 | }; 33 | 34 | let mainWindow: BrowserWindow | null = null; 35 | 36 | export const createWindow = async (): Promise => { 37 | if (isDebug) { 38 | await installExtensions(); 39 | } 40 | 41 | mainWindow = new BrowserWindow({ 42 | width: 580, 43 | height: 640, 44 | icon: getIconPath('logo'), 45 | show: false, 46 | autoHideMenuBar: true, 47 | webPreferences: { 48 | nodeIntegration: true, 49 | preload: app.isPackaged 50 | ? path.join(__dirname, 'preload.js') 51 | : path.join(__dirname, '../../.erb/dll/preload.js'), 52 | }, 53 | }); 54 | 55 | mainWindow.loadURL(resolveHtmlPath('index.html')); 56 | 57 | mainWindow.on('close', (event) => { 58 | if (mainWindow?.isVisible()) { 59 | event.preventDefault(); 60 | mainWindow.hide(); 61 | } 62 | }); 63 | 64 | mainWindow.on('closed', () => { 65 | mainWindow = null; 66 | }); 67 | 68 | mainWindow.webContents.setWindowOpenHandler((edata) => { 69 | shell.openExternal(edata.url); 70 | return { action: 'deny' }; 71 | }); 72 | 73 | handleSingleInstance(mainWindow); 74 | 75 | // eslint-disable-next-line 76 | new AppUpdater(); 77 | 78 | return mainWindow; 79 | }; 80 | 81 | export const getMainWindow = (): BrowserWindow | null => mainWindow; 82 | -------------------------------------------------------------------------------- /src/renderer/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, sans-serif; 3 | margin: 0; 4 | padding: 0; 5 | background-color: #f5f5f5; 6 | color: darkslategray; 7 | } 8 | 9 | h1 { 10 | margin-top: 0; 11 | color: #333; 12 | font-size: 18px; 13 | } 14 | 15 | h3 { 16 | margin-top: 0; 17 | color: #333; 18 | font-size: 16px; 19 | } 20 | 21 | h4 { 22 | margin-top: 18px; 23 | margin-bottom: 12px; 24 | color: #333; 25 | font-size: 16px; 26 | } 27 | 28 | span { 29 | font-weight: 600; 30 | font-size: 14px; 31 | } 32 | 33 | label { 34 | font-size: 14px; 35 | } 36 | 37 | button { 38 | padding: 8px 16px; 39 | background-color: #007bff; 40 | color: #fff; 41 | border: none; 42 | border-radius: 3px; 43 | cursor: pointer; 44 | transition: background-color 0.3s; 45 | } 46 | 47 | button:hover { 48 | background-color: #0056b3; 49 | } 50 | 51 | .display-flex { 52 | display: flex; 53 | } 54 | 55 | .align-center { 56 | align-items: center; 57 | } 58 | 59 | .justify-between { 60 | justify-content: space-between; 61 | } 62 | 63 | .flex-grow { 64 | flex-grow: 1; 65 | } 66 | 67 | .flex-col { 68 | flex-direction: column; 69 | } 70 | 71 | /* Margin Util Classes */ 72 | 73 | .mr-4 { 74 | margin-right: 16px; 75 | } 76 | 77 | .mr-2 { 78 | margin-right: 8px; 79 | } 80 | 81 | .mt-4 { 82 | margin-top: 16px; 83 | } 84 | -------------------------------------------------------------------------------- /src/renderer/App.tsx: -------------------------------------------------------------------------------- 1 | import { MemoryRouter as Router, Routes, Route } from 'react-router-dom'; 2 | import AppContainer from './components/AppContainer/AppContainer'; 3 | import './App.css'; 4 | import Footer from './components/Footer/Footer'; 5 | import TabsView from './components/tabs/TabsView/TabsView'; 6 | 7 | function MyApp() { 8 | return ( 9 |
10 | 11 | 12 |
13 | 14 |
15 | ); 16 | } 17 | export default function App() { 18 | return ( 19 | 20 | 21 | } /> 22 | 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/renderer/components/About/About.css: -------------------------------------------------------------------------------- 1 | .about { 2 | background-color: #f9f9f9; 3 | border-radius: 5px; 4 | } 5 | 6 | /* Style the summary to make it look like a button */ 7 | summary { 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | cursor: pointer; 12 | padding: 8px; 13 | background-color: #f9f9f9; 14 | font-size: 11px; 15 | border-radius: 5px; 16 | text-align: center; 17 | } 18 | 19 | /* Create a new custom triangle on the right side */ 20 | summary::before { 21 | margin-right: 2px; 22 | display: inline-block; 23 | scale: 0.8; 24 | content: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiMwMDAwMDAiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBjbGFzcz0ibHVjaWRlIGx1Y2lkZS1jaGV2cm9uLXJpZ2h0Ij48cGF0aCBkPSJtOSAxOCA2LTYtNi02Ii8+PC9zdmc+'); 25 | transition: 0.2s; 26 | } 27 | 28 | details[open] > summary::before { 29 | transform: rotate(90deg); 30 | } 31 | 32 | .read-more-content p { 33 | margin: 4px 0; 34 | } 35 | 36 | .read-more-content ul { 37 | padding-inline-start: 22px; 38 | } 39 | 40 | /* Style the content inside the details */ 41 | .read-more-content { 42 | font-size: 11px; 43 | padding: 8px 4px; 44 | border-top: none; 45 | border-radius: 0 0 5px 5px; 46 | background-color: #f9f9f9; 47 | bottom: 0; 48 | width: 90%; 49 | margin: 0 auto; 50 | max-width: 600px; 51 | } 52 | -------------------------------------------------------------------------------- /src/renderer/components/About/About.tsx: -------------------------------------------------------------------------------- 1 | import './About.css'; 2 | 3 | function About() { 4 | return ( 5 |
6 |
7 | Streamlined Window Management (Read more) 8 |
9 |

10 | Power users and multitaskers, maximize focus with Window Center & 11 | Resizer. 12 |

13 |
    14 |
  • 15 |

    16 | Customizable Keyboard Shortcuts: Control window 17 | actions efficiently for an uninterrupted workflow. 18 |

    19 |
  • 20 |
  • 21 |

    22 | Predefined Window Layouts: Optimize your 23 | workspace with pre-defined dimensions, ideal for developers, 24 | designers, and data analysts. 25 |

    26 |
  • 27 |
28 |

29 | Free, open-source, and ad-free! (Details & updates 30 | on GitHub: 31 | 36 | repository 37 | 38 | ) 39 |

40 |

41 | Compatibility: .NET Framework 4.5, Windows 7, 8, 10 42 |

43 |
44 |
45 |
46 | ); 47 | } 48 | 49 | export default About; 50 | -------------------------------------------------------------------------------- /src/renderer/components/AppContainer/AppContainer.css: -------------------------------------------------------------------------------- 1 | #container { 2 | padding: 24px; 3 | } 4 | -------------------------------------------------------------------------------- /src/renderer/components/AppContainer/AppContainer.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, createContext } from 'react'; 2 | import './AppContainer.css'; 3 | import TabsProvider from '@/renderer/providers/TabsProvider'; 4 | import SettingsProvider from '@/renderer/providers/SettingsProvider'; 5 | 6 | export type TTabAction = 'center' | 'resize'; 7 | 8 | export interface TabsState { 9 | activeTab: TTabAction; 10 | } 11 | 12 | interface TabsContextProps extends TabsState { 13 | setActiveTab: (tab: TTabAction) => void; 14 | } 15 | 16 | export const TabsContext = createContext(null); 17 | 18 | function AppContainer({ children }: { children: ReactNode }) { 19 | return ( 20 |
21 | 22 | {children} 23 | 24 |
25 | ); 26 | } 27 | 28 | export default AppContainer; 29 | -------------------------------------------------------------------------------- /src/renderer/components/Footer/Footer.css: -------------------------------------------------------------------------------- 1 | footer { 2 | background-color: #333; 3 | text-align: center; 4 | padding: 10px 0; 5 | position: fixed; 6 | bottom: 0; 7 | right: 0; 8 | width: 100%; 9 | } 10 | 11 | footer p { 12 | padding: 0; 13 | margin: 0; 14 | font-size: 11px; 15 | } 16 | 17 | footer a { 18 | color: lightgray; 19 | text-decoration: none; 20 | } 21 | 22 | footer a:hover { 23 | text-shadow: #ddd 1px 0 4px; 24 | color: #ddd; 25 | transition: all; 26 | } 27 | -------------------------------------------------------------------------------- /src/renderer/components/Footer/Footer.tsx: -------------------------------------------------------------------------------- 1 | import './Footer.css'; 2 | 3 | function Footer() { 4 | return ( 5 | 16 | ); 17 | } 18 | 19 | export default Footer; 20 | -------------------------------------------------------------------------------- /src/renderer/components/tabs/TabsList/TabsList.css: -------------------------------------------------------------------------------- 1 | .nav { 2 | display: flex; 3 | justify-content: space-between; 4 | align-items: baseline; 5 | padding-bottom: 20px; 6 | border-bottom: 1px solid #ccc; 7 | margin: 20px 0; 8 | padding: 20px; 9 | } 10 | 11 | .tab { 12 | display: flex; 13 | gap: 8px; 14 | overflow: hidden; 15 | } 16 | 17 | .tab button { 18 | background-color: #f5f5f5; 19 | color: #333; 20 | border: none; 21 | outline: none; 22 | cursor: pointer; 23 | padding: 8px 16px; 24 | transition: background-color 0.3s; 25 | } 26 | 27 | .tab button.active { 28 | background-color: #007bff; 29 | color: white; 30 | } 31 | 32 | .tab button:hover { 33 | background-color: #0056b3; 34 | color: white; 35 | } 36 | 37 | #reset-button { 38 | margin-left: auto; 39 | background-color: darkgray; 40 | font-size: 12px; 41 | } 42 | 43 | #reset-button:hover { 44 | background-color: gray; 45 | } 46 | -------------------------------------------------------------------------------- /src/renderer/components/tabs/TabsList/TabsList.tsx: -------------------------------------------------------------------------------- 1 | import './TabsList.css'; 2 | import { useTabsContext } from '@/renderer/providers/TabsProvider'; 3 | import { TTabAction } from '@/renderer/components/AppContainer/AppContainer'; 4 | import { useSettingsContext } from '@/renderer/providers/SettingsProvider'; 5 | 6 | function TabsList() { 7 | const { resetSettings } = useSettingsContext(); 8 | 9 | const { activeTab, setActiveTab } = useTabsContext(); 10 | 11 | const handleClick = (tabName: TTabAction) => { 12 | setActiveTab(tabName); 13 | }; 14 | 15 | const handleReset = () => { 16 | resetSettings(); 17 | }; 18 | 19 | return ( 20 |
21 |
22 | 29 | 36 |
37 | 40 |
41 | ); 42 | } 43 | 44 | export default TabsList; 45 | -------------------------------------------------------------------------------- /src/renderer/components/tabs/TabsView/TabsView.css: -------------------------------------------------------------------------------- 1 | .inner-container { 2 | border-radius: 5px; 3 | background-color: #fff; 4 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 5 | min-width: 375px; 6 | max-width: 550px; 7 | margin: 18px auto; 8 | border: 1px solid #ccc; 9 | } 10 | 11 | .title { 12 | padding: 20px 0 0 20px; 13 | } 14 | 15 | .tabs-content { 16 | padding: 20px; 17 | } 18 | -------------------------------------------------------------------------------- /src/renderer/components/tabs/TabsView/TabsView.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useTabsContext } from '@/renderer/providers/TabsProvider'; 3 | import About from '@/renderer/components/About/About'; 4 | import TabsList from '../TabsList/TabsList'; 5 | import CenterTabContent from '../content/CenterTabContent/CenterTabContent'; 6 | import ResizeTabContent from '../content/ResizeTabContent/ResizeTabContent'; 7 | import './TabsView.css'; 8 | 9 | export default function TabsView() { 10 | const { activeTab } = useTabsContext(); 11 | 12 | useEffect(() => { 13 | if (activeTab === 'center') { 14 | document.title = 'Center Window'; 15 | } else if (activeTab === 'resize') { 16 | document.title = 'Resize Window'; 17 | } 18 | }, [activeTab]); 19 | 20 | return ( 21 |
22 |

Window Snapper & Resizer

23 | 24 |
25 | {activeTab === 'center' && } 26 | {activeTab === 'resize' && } 27 |
28 | 29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/renderer/components/tabs/content/CenterTabContent/CenterTabContent.css: -------------------------------------------------------------------------------- 1 | .save-button { 2 | display: block; 3 | margin-left: auto; 4 | margin-top: 34px; 5 | } 6 | 7 | .input-help { 8 | font-size: 12px; 9 | color: gray; 10 | } 11 | 12 | .keybind-label { 13 | display: flex; 14 | gap: 8px; 15 | align-items: center; 16 | } 17 | -------------------------------------------------------------------------------- /src/renderer/components/tabs/content/CenterTabContent/CenterTabContent.tsx: -------------------------------------------------------------------------------- 1 | import { useSettingsContext } from '@/renderer/providers/SettingsProvider'; 2 | import useKeybindHandler from '@/renderer/hooks/useKeybindHandler'; 3 | import './CenterTabContent.css'; 4 | 5 | function CenterTabContent() { 6 | const { settings, saveCenterSettings } = useSettingsContext(); 7 | 8 | const { 9 | inputRef, 10 | keybind, 11 | handleKeyDown, 12 | handleSubmit, 13 | handleFocus, 14 | handleBlur, 15 | } = useKeybindHandler(settings.centerWindow.keybinding, saveCenterSettings); 16 | 17 | return ( 18 |
19 |

Center Window

20 |
21 |
22 | 38 |
39 | 40 | 43 |
44 |
45 | ); 46 | } 47 | 48 | export default CenterTabContent; 49 | -------------------------------------------------------------------------------- /src/renderer/components/tabs/content/ResizeTabContent/ResizeTabContent.css: -------------------------------------------------------------------------------- 1 | .resize-content { 2 | display: grid; 3 | grid-template-columns: 1fr; 4 | grid-gap: 16px; 5 | } 6 | 7 | .keybind-label, 8 | .dimensions-label { 9 | font-size: 14px; 10 | margin-bottom: 4px; 11 | } 12 | 13 | .resize-content form { 14 | margin: 16px 0; 15 | } 16 | 17 | .keybind-label { 18 | display: flex; 19 | gap: 8px; 20 | align-items: center; 21 | } 22 | 23 | hr { 24 | opacity: 35%; 25 | margin-top: 28px; 26 | margin-bottom: 8px; 27 | width: 45%; 28 | } 29 | 30 | input[type='range'] { 31 | max-width: calc(100% - 82px); 32 | -webkit-appearance: none; /* Remove default appearance */ 33 | appearance: none; 34 | width: 100%; 35 | height: 6px; 36 | background: #ddd; /* Track background color */ 37 | border-radius: 3px; /* Rounded corners for track */ 38 | cursor: pointer; /* Pointer cursor for interaction */ 39 | margin-right: 34px; 40 | } 41 | 42 | input[type='range']::-webkit-slider-thumb { 43 | -webkit-appearance: none; /* Remove default appearance */ 44 | appearance: none; 45 | width: 16px; 46 | height: 16px; 47 | background: #007bff; /* Thumb background color */ 48 | border-radius: 50%; /* Circular thumb */ 49 | } 50 | 51 | /* CSS to remove number input arrows */ 52 | .range-input-value::-webkit-inner-spin-button, 53 | .range-input-value::-webkit-outer-spin-button { 54 | -webkit-appearance: none; 55 | margin: 0; 56 | } 57 | 58 | .range-input-value { 59 | appearance: textfield; 60 | -moz-appearance: textfield; 61 | display: inline; 62 | color: darkslategray; 63 | width: 14px; 64 | margin: 6px 0; 65 | border: none; 66 | border-radius: 4px; 67 | font-size: 13px; 68 | text-align: center; 69 | } 70 | 71 | .input-value-wrapper { 72 | font-size: 12px; 73 | padding: 4px; 74 | padding-bottom: 3px; 75 | border: 1px solid #ccc; 76 | border-radius: 4px; 77 | } 78 | 79 | .input-value-wrapper input { 80 | width: 24px; 81 | 82 | border-radius: 4px; 83 | border: none; 84 | padding: 0px; 85 | outline: none; 86 | } 87 | 88 | .dimensions { 89 | display: flex; 90 | flex-direction: column; 91 | gap: 8px; 92 | } 93 | 94 | button[type='button'], 95 | button[type='submit'] { 96 | padding: 8px 16px; /* Existing styles... */ 97 | border: none; 98 | border-radius: 4px; 99 | cursor: pointer; 100 | } 101 | 102 | .save-button { 103 | background-color: #007bff; 104 | color: white; 105 | } 106 | 107 | button[type='button'] { 108 | background-color: #eee; 109 | color: black; 110 | } 111 | 112 | /* Styles for the "X" icon button */ 113 | .delete-preset-button { 114 | padding: 6px !important; /* Reduce padding for smaller button */ 115 | width: 24px !important; /* Set fixed width for the button */ 116 | height: 24px !important; /* Set fixed height for the button */ 117 | display: flex !important; 118 | justify-content: center !important; 119 | align-items: center !important; 120 | background-color: #721c24 !important; 121 | opacity: 50%; 122 | font-size: xx-small; 123 | color: #eee !important; 124 | border: 1px solid #f5c6cb; 125 | border-radius: 8px !important; 126 | font-weight: 100 !important; 127 | } 128 | 129 | .delete-preset-button:hover { 130 | opacity: 75%; 131 | transition: opacity 0.3s; 132 | } 133 | -------------------------------------------------------------------------------- /src/renderer/components/tabs/content/ResizeTabContent/ResizeTabContent.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import useKeybindHandler from '@/renderer/hooks/useKeybindHandler'; 3 | import { 4 | WindowSizePercentage, 5 | useSettingsContext, 6 | } from '@/renderer/providers/SettingsProvider'; 7 | import './ResizeTabContent.css'; 8 | 9 | function ResizeTabContent() { 10 | const { settings, saveResizeSettings } = useSettingsContext(); 11 | 12 | const [presets, setPresets] = useState( 13 | settings.resizeWindow.windowSizePercentages, 14 | ); 15 | 16 | const { 17 | inputRef, 18 | keybind, 19 | handleKeyDown, 20 | handleSubmit, 21 | handleFocus, 22 | handleBlur, 23 | } = useKeybindHandler(settings.resizeWindow.keybinding, (newKeybind) => 24 | saveResizeSettings(newKeybind, presets), 25 | ); 26 | 27 | const handlePresetChange = ( 28 | index: number, 29 | field: 'width' | 'height', 30 | value: string, 31 | ) => { 32 | const updatedPresets = [...presets]; 33 | updatedPresets[index][field] = value; 34 | setPresets(updatedPresets); 35 | }; 36 | 37 | const addPreset = () => { 38 | setPresets([...presets, { width: '50', height: '50' }]); 39 | }; 40 | 41 | const removePreset = (index: number) => { 42 | const updatedPresets = [...presets]; 43 | updatedPresets.splice(index, 1); // Remove the preset at the specified index 44 | setPresets(updatedPresets); 45 | }; 46 | 47 | return ( 48 |
49 |

Resize Window

50 |
51 |
52 | 68 | 69 | {presets.map((preset, index) => ( 70 |
71 |
72 |

Preset {index + 1}

73 | 97 |
98 |
99 | 126 | 127 | 154 |
155 | {index !== presets.length - 1 &&
} 156 |
157 | ))} 158 | 159 | 162 | 165 |
166 |
167 |
168 | ); 169 | } 170 | 171 | export default ResizeTabContent; 172 | -------------------------------------------------------------------------------- /src/renderer/hooks/useKeybindHandler.ts: -------------------------------------------------------------------------------- 1 | import { 2 | useCallback, 3 | useEffect, 4 | useRef, 5 | useState, 6 | KeyboardEvent, 7 | FormEvent, 8 | } from 'react'; 9 | import { useRecordHotkeys } from 'react-hotkeys-hook'; 10 | import { capitalizeFirstLetterOfEachWord } from '@/renderer/utils'; 11 | 12 | export default function useKeybindHandler( 13 | initialKeybind: string, 14 | onSave: (keybind: string) => void, 15 | ) { 16 | const inputRef = useRef(null); 17 | const [keybind, setKeybind] = useState( 18 | capitalizeFirstLetterOfEachWord(initialKeybind), 19 | ); 20 | 21 | const [keys, { start, stop, resetKeys, isRecording }] = useRecordHotkeys(); 22 | 23 | const filterUnwantedKeys = (keysList: string[]) => 24 | keysList.filter((s) => s !== 'escape' && s !== 'backspace'); 25 | 26 | const updateInputRef = useCallback(() => { 27 | const recordedKeys = filterUnwantedKeys(Array.from(keys)).join(' + '); 28 | const formattedString = capitalizeFirstLetterOfEachWord( 29 | recordedKeys.toString(), 30 | ); 31 | if (keys.size && inputRef?.current && formattedString) { 32 | inputRef.current.value = formattedString; 33 | setKeybind(formattedString); 34 | } 35 | }, [keys]); 36 | 37 | // Required for immidiate ui updates in the input 38 | useEffect(() => { 39 | updateInputRef(); 40 | }, [updateInputRef]); 41 | 42 | const handleKeyDown = useCallback( 43 | (e: KeyboardEvent) => { 44 | const filteredKeys = filterUnwantedKeys(Array.from(keys)); 45 | const lastKeyInput = e.key; 46 | 47 | switch (lastKeyInput) { 48 | case 'Escape': 49 | setKeybind(''); 50 | resetKeys(); 51 | break; 52 | 53 | case 'Backspace': 54 | if (filteredKeys.length) { 55 | filteredKeys.pop(); 56 | keys.clear(); 57 | filteredKeys.forEach((key) => keys.add(key)); 58 | } 59 | break; 60 | 61 | default: 62 | break; 63 | } 64 | updateInputRef(); 65 | }, 66 | [keys, updateInputRef, resetKeys], 67 | ); 68 | 69 | const handleSubmit = (event: FormEvent) => { 70 | event.preventDefault(); 71 | onSave(keybind); 72 | stop(); 73 | }; 74 | 75 | const handleFocus = () => { 76 | if (!isRecording || !keys.size) start(); 77 | }; 78 | 79 | const handleBlur = () => { 80 | if (isRecording) stop(); 81 | }; 82 | 83 | return { 84 | inputRef, 85 | keybind, 86 | handleKeyDown, 87 | handleSubmit, 88 | handleFocus, 89 | handleBlur, 90 | }; 91 | } 92 | -------------------------------------------------------------------------------- /src/renderer/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Window Center and Resize! 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /src/renderer/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client'; 2 | import App from './App'; 3 | 4 | const container = document.getElementById('root') as HTMLElement; 5 | const root = createRoot(container); 6 | root.render(); 7 | -------------------------------------------------------------------------------- /src/renderer/preload.d.ts: -------------------------------------------------------------------------------- 1 | import { ElectronHandler } from '../main/preload'; 2 | 3 | declare global { 4 | // eslint-disable-next-line no-unused-vars 5 | interface Window { 6 | electron: ElectronHandler; 7 | } 8 | } 9 | 10 | export {}; 11 | -------------------------------------------------------------------------------- /src/renderer/providers/SettingsProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | createContext, 3 | useContext, 4 | useReducer, 5 | useEffect, 6 | useMemo, 7 | ReactNode, 8 | } from 'react'; 9 | 10 | import defaultSettings from '@/constants/defaultSettings.json'; 11 | 12 | // Define the shape of your settings 13 | export interface WindowSizePercentage { 14 | width: string; 15 | height: string; 16 | } 17 | 18 | export interface Settings { 19 | centerWindow: { 20 | keybinding: string; 21 | }; 22 | resizeWindow: { 23 | keybinding: string; 24 | windowSizePercentages: WindowSizePercentage[]; 25 | }; 26 | } 27 | 28 | interface SettingsContextProps extends Settings { 29 | settings: Settings; 30 | saveCenterSettings: (centerKeybind: string) => void; 31 | saveResizeSettings: ( 32 | keybinding: string, 33 | windowSizePercentages: WindowSizePercentage[], 34 | ) => void; 35 | resetSettings: () => void; 36 | } 37 | 38 | export const SettingsContext = createContext(null); 39 | 40 | export const useSettingsContext = () => { 41 | const context = useContext(SettingsContext); 42 | if (!context) { 43 | throw new Error( 44 | 'useSettingsContext must be used within a SettingsProvider', 45 | ); 46 | } 47 | return context; 48 | }; 49 | 50 | type SettingsAction = 51 | | { type: 'LOAD_SETTINGS'; payload: Settings } 52 | | { type: 'SAVE_CENTER_SETTINGS'; payload: { centerKeybind: string } } 53 | | { 54 | type: 'SAVE_RESIZE_SETTINGS'; 55 | payload: { 56 | keybinding: string; 57 | windowSizePercentages: WindowSizePercentage[]; 58 | }; 59 | } 60 | | { type: 'RESET_SETTINGS' }; 61 | 62 | function settingsReducer(state: Settings, action: SettingsAction): Settings { 63 | switch (action.type) { 64 | case 'LOAD_SETTINGS': 65 | return { ...state, ...action.payload }; 66 | case 'SAVE_CENTER_SETTINGS': 67 | return { 68 | ...state, 69 | centerWindow: { 70 | ...state.centerWindow, 71 | keybinding: action.payload.centerKeybind, 72 | }, 73 | }; 74 | case 'SAVE_RESIZE_SETTINGS': 75 | return { 76 | ...state, 77 | resizeWindow: { 78 | ...state.resizeWindow, 79 | keybinding: action.payload.keybinding, 80 | windowSizePercentages: action.payload.windowSizePercentages, 81 | }, 82 | }; 83 | case 'RESET_SETTINGS': 84 | return defaultSettings; 85 | default: 86 | return state; 87 | } 88 | } 89 | 90 | function SettingsProvider({ children }: { children: ReactNode }) { 91 | const [state, dispatch] = useReducer(settingsReducer, defaultSettings); 92 | 93 | useEffect(() => { 94 | const loadSettings = async () => { 95 | const settings = await window.electron.ipcRenderer.invoke('get-settings'); 96 | 97 | dispatch({ type: 'LOAD_SETTINGS', payload: settings }); 98 | }; 99 | 100 | loadSettings(); 101 | 102 | window.electron.ipcRenderer.on( 103 | 'settings-updated', 104 | (event, updatedSettings: any) => { 105 | dispatch({ type: 'LOAD_SETTINGS', payload: updatedSettings }); 106 | }, 107 | ); 108 | 109 | return () => { 110 | window.electron.ipcRenderer.removeAllListeners('settings-updated'); 111 | }; 112 | }, []); 113 | 114 | const saveCenterSettings = (centerKeybind: string) => { 115 | window.electron.ipcRenderer.invoke('save-center-settings', centerKeybind); 116 | dispatch({ type: 'SAVE_CENTER_SETTINGS', payload: { centerKeybind } }); 117 | }; 118 | 119 | const saveResizeSettings = ( 120 | keybinding: string, 121 | windowSizePercentages: WindowSizePercentage[], 122 | ) => { 123 | window.electron.ipcRenderer.invoke('save-resize-settings', { 124 | keybinding, 125 | windowSizePercentages, 126 | }); 127 | dispatch({ 128 | type: 'SAVE_RESIZE_SETTINGS', 129 | payload: { keybinding, windowSizePercentages }, 130 | }); 131 | }; 132 | 133 | const resetSettings = () => { 134 | window.electron.ipcRenderer.invoke('reset-settings'); 135 | dispatch({ type: 'RESET_SETTINGS' }); 136 | }; 137 | 138 | const contextValue = useMemo( 139 | () => ({ 140 | ...state, 141 | settings: state, 142 | saveCenterSettings, 143 | saveResizeSettings, 144 | resetSettings, 145 | }), 146 | [state], 147 | ); 148 | 149 | return ( 150 | 151 | {children} 152 | 153 | ); 154 | } 155 | 156 | export default SettingsProvider; 157 | -------------------------------------------------------------------------------- /src/renderer/providers/TabsProvider.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ReactNode, 3 | useContext, 4 | useMemo, 5 | useReducer, 6 | createContext, 7 | } from 'react'; 8 | 9 | export type TTabAction = 'center' | 'resize'; 10 | 11 | export interface TabsState { 12 | activeTab: TTabAction; 13 | } 14 | 15 | interface TabsContextProps extends TabsState { 16 | setActiveTab: (tab: TTabAction) => void; 17 | } 18 | 19 | export const TabsContext = createContext(null); 20 | 21 | export const useTabsContext = () => { 22 | const context = useContext(TabsContext); 23 | if (!context) { 24 | throw new Error('useTabsContext must be used within a TabsProvider'); 25 | } 26 | return context; 27 | }; 28 | 29 | type TabsAction = { type: 'SET_ACTIVE_TAB'; payload: TTabAction }; 30 | 31 | function TabsProvider({ children }: { children: ReactNode }) { 32 | const tabsReducer = (state: TabsState, action: TabsAction): TabsState => { 33 | switch (action.type) { 34 | case 'SET_ACTIVE_TAB': 35 | return { ...state, activeTab: action.payload }; 36 | default: 37 | return state; 38 | } 39 | }; 40 | 41 | const [state, dispatch] = useReducer(tabsReducer, { activeTab: 'center' }); 42 | 43 | const setActiveTab = (tab: TTabAction) => { 44 | dispatch({ type: 'SET_ACTIVE_TAB', payload: tab }); 45 | }; 46 | 47 | const contextValue = useMemo(() => ({ ...state, setActiveTab }), [state]); 48 | 49 | return ( 50 | {children} 51 | ); 52 | } 53 | 54 | export default TabsProvider; 55 | -------------------------------------------------------------------------------- /src/renderer/utils.ts: -------------------------------------------------------------------------------- 1 | /* eslint import/prefer-default-export: off */ 2 | 3 | export function capitalizeFirstLetterOfEachWord(str: string): string { 4 | if (!str) return ''; 5 | return str 6 | .split(' ') 7 | .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) 8 | .join(' '); 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": ["src/*"] 6 | }, 7 | "incremental": true, 8 | "target": "es2022", 9 | "module": "commonjs", 10 | "lib": ["dom", "es2022"], 11 | "jsx": "react-jsx", 12 | "strict": true, 13 | "sourceMap": true, 14 | "moduleResolution": "node", 15 | "esModuleInterop": true, 16 | "allowSyntheticDefaultImports": true, 17 | "resolveJsonModule": true, 18 | "allowJs": true, 19 | "outDir": ".erb/dll", 20 | "skipLibCheck": true 21 | }, 22 | "exclude": ["test", "release/build", "release/app/dist", ".erb/dll"] 23 | } 24 | --------------------------------------------------------------------------------