├── .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 └── workflows │ ├── codeql-analysis.yml │ ├── publish.yml │ └── test.yml ├── .gitignore ├── .husky └── pre-commit ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── assets ├── assets.d.ts ├── entitlements.mac.plist ├── icon.icns ├── icon.ico ├── icon.png ├── icon.svg ├── logo_full.png ├── stage_previews │ ├── stage_2_75.jpg │ ├── stage_2_animal_city.jpg │ ├── stage_2_animal_island.jpg │ ├── stage_2_animal_village.jpg │ ├── stage_2_balloonfight.jpg │ ├── stage_2_battlefield.jpg │ ├── stage_2_battlefieldl.jpg │ ├── stage_2_bayo_clock.jpg │ ├── stage_2_bossstage_final2.jpg │ ├── stage_2_brave_altar.jpg │ ├── stage_2_buddy_spiral.jpg │ ├── stage_2_demon_dojo.jpg │ ├── stage_2_dk_jungle.jpg │ ├── stage_2_dk_lodge.jpg │ ├── stage_2_dk_waterfall.jpg │ ├── stage_2_dolly_stadium.jpg │ ├── stage_2_dracula_castle.jpg │ ├── stage_2_duckhunt.jpg │ ├── stage_2_end.jpg │ ├── stage_2_fe_arena.jpg │ ├── stage_2_fe_colloseum.jpg │ ├── stage_2_fe_shrine.jpg │ ├── stage_2_fe_siege.jpg │ ├── stage_2_ff_cave.jpg │ ├── stage_2_ff_midgar.jpg │ ├── stage_2_flatzonex.jpg │ ├── stage_2_fox_corneria.jpg │ ├── stage_2_fox_lylatcruise.jpg │ ├── stage_2_fox_venom.jpg │ ├── stage_2_fzero_bigblue.jpg │ ├── stage_2_fzero_mutecity3ds.jpg │ ├── stage_2_fzero_porttown.jpg │ ├── stage_2_icarus_angeland.jpg │ ├── stage_2_icarus_skyworld.jpg │ ├── stage_2_icarus_uprising.jpg │ ├── stage_2_ice_top.jpg │ ├── stage_2_jack_mementoes.jpg │ ├── stage_2_kart_circuitfor.jpg │ ├── stage_2_kart_circuitx.jpg │ ├── stage_2_kirby_cave.jpg │ ├── stage_2_kirby_fountain.jpg │ ├── stage_2_kirby_gameboy.jpg │ ├── stage_2_kirby_greens.jpg │ ├── stage_2_kirby_halberd.jpg │ ├── stage_2_kirby_pupupu64.jpg │ ├── stage_2_luigimansion.jpg │ ├── stage_2_mario_3dland.jpg │ ├── stage_2_mario_castle64.jpg │ ├── stage_2_mario_castledx.jpg │ ├── stage_2_mario_dolpic.jpg │ ├── stage_2_mario_galaxy.jpg │ ├── stage_2_mario_maker+us.jpg │ ├── stage_2_mario_newbros2.jpg │ ├── stage_2_mario_odyssey.jpg │ ├── stage_2_mario_paper.jpg │ ├── stage_2_mario_past64.jpg │ ├── stage_2_mario_pastusa.jpg │ ├── stage_2_mario_pastx.jpg │ ├── stage_2_mario_rainbow.jpg │ ├── stage_2_mario_uworld.jpg │ ├── stage_2_mariobros.jpg │ ├── stage_2_metroid_kraid.jpg │ ├── stage_2_metroid_norfair.jpg │ ├── stage_2_metroid_orpheon.jpg │ ├── stage_2_metroid_zebesdx.jpg │ ├── stage_2_mg_shadowmoses.jpg │ ├── stage_2_mother_fourside.jpg │ ├── stage_2_mother_magicant.jpg │ ├── stage_2_mother_newpork.jpg │ ├── stage_2_mother_onett.jpg │ ├── stage_2_nintendogs.jpg │ ├── stage_2_pac_land.jpg │ ├── stage_2_pickel_world.jpg │ ├── stage_2_pictochat.jpg │ ├── stage_2_pikmin_garden.jpg │ ├── stage_2_pikmin_planet.jpg │ ├── stage_2_pilotwings.jpg │ ├── stage_2_plankton.jpg │ ├── stage_2_poke_kalos.jpg │ ├── stage_2_poke_stadium.jpg │ ├── stage_2_poke_stadium2.jpg │ ├── stage_2_poke_tengam.jpg │ ├── stage_2_poke_tower.jpg │ ├── stage_2_poke_unova.jpg │ ├── stage_2_poke_yamabuki.jpg │ ├── stage_2_punchoutsb.jpg │ ├── stage_2_rock_wily.jpg │ ├── stage_2_sf_suzaku.jpg │ ├── stage_2_sonic_greenhill.jpg │ ├── stage_2_sonic_windyhill.jpg │ ├── stage_2_spla_parking.jpg │ ├── stage_2_streetpass.jpg │ ├── stage_2_tantan_spring.jpg │ ├── stage_2_tomodachi.jpg │ ├── stage_2_trail_castle.jpg │ ├── stage_2_training.jpg │ ├── stage_2_wario_gamer.jpg │ ├── stage_2_wario_madein.jpg │ ├── stage_2_wiifit.jpg │ ├── stage_2_wreckingcrew.jpg │ ├── stage_2_wufuisland.jpg │ ├── stage_2_xeno_alst.jpg │ ├── stage_2_xeno_gaur.jpg │ ├── stage_2_yoshi_cartboard.jpg │ ├── stage_2_yoshi_island.jpg │ ├── stage_2_yoshi_story.jpg │ ├── stage_2_yoshi_yoster.jpg │ ├── stage_2_zelda_gerudo.jpg │ ├── stage_2_zelda_greatbay.jpg │ ├── stage_2_zelda_hyrule.jpg │ ├── stage_2_zelda_oldin.jpg │ ├── stage_2_zelda_pirates.jpg │ ├── stage_2_zelda_skyward.jpg │ ├── stage_2_zelda_temple.jpg │ ├── stage_2_zelda_tower.jpg │ └── stage_2_zelda_train.jpg └── theme.wav ├── build.py ├── package-lock.json ├── package.json ├── release └── app │ ├── package-lock.json │ └── package.json ├── src ├── __tests__ │ └── App.test.tsx ├── main │ ├── config.ts │ ├── main.ts │ ├── menu.ts │ ├── preload.ts │ ├── request_handler.ts │ └── util.ts └── renderer │ ├── @types │ └── image.d.ts │ ├── app.tsx │ ├── components │ ├── buttons │ │ ├── dev_tools_buttons.tsx │ │ ├── focus_button.tsx │ │ ├── focus_checkbox.tsx │ │ ├── focus_combo.tsx │ │ ├── navigate_button.tsx │ │ ├── prerelease_beta_button.tsx │ │ ├── scroll_focus_button.tsx │ │ └── update_button.tsx │ ├── changelog.tsx │ ├── error_page.tsx │ ├── fullscreen_div.tsx │ ├── header.tsx │ ├── logging │ │ ├── log_list.tsx │ │ ├── log_popout.tsx │ │ └── log_window.tsx │ ├── logo_right.tsx │ ├── popup.tsx │ ├── progress_bar.tsx │ ├── sidebar.tsx │ └── sliding_background.tsx │ ├── constants.ts │ ├── index.ejs │ ├── index.tsx │ ├── operations │ ├── backend.ts │ ├── background_music.ts │ ├── focus_singleton.ts │ ├── github_utils.ts │ ├── install.ts │ ├── launcher_config.ts │ ├── log_listener.ts │ ├── log_singleton.ts │ ├── popup_data.ts │ ├── stage_config.ts │ ├── stage_info.ts │ ├── update.ts │ └── verify.ts │ ├── preload.d.ts │ ├── routes │ ├── loading.tsx │ ├── menus │ │ ├── abstract_menu.tsx │ │ ├── checking_installed.tsx │ │ ├── info_box.tsx │ │ ├── main.tsx │ │ ├── main_menu.tsx │ │ ├── menu.tsx │ │ ├── not_installed_menu.tsx │ │ ├── options_menu.tsx │ │ ├── pr_installed_menu.tsx │ │ ├── pull_request_preview.tsx │ │ └── tools_menu.tsx │ ├── pull_request_menu.tsx │ └── stage_config │ │ ├── stage_config_menu.tsx │ │ ├── stage_list_box.tsx │ │ └── stage_preview.tsx │ └── styles │ ├── index.css │ ├── infobox.css │ ├── opening.css │ ├── progress.css │ └── sidebar.css ├── switch ├── Cargo.lock ├── Cargo.toml ├── build.rs └── src │ ├── lib.rs │ └── stage_config.rs └── 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 webpackPaths from './webpack.paths'; 7 | import { dependencies as externals } from '../../release/app/package.json'; 8 | 9 | const WebpackAssetsManifest = require('webpack-assets-manifest'); 10 | 11 | const configuration: webpack.Configuration = { 12 | externals: [...Object.keys(externals || {})], 13 | 14 | stats: 'errors-only', 15 | 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.[jt]sx?$/, 20 | exclude: /node_modules/, 21 | use: { 22 | loader: 'ts-loader', 23 | options: { 24 | // Remove this line to enable type checking in webpack builds 25 | transpileOnly: true, 26 | compilerOptions: { 27 | module: 'esnext', 28 | }, 29 | }, 30 | }, 31 | }, 32 | ], 33 | }, 34 | 35 | output: { 36 | path: webpackPaths.srcPath, 37 | // https://github.com/webpack/webpack/issues/1114 38 | library: { 39 | type: 'commonjs2', 40 | }, 41 | }, 42 | 43 | /** 44 | * Determine the array of extensions that should be used to resolve modules. 45 | */ 46 | resolve: { 47 | extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'], 48 | modules: [webpackPaths.srcPath, 'node_modules'], 49 | }, 50 | 51 | plugins: [ 52 | new WebpackAssetsManifest({ 53 | writeToDisk: true, 54 | }), 55 | new webpack.EnvironmentPlugin({ 56 | NODE_ENV: 'production', 57 | }), 58 | ], 59 | }; 60 | 61 | export default configuration; 62 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.eslint.ts: -------------------------------------------------------------------------------- 1 | /* eslint import/no-unresolved: off, import/no-self-import: off */ 2 | 3 | module.exports = require('./webpack.config.renderer.dev').default; 4 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.main.prod.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Webpack config for production electron main process 3 | */ 4 | 5 | import path from 'path'; 6 | import webpack from 'webpack'; 7 | import { merge } from 'webpack-merge'; 8 | import TerserPlugin from 'terser-webpack-plugin'; 9 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 10 | import baseConfig from './webpack.config.base'; 11 | import webpackPaths from './webpack.paths'; 12 | import checkNodeEnv from '../scripts/check-node-env'; 13 | import deleteSourceMaps from '../scripts/delete-source-maps'; 14 | 15 | checkNodeEnv('production'); 16 | deleteSourceMaps(); 17 | 18 | const configuration: webpack.Configuration = { 19 | devtool: 'source-map', 20 | 21 | mode: 'production', 22 | 23 | target: 'electron-main', 24 | 25 | entry: { 26 | main: path.join(webpackPaths.srcMainPath, 'main.ts'), 27 | preload: path.join(webpackPaths.srcMainPath, 'preload.ts'), 28 | }, 29 | 30 | output: { 31 | path: webpackPaths.distMainPath, 32 | filename: '[name].js', 33 | library: { 34 | type: 'umd', 35 | }, 36 | }, 37 | 38 | optimization: { 39 | minimizer: [ 40 | new TerserPlugin({ 41 | parallel: true, 42 | }), 43 | ], 44 | }, 45 | 46 | plugins: [ 47 | new BundleAnalyzerPlugin({ 48 | analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled', 49 | }), 50 | 51 | /** 52 | * Create global constants which can be configured at compile time. 53 | * 54 | * Useful for allowing different behaviour between development builds and 55 | * release builds 56 | * 57 | * NODE_ENV should be production so that modules do not perform certain 58 | * development checks 59 | */ 60 | new webpack.EnvironmentPlugin({ 61 | NODE_ENV: 'production', 62 | DEBUG_PROD: false, 63 | START_MINIMIZED: false, 64 | }), 65 | 66 | new webpack.DefinePlugin({ 67 | 'process.type': '"main"', 68 | }), 69 | ], 70 | 71 | /** 72 | * Disables webpack processing of __dirname and __filename. 73 | * If you run the bundle in node.js it falls back to these values of node.js. 74 | * https://github.com/webpack/webpack/issues/2010 75 | */ 76 | node: { 77 | __dirname: false, 78 | __filename: false, 79 | }, 80 | }; 81 | 82 | export default merge(baseConfig, configuration); 83 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.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 | const WebpackAssetsManifest = require('webpack-assets-manifest'); 15 | 16 | // When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's 17 | // at the dev webpack config is not accidentally run in a production environment 18 | if (process.env.NODE_ENV === 'production') { 19 | checkNodeEnv('development'); 20 | } 21 | 22 | const port = process.env.PORT || 1212; 23 | const manifest = path.resolve(webpackPaths.dllPath, 'renderer.json'); 24 | const skipDLLs = 25 | module.parent?.filename.includes('webpack.config.renderer.dev.dll') || 26 | module.parent?.filename.includes('webpack.config.eslint'); 27 | 28 | /** 29 | * Warn if the DLL is not built 30 | */ 31 | if ( 32 | !skipDLLs && 33 | !(fs.existsSync(webpackPaths.dllPath) && fs.existsSync(manifest)) 34 | ) { 35 | console.log( 36 | chalk.black.bgYellow.bold( 37 | 'The DLL files are missing. Sit back while we build them for you with "npm run build-dll"' 38 | ) 39 | ); 40 | execSync('npm run postinstall'); 41 | } 42 | 43 | const configuration: webpack.Configuration = { 44 | devtool: 'inline-source-map', 45 | 46 | mode: 'development', 47 | 48 | target: ['web', 'electron-renderer'], 49 | 50 | entry: [ 51 | `webpack-dev-server/client?http://localhost:${port}/dist`, 52 | 'webpack/hot/only-dev-server', 53 | path.join(webpackPaths.srcRendererPath, 'index.tsx'), 54 | ], 55 | 56 | output: { 57 | path: webpackPaths.distRendererPath, 58 | publicPath: '/', 59 | filename: 'renderer.dev.js', 60 | library: { 61 | type: 'umd', 62 | }, 63 | }, 64 | 65 | module: { 66 | rules: [ 67 | { 68 | test: /\.s?css$/, 69 | use: [ 70 | 'style-loader', 71 | { 72 | loader: 'css-loader', 73 | options: { 74 | modules: true, 75 | sourceMap: true, 76 | importLoaders: 1, 77 | }, 78 | }, 79 | 'sass-loader', 80 | ], 81 | include: /\.module\.s?(c|a)ss$/, 82 | }, 83 | { 84 | test: /\.s?css$/, 85 | use: ['style-loader', 'css-loader', 'sass-loader'], 86 | exclude: /\.module\.s?(c|a)ss$/, 87 | }, 88 | // Fonts 89 | { 90 | test: /\.(woff|woff2|eot|ttf|otf)$/i, 91 | type: 'asset/resource', 92 | }, 93 | // Images 94 | { 95 | test: /\.(png|jpg|jpeg|gif)$/i, 96 | type: 'asset/resource', 97 | generator: { 98 | filename: 'static/[name][ext]', 99 | }, 100 | }, 101 | // music 102 | { 103 | test: /\.(wav)$/i, 104 | type: 'asset/resource', 105 | generator: { 106 | filename: 'static/[name][ext]', 107 | }, 108 | }, 109 | // SVG 110 | { 111 | test: /\.svg$/, 112 | use: [ 113 | { 114 | loader: '@svgr/webpack', 115 | options: { 116 | prettier: false, 117 | svgo: false, 118 | svgoConfig: { 119 | plugins: [{ removeViewBox: false }], 120 | }, 121 | titleProp: true, 122 | ref: true, 123 | }, 124 | }, 125 | 'file-loader', 126 | ], 127 | }, 128 | ], 129 | }, 130 | plugins: [ 131 | ...(skipDLLs 132 | ? [] 133 | : [ 134 | new webpack.DllReferencePlugin({ 135 | context: webpackPaths.dllPath, 136 | manifest: require(manifest), 137 | sourceType: 'var', 138 | }), 139 | ]), 140 | 141 | new webpack.NoEmitOnErrorsPlugin(), 142 | 143 | /** 144 | * Create global constants which can be configured at compile time. 145 | * 146 | * Useful for allowing different behaviour between development builds and 147 | * release builds 148 | * 149 | * NODE_ENV should be production so that modules do not perform certain 150 | * development checks 151 | * 152 | * By default, use 'development' as NODE_ENV. This can be overriden with 153 | * 'staging', for example, by changing the ENV variables in the npm scripts 154 | */ 155 | new WebpackAssetsManifest({ 156 | writeToDisk: true, 157 | }), 158 | new webpack.EnvironmentPlugin({ 159 | NODE_ENV: 'development', 160 | }), 161 | 162 | new webpack.LoaderOptionsPlugin({ 163 | debug: true, 164 | }), 165 | 166 | new ReactRefreshWebpackPlugin(), 167 | 168 | new HtmlWebpackPlugin({ 169 | filename: path.join('index.html'), 170 | template: path.join(webpackPaths.srcRendererPath, 'index.ejs'), 171 | minify: { 172 | collapseWhitespace: true, 173 | removeAttributeQuotes: true, 174 | removeComments: true, 175 | }, 176 | isBrowser: false, 177 | env: process.env.NODE_ENV, 178 | isDevelopment: process.env.NODE_ENV !== 'production', 179 | nodeModules: webpackPaths.appNodeModulesPath, 180 | }), 181 | ], 182 | 183 | node: { 184 | __dirname: false, 185 | __filename: false, 186 | }, 187 | 188 | devServer: { 189 | port, 190 | compress: true, 191 | hot: true, 192 | headers: { 'Access-Control-Allow-Origin': '*' }, 193 | static: { 194 | publicPath: '/', 195 | }, 196 | historyApiFallback: { 197 | verbose: true, 198 | disableDotRule: true, 199 | }, 200 | setupMiddlewares(middlewares) { 201 | console.log('Starting preload.js builder...'); 202 | const preloadProcess = spawn('npm', ['run', 'start:preload'], { 203 | shell: true, 204 | stdio: 'inherit', 205 | }) 206 | .on('close', (code: number) => process.exit(code!)) 207 | .on('error', (spawnError) => console.error(spawnError)); 208 | 209 | console.log('Starting Main Process...'); 210 | let args = ['run', 'start:main']; 211 | if (process.env.MAIN_ARGS) { 212 | args = args.concat( 213 | ['--', ...process.env.MAIN_ARGS.matchAll(/"[^"]+"|[^\s"]+/g)].flat() 214 | ); 215 | } 216 | spawn('npm', args, { 217 | shell: true, 218 | stdio: 'inherit', 219 | }) 220 | .on('close', (code: number) => { 221 | preloadProcess.kill(); 222 | process.exit(code!); 223 | }) 224 | .on('error', (spawnError) => console.error(spawnError)); 225 | return middlewares; 226 | }, 227 | }, 228 | }; 229 | 230 | export default merge(baseConfig, configuration); 231 | -------------------------------------------------------------------------------- /.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 | const WebpackAssetsManifest = require('webpack-assets-manifest'); 19 | 20 | checkNodeEnv('production'); 21 | deleteSourceMaps(); 22 | 23 | const configuration: webpack.Configuration = { 24 | devtool: 'source-map', 25 | 26 | mode: 'production', 27 | 28 | target: ['web', 'electron-renderer'], 29 | 30 | entry: [path.join(webpackPaths.srcRendererPath, 'index.tsx')], 31 | 32 | output: { 33 | path: webpackPaths.distRendererPath, 34 | publicPath: './', 35 | filename: 'renderer.js', 36 | library: { 37 | type: 'umd', 38 | }, 39 | }, 40 | 41 | module: { 42 | rules: [ 43 | { 44 | test: /\.s?(a|c)ss$/, 45 | use: [ 46 | MiniCssExtractPlugin.loader, 47 | { 48 | loader: 'css-loader', 49 | options: { 50 | modules: true, 51 | sourceMap: true, 52 | importLoaders: 1, 53 | }, 54 | }, 55 | 'sass-loader', 56 | ], 57 | include: /\.module\.s?(c|a)ss$/, 58 | }, 59 | { 60 | test: /\.s?(a|c)ss$/, 61 | use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'], 62 | exclude: /\.module\.s?(c|a)ss$/, 63 | }, 64 | // Fonts 65 | { 66 | test: /\.(woff|woff2|eot|ttf|otf)$/i, 67 | type: 'asset/resource', 68 | }, 69 | // Images 70 | { 71 | test: /\.(png|jpg|jpeg|gif)$/i, 72 | type: 'asset/resource', 73 | generator: { 74 | filename: 'static/[name][ext]', 75 | }, 76 | }, 77 | // music 78 | { 79 | test: /\.(wav)$/i, 80 | type: 'asset/resource', 81 | generator: { 82 | filename: 'static/[name][ext]', 83 | }, 84 | }, 85 | // SVG 86 | { 87 | test: /\.svg$/, 88 | use: [ 89 | { 90 | loader: '@svgr/webpack', 91 | options: { 92 | prettier: false, 93 | svgo: false, 94 | svgoConfig: { 95 | plugins: [{ removeViewBox: false }], 96 | }, 97 | titleProp: true, 98 | ref: true, 99 | }, 100 | }, 101 | 'file-loader', 102 | ], 103 | }, 104 | ], 105 | }, 106 | 107 | optimization: { 108 | minimize: true, 109 | minimizer: [ 110 | new TerserPlugin({ 111 | parallel: true, 112 | }), 113 | new CssMinimizerPlugin(), 114 | ], 115 | }, 116 | 117 | plugins: [ 118 | /** 119 | * Create global constants which can be configured at compile time. 120 | * 121 | * Useful for allowing different behaviour between development builds and 122 | * release builds 123 | * 124 | * NODE_ENV should be production so that modules do not perform certain 125 | * development checks 126 | */ 127 | new WebpackAssetsManifest({ 128 | writeToDisk: true, 129 | }), 130 | new webpack.EnvironmentPlugin({ 131 | NODE_ENV: 'production', 132 | DEBUG_PROD: false, 133 | }), 134 | 135 | new MiniCssExtractPlugin({ 136 | filename: 'style.css', 137 | }), 138 | 139 | new BundleAnalyzerPlugin({ 140 | analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled', 141 | }), 142 | 143 | new HtmlWebpackPlugin({ 144 | filename: 'index.html', 145 | template: path.join(webpackPaths.srcRendererPath, 'index.ejs'), 146 | minify: { 147 | collapseWhitespace: true, 148 | removeAttributeQuotes: true, 149 | removeComments: true, 150 | }, 151 | isBrowser: false, 152 | isDevelopment: process.env.NODE_ENV !== 'production', 153 | }), 154 | 155 | new webpack.DefinePlugin({ 156 | 'process.type': '"renderer"', 157 | }), 158 | ], 159 | }; 160 | 161 | export default merge(baseConfig, configuration); 162 | -------------------------------------------------------------------------------- /.erb/configs/webpack.paths.ts: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const rootPath = path.join(__dirname, '../..'); 4 | 5 | const dllPath = path.join(__dirname, '../dll'); 6 | 7 | const srcPath = path.join(rootPath, 'src'); 8 | const srcMainPath = path.join(srcPath, 'main'); 9 | const srcRendererPath = path.join(srcPath, 'renderer'); 10 | 11 | const releasePath = path.join(rootPath, 'release'); 12 | const appPath = path.join(releasePath, 'app'); 13 | const appPackagePath = path.join(appPath, 'package.json'); 14 | const appNodeModulesPath = path.join(appPath, 'node_modules'); 15 | const srcNodeModulesPath = path.join(srcPath, 'node_modules'); 16 | 17 | const distPath = path.join(appPath, 'dist'); 18 | const distMainPath = path.join(distPath, 'main'); 19 | const distRendererPath = path.join(distPath, 'renderer'); 20 | 21 | const buildPath = path.join(releasePath, 'build'); 22 | 23 | export default { 24 | rootPath, 25 | dllPath, 26 | srcPath, 27 | srcMainPath, 28 | srcRendererPath, 29 | releasePath, 30 | appPath, 31 | appPackagePath, 32 | appNodeModulesPath, 33 | srcNodeModulesPath, 34 | distPath, 35 | distMainPath, 36 | distRendererPath, 37 | buildPath, 38 | }; 39 | -------------------------------------------------------------------------------- /.erb/img/erb-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/.erb/img/erb-logo.png -------------------------------------------------------------------------------- /.erb/mocks/fileMock.js: -------------------------------------------------------------------------------- 1 | export default 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /.erb/scripts/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": "off", 4 | "global-require": "off", 5 | "import/no-dynamic-require": "off", 6 | "import/no-extraneous-dependencies": "off" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.erb/scripts/check-build-exists.ts: -------------------------------------------------------------------------------- 1 | // Check if the renderer and main bundles are built 2 | import path from 'path'; 3 | import chalk from 'chalk'; 4 | import fs from 'fs'; 5 | import webpackPaths from '../configs/webpack.paths'; 6 | 7 | const mainPath = path.join(webpackPaths.distMainPath, 'main.js'); 8 | const rendererPath = path.join(webpackPaths.distRendererPath, 'renderer.js'); 9 | 10 | if (!fs.existsSync(mainPath)) { 11 | throw new Error( 12 | chalk.whiteBright.bgRed.bold( 13 | 'The main process is not built yet. Build it by running "npm run build:main"' 14 | ) 15 | ); 16 | } 17 | 18 | if (!fs.existsSync(rendererPath)) { 19 | throw new Error( 20 | chalk.whiteBright.bgRed.bold( 21 | 'The renderer process is not built yet. Build it by running "npm run build:renderer"' 22 | ) 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /.erb/scripts/check-native-dep.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import chalk from 'chalk'; 3 | import { execSync } from 'child_process'; 4 | import { dependencies } from '../../package.json'; 5 | 6 | if (dependencies) { 7 | const dependenciesKeys = Object.keys(dependencies); 8 | const nativeDeps = fs 9 | .readdirSync('node_modules') 10 | .filter((folder) => fs.existsSync(`node_modules/${folder}/binding.gyp`)); 11 | if (nativeDeps.length === 0) { 12 | process.exit(0); 13 | } 14 | try { 15 | // Find the reason for why the dependency is installed. If it is installed 16 | // because of a devDependency then that is okay. Warn when it is installed 17 | // because of a dependency 18 | const { dependencies: dependenciesObject } = JSON.parse( 19 | execSync(`npm ls ${nativeDeps.join(' ')} --json`).toString() 20 | ); 21 | const rootDependencies = Object.keys(dependenciesObject); 22 | const filteredRootDependencies = rootDependencies.filter((rootDependency) => 23 | dependenciesKeys.includes(rootDependency) 24 | ); 25 | if (filteredRootDependencies.length > 0) { 26 | const plural = filteredRootDependencies.length > 1; 27 | console.log(` 28 | ${chalk.whiteBright.bgYellow.bold( 29 | 'Webpack does not work with native dependencies.' 30 | )} 31 | ${chalk.bold(filteredRootDependencies.join(', '))} ${ 32 | plural ? 'are native dependencies' : 'is a native dependency' 33 | } and should be installed inside of the "./release/app" folder. 34 | First, uninstall the packages from "./package.json": 35 | ${chalk.whiteBright.bgGreen.bold('npm uninstall your-package')} 36 | ${chalk.bold( 37 | 'Then, instead of installing the package to the root "./package.json":' 38 | )} 39 | ${chalk.whiteBright.bgRed.bold('npm install your-package')} 40 | ${chalk.bold('Install the package to "./release/app/package.json"')} 41 | ${chalk.whiteBright.bgGreen.bold( 42 | 'cd ./release/app && npm install your-package' 43 | )} 44 | Read more about native dependencies at: 45 | ${chalk.bold( 46 | 'https://electron-react-boilerplate.js.org/docs/adding-dependencies/#module-structure' 47 | )} 48 | `); 49 | process.exit(1); 50 | } 51 | } catch (e) { 52 | console.log('Native dependencies could not be checked'); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /.erb/scripts/check-node-env.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | export default function checkNodeEnv(expectedEnv) { 4 | if (!expectedEnv) { 5 | throw new Error('"expectedEnv" not set'); 6 | } 7 | 8 | if (process.env.NODE_ENV !== expectedEnv) { 9 | console.log( 10 | chalk.whiteBright.bgRed.bold( 11 | `"process.env.NODE_ENV" must be "${expectedEnv}" to use this webpack config` 12 | ) 13 | ); 14 | process.exit(2); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.erb/scripts/check-port-in-use.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import detectPort from 'detect-port'; 3 | 4 | const port = process.env.PORT || '1212'; 5 | 6 | detectPort(port, (err, availablePort) => { 7 | if (port !== String(availablePort)) { 8 | throw new Error( 9 | chalk.whiteBright.bgRed.bold( 10 | `Port "${port}" on "localhost" is already in use. Please use another port. ex: PORT=4343 npm start` 11 | ) 12 | ); 13 | } else { 14 | process.exit(0); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /.erb/scripts/clean.js: -------------------------------------------------------------------------------- 1 | import rimraf from 'rimraf'; 2 | import webpackPaths from '../configs/webpack.paths'; 3 | 4 | const foldersToRemove = [ 5 | webpackPaths.distPath, 6 | webpackPaths.buildPath, 7 | webpackPaths.dllPath, 8 | ]; 9 | 10 | foldersToRemove.forEach((folder) => { 11 | rimraf.sync(folder); 12 | }); 13 | -------------------------------------------------------------------------------- /.erb/scripts/delete-source-maps.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import rimraf from 'rimraf'; 3 | import webpackPaths from '../configs/webpack.paths'; 4 | 5 | export default function deleteSourceMaps() { 6 | rimraf.sync(path.join(webpackPaths.distMainPath, '*.js.map')); 7 | rimraf.sync(path.join(webpackPaths.distRendererPath, '*.js.map')); 8 | } 9 | -------------------------------------------------------------------------------- /.erb/scripts/electron-rebuild.js: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | import fs from 'fs'; 3 | import { dependencies } from '../../release/app/package.json'; 4 | import webpackPaths from '../configs/webpack.paths'; 5 | 6 | if ( 7 | Object.keys(dependencies || {}).length > 0 && 8 | fs.existsSync(webpackPaths.appNodeModulesPath) 9 | ) { 10 | const electronRebuildCmd = 11 | '../../node_modules/.bin/electron-rebuild --force --types prod,dev,optional --module-dir .'; 12 | const cmd = 13 | process.platform === 'win32' 14 | ? electronRebuildCmd.replace(/\//g, '\\') 15 | : electronRebuildCmd; 16 | execSync(cmd, { 17 | cwd: webpackPaths.appPath, 18 | stdio: 'inherit', 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /.erb/scripts/link-modules.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import webpackPaths from '../configs/webpack.paths'; 3 | 4 | const { srcNodeModulesPath } = webpackPaths; 5 | const { appNodeModulesPath } = webpackPaths; 6 | 7 | if (!fs.existsSync(srcNodeModulesPath) && fs.existsSync(appNodeModulesPath)) { 8 | fs.symlinkSync(appNodeModulesPath, srcNodeModulesPath, 'junction'); 9 | } 10 | -------------------------------------------------------------------------------- /.erb/scripts/notarize.js: -------------------------------------------------------------------------------- 1 | const { notarize } = require('electron-notarize'); 2 | const { build } = require('../../package.json'); 3 | 4 | exports.default = async function notarizeMacos(context) { 5 | const { electronPlatformName, appOutDir } = context; 6 | if (electronPlatformName !== 'darwin') { 7 | return; 8 | } 9 | 10 | if (process.env.CI !== 'true') { 11 | console.warn('Skipping notarizing step. Packaging is not running in CI'); 12 | return; 13 | } 14 | 15 | if (!('APPLE_ID' in process.env && 'APPLE_ID_PASS' in process.env)) { 16 | console.warn( 17 | 'Skipping notarizing step. APPLE_ID and APPLE_ID_PASS env variables must be set' 18 | ); 19 | return; 20 | } 21 | 22 | const appName = context.packager.appInfo.productFilename; 23 | 24 | await notarize({ 25 | appBundleId: build.appId, 26 | appPath: `${appOutDir}/${appName}.app`, 27 | appleId: process.env.APPLE_ID, 28 | appleIdPassword: process.env.APPLE_ID_PASS, 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Coverage directory used by tools like istanbul 11 | coverage 12 | .eslintcache 13 | 14 | # Dependency directory 15 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 16 | node_modules 17 | 18 | # OSX 19 | .DS_Store 20 | 21 | release/app/dist 22 | release/build 23 | .erb/dll 24 | 25 | .idea 26 | npm-debug.log.* 27 | *.css.d.ts 28 | *.sass.d.ts 29 | *.scss.d.ts 30 | 31 | # eslint ignores hidden directories by default: 32 | # https://github.com/eslint/eslint/issues/8429 33 | !.erb 34 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'erb', 3 | rules: { 4 | // A temporary hack related to IDE not resolving correct package.json 5 | 'import/no-extraneous-dependencies': 'off', 6 | 'import/no-unresolved': 'error', 7 | // Since React 17 and typescript 4.1 you can safely disable the rule 8 | 'react/react-in-jsx-scope': 'off', 9 | 'no-console': 'off', 10 | 'no-alert': 'off', 11 | }, 12 | parserOptions: { 13 | ecmaVersion: 2020, 14 | sourceType: 'module', 15 | project: './tsconfig.json', 16 | tsconfigRootDir: __dirname, 17 | createDefaultProgram: true, 18 | }, 19 | settings: { 20 | 'import/resolver': { 21 | // See https://github.com/benmosher/eslint-plugin-import/issues/1396#issuecomment-575727774 for line below 22 | node: {}, 23 | webpack: { 24 | config: require.resolve('./.erb/configs/webpack.config.eslint.ts'), 25 | }, 26 | typescript: {}, 27 | }, 28 | 'import/parsers': { 29 | '@typescript-eslint/parser': ['.ts', '.tsx'], 30 | }, 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /.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/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "main" ] 20 | schedule: 21 | - cron: '44 16 * * 4' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | release_name: 6 | description: The name of the release 7 | required: true 8 | 9 | jobs: 10 | publish: 11 | # To enable auto publishing to github, update your electron publisher 12 | # config in package.json > "build" and remove the conditional below 13 | # if: ${{ github.repository_owner == 'electron-react-boilerplate' }} 14 | 15 | runs-on: ${{ matrix.os }} 16 | 17 | strategy: 18 | matrix: 19 | os: [windows-latest, ubuntu-latest, macos-latest] 20 | 21 | steps: 22 | - name: Checkout git repo 23 | uses: actions/checkout@v3 24 | 25 | - name: Install Node and NPM 26 | uses: actions/setup-node@v4.0.3 27 | with: 28 | node-version: 20 29 | cache: npm 30 | 31 | - name: Install and build 32 | run: | 33 | npm install 34 | npm run postinstall 35 | npm run build 36 | 37 | - name: Publish 38 | env: 39 | # These values are used for auto updates signing 40 | #APPLE_ID: ${{ secrets.APPLE_ID }} 41 | #APPLE_ID_PASS: ${{ secrets.APPLE_ID_PASS }} 42 | #CSC_LINK: ${{ secrets.CSC_LINK }} 43 | #CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} 44 | # This is used for uploading release assets to github 45 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | run: | 47 | npm exec electron-builder -- --publish always 48 | 49 | build_frontend: 50 | env: 51 | PLUGIN_NAME: hdr-launcher 52 | runs-on: ubuntu-latest 53 | steps: 54 | - name: checkout version 55 | uses: actions/checkout@v4.1.4 56 | 57 | - name: setup python 58 | uses: actions/setup-python@v2 59 | with: 60 | python-version: '3.9' 61 | 62 | - uses: actions/setup-node@v4.0.3 63 | with: 64 | node-version: 20 65 | 66 | - run: npm install 67 | 68 | - run: npm run build 69 | 70 | - run: mkdir frontend 71 | - run: cp -r release frontend 72 | 73 | # upload built web files 74 | - name: Upload frontend artifact 75 | uses: actions/upload-artifact@v4 76 | with: 77 | name: frontend 78 | path: frontend 79 | 80 | build_the_plugin: 81 | runs-on: ubuntu-24.04 82 | container: 83 | image: wuboyth/skyline-plugin-builder:latest 84 | needs: build_frontend 85 | steps: 86 | - name: checkout version 87 | uses: actions/checkout@v4.1.4 88 | - uses: actions/download-artifact@v4 89 | name: frontend 90 | 91 | - run: ls -lart 92 | - run: "rm -rf release && cp -r frontend/* ." 93 | - run: "ls -lart release/*" 94 | - run: | 95 | export PATH="$PATH:/root/.cargo/bin:/opt/devkitpro/devkitA64/bin" \ 96 | && cd switch \ 97 | && cargo skyline build --release --features "no-npm"\ 98 | && cd .. 99 | env: 100 | HOME: /root 101 | 102 | - run: mv switch/target/aarch64-skyline-switch/release/lib*.nro hdr-launcher.nro 103 | 104 | - run: | 105 | md5sum hdr-launcher.nro > checksum.txt 106 | 107 | # upload plugin 108 | - name: upload plugin 109 | id: upload1 110 | continue-on-error: true 111 | uses: ncipollo/release-action@v1 112 | if: startsWith(github.ref, 'refs/tags/') 113 | with: 114 | name: "${{ github.event.inputs.release_name }}" 115 | artifacts: hdr-launcher.nro,checksum.txt 116 | generateReleaseNotes: true 117 | draft: true 118 | allowUpdates: true 119 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build_frontend: 7 | env: 8 | PLUGIN_NAME: hdr-launcher 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: checkout version 12 | uses: actions/checkout@v4.1.4 13 | 14 | - name: setup python 15 | uses: actions/setup-python@v2 16 | with: 17 | python-version: '3.9' 18 | 19 | - uses: actions/setup-node@v4.0.3 20 | with: 21 | node-version: 20 22 | 23 | - run: npm install 24 | 25 | - run: npm run build 26 | 27 | - run: mkdir frontend 28 | - run: cp -r release frontend 29 | 30 | # upload built web files 31 | - name: Upload frontend artifact 32 | uses: actions/upload-artifact@v4 33 | with: 34 | name: frontend 35 | path: frontend 36 | 37 | build_the_plugin: 38 | runs-on: ubuntu-24.04 39 | needs: build_frontend 40 | container: 41 | image: wuboyth/skyline-plugin-builder:latest 42 | steps: 43 | - name: checkout version 44 | uses: actions/checkout@v4.1.4 45 | - uses: actions/download-artifact@v4 46 | name: frontend 47 | 48 | - run: ls -lart 49 | - run: "rm -rf release && cp -r frontend/* ." 50 | - run: "ls -lart release/*" 51 | - run: | 52 | export PATH="$PATH:/root/.cargo/bin:/opt/devkitpro/devkitA64/bin" \ 53 | && cd switch \ 54 | && cargo skyline build --release --features "updater no-npm"\ 55 | && cd .. 56 | env: 57 | HOME: /root 58 | 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | packages 4 | .webpack 5 | out 6 | target 7 | web-build 8 | launcher-config.json 9 | 10 | # Logs 11 | logs 12 | *.log 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | .eslintcache 22 | 23 | # Dependency directory 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 25 | node_modules 26 | 27 | # OSX 28 | .DS_Store 29 | 30 | release/app/dist 31 | release/build 32 | .erb/dll 33 | 34 | .idea 35 | npm-debug.log.* 36 | *.css.d.ts 37 | *.sass.d.ts 38 | *.scss.d.ts 39 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # . "$(dirname "$0")/_/husky.sh" 3 | 4 | # npx lint-staged 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint", "EditorConfig.EditorConfig"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Electron: Main", 6 | "type": "node", 7 | "request": "launch", 8 | "protocol": "inspector", 9 | "runtimeExecutable": "npm", 10 | "runtimeArgs": ["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 | -------------------------------------------------------------------------------- /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 | The HDR Launcher, this time written in the React framework with TypeScript with both a Switch and Desktop backend. 2 | Switch backend runs as a skyline plugin using skyline-web. 3 | Desktop application is an electron app for use with Emulators. 4 | 5 | Releases can be found on the [releases page](https://github.com/techyCoder81/hdr-launcher-react/releases), or bundled as part of HDR [Beta](https://github.com/HDR-Development/HDR-Releases/releases) and [Beta PreRelease](https://github.com/HDR-Development/HDR-Prereleases/releases) full packages. 6 | 7 | ![launcher_main](https://user-images.githubusercontent.com/42820193/205082618-e6fbaf05-cced-4625-bbfb-372536f5f2aa.png) 8 | ![launcher_verify](https://user-images.githubusercontent.com/42820193/205082615-de3591f8-5054-4d26-98ff-706afe0f1159.png) 9 | ![launcher_logs](https://user-images.githubusercontent.com/42820193/205082612-b96e96c0-a93d-4519-a9e6-940d51c95fd7.png) 10 | ![pr_menu](https://github.com/techyCoder81/hdr-launcher-react/assets/42820193/976f3af1-c1f3-42d0-a3fc-461efca22244) 11 | ![stage_config](https://github.com/techyCoder81/hdr-launcher-react/assets/42820193/9428ac6b-ad0f-4ebe-8348-48ea790a968e) 12 | 13 | 14 | 15 | ## Building 16 | 17 | Use a package manager of your choice (npm, yarn, etc.) in order to install all dependencies 18 | 19 | ```bash 20 | yarn install 21 | ``` 22 | 23 | ## Usage 24 | 25 | Just run `start` script to run as electron app. 26 | 27 | ```bash 28 | yarn start 29 | ``` 30 | To package the electron app, and then compile the skyline plugin, use: 31 | ```bash 32 | python3 build.py package 33 | ``` 34 | 35 | ## Packaging 36 | 37 | To generate the electron project package based on the OS you're running on, just run: 38 | 39 | ```bash 40 | yarn package 41 | ``` 42 | 43 | ## License 44 | 45 | [MIT](https://choosealicense.com/licenses/mit/) 46 | -------------------------------------------------------------------------------- /assets/assets.d.ts: -------------------------------------------------------------------------------- 1 | type Styles = Record; 2 | 3 | declare module '*.svg' { 4 | export const ReactComponent: React.FC>; 5 | 6 | const content: string; 7 | export default content; 8 | } 9 | 10 | declare module '*.png' { 11 | const content: string; 12 | export default content; 13 | } 14 | 15 | declare module '*.jpg' { 16 | const content: string; 17 | export default content; 18 | } 19 | 20 | declare module '*.scss' { 21 | const content: Styles; 22 | export default content; 23 | } 24 | 25 | declare module '*.sass' { 26 | const content: Styles; 27 | export default content; 28 | } 29 | 30 | declare module '*.css' { 31 | const content: Styles; 32 | export default content; 33 | } 34 | -------------------------------------------------------------------------------- /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/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/icon.icns -------------------------------------------------------------------------------- /assets/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/icon.ico -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/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/logo_full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/logo_full.png -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_75.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_75.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_animal_city.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_animal_city.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_animal_island.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_animal_island.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_animal_village.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_animal_village.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_balloonfight.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_balloonfight.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_battlefield.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_battlefield.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_battlefieldl.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_battlefieldl.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_bayo_clock.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_bayo_clock.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_bossstage_final2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_bossstage_final2.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_brave_altar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_brave_altar.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_buddy_spiral.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_buddy_spiral.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_demon_dojo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_demon_dojo.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_dk_jungle.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_dk_jungle.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_dk_lodge.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_dk_lodge.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_dk_waterfall.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_dk_waterfall.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_dolly_stadium.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_dolly_stadium.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_dracula_castle.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_dracula_castle.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_duckhunt.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_duckhunt.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_end.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_end.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_fe_arena.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_fe_arena.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_fe_colloseum.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_fe_colloseum.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_fe_shrine.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_fe_shrine.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_fe_siege.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_fe_siege.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_ff_cave.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_ff_cave.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_ff_midgar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_ff_midgar.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_flatzonex.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_flatzonex.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_fox_corneria.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_fox_corneria.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_fox_lylatcruise.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_fox_lylatcruise.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_fox_venom.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_fox_venom.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_fzero_bigblue.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_fzero_bigblue.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_fzero_mutecity3ds.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_fzero_mutecity3ds.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_fzero_porttown.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_fzero_porttown.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_icarus_angeland.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_icarus_angeland.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_icarus_skyworld.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_icarus_skyworld.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_icarus_uprising.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_icarus_uprising.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_ice_top.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_ice_top.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_jack_mementoes.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_jack_mementoes.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_kart_circuitfor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_kart_circuitfor.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_kart_circuitx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_kart_circuitx.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_kirby_cave.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_kirby_cave.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_kirby_fountain.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_kirby_fountain.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_kirby_gameboy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_kirby_gameboy.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_kirby_greens.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_kirby_greens.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_kirby_halberd.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_kirby_halberd.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_kirby_pupupu64.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_kirby_pupupu64.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_luigimansion.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_luigimansion.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_mario_3dland.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_mario_3dland.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_mario_castle64.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_mario_castle64.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_mario_castledx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_mario_castledx.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_mario_dolpic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_mario_dolpic.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_mario_galaxy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_mario_galaxy.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_mario_maker+us.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_mario_maker+us.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_mario_newbros2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_mario_newbros2.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_mario_odyssey.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_mario_odyssey.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_mario_paper.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_mario_paper.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_mario_past64.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_mario_past64.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_mario_pastusa.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_mario_pastusa.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_mario_pastx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_mario_pastx.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_mario_rainbow.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_mario_rainbow.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_mario_uworld.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_mario_uworld.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_mariobros.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_mariobros.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_metroid_kraid.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_metroid_kraid.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_metroid_norfair.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_metroid_norfair.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_metroid_orpheon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_metroid_orpheon.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_metroid_zebesdx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_metroid_zebesdx.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_mg_shadowmoses.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_mg_shadowmoses.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_mother_fourside.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_mother_fourside.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_mother_magicant.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_mother_magicant.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_mother_newpork.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_mother_newpork.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_mother_onett.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_mother_onett.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_nintendogs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_nintendogs.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_pac_land.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_pac_land.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_pickel_world.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_pickel_world.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_pictochat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_pictochat.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_pikmin_garden.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_pikmin_garden.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_pikmin_planet.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_pikmin_planet.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_pilotwings.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_pilotwings.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_plankton.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_plankton.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_poke_kalos.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_poke_kalos.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_poke_stadium.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_poke_stadium.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_poke_stadium2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_poke_stadium2.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_poke_tengam.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_poke_tengam.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_poke_tower.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_poke_tower.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_poke_unova.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_poke_unova.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_poke_yamabuki.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_poke_yamabuki.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_punchoutsb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_punchoutsb.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_rock_wily.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_rock_wily.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_sf_suzaku.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_sf_suzaku.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_sonic_greenhill.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_sonic_greenhill.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_sonic_windyhill.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_sonic_windyhill.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_spla_parking.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_spla_parking.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_streetpass.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_streetpass.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_tantan_spring.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_tantan_spring.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_tomodachi.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_tomodachi.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_trail_castle.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_trail_castle.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_training.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_training.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_wario_gamer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_wario_gamer.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_wario_madein.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_wario_madein.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_wiifit.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_wiifit.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_wreckingcrew.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_wreckingcrew.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_wufuisland.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_wufuisland.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_xeno_alst.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_xeno_alst.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_xeno_gaur.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_xeno_gaur.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_yoshi_cartboard.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_yoshi_cartboard.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_yoshi_island.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_yoshi_island.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_yoshi_story.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_yoshi_story.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_yoshi_yoster.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_yoshi_yoster.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_zelda_gerudo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_zelda_gerudo.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_zelda_greatbay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_zelda_greatbay.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_zelda_hyrule.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_zelda_hyrule.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_zelda_oldin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_zelda_oldin.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_zelda_pirates.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_zelda_pirates.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_zelda_skyward.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_zelda_skyward.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_zelda_temple.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_zelda_temple.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_zelda_tower.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_zelda_tower.jpg -------------------------------------------------------------------------------- /assets/stage_previews/stage_2_zelda_train.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/stage_previews/stage_2_zelda_train.jpg -------------------------------------------------------------------------------- /assets/theme.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyCoder81/hdr-launcher-react/eaaf4dafd2e00bb425c0dacfd7f8a1f48848717d/assets/theme.wav -------------------------------------------------------------------------------- /build.py: -------------------------------------------------------------------------------- 1 | #! python3 2 | import os, sys 3 | 4 | root_dir = os.getcwd() 5 | 6 | ip = "" 7 | electron = False 8 | listen = False 9 | no_npm = "" 10 | updater = "" 11 | 12 | for arg in sys.argv: 13 | if "ip" in arg: 14 | if not "=" in arg: 15 | print("ip specified but not ip argument given!") 16 | else: 17 | ip = arg.split('=')[1] 18 | if "electron" in arg: 19 | electron = True 20 | if "listen" in arg: 21 | listen = True 22 | if "updater" in arg: 23 | updater = " --features updater" 24 | if "no-npm" in arg: 25 | no_npm = " --features no-npm" 26 | if "help" in arg: 27 | print("usage:") 28 | print("ip=0.0.0.0 : send the plugin to the switch at the given IP") 29 | print("electron : 'npm start'") 30 | print("listen : 'cargo skyline listen'") 31 | print("updater : enables the updater feature on the plugin") 32 | print("help : shows this help message.") 33 | print("no-npm : enable the no-npm feature") 34 | exit() 35 | 36 | os.chdir("switch") 37 | 38 | success = os.system("cargo skyline build --release" + updater + no_npm); 39 | if success != 0: 40 | exit("SWITCH BUILD FAILED!") 41 | 42 | if not ip == "": 43 | if os.name == 'nt': 44 | os.system('curl.exe -T target\\aarch64-skyline-switch\\release\\libhdr_launcher_react.nro ftp://' + ip + ':5000/atmosphere/contents/01006A800016E000/romfs/skyline/plugins/hdr-launcher.nro') 45 | else: 46 | os.system('curl -T target/aarch64-skyline-switch/release/libhdr_launcher_react.nro ftp://' + ip + ':5000/atmosphere/contents/01006A800016E000/romfs/skyline/plugins/hdr-launcher.nro') 47 | 48 | if listen: 49 | os.system("cargo skyline listen") 50 | 51 | os.chdir(root_dir) 52 | 53 | if electron: 54 | os.system("npm start") -------------------------------------------------------------------------------- /release/app/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hdr-launcher", 3 | "version": "0.7.2", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "hdr-launcher", 9 | "version": "0.7.2", 10 | "hasInstallScript": true, 11 | "license": "MIT" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /release/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hdr-launcher", 3 | "author": "techyCoder81", 4 | "version": "0.7.2", 5 | "description": "The HDR Launcher", 6 | "repository": "techyCoder81/hdr-launcher-react", 7 | "license": "MIT", 8 | "contributors": [ 9 | { 10 | "name": "techyCoder81" 11 | } 12 | ], 13 | "main": "./dist/main/main.js", 14 | "scripts": { 15 | "rebuild": "node -r ts-node/register ../../.erb/scripts/electron-rebuild.js", 16 | "postinstall": "npm run rebuild && npm run link-modules", 17 | "link-modules": "node -r ts-node/register ../../.erb/scripts/link-modules.ts" 18 | }, 19 | "dependencies": {} 20 | } 21 | -------------------------------------------------------------------------------- /src/__tests__/App.test.tsx: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | import { render } from '@testing-library/react'; 3 | import App from '../renderer/App'; 4 | 5 | describe('App', () => { 6 | it('should render', () => { 7 | expect(render()).toBeTruthy(); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/main/config.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as os from 'os'; 3 | import xdg from 'xdg-portable'; 4 | 5 | export default class Config { 6 | private static CONFIG_FILE = 'launcher-config.json'; 7 | 8 | ryuPath: string = ''; 9 | 10 | sdcardPath: string = ''; 11 | 12 | private static createFile() { 13 | if (!fs.existsSync(Config.configFilePath())) { 14 | const configStr = JSON.stringify({ 15 | ryuPath: null, 16 | sdcardPath: null, 17 | }); 18 | fs.writeFileSync(Config.configFilePath(), configStr); 19 | } 20 | } 21 | 22 | private static readFile(): Config { 23 | // create the file if necessary 24 | Config.createFile(); 25 | 26 | // read the file 27 | return JSON.parse(fs.readFileSync(Config.configFilePath(), 'utf-8')) as Config; 28 | } 29 | 30 | private static saveConfig(config: Config) { 31 | fs.writeFileSync(Config.configFilePath(), JSON.stringify(config)); 32 | } 33 | 34 | static getRyuPath() { 35 | const config = Config.readFile(); 36 | return config.ryuPath; 37 | } 38 | 39 | static setRyuPath(path: string) { 40 | const config = Config.readFile(); 41 | config.ryuPath = path; 42 | Config.saveConfig(config); 43 | } 44 | 45 | static getSdcardPath() { 46 | const config = Config.readFile(); 47 | return config.sdcardPath; 48 | } 49 | 50 | static setSdcardPath(path: string) { 51 | const config = Config.readFile(); 52 | config.sdcardPath = path; 53 | Config.saveConfig(config); 54 | } 55 | 56 | // Ensure that the config file only lives in one place on linux 57 | // Path is `/home/user/.config/hdr-launcher/launcher-config.json` 58 | private static configFilePath() { 59 | if (os.platform() == 'linux') { 60 | return xdg.config() + '/hdr-launcher/' + this.CONFIG_FILE; 61 | } 62 | 63 | return this.CONFIG_FILE; 64 | } 65 | 66 | private constructor() {} 67 | } 68 | -------------------------------------------------------------------------------- /src/main/preload.ts: -------------------------------------------------------------------------------- 1 | import { contextBridge, ipcRenderer } from 'electron'; 2 | import { Responses } from 'nx-request-api'; 3 | 4 | export const api = { 5 | invoke: (message: string, object: Object): Promise => { 6 | return ipcRenderer.invoke(message, object); 7 | }, 8 | 9 | /** 10 | * Provide an easier way to listen to events 11 | */ 12 | on: (channel: string, callback: Function) => { 13 | ipcRenderer.on(channel, (_, data) => callback(data)); 14 | }, 15 | 16 | /** 17 | * Provide an easier way to listen to events 18 | */ 19 | once: (channel: string, callback: Function) => { 20 | ipcRenderer.once(channel, (_, data) => callback(data)); 21 | }, 22 | }; 23 | 24 | contextBridge.exposeInMainWorld('Main', api); 25 | -------------------------------------------------------------------------------- /src/main/util.ts: -------------------------------------------------------------------------------- 1 | /* eslint import/prefer-default-export: off */ 2 | import { URL } from 'url'; 3 | import path from 'path'; 4 | 5 | export function resolveHtmlPath(htmlFileName: string) { 6 | if (process.env.NODE_ENV === 'development') { 7 | const port = process.env.PORT || 1212; 8 | const url = new URL(`http://localhost:${port}`); 9 | url.pathname = htmlFileName; 10 | return url.href; 11 | } 12 | return `file://${path.resolve(__dirname, '../renderer/', htmlFileName)}`; 13 | } 14 | -------------------------------------------------------------------------------- /src/renderer/@types/image.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png'; 2 | declare module '*.jpeg'; 3 | declare module '*.jpg'; 4 | declare module '*.gif'; 5 | -------------------------------------------------------------------------------- /src/renderer/app.tsx: -------------------------------------------------------------------------------- 1 | import './styles/index.css'; 2 | import Loading from './routes/loading'; 3 | import { Logs } from './operations/log_singleton'; 4 | import './operations/background_music'; 5 | import { 6 | BrowserRouter, 7 | HashRouter, 8 | Navigate, 9 | Route, 10 | Routes, 11 | useNavigate, 12 | } from 'react-router-dom'; 13 | import React, { useEffect } from 'react'; 14 | import { Pages } from './constants'; 15 | import PullRequestMenu from './routes/pull_request_menu'; 16 | import StageConfigMenu from './routes/stage_config/stage_config_menu'; 17 | import Main from './routes/menus/main'; 18 | import { FocusButton } from './components/buttons/focus_button'; 19 | import { NavigateButton } from './components/buttons/navigate_button'; 20 | import { LogPopout } from './components/logging/log_popout'; 21 | 22 | export default function App() { 23 | useEffect(() => { 24 | Logs.instance(); 25 | }, []); 26 | 27 | return ( 28 | 29 | 30 | } 33 | /> 34 | } /> 35 | }> 39 |
40 | 41 | } 42 | /> 43 | } 46 | /> 47 | }> 51 | 52 | 53 | } 54 | /> 55 | }> 59 | 60 | 61 | } 62 | /> 63 | 64 | 65 | ); 66 | } 67 | 68 | const ErrorPage = () => { 69 | return ( 70 |
71 |
80 |
89 | An unexpected error ocurred in the launcher! Check the logs to 90 | investigate. 91 | 96 |
97 |
98 | failed to load log window.
}> 99 | 100 | 101 | 102 | ); 103 | }; 104 | 105 | class ErrorBoundary extends React.Component<{ 106 | children: JSX.Element[] | JSX.Element; 107 | fallback: JSX.Element; 108 | }> { 109 | state = { 110 | hasError: false, 111 | }; 112 | 113 | constructor(props: { 114 | children: JSX.Element[] | JSX.Element; 115 | fallback: JSX.Element; 116 | }) { 117 | super(props); 118 | this.state = { hasError: false }; 119 | } 120 | 121 | static getDerivedStateFromError(error: any) { 122 | // Update state so the next render will show the fallback UI. 123 | return { hasError: true }; 124 | } 125 | 126 | componentDidCatch(error: any, info: any) { 127 | // Example "componentStack": 128 | // in ComponentThatThrows (created by App) 129 | // in ErrorBoundary (created by App) 130 | // in div (created by App) 131 | // in App 132 | // logErrorToMyService(error, info.componentStack); 133 | } 134 | 135 | render() { 136 | if (this.state.hasError) { 137 | // You can render any custom fallback UI 138 | return this.props.fallback; 139 | } 140 | 141 | return this.props.children; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/renderer/components/buttons/dev_tools_buttons.tsx: -------------------------------------------------------------------------------- 1 | import { Progress } from 'nx-request-api'; 2 | import { useEffect, useState } from 'react'; 3 | import { Backend } from 'renderer/operations/backend'; 4 | import * as LauncherConfig from '../../operations/launcher_config'; 5 | import { ScrollFocusButton } from './scroll_focus_button'; 6 | 7 | export const CloneFolderForDev = (props: { 8 | modName: string; 9 | setInfo: (info: string) => void; 10 | onComplete: () => void; 11 | showProgress: (p: Progress) => void; 12 | then?: () => Promise; 13 | }) => { 14 | const [enabled, setEnabled] = useState(false); 15 | 16 | useEffect(() => { 17 | LauncherConfig.getBoolean('enable_dev_tools') 18 | .then((enabled) => { 19 | setEnabled(enabled); 20 | }) 21 | .catch((e) => 22 | console.error(`Error while if dev tools were enabled: ${e}`) 23 | ); 24 | }, []); 25 | 26 | if (enabled) { 27 | return ( 28 | { 32 | try { 33 | props.showProgress( 34 | new Progress( 35 | `Creating ${props.modName} folder`, 36 | `Creating ${props.modName} folder`, 37 | 0 38 | ) 39 | ); 40 | await Backend.instance().cloneMod( 41 | props.modName, 42 | `${props.modName}-dev` 43 | ); 44 | if (props.then !== undefined) { 45 | await props.then(); 46 | } 47 | props.onComplete(); 48 | } catch (e) { 49 | alert(`Error while cloning ${props.modName}: ${e}`); 50 | } 51 | }} 52 | onFocus={() => 53 | props.setInfo( 54 | `Create an ${props.modName}-dev mod folder from your current ${props.modName} folder` 55 | ) 56 | } 57 | /> 58 | ); 59 | } 60 | return
; 61 | }; 62 | -------------------------------------------------------------------------------- /src/renderer/components/buttons/focus_button.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import FocusTimer from '../../operations/focus_singleton'; 3 | 4 | export const FocusButton = React.forwardRef< 5 | HTMLButtonElement, 6 | { 7 | className: string; 8 | onClick: () => void; 9 | text: string; 10 | children?: any; 11 | autofocus?: boolean; 12 | onFocus?: () => void; 13 | style?: React.CSSProperties; 14 | } 15 | >((props, ref) => { 16 | return ( 17 | 43 | ); 44 | }); 45 | -------------------------------------------------------------------------------- /src/renderer/components/buttons/focus_checkbox.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import FocusTimer from '../../operations/focus_singleton'; 3 | 4 | export function FocusCheckbox(props: { 5 | onClick: () => Promise; 6 | className: string; 7 | text: string; 8 | autofocus?: boolean; 9 | checkStatus?: () => Promise; 10 | onFocus?: () => void; 11 | }) { 12 | const [isChecked, setChecked] = useState(false); 13 | 14 | useEffect(() => { 15 | if (props.checkStatus !== undefined) { 16 | props 17 | .checkStatus() 18 | .then((checked) => setChecked(checked)) 19 | .catch((e) => alert(e)); 20 | } 21 | }, []); 22 | 23 | return ( 24 | 68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /src/renderer/components/buttons/focus_combo.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import { Backend } from 'renderer/operations/backend'; 3 | import FocusTimer from '../../operations/focus_singleton'; 4 | 5 | export const FocusCombo = React.forwardRef< 6 | HTMLSelectElement, 7 | { 8 | className: string; 9 | onChange: (item: { target: { value: string } }) => void; 10 | options: string[]; 11 | autofocus?: boolean; 12 | defaultValue?: string; 13 | forcedValue?: string; 14 | onFocus?: () => void; 15 | style?: React.CSSProperties; 16 | disabled?: boolean; 17 | } 18 | >((props, ref) => { 19 | 20 | let options = props.options.map((option) => ( 21 | 24 | )); 25 | let disabledOption = [ 26 | 29 | ]; 30 | 31 | return ( 32 | 60 | ); 61 | }); 62 | -------------------------------------------------------------------------------- /src/renderer/components/buttons/navigate_button.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from 'react-router-dom'; 2 | import { Pages } from 'renderer/constants'; 3 | import { FocusButton } from './focus_button'; 4 | 5 | export function NavigateButton(props: { 6 | text: string; 7 | page: Pages; 8 | className: string; 9 | onFocus?: () => void; 10 | }) { 11 | const navigate = useNavigate(); 12 | return ( 13 | { 17 | navigate(props.page); 18 | }} 19 | onFocus={props.onFocus} 20 | /> 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/components/buttons/prerelease_beta_button.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { Backend } from '../../operations/backend'; 3 | import { FocusButton } from './focus_button'; 4 | import { ScrollFocusButton } from './scroll_focus_button'; 5 | 6 | export const PrereleaseBetaButton = (props: { 7 | setInfo: (info: string) => void; 8 | onClick: (version: string) => void; 9 | }) => { 10 | const [version, setVersion] = useState('...'); 11 | 12 | useEffect(() => { 13 | Backend.instance() 14 | .getVersion() 15 | .then((version) => { 16 | setVersion(version); 17 | }) 18 | .catch((e) => 19 | console.error(`Error while loading version for switch button: ${e}`) 20 | ); 21 | }, []); 22 | 23 | let buttonText = '...'; 24 | if (version.toLowerCase().includes('prerelease')) { 25 | buttonText = 'Beta'; 26 | } else if (version.toLowerCase().includes('beta')) { 27 | buttonText = 'PreRelease'; 28 | } 29 | 30 | return ( 31 | { 35 | props.onClick(version); 36 | Backend.instance() 37 | .getVersion() 38 | .then((version) => { 39 | setVersion(version); 40 | }) 41 | .catch((e) => 42 | console.error(`Error while loading version for switch button: ${e}`) 43 | ); 44 | }} 45 | onFocus={() => 46 | props.setInfo(`Switch to the ${buttonText} version of HDR`) 47 | } 48 | /> 49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /src/renderer/components/buttons/scroll_focus_button.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import { Backend } from 'renderer/operations/backend'; 3 | import FocusTimer from '../../operations/focus_singleton'; 4 | import { FocusButton } from './focus_button'; 5 | 6 | export function ScrollFocusButton(props: { 7 | className: string; 8 | onClick: () => void; 9 | text: string; 10 | children?: any; 11 | autofocus?: boolean; 12 | onFocus?: () => void; 13 | }) { 14 | const selfRef = useRef(null); 15 | 16 | return ( 17 | { 22 | if (props.onFocus) { 23 | props.onFocus(); 24 | } 25 | if (selfRef == null) { 26 | console.warn('Self ref not found for ScrollFocusButton!'); 27 | return; 28 | } 29 | 30 | // don't need the scrolling feature on electron 31 | if (Backend.isNode()) { 32 | return; 33 | } 34 | 35 | const parent = selfRef.current?.parentElement; 36 | if (parent === null || parent === undefined) { 37 | console.warn('no parent found for scrolling button!'); 38 | return; 39 | } 40 | const parentBottom = parent.getBoundingClientRect().bottom; 41 | const parentTop = parent.getBoundingClientRect().top; 42 | 43 | // scroll down 44 | const nextButton = selfRef.current?.nextElementSibling; 45 | if (nextButton !== null && nextButton !== undefined) { 46 | const nextBottom = nextButton.getBoundingClientRect().bottom; 47 | // if the bottom of the next button is not visible (blocked by parent), 48 | // then scroll down. 49 | if (nextBottom > parentBottom) { 50 | nextButton.scrollIntoView(false); 51 | } 52 | } 53 | 54 | // scroll up 55 | const prevButton = selfRef.current?.previousElementSibling; 56 | if (prevButton !== null && prevButton !== undefined) { 57 | const prevTop = prevButton.getBoundingClientRect().top; 58 | // if the top of the previous button is not visible (blocked by parent), 59 | // then scroll up. 60 | if (prevTop < parentTop) { 61 | prevButton.scrollIntoView(true); 62 | } 63 | } 64 | }} 65 | onClick={props.onClick} 66 | text={props.text} 67 | children={props.children} 68 | /> 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /src/renderer/components/buttons/update_button.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-alert */ 2 | import { useEffect, useState } from 'react'; 3 | import * as update from '../../operations/update'; 4 | import { FocusButton } from './focus_button'; 5 | 6 | /// check for updates when the button is loaded 7 | const UpdateButton = (props: { 8 | onClick: () => Promise; 9 | onFocus: () => void; 10 | }) => { 11 | const [available, setAvailable] = useState(false); 12 | 13 | const { onClick, onFocus } = props; 14 | 15 | useEffect(() => { 16 | update 17 | .isAvailable() 18 | .then((isAvailable) => setAvailable(isAvailable)) 19 | .catch((e) => alert(e)); 20 | }, []); 21 | 22 | return ( 23 | { 27 | onClick() 28 | .then(() => setAvailable(false)) 29 | .then(() => update.isAvailable()) 30 | .then((isAvailable) => setAvailable(isAvailable)) 31 | .catch((e) => alert(e)); 32 | }} 33 | onFocus={() => onFocus()} 34 | /> 35 | ); 36 | }; 37 | 38 | export { UpdateButton as default }; 39 | -------------------------------------------------------------------------------- /src/renderer/components/changelog.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Remark } from 'react-remark'; 3 | import { Backend } from '../operations/backend'; 4 | import { getInstallType, getRepoName } from '../operations/install'; 5 | 6 | type Props = { 7 | versions: string[]; 8 | }; 9 | 10 | /** 11 | * changelog implementation 12 | */ 13 | export default class Changelog extends React.Component { 14 | state = { 15 | text: 'Getting Updates...', 16 | }; 17 | 18 | constructor(props: Props) { 19 | super(props); 20 | const check = async () => { 21 | let logs = ''; 22 | for (const version of props.versions) { 23 | await Backend.instance() 24 | .getJson( 25 | `https://github.com/HDR-Development/${getRepoName( 26 | getInstallType(version) 27 | )}/releases/download/${version.split('-')[0]}/CHANGELOG.md` 28 | ) 29 | .then((str) => (logs += `${str}\n`)) 30 | .catch((e) => console.info(e)); 31 | } 32 | this.setState({ text: logs }); 33 | }; 34 | check(); 35 | } 36 | 37 | render() { 38 | return ( 39 |
40 |

41 | {this.state.text} 42 |

43 |
44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/renderer/components/error_page.tsx: -------------------------------------------------------------------------------- 1 | import { Link, useRouteError } from 'react-router-dom'; 2 | 3 | export default function ErrorPage() { 4 | const error: any = useRouteError(); 5 | console.error(error); 6 | 7 | return ( 8 |
9 |

Oops!

10 |

Sorry, an unexpected error has occurred.

11 |

12 | {error.statusText || error.message} 13 |

14 | Go Back. 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/renderer/components/fullscreen_div.tsx: -------------------------------------------------------------------------------- 1 | export const FullScreenDiv = (props: { 2 | children: JSX.Element[] | JSX.Element; 3 | }) => { 4 | return
{props.children}
; 5 | }; 6 | -------------------------------------------------------------------------------- /src/renderer/components/header.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useEffect, useState } from 'react'; 3 | import { Backend } from '../operations/backend'; 4 | 5 | /** 6 | * header implementation 7 | */ 8 | function HeaderInner(props: { version: string; submenu: string[] }) { 9 | const [launcherVersion, setLauncherVersion] = useState('unknown'); 10 | 11 | useEffect(() => { 12 | Backend.instance() 13 | .getLauncherVersion() 14 | .then((version) => setLauncherVersion(version)) 15 | .catch((e) => alert(e)); 16 | }, []); 17 | 18 | return ( 19 | 28 | ); 29 | } 30 | 31 | export const Header = React.memo(HeaderInner); 32 | -------------------------------------------------------------------------------- /src/renderer/components/logging/log_list.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | import { Logs } from '../../operations/log_singleton'; 3 | 4 | function buildList() { 5 | const list = Logs.instance().getAll(); 6 | const out = []; 7 | let i = 0; 8 | let node = list.head; 9 | // iterate across the logs, but only show the most recent 500 logs. 10 | while (node !== null && i < 500) { 11 | out.push( 12 |
13 | {`${node.entry.level.toString()} (${node.entry.time.toLocaleTimeString()}): ${ 14 | node.entry.data 15 | }`} 16 |
17 | ); 18 | ++i; 19 | node = node.next; 20 | } 21 | return out; 22 | } 23 | 24 | export const LogList = () => { 25 | return ( 26 |
27 | {buildList()} 28 |
29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/renderer/components/logging/log_popout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Backend } from '../../operations/backend'; 3 | import '../../styles/sidebar.css'; 4 | import { FocusButton } from '../buttons/focus_button'; 5 | import { LogWindow } from './log_window'; 6 | 7 | export class LogPopout extends React.Component { 8 | state = { 9 | isOpen: false, 10 | contents: 'No logs.', 11 | }; 12 | 13 | render() { 14 | return ( 15 |
16 |
17 | 20 | this.setState({ 21 | isOpen: !this.state.isOpen, 22 | contents: this.state.contents, 23 | }) 24 | } 25 | text={this.state.isOpen ? 'Close Logs' : 'Open Logs'} 26 | /> 27 |
28 | {this.state.isOpen ? ( 29 |
34 | 35 |
36 | ) : ( 37 |
38 | )} 39 |
40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/renderer/components/logging/log_window.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Level, Logs } from '../../operations/log_singleton'; 3 | import { LogListener } from '../../operations/log_listener'; 4 | import { LogList } from './log_list'; 5 | 6 | /** 7 | * log window implementation 8 | */ 9 | export class LogWindow extends React.Component implements LogListener { 10 | state = { 11 | logs: Logs.instance().getAll(), 12 | }; 13 | 14 | update() { 15 | this.setState({ logs: Logs.instance().getAll() }); 16 | } 17 | 18 | /** constructor */ 19 | constructor(props: {} | Readonly<{}>) { 20 | super(props); 21 | Logs.instance().registerChangeCallback(this); 22 | } 23 | 24 | componentWillUnmount(): void { 25 | Logs.instance().unregisterChangeCallback(this); 26 | } 27 | 28 | render() { 29 | function getOption(level: Level | string) { 30 | return ( 31 | 34 | ); 35 | } 36 | 37 | return ( 38 |
39 | 53 | 61 | 67 | 68 |
69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/renderer/components/logo_right.tsx: -------------------------------------------------------------------------------- 1 | import logo from '../../../assets/logo_full.png'; 2 | 3 | export const LogoRight = () => { 4 | return ( 5 |
6 |
7 | Logo 8 |
9 |
10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /src/renderer/components/popup.tsx: -------------------------------------------------------------------------------- 1 | import { Remark } from 'react-remark'; 2 | import { PopupData } from '../operations/popup_data'; 3 | import { FocusButton } from './buttons/focus_button'; 4 | 5 | export const Popup = (props: { data: PopupData }) => { 6 | return ( 7 |
8 |
9 | {props.data.text} 10 | {props.data.options.map((option, index) => { 11 | return ( 12 | props.data.onSelect(option)} 17 | text={option} 18 | /> 19 | ); 20 | })} 21 |
22 |
23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/renderer/components/progress_bar.tsx: -------------------------------------------------------------------------------- 1 | import ProgressBar from '@ramonak/react-progress-bar'; 2 | import React from 'react'; 3 | import ReactModal from 'react-modal'; 4 | import { Progress } from 'nx-request-api'; 5 | import { Backend } from '../operations/backend'; 6 | import '../styles/progress.css'; 7 | import { LogPopout } from './logging/log_popout'; 8 | import SlidingBackground from './sliding_background'; 9 | 10 | ReactModal.setAppElement('#root'); 11 | 12 | const customStyles = { 13 | content: {}, 14 | overlay: { zIndex: 1000 }, 15 | }; 16 | 17 | /** 18 | * progress bar implementation 19 | */ 20 | function ProgressDisplayInner(props: { progress: Progress; animate: boolean }) { 21 | if (props.progress === undefined || props.progress == null) { 22 | return
; 23 | } 24 | 25 | return ( 26 |
27 |
28 | {props.animate ? :
} 29 |

{props.progress.title}

30 | {/*

{props.progress.info}

*/} 31 | 41 |
42 | 43 |
44 | ); 45 | } 46 | 47 | export const ProgressDisplay = React.memo(ProgressDisplayInner); 48 | -------------------------------------------------------------------------------- /src/renderer/components/sidebar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { LogWindow } from './logging/log_window'; 3 | import { FocusButton } from './buttons/focus_button'; 4 | 5 | enum ContentType { 6 | Logs, 7 | Twitter, 8 | Changelogs, 9 | } 10 | 11 | /** 12 | * header implementation 13 | */ 14 | export default class Sidebar extends React.PureComponent { 15 | state = { 16 | mode: ContentType.Changelogs, 17 | }; 18 | 19 | getContent() { 20 | switch (this.state.mode) { 21 | case ContentType.Logs: 22 | return ( 23 |
24 | 25 |
26 | ); 27 | default: 28 | return ( 29 |
30 | 31 |
32 | ); 33 | } 34 | } 35 | 36 | render() { 37 | return ( 38 |
39 |
40 | this.setState({ mode: ContentType.Changelogs })} 44 | /> 45 | this.setState({ mode: ContentType.Logs })} 49 | /> 50 |
51 | 52 | {this.getContent()} 53 |
54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/renderer/components/sliding_background.tsx: -------------------------------------------------------------------------------- 1 | import { Backend } from '../operations/backend'; 2 | 3 | export default function SlidingBackground() { 4 | return ( 5 |
6 | {Backend.isNode() ? ( 7 |
8 |
9 |
10 |
11 |
12 | ) : ( 13 |
14 | )} 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/renderer/constants.ts: -------------------------------------------------------------------------------- 1 | export enum Pages { 2 | STARTUP = '/', 3 | LOADING_SCREEN = '/loading', 4 | MAIN_MENU = '/mainmenu', 5 | STAGE_CONFIG = '/stageconfig', 6 | STAGE_CONFIG_REFRESH = '/stageconfigrefresh', 7 | PULL_REQUESTS = '/pullrequests', 8 | } 9 | -------------------------------------------------------------------------------- /src/renderer/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 12 | HDR Launcher 13 | 28 | 29 | 30 |
31 |

loading...

32 |
33 | 34 | 35 | -------------------------------------------------------------------------------- /src/renderer/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client'; 2 | import { skyline } from 'nx-request-api'; 3 | import App from './app'; 4 | 5 | const container = document.getElementById('root')!; 6 | const root = createRoot(container); 7 | root.render(); 8 | 9 | // override B/X buttons closing the webpage on switch 10 | skyline.setButtonAction('B', () => {}); 11 | skyline.setButtonAction('X', () => {}); 12 | -------------------------------------------------------------------------------- /src/renderer/operations/backend.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Progress, 3 | BackendSupplier, 4 | DefaultMessenger, 5 | Messages, 6 | } from 'nx-request-api'; 7 | 8 | type BackendType = 'Node' | 'Switch'; 9 | 10 | /** 11 | * this will represent the backend interface, which 12 | * could be either node.js or Skyline web. 13 | */ 14 | export class Backend extends DefaultMessenger { 15 | /** singleton instance of the backend */ 16 | private static backendInstance: Backend | null = null; 17 | 18 | private backendType: BackendType; 19 | 20 | constructor(backendType: BackendType, supplier?: BackendSupplier) { 21 | super(supplier); 22 | this.backendType = backendType; 23 | } 24 | 25 | public static instance(): Backend { 26 | if (Backend.backendInstance == null) { 27 | if (window.Main == undefined) { 28 | Backend.backendInstance = new Backend('Switch'); 29 | } else { 30 | Backend.backendInstance = new Backend('Node', new NodeBackend()); 31 | } 32 | } 33 | return Backend.backendInstance; 34 | } 35 | 36 | /** 37 | * returns whether this is running on pc or not, 38 | * without making a backend call 39 | * @returns whether this is running on pc (node.js) 40 | */ 41 | public static isNode() { 42 | return Backend.instance().backendType == 'Node'; 43 | } 44 | 45 | /** 46 | * returns whether this is running on switch or not, 47 | * without making a backend call 48 | * @returns whether this is running on switch 49 | */ 50 | public static isSwitch() { 51 | return Backend.instance().backendType == 'Switch'; 52 | } 53 | 54 | /** 55 | * Gets the user friendly backend name, without making a backend call. 56 | * @returns the platform name 57 | */ 58 | public static platformName() { 59 | return this.isNode() ? 'Emulator' : 'Switch'; 60 | } 61 | 62 | private static platform: string; 63 | 64 | /** gets the platform of the current backend, 65 | * according to the backend itself. */ 66 | async getPlatform(): Promise { 67 | if (Backend.platform === undefined) { 68 | Backend.platform = await this.customRequest('get_platform', null); 69 | } 70 | return Backend.platform; 71 | } 72 | 73 | /** gets the platform of the current backend, 74 | * according to the backend itself. */ 75 | async getSdRoot(): Promise { 76 | return this.customRequest('get_sdcard_root', null); 77 | } 78 | 79 | /** gets whether hdr is installed */ 80 | async isInstalled(): Promise { 81 | return this.booleanRequest('is_installed', null); 82 | } 83 | 84 | /** gets the hdr version installed */ 85 | async getVersion(): Promise { 86 | return this.customRequest('get_version', null); 87 | } 88 | 89 | /** sends the play message to the backend */ 90 | play(): Promise { 91 | return this.exitSession(); 92 | } 93 | 94 | /** sends the mod manager message to the backend */ 95 | openModManager(): Promise { 96 | return this.invoke('open_mod_manager', null); 97 | } 98 | 99 | /** checks if the given mod path is enabled (relative to sd:/) */ 100 | isModEnabled(mod_path: string): Promise { 101 | return this.booleanRequest('is_mod_enabled', [mod_path]); 102 | } 103 | 104 | /** gets the arcropolis api version */ 105 | getArcropApiVersion(): Promise { 106 | return this.customRequest('get_arcrop_api_version', null); 107 | } 108 | 109 | /** gets the the launcher version */ 110 | getLauncherVersion(): Promise { 111 | return this.customRequest('get_launcher_version', null); 112 | } 113 | 114 | /** request to relaunch the application (does nothing on pc) */ 115 | relaunchApplication(): Promise { 116 | if (Backend.isNode()) { 117 | return new Promise((resolve) => resolve('relaunch is NOP on PC')); 118 | } 119 | return this.customRequest('relaunch_application', null); 120 | } 121 | 122 | /** clones src into dest */ 123 | cloneMod( 124 | src: string, 125 | dest: string, 126 | progressCallback?: (p: Progress) => void 127 | ): Promise { 128 | return this.customRequest('clone_mod', [src, dest], progressCallback); 129 | } 130 | 131 | /** removes the given directory and all its contents */ 132 | removeDirAll(path: string): Promise { 133 | return this.customRequest('remove_dir_all', [path]); 134 | } 135 | 136 | override customRequest( 137 | name: string, 138 | args: string[] | null, 139 | progressCallback?: ((p: Progress) => void) | undefined 140 | ): Promise { 141 | return new Promise((resolve, reject) => { 142 | super 143 | .customRequest(name, args, progressCallback) 144 | .then((result) => resolve(result)) 145 | .catch((e) => { 146 | console.error(`request ${name} rejected, args: ${args?.join(',')}`); 147 | reject(e); 148 | }); 149 | }); 150 | } 151 | } 152 | 153 | /** 154 | * this is an implementation for the electron node backend 155 | */ 156 | export class NodeBackend implements BackendSupplier { 157 | invoke( 158 | call_name: string, 159 | args: string[] | null, 160 | progressCallback?: (p: Progress) => void 161 | ): Promise { 162 | const message = new Messages.Message(call_name, args); 163 | console.debug(`invoking on node backend:\n${JSON.stringify(message)}`); 164 | const retval = null; 165 | return new Promise((resolve, reject) => { 166 | // if defined, set the progress callback 167 | if (typeof progressCallback !== 'undefined') { 168 | window.Main.on('progress', (progress: Progress) => { 169 | progressCallback(progress); 170 | }); 171 | } 172 | // send the request 173 | window.Main.invoke('request', message) 174 | .then((response: any) => { 175 | console.debug(`got response: ${JSON.stringify(response)}`); 176 | const output = JSON.stringify(response); 177 | // console.debug("resolving with: " + output); 178 | resolve(output); 179 | }) 180 | .catch((e: any) => { 181 | console.error( 182 | `error while invoking on node backend. ${JSON.stringify(e)}` 183 | ); 184 | throw e; 185 | }); 186 | }); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/renderer/operations/background_music.ts: -------------------------------------------------------------------------------- 1 | export default class BackgroundMusic { 2 | private static instance: BackgroundMusic; 3 | 4 | music: HTMLAudioElement; 5 | 6 | fadeInterval: NodeJS.Timer | null = null; 7 | 8 | public static singleton(): BackgroundMusic { 9 | if (BackgroundMusic.instance === undefined) { 10 | BackgroundMusic.instance = new BackgroundMusic(); 11 | } 12 | return BackgroundMusic.instance; 13 | } 14 | 15 | private constructor() { 16 | this.music = new Audio('assets/theme.wav'); 17 | this.music.load(); 18 | this.music.volume = 0.1; 19 | 20 | this.music.play(); 21 | this.music.addEventListener('ended', () => { 22 | this.music.currentTime = 0; 23 | this.music.play(); 24 | }); 25 | } 26 | 27 | public isFading() { 28 | return this.fadeInterval; 29 | } 30 | 31 | public play(): Promise { 32 | return new Promise(() => this.music.play()); 33 | } 34 | 35 | public pause(): Promise { 36 | return new Promise(() => this.music.pause()); 37 | } 38 | 39 | public fadeOut(): Promise { 40 | return this.fadeTo(0); 41 | } 42 | 43 | public fadeIn(): Promise { 44 | return this.fadeTo(0.95); 45 | } 46 | 47 | public fadeTo(target_volume: number): Promise { 48 | target_volume = Math.min(Math.max(0, target_volume), 0.95); 49 | // if we are already fading, cancel the previous fade 50 | if (this.fadeInterval !== null) { 51 | clearInterval(this.fadeInterval); 52 | } 53 | const { music } = this; 54 | let vol = this.music.volume; 55 | 56 | return new Promise((resolve) => { 57 | BackgroundMusic.singleton().fadeInterval = setInterval(function () { 58 | if (vol !== target_volume) { 59 | vol = Math.min( 60 | Math.max(vol + 0.05 * Math.sign(target_volume - vol), 0), 61 | 0.95 62 | ); 63 | music.volume = vol; 64 | } else { 65 | const interval = BackgroundMusic.singleton().fadeInterval; 66 | if (interval !== null) { 67 | clearInterval(interval); 68 | BackgroundMusic.singleton().fadeInterval = null; 69 | resolve(vol); 70 | } 71 | } 72 | }, 150); 73 | }); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/renderer/operations/focus_singleton.ts: -------------------------------------------------------------------------------- 1 | import { Backend } from './backend'; 2 | 3 | /** 4 | * This class exists to fill a specific need. 5 | * On Switch, the control stick is too sensitive 6 | * when navigating components. The dpad is fine, 7 | * but moving the stick up or down results very 8 | * easily in navigating multiple components in 9 | * sequence. This class exists to measure how 10 | * often focus transitions should be allowed. 11 | */ 12 | export default class FocusTimer { 13 | /** the last time a transition was allowed */ 14 | private static last_time = new Date().getTime(); 15 | 16 | /** the minimum waiting time between transitions */ 17 | private static MIN_WAIT_TIME = 150; 18 | 19 | static request(): boolean { 20 | // focus changes don't need to be limited on electron 21 | if (Backend.isNode()) { 22 | return true; 23 | } 24 | 25 | const current_time = new Date().getTime(); 26 | if (current_time - this.last_time < this.MIN_WAIT_TIME) { 27 | return false; 28 | } 29 | this.last_time = current_time; 30 | return true; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/renderer/operations/github_utils.ts: -------------------------------------------------------------------------------- 1 | import { Backend } from './backend'; 2 | 3 | export class Github { 4 | private static prs: null | any = null; 5 | 6 | // singleton accessor for PR data 7 | static async pullRequests(): Promise { 8 | return new Promise(async (resolve, reject) => { 9 | if (Github.prs == null) { 10 | await Backend.instance() 11 | .getJson( 12 | 'https://api.github.com/repos/HDR-Development/HewDraw-Remix/pulls?per_page=100&state=open' 13 | ) 14 | .then((data) => (Github.prs = data)) 15 | .catch((e) => reject(e)); 16 | } 17 | resolve(Github.prs); 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/renderer/operations/install.ts: -------------------------------------------------------------------------------- 1 | import { Progress } from 'nx-request-api'; 2 | import { Backend } from './backend'; 3 | import { handleDeletions } from './update'; 4 | import verify from './verify'; 5 | 6 | export enum InstallType { 7 | Beta, 8 | PreRelease, 9 | Unknown, 10 | } 11 | 12 | export function getInstallType(version: string): InstallType { 13 | if (version.toLowerCase().includes('prerelease')) { 14 | return InstallType.PreRelease; 15 | } 16 | if (version.toLowerCase().includes('beta')) { 17 | return InstallType.Beta; 18 | } 19 | return InstallType.Unknown; 20 | } 21 | 22 | export function getRepoName(type: InstallType) { 23 | switch (type) { 24 | case InstallType.Beta: 25 | return 'HDR-Releases'; 26 | case InstallType.PreRelease: 27 | return 'HDR-PreReleases'; 28 | default: 29 | return 'unknown'; 30 | } 31 | } 32 | 33 | export async function installLatest( 34 | progressCallback?: (p: Progress) => void, 35 | type?: InstallType 36 | ) { 37 | if (typeof type === 'undefined') { 38 | console.info('defaulting to beta installation'); 39 | return installArtifact( 40 | 'switch-package.zip', 41 | 'latest', 42 | InstallType.Beta, 43 | progressCallback 44 | ); 45 | } 46 | 47 | return installArtifact( 48 | 'switch-package.zip', 49 | 'latest', 50 | type, 51 | progressCallback 52 | ); 53 | } 54 | 55 | export async function switchToPrerelease( 56 | currentVersion: string, 57 | progressCallback?: (p: Progress) => void 58 | ) { 59 | return installArtifact( 60 | 'to-prerelease.zip', 61 | currentVersion, 62 | InstallType.Beta, 63 | progressCallback 64 | ).then(() => 65 | handleDeletions( 66 | currentVersion, 67 | 'to_prerelease_deletions.json', 68 | progressCallback 69 | ) 70 | ); 71 | } 72 | 73 | export async function switchToBeta( 74 | currentVersion: string, 75 | progressCallback?: (p: Progress) => void 76 | ) { 77 | return installArtifact( 78 | 'to-beta.zip', 79 | currentVersion, 80 | InstallType.PreRelease, 81 | progressCallback 82 | ).then(() => 83 | handleDeletions(currentVersion, 'to_beta_deletions.json', progressCallback) 84 | ); 85 | } 86 | 87 | async function installArtifact( 88 | artifact: string, 89 | version: string, 90 | type: InstallType, 91 | progressCallback?: (p: Progress) => void 92 | ) { 93 | try { 94 | const backend = Backend.instance(); 95 | let sdroot = ''; 96 | 97 | await backend 98 | .getSdRoot() 99 | .then((value) => { 100 | sdroot = value; 101 | }) 102 | .catch((e) => { 103 | console.error(`Could not get SD root. ${e}`); 104 | }); 105 | 106 | if (progressCallback) { 107 | progressCallback(new Progress('Checking version', 'Checking', null)); 108 | } 109 | 110 | const downloads = `${sdroot}downloads/`; 111 | 112 | console.info(`version: ${version}`); 113 | 114 | const repoName = getRepoName(type); 115 | 116 | const url = 117 | version == 'latest' 118 | ? `https://github.com/HDR-Development/${repoName}/releases/latest/download/${artifact}` 119 | : `https://github.com/HDR-Development/${repoName}/releases/download/${ 120 | version.split('-')[0] 121 | }/${artifact}`; 122 | 123 | console.info(`downloading from: ${url}`); 124 | if (progressCallback) { 125 | progressCallback( 126 | new Progress(`Downloading ${artifact}`, 'Downloading', null) 127 | ); 128 | } 129 | await backend 130 | .downloadFile(url, `${downloads}hdr-install.zip`, progressCallback) 131 | .then((result) => console.info(`Result of download: ${result}`)) 132 | .then(() => { 133 | if (progressCallback) { 134 | progressCallback(new Progress('Extracting', 'Extracting files', 0)); 135 | } 136 | }) 137 | .then(() => 138 | backend.unzip(`${downloads}hdr-install.zip`, sdroot, progressCallback) 139 | ) 140 | .then((result) => console.info(`Result of extraction: ${result}`)) 141 | .catch((e) => { 142 | console.error(`Error during install! ${e}`); 143 | alert(`Error during install: ${e}`); 144 | }); 145 | } catch (e) { 146 | alert(`Exception while installing: ${e}`); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/renderer/operations/launcher_config.ts: -------------------------------------------------------------------------------- 1 | import { Backend } from './backend'; 2 | 3 | type BooleanSetting = 'skip_launcher' | 'ignore_music' | 'enable_dev_tools'; 4 | 5 | const CONFIG_PATH = 'ultimate/hdr-config'; 6 | 7 | export async function setBoolean( 8 | setting: BooleanSetting, 9 | enabled: boolean 10 | ): Promise { 11 | return new Promise(async (resolve, reject) => { 12 | try { 13 | const backend = Backend.instance(); 14 | const sdroot = await backend.getSdRoot(); 15 | const configDir = sdroot + CONFIG_PATH; 16 | const exists = await backend.fileExists(`${configDir}/${setting}`); 17 | if (exists && !enabled) { 18 | // the file exists and should be removed. 19 | await backend.deleteFile(`${configDir}/${setting}`); 20 | } else if (!exists && enabled) { 21 | // the file does not exist and should be created. 22 | await backend.mkdir(configDir); 23 | await backend.writeFile(`${configDir}/${setting}`, 'foo'); 24 | } 25 | 26 | resolve(); 27 | } catch (e) { 28 | alert(`Could not set config setting ${setting}\n${e}`); 29 | reject(e); 30 | } 31 | }); 32 | } 33 | 34 | export async function getBoolean(setting: BooleanSetting): Promise { 35 | return new Promise(async (resolve, reject) => { 36 | try { 37 | const backend = Backend.instance(); 38 | const sdroot = await backend.getSdRoot(); 39 | const configDir = sdroot + CONFIG_PATH; 40 | const exists = await backend.fileExists(`${configDir}/${setting}`); 41 | if (exists) { 42 | resolve(true); 43 | } else { 44 | resolve(false); 45 | } 46 | } catch (e) { 47 | alert(`Could not get config setting ${setting}\n${e}`); 48 | reject(e); 49 | } 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /src/renderer/operations/log_listener.ts: -------------------------------------------------------------------------------- 1 | /// this interface represents a listener of logging updates 2 | export interface LogListener { 3 | update(): void; 4 | } 5 | -------------------------------------------------------------------------------- /src/renderer/operations/popup_data.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * holds the data necessary for a popup 3 | */ 4 | export class PopupData { 5 | public options: string[]; 6 | 7 | public text: string; 8 | 9 | public onSelect: (selected: string) => void; 10 | 11 | /** 12 | * creates a PopupData. 13 | * @param options a list of button options to be available 14 | * @param text the text to be shown 15 | * @param onSelect a callback to be performed once an option is selected 16 | */ 17 | constructor( 18 | options: string[], 19 | text: string, 20 | onSelect: (selected: string) => void 21 | ) { 22 | this.options = options; 23 | this.text = text; 24 | this.onSelect = onSelect; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/renderer/operations/stage_config.ts: -------------------------------------------------------------------------------- 1 | import { Backend } from './backend'; 2 | import { Stage, StageInfo } from './stage_info'; 3 | 4 | export const ACTIVE_CONFIG_FILE = 'ultimate/hdr-config/tourney_mode.json'; 5 | export const BACKUP_STAGE_CONFIG = 'ultimate/hdr-config/tourney_mode_backup.json'; 6 | export const OFFICIAL_STAGE_CONFIG = 'ultimate/mods/hdr-stages/tourney_mode_official.json'; 7 | const CONFIG_PATH = 'ultimate/hdr-config/'; 8 | 9 | // require() all of the stage previews 10 | new StageInfo().list().then((stages) => 11 | stages.forEach((stage) => { 12 | try { 13 | require(`../../../assets/stage_previews/stage_2_${stage.name_id.toLowerCase()}.jpg`); 14 | } catch { 15 | console.warn(`Could not find stage preview for: ${stage.name_id}`); 16 | } 17 | }) 18 | ); 19 | 20 | /** 21 | * the ephemeral configuration data 22 | */ 23 | export class ConfigData { 24 | public enabled: boolean; 25 | 26 | public useOfficial: boolean; 27 | 28 | public starters: Stage[]; 29 | 30 | public counterpicks: Stage[]; 31 | 32 | constructor(enabled: boolean, useOfficial: boolean, starters: Stage[], counterpicks: Stage[]) { 33 | this.enabled = enabled; 34 | this.useOfficial = useOfficial; 35 | this.starters = starters; 36 | this.counterpicks = counterpicks; 37 | } 38 | 39 | public includes(stage: Stage) { 40 | return !( 41 | this.starters.find((thisStage) => stage.name_id === thisStage.name_id) === 42 | undefined || 43 | this.counterpicks.find( 44 | (thisStage) => stage.name_id === thisStage.name_id 45 | ) === undefined 46 | ); 47 | } 48 | } 49 | 50 | /** 51 | * this mirrors the tourney config in the plugin, written to the json file 52 | */ 53 | export type FileFormat = { 54 | enabled: boolean; 55 | useOfficial: boolean; 56 | starters: string[]; 57 | counterpicks: string[]; 58 | }; 59 | 60 | /** 61 | * loads the currently tourney config from the sd card 62 | * @returns void when completed 63 | */ 64 | export async function loadConfigData(location: string): Promise { 65 | return new Promise(async (resolve, reject) => { 66 | try { 67 | const backend = Backend.instance(); 68 | const root = await backend.getSdRoot(); 69 | 70 | // if the config doesn't already exist, default to empty 71 | if (!(await backend.fileExists(root + location))) { 72 | const data = new ConfigData(false, false, [], []); 73 | resolve(data); 74 | return; 75 | } 76 | 77 | await backend 78 | .readFile(root + location) 79 | .then(async (json) => { 80 | const fileData: FileFormat = JSON.parse(json); 81 | const info = new StageInfo(); 82 | const data = new ConfigData(false, false, [], []); 83 | data.enabled = fileData.enabled; 84 | data.useOfficial = fileData.useOfficial ?? false; 85 | data.counterpicks = []; 86 | for (const nameId of fileData.counterpicks) { 87 | try { 88 | let stage = await info.getById(nameId); 89 | if (!stage) { 90 | // default to the first stage if the named stage could not be loaded 91 | stage = (await info.list())[0]; 92 | } 93 | data.counterpicks.push(stage); 94 | } catch (e) { 95 | console.error(`Error loading stage ${nameId}: ${e}`); 96 | } 97 | } 98 | data.starters = []; 99 | for (const nameId of fileData.starters) { 100 | try { 101 | data.starters.push(await info.getById(nameId)); 102 | } catch (e) { 103 | console.error(`Error loading stage ${nameId}: ${e}`); 104 | } 105 | } 106 | 107 | resolve(data); 108 | }) 109 | .catch((e) => reject(e)); 110 | } catch (e) { 111 | reject(e); 112 | } 113 | }); 114 | } 115 | 116 | export async function save(location: string, data: ConfigData): Promise { 117 | return new Promise(async (resolve, reject) => { 118 | try { 119 | const backend = Backend.instance(); 120 | const root = await backend.getSdRoot(); 121 | const config: FileFormat = { 122 | enabled: data.enabled, 123 | useOfficial: data.useOfficial, 124 | starters: [], 125 | counterpicks: [], 126 | }; 127 | const info = new StageInfo(); 128 | config.counterpicks = data.counterpicks.map((stage) => stage.name_id); 129 | config.starters = data.starters.map((stage) => stage.name_id); 130 | const json = JSON.stringify(config); 131 | 132 | const configDir = root + CONFIG_PATH; 133 | const exists = await backend.fileExists(configDir); 134 | if (!exists) { 135 | await backend.mkdir(configDir); 136 | } 137 | 138 | await Backend.instance().writeFile(root + location, json); 139 | resolve(); 140 | } catch (e) { 141 | reject(e); 142 | } 143 | }); 144 | } 145 | -------------------------------------------------------------------------------- /src/renderer/preload.d.ts: -------------------------------------------------------------------------------- 1 | import { api } from '../main/preload'; 2 | 3 | declare global { 4 | // eslint-disable-next-line 5 | interface Window { 6 | Main: typeof api; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/renderer/routes/loading.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { Pages } from 'renderer/constants'; 4 | import { Backend } from '../operations/backend'; 5 | import * as launcher_config from '../operations/launcher_config'; 6 | import * as update from '../operations/update'; 7 | import '../styles/opening.css'; 8 | 9 | let will_skip = false; 10 | 11 | export default function Loading() { 12 | const [showButton, setShowButton] = useState(false); 13 | const navigate = useNavigate(); 14 | 15 | useEffect(() => { 16 | if (Backend.isSwitch()) { 17 | launcher_config 18 | .getBoolean('skip_launcher') 19 | .then((skip) => { 20 | will_skip = skip; 21 | setShowButton(skip); 22 | }) 23 | .then(() => update.isAvailable()) 24 | .then((available) => { 25 | if (available && will_skip) { 26 | alert('An HDR update is available!'); 27 | will_skip = false; 28 | } 29 | }) 30 | .catch((e) => alert(e)); 31 | } else { 32 | will_skip = false; 33 | } 34 | // set a timeout on exiting the session early if the user hasnt opened the launcher 35 | setTimeout(() => { 36 | if (will_skip) { 37 | Backend.instance().exitSession(); 38 | } 39 | navigate(Pages.MAIN_MENU); 40 | }, 2500); 41 | }, []); 42 | 43 | return ( 44 |
45 |
46 |

HewDraw

47 |

Remix

48 |
49 | {will_skip ? ( 50 | 60 | ) : ( 61 |
62 | )} 63 |
64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /src/renderer/routes/menus/abstract_menu.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Progress } from 'nx-request-api'; 3 | import { Backend } from '../../operations/backend'; 4 | import { PopupData } from '../../operations/popup_data'; 5 | import { Popup } from '../../components/popup'; 6 | import { ProgressDisplay } from '../../components/progress_bar'; 7 | 8 | export abstract class AbstractMenu extends React.Component { 9 | state = { 10 | progress: null, 11 | popup: null, 12 | }; 13 | 14 | showProgress(progress: Progress | null) { 15 | this.setState({ progress, popup: null }); 16 | } 17 | 18 | showPopupData(popupData: PopupData | null) { 19 | this.setState({ progress: null, popup: popupData }); 20 | } 21 | 22 | showMenu() { 23 | this.setState({ progress: null, popup: null }); 24 | } 25 | 26 | render(): JSX.Element { 27 | return ( 28 |
29 | {this.state.popup != null ? :
} 30 | {this.state.progress != null ? ( 31 | 35 | ) : ( 36 |
37 | )} 38 |
39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/renderer/routes/menus/checking_installed.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { Backend } from '../../operations/backend'; 3 | 4 | export const CheckingInstalled = (props: { 5 | onComplete: (installedVersion: string | null) => void; 6 | }) => { 7 | const [installed, setInstalled] = useState(null as null | string); 8 | 9 | useEffect(() => { 10 | Backend.instance() 11 | .getVersion() 12 | .then((version) => props.onComplete(version)) 13 | .catch(async (e) => { 14 | const backend = Backend.instance(); 15 | const root = await backend.getSdRoot(); 16 | try { 17 | const prVersion = await backend.readFile( 18 | `${root}ultimate/mods/hdr-pr/ui/hdr_version.txt` 19 | ); 20 | const prEnabled = await backend.isModEnabled( 21 | 'sd:/ultimate/mods/hdr-pr' 22 | ); 23 | // if the PR build is enabled, then use that 24 | if (prEnabled) { 25 | props.onComplete(prVersion); 26 | } else { 27 | props.onComplete(null); 28 | } 29 | } catch (e) { 30 | console.error(`Error while checking if HDR is installed!\n${e}`); 31 | alert(`Error while checking if HDR is installed!\n${e}`); 32 | props.onComplete(null); 33 | } 34 | }); 35 | }, []); 36 | 37 | return
checking if HDR is installed...
; 38 | }; 39 | -------------------------------------------------------------------------------- /src/renderer/routes/menus/info_box.tsx: -------------------------------------------------------------------------------- 1 | import '../../styles/infobox.css'; 2 | 3 | export default function InfoBox(props: { text: string }) { 4 | return ( 5 |
6 |
7 |
{props.text}
8 |
9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/renderer/routes/menus/main.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { LogPopout } from '../../components/logging/log_popout'; 3 | import Menu from './menu'; 4 | import SlidingBackground from '../../components/sliding_background'; 5 | 6 | export default function Main() { 7 | useEffect(() => { 8 | console.log('we are on the main menu.'); 9 | }, []); 10 | 11 | return ( 12 |
13 | 14 | 15 | 16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/renderer/routes/menus/main_menu.tsx: -------------------------------------------------------------------------------- 1 | import { Progress } from 'nx-request-api'; 2 | import { Backend } from '../../operations/backend'; 3 | import { PopupData } from '../../operations/popup_data'; 4 | import update from '../../operations/update'; 5 | import { FocusButton } from '../../components/buttons/focus_button'; 6 | import { MenuType } from './menu'; 7 | import UpdateButton from '../../components/buttons/update_button'; 8 | import { AbstractMenu } from './abstract_menu'; 9 | import BackgroundMusic from '../../operations/background_music'; 10 | 11 | /** 12 | * builds the main menu components 13 | * @returns the main menu 14 | */ 15 | export default class MainMenu extends AbstractMenu<{ 16 | setInfo: (info: string) => void; 17 | switchTo: (menu: MenuType) => void; 18 | }> { 19 | public constructor(props: { 20 | setInfo: (info: string) => void; 21 | switchTo: (menu: MenuType) => void; 22 | }) { 23 | super(props); 24 | } 25 | 26 | override showMenu(): void { 27 | super.showMenu(); 28 | this.props.switchTo(MenuType.MainMenu); 29 | } 30 | 31 | render(): JSX.Element { 32 | return ( 33 | 117 | ); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/renderer/routes/menus/menu.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Backend, NodeBackend } from '../../operations/backend'; 3 | import '../../styles/progress.css'; 4 | import InfoBox from './info_box'; 5 | import { Header } from '../../components/header'; 6 | import { LogoRight } from '../../components/logo_right'; 7 | import MainMenu from './main_menu'; 8 | import { CheckingInstalled } from './checking_installed'; 9 | import ToolsMenu from './tools_menu'; 10 | import OptionsMenu from './options_menu'; 11 | import NotInstalledMenu from './not_installed_menu'; 12 | import { skyline } from 'nx-request-api'; 13 | import PrInstalledMenu from './pr_installed_menu'; 14 | 15 | export enum MenuType { 16 | MainMenu, 17 | Options, 18 | Tools, 19 | NotInstalled, 20 | CheckingInstalled, 21 | PrInstalled, 22 | } 23 | 24 | /** 25 | * main menu implementation 26 | */ 27 | export default class Menu extends React.PureComponent { 28 | state = { 29 | currentMenu: MenuType.CheckingInstalled, 30 | version: 'unknown', 31 | info: ' ', 32 | }; 33 | 34 | switchTo(menu: MenuType) { 35 | this.setState({ 36 | currentMenu: menu, 37 | version: this.state.version, 38 | info: this.state.info, 39 | }); 40 | this.loadVersion(); 41 | 42 | // assign button actions for switch 43 | skyline.setButtonAction('X', () => {}); 44 | switch (this.state.currentMenu) { 45 | case MenuType.Options: 46 | skyline.setButtonAction('B', () => this.switchTo(MenuType.MainMenu)); 47 | break; 48 | case MenuType.Tools: 49 | skyline.setButtonAction('B', () => this.switchTo(MenuType.MainMenu)); 50 | break; 51 | default: 52 | skyline.setButtonAction('B', () => {}); 53 | break; 54 | } 55 | } 56 | 57 | setVersion(version: string) { 58 | console.debug(`setting version: ${version}`); 59 | this.setState({ 60 | currentMenu: this.state.currentMenu, 61 | version, 62 | info: this.state.info, 63 | }); 64 | } 65 | 66 | loadVersion() { 67 | Backend.instance() 68 | .getVersion() 69 | .then((ver) => { 70 | console.debug(`loaded version: ${ver}`); 71 | this.setVersion(ver); 72 | }) 73 | .catch((e) => console.error(`console error: ${e}`)); 74 | } 75 | 76 | setInfo(info: string) { 77 | this.setState({ 78 | currentMenu: this.state.currentMenu, 79 | version: this.state.version, 80 | info, 81 | }); 82 | } 83 | 84 | getMenu() { 85 | switch (this.state.currentMenu) { 86 | case MenuType.Options: 87 | return ( 88 | this.setInfo(info)} 90 | switchTo={(menu: MenuType) => this.switchTo(menu)} 91 | version={this.state.version} 92 | /> 93 | ); 94 | case MenuType.Tools: 95 | return ( 96 | this.setInfo(info)} 98 | switchTo={(menu: MenuType) => this.switchTo(menu)} 99 | /> 100 | ); 101 | case MenuType.CheckingInstalled: 102 | return ( 103 | { 105 | console.info(installed); 106 | installed !== null 107 | ? installed.endsWith('pr') 108 | ? this.switchTo(MenuType.PrInstalled) 109 | : this.switchTo(MenuType.MainMenu) 110 | : this.switchTo(MenuType.NotInstalled); 111 | }} 112 | /> 113 | ); 114 | case MenuType.NotInstalled: 115 | return ( 116 | this.setInfo(info)} 118 | switchTo={(menu: MenuType) => this.switchTo(menu)} 119 | /> 120 | ); 121 | case MenuType.PrInstalled: 122 | return ( 123 | this.setInfo(info)} 125 | switchTo={(menu: MenuType) => this.switchTo(menu)} 126 | /> 127 | ); 128 | default: 129 | return ( 130 | this.setInfo(info)} 132 | switchTo={(menu: MenuType) => this.switchTo(menu)} 133 | /> 134 | ); 135 | } 136 | } 137 | 138 | render() { 139 | return ( 140 |
141 |
151 |
152 |
153 | {this.getMenu()} 154 |
155 | 156 |
157 | 158 |
159 | ); 160 | } 161 | 162 | componentDidMount() { 163 | this.loadVersion(); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/renderer/routes/menus/not_installed_menu.tsx: -------------------------------------------------------------------------------- 1 | import { Progress } from 'nx-request-api'; 2 | import { PopupData } from 'renderer/operations/popup_data'; 3 | import { Backend } from '../../operations/backend'; 4 | import { 5 | installLatest, 6 | switchToBeta, 7 | switchToPrerelease, 8 | } from '../../operations/install'; 9 | import verify from '../../operations/verify'; 10 | import { FocusButton } from '../../components/buttons/focus_button'; 11 | import { MenuType } from './menu'; 12 | import { AbstractMenu } from './abstract_menu'; 13 | 14 | /** 15 | * builds the menu that appears when HDR is not installed 16 | * @returns the "not installed" menu 17 | */ 18 | export default class NotInstalledMenu extends AbstractMenu<{ 19 | setInfo: (info: string) => void; 20 | switchTo: (menu: MenuType) => void; 21 | }> { 22 | public constructor(props: { 23 | setInfo: (info: string) => void; 24 | switchTo: (menu: MenuType) => void; 25 | version: string; 26 | }) { 27 | super(props); 28 | } 29 | 30 | render(): JSX.Element { 31 | return ( 32 | 80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/renderer/routes/menus/options_menu.tsx: -------------------------------------------------------------------------------- 1 | import * as config from '../../operations/launcher_config'; 2 | import { Backend } from '../../operations/backend'; 3 | import { FocusButton } from '../../components/buttons/focus_button'; 4 | import { FocusCheckbox } from '../../components/buttons/focus_checkbox'; 5 | import { MenuType } from './menu'; 6 | import { AbstractMenu } from './abstract_menu'; 7 | import { PopupData } from '../../operations/popup_data'; 8 | import * as LauncherConfig from '../../operations/launcher_config'; 9 | 10 | /** 11 | * builds the options menu components 12 | * @returns the options menu 13 | */ 14 | export default class OptionsMenu extends AbstractMenu<{ 15 | setInfo: (info: string) => void; 16 | switchTo: (menu: MenuType) => void; 17 | version: string; 18 | }> { 19 | public constructor(props: { 20 | setInfo: (info: string) => void; 21 | switchTo: (menu: MenuType) => void; 22 | version: string; 23 | }) { 24 | super(props); 25 | } 26 | 27 | render(): JSX.Element { 28 | return ( 29 |
30 | this.props.switchTo(MenuType.MainMenu)} 34 | onFocus={() => this.props.setInfo('Return to the Main menu')} 35 | /> 36 | {Backend.isSwitch() ? ( 37 | { 40 | const enabled = await config.getBoolean('skip_launcher'); 41 | await config.setBoolean('skip_launcher', !enabled); 42 | }} 43 | checkStatus={async () => { 44 | return config.getBoolean('skip_launcher'); 45 | }} 46 | text={'Skip Launcher\u00A0'} 47 | onFocus={() => 48 | this.props.setInfo( 49 | 'Skip the launcher on boot unless updates are available.' 50 | ) 51 | } 52 | /> 53 | ) : ( 54 |
55 | )} 56 | { 59 | const enabled = await config.getBoolean('enable_dev_tools'); 60 | await config.setBoolean('enable_dev_tools', !enabled); 61 | }} 62 | checkStatus={async () => { 63 | return config.getBoolean('enable_dev_tools'); 64 | }} 65 | text={'Enable Dev Tools\u00A0'} 66 | onFocus={() => 67 | this.props.setInfo( 68 | 'Enable the dev tools menu (for HDR developers and contributors)' 69 | ) 70 | } 71 | /> 72 | { 75 | const enabled = await config.getBoolean('ignore_music'); 76 | await config.setBoolean('ignore_music', !enabled); 77 | console.info(`setting: ${!enabled}`); 78 | }} 79 | checkStatus={async () => { 80 | const checked = !(await config.getBoolean('ignore_music')); 81 | console.info(`Checked: ${checked}`); 82 | return checked; 83 | }} 84 | text={'Verify Music\u00A0'} 85 | onFocus={() => 86 | this.props.setInfo( 87 | 'Disable this if you wish to use music mods which conflict with HDR.' 88 | ) 89 | } 90 | /> 91 | {super.render()} 92 |
93 | ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/renderer/routes/menus/pr_installed_menu.tsx: -------------------------------------------------------------------------------- 1 | import { Progress } from 'nx-request-api'; 2 | import { PopupData } from 'renderer/operations/popup_data'; 3 | import { Backend } from '../../operations/backend'; 4 | import { 5 | installLatest, 6 | switchToBeta, 7 | switchToPrerelease, 8 | } from '../../operations/install'; 9 | import verify from '../../operations/verify'; 10 | import { FocusButton } from '../../components/buttons/focus_button'; 11 | import { MenuType } from './menu'; 12 | import { AbstractMenu } from './abstract_menu'; 13 | 14 | /** 15 | * builds the menu that appears when HDR is not installed 16 | * @returns the "not installed" menu 17 | */ 18 | export default class PrInstalledMenu extends AbstractMenu<{ 19 | setInfo: (info: string) => void; 20 | switchTo: (menu: MenuType) => void; 21 | }> { 22 | public constructor(props: { 23 | setInfo: (info: string) => void; 24 | switchTo: (menu: MenuType) => void; 25 | version: string; 26 | }) { 27 | super(props); 28 | } 29 | 30 | render(): JSX.Element { 31 | return ( 32 | 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/renderer/routes/menus/pull_request_preview.tsx: -------------------------------------------------------------------------------- 1 | import { Remark } from 'react-remark'; 2 | 3 | export function PullRequestPreview(props: { pullRequest: any }) { 4 | return ( 5 |
6 |

7 | {props.pullRequest.title} 8 |

9 |

13 | Labels:{' '} 14 | {props.pullRequest.labels.map((label: any) => ( 15 | 16 | [{String(label.name)}] 17 | 18 | ))} 19 |

20 | {props.pullRequest.labels.filter((label: any) => 21 | String(label.name).includes('includes assets') 22 | ).length != 0 ? ( 23 |

27 | Warning: Pull Requests including assets typically take ~10 minutes to 28 | clone existing assets and install the PR changes. 29 |

30 | ) : ( 31 |
32 | )} 33 |
34 |

35 | {props.pullRequest.body} 36 |

37 |
38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/renderer/routes/menus/tools_menu.tsx: -------------------------------------------------------------------------------- 1 | import { Progress } from 'nx-request-api'; 2 | import { Link, useNavigate } from 'react-router-dom'; 3 | import { Pages } from 'renderer/constants'; 4 | import { NavigateButton } from 'renderer/components/buttons/navigate_button'; 5 | import { Backend } from '../../operations/backend'; 6 | import { 7 | getInstallType, 8 | InstallType, 9 | switchToBeta, 10 | switchToPrerelease, 11 | } from '../../operations/install'; 12 | import verify from '../../operations/verify'; 13 | import { PrereleaseBetaButton } from '../../components/buttons/prerelease_beta_button'; 14 | import { MenuType } from './menu'; 15 | import { AbstractMenu } from './abstract_menu'; 16 | import { PopupData } from '../../operations/popup_data'; 17 | import { ScrollFocusButton } from '../../components/buttons/scroll_focus_button'; 18 | import { CloneFolderForDev } from '../../components/buttons/dev_tools_buttons'; 19 | 20 | /** 21 | * builds the tools menu components 22 | * @returns the tools menu 23 | */ 24 | export default class ToolsMenu extends AbstractMenu<{ 25 | setInfo: (info: string) => void; 26 | switchTo: (menu: MenuType) => void; 27 | }> { 28 | public constructor(props: { 29 | setInfo: (info: string) => void; 30 | switchTo: (menu: MenuType) => void; 31 | version: string; 32 | }) { 33 | super(props); 34 | } 35 | 36 | override showMenu(): void { 37 | super.showMenu(); 38 | this.props.switchTo(MenuType.Tools); 39 | } 40 | 41 | render(): JSX.Element { 42 | return ( 43 |
44 |
45 | this.props.switchTo(MenuType.MainMenu)} 49 | onFocus={() => this.props.setInfo('Return to the Main menu')} 50 | /> 51 | Backend.instance().openModManager()} 55 | onFocus={() => this.props.setInfo('Open the Mod Manager')} 56 | /> 57 | { 61 | verify((p: Progress) => this.showProgress(p)) 62 | .then((results) => { 63 | console.info('finished verifying successfully'); 64 | this.showMenu(); 65 | this.showPopupData( 66 | new PopupData(['Ok'], results, () => this.showMenu()) 67 | ); 68 | }) 69 | .catch((results) => { 70 | console.info('finished verifying, issues reported.'); 71 | this.showMenu(); 72 | this.showPopupData( 73 | new PopupData(['Ok'], results, () => this.showMenu()) 74 | ); 75 | }); 76 | }} 77 | onFocus={() => this.props.setInfo('Verify your HDR files')} 78 | /> 79 | 84 | this.props.setInfo('Open the stage configuration menu') 85 | } 86 | /> 87 | 88 | this.props.setInfo(info)} 90 | onClick={async (version: string) => { 91 | const installType = getInstallType(version); 92 | switch (installType) { 93 | case InstallType.Beta: 94 | await switchToPrerelease(version, (p: Progress) => 95 | this.showProgress(p) 96 | ) 97 | // .then(() => verify((p: Progress) => this.setProgress(p))) 98 | .then(() => { 99 | alert('Switched successfully!'); 100 | if (Backend.isSwitch()) { 101 | Backend.instance().relaunchApplication(); 102 | } 103 | this.props.switchTo(MenuType.MainMenu); 104 | }) 105 | .catch((e) => { 106 | this.props.switchTo(MenuType.MainMenu); 107 | alert(`Error during prerelease switch: ${e}`); 108 | }); 109 | break; 110 | case InstallType.PreRelease: 111 | await switchToBeta(version, (p: Progress) => 112 | this.showProgress(p) 113 | ) 114 | // .then(() => verify((p: Progress) => this.setProgress(p))) 115 | .then(() => { 116 | alert('Switched successfully!'); 117 | if (Backend.isSwitch()) { 118 | Backend.instance().relaunchApplication(); 119 | } 120 | this.props.switchTo(MenuType.MainMenu); 121 | }) 122 | .catch((e) => { 123 | this.props.switchTo(MenuType.MainMenu); 124 | alert(`Error during beta switch: ${e}`); 125 | }); 126 | break; 127 | default: 128 | console.error( 129 | 'Could not switch! Current version is unknown!' 130 | ); 131 | alert('Could not switch! Current version is unknown!'); 132 | break; 133 | } 134 | }} 135 | /> 136 | 141 | this.props.setInfo('Open the stage configuration menu') 142 | } 143 | /> 144 | 145 | this.showMenu()} 148 | setInfo={(info) => this.props.setInfo(info)} 149 | showProgress={(p) => this.showProgress(p)} 150 | then={async () => { 151 | const backend = Backend.instance(); 152 | const root = await backend.getSdRoot(); 153 | await backend.writeFile( 154 | `${root}ultimate/mods/hdr-dev/ui/hdr_version.txt`, 155 | 'v0.69.420-dev' 156 | ); 157 | }} 158 | /> 159 | this.showMenu()} 162 | setInfo={(info) => this.props.setInfo(info)} 163 | showProgress={(p) => this.showProgress(p)} 164 | /> 165 |
166 | {super.render()} 167 |
168 | ); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/renderer/routes/stage_config/stage_config_menu.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { FocusCheckbox } from 'renderer/components/buttons/focus_checkbox'; 4 | import { Pages } from 'renderer/constants'; 5 | import { ACTIVE_CONFIG_FILE, BACKUP_STAGE_CONFIG, ConfigData, loadConfigData, OFFICIAL_STAGE_CONFIG, save } from 'renderer/operations/stage_config'; 6 | import { Stage, StageInfo } from 'renderer/operations/stage_info'; 7 | import { FocusButton } from '../../components/buttons/focus_button'; 8 | import { FullScreenDiv } from '../../components/fullscreen_div'; 9 | import StageListBox from './stage_list_box'; 10 | import { StagePreview } from './stage_preview'; 11 | 12 | export default function StageConfigMenu() { 13 | const navigate = useNavigate(); 14 | const [hoveredStage, setHoveredStage] = useState(null as Stage | null); 15 | const [config, setConfig] = useState(null as null | ConfigData); 16 | const [options, setOptions] = useState(null as null | string[]); 17 | 18 | useEffect(() => { 19 | if (config !== null) { 20 | return; 21 | } 22 | 23 | loadConfigData(ACTIVE_CONFIG_FILE) 24 | .then(async (data) => { 25 | let enabled = data.enabled; 26 | if (data.useOfficial) { 27 | data = await loadConfigData(OFFICIAL_STAGE_CONFIG); 28 | data.enabled = enabled; 29 | } 30 | setConfig(data); 31 | }) 32 | .catch((e) => alert(`failed to preload stage config: ${e}`)); 33 | }, []); 34 | 35 | useEffect(() => { 36 | new StageInfo() 37 | .list() 38 | .then((list) => setOptions(list.map((stage) => stage?.display_name))) 39 | .catch((e) => alert(`failed to set new options: ${e}`)); 40 | }, [config]); 41 | 42 | return ( 43 | 44 |
45 | { 48 | if (config !== null) { 49 | save(ACTIVE_CONFIG_FILE, config); 50 | } 51 | navigate(Pages.MAIN_MENU); 52 | }} 53 | className="simple-button-bigger" 54 | onFocus={() => {}} 55 | autofocus 56 | /> 57 | {config ? ( 58 | { 61 | config.enabled = !config.enabled; 62 | save(ACTIVE_CONFIG_FILE, config); 63 | }} 64 | className="simple-button-bigger" 65 | onFocus={() => {}} 66 | checkStatus={async () => { 67 | return config.enabled; 68 | }} 69 | /> 70 | ) : ( 71 |
72 | )} 73 | {config ? ( 74 | { 77 | config.useOfficial = !config.useOfficial; 78 | if (config.useOfficial === true) { 79 | // save the current stagelist to the backup file, then load the official stagelist 80 | await save(BACKUP_STAGE_CONFIG, config); 81 | loadConfigData(OFFICIAL_STAGE_CONFIG).then(async (data) => { 82 | const newConfig = new ConfigData( 83 | config.enabled, 84 | true, 85 | data.starters, 86 | data.counterpicks 87 | ); 88 | await save(ACTIVE_CONFIG_FILE, newConfig); 89 | setConfig(newConfig); 90 | navigate(Pages.STAGE_CONFIG_REFRESH); 91 | }) 92 | } else { 93 | // load the stagelist from the backup file 94 | loadConfigData(BACKUP_STAGE_CONFIG).then(async (data) => { 95 | const newConfig = new ConfigData( 96 | config.enabled, 97 | false, 98 | data.starters, 99 | data.counterpicks 100 | ); 101 | await save(ACTIVE_CONFIG_FILE, newConfig); 102 | setConfig(newConfig); 103 | navigate(Pages.STAGE_CONFIG_REFRESH); 104 | }) 105 | } 106 | }} 107 | className="simple-button-bigger" 108 | onFocus={() => {}} 109 | checkStatus={async () => { 110 | return config.useOfficial; 111 | }} 112 | /> 113 | ) : ( 114 |
115 | )} 116 |
117 | {config && options ? ( 118 |
119 |
120 | { 125 | const newConfig = new ConfigData( 126 | config.enabled, 127 | config.useOfficial, 128 | stages, 129 | config.counterpicks 130 | ); 131 | save(ACTIVE_CONFIG_FILE, newConfig); 132 | setConfig(newConfig); 133 | }} 134 | onHover={(stage) => setHoveredStage(stage)} 135 | disabled={config.useOfficial} 136 | /> 137 | { 142 | const newConfig = new ConfigData( 143 | config.enabled, 144 | config.useOfficial, 145 | config.starters, 146 | stages 147 | ); 148 | save(ACTIVE_CONFIG_FILE, newConfig); 149 | setConfig(newConfig); 150 | }} 151 | onHover={(stage) => setHoveredStage(stage)} 152 | disabled={config.useOfficial} 153 | /> 154 |
155 |
156 |
157 | {hoveredStage !== null ? ( 158 | 159 | ) : ( 160 |
161 | )} 162 |
163 |
164 |
165 | ) : ( 166 |
loading...
167 | )} 168 | 169 | ); 170 | } 171 | -------------------------------------------------------------------------------- /src/renderer/routes/stage_config/stage_list_box.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { FocusButton } from 'renderer/components/buttons/focus_button'; 3 | import { FocusCombo } from 'renderer/components/buttons/focus_combo'; 4 | import { Backend } from 'renderer/operations/backend'; 5 | import {} from 'renderer/operations/stage_config'; 6 | import { Stage, StageInfo } from 'renderer/operations/stage_info'; 7 | 8 | type Categories = 'Starter' | 'Counterpick'; 9 | const MAX_STAGES = 7; 10 | const BACKGROUND_COLOR = 'var(--main-button-bg-color)'; 11 | const LEGACY_CONFIG_FILE = 12 | 'ultimate/mods/hdr-stages/ui/param/database/ui_stage_db.prcxml'; 13 | 14 | export default function StageListBox(props: { 15 | category: Categories; 16 | stages: Stage[]; 17 | options: string[]; 18 | onUpdate: (stages: Stage[]) => void; 19 | onHover?: (stage: Stage) => void; 20 | disabled?: boolean; 21 | }) { 22 | const [stages, setStages] = useState(props.stages); 23 | 24 | return ( 25 |
26 |
36 |

44 | {props.category}s 45 |

46 | {props.options ? ( 47 | stages.map((entry, idx) => ( 48 | { 52 | const info = new StageInfo(); 53 | const stage = await info.getByDisplay(item.target.value); 54 | props.stages[idx] = stage; 55 | props.onUpdate(props.stages); 56 | }} 57 | onHover={props.onHover} 58 | onRemove={() => { 59 | const newSelected: Stage[] = []; 60 | console.info(`ignoring: ${idx}`); 61 | props.stages.forEach((entry, thisIdx) => { 62 | if (idx != thisIdx) { 63 | newSelected.push(entry); 64 | } 65 | }); 66 | props.onUpdate(newSelected); 67 | setStages(newSelected); 68 | }} 69 | disabled={props.disabled} 70 | /> 71 | )) 72 | ) : ( 73 |
74 | )} 75 | {props.stages.length < MAX_STAGES && !props.disabled ? ( 76 | { 90 | const newSelected = []; 91 | const info = new StageInfo(); 92 | const firstAvailable = await info.getByDisplay( 93 | ( 94 | await info.list() 95 | )[0]?.display_name 96 | ); 97 | props.stages.forEach((entry) => newSelected.push(entry)); 98 | newSelected.push(firstAvailable); 99 | props.onUpdate(newSelected); 100 | setStages(newSelected); 101 | }} 102 | onFocus={() => {}} 103 | /> 104 | ) : ( 105 |
106 | )} 107 |
108 |
109 | ); 110 | } 111 | 112 | /** 113 | * Represents a row in the stage list window, including a combo box (dropdown) and a 'remove' button 114 | * @param props.onChange what to do if the selection changes 115 | * @param props.onRemove what to do if the remove button is pressed 116 | * @param props.selected the selected value 117 | * @returns void 118 | */ 119 | function StageListItem(props: { 120 | options: string[]; 121 | onChange: (item: { target: { value: string } }) => void; 122 | onRemove: () => void; 123 | selected: Stage; 124 | onHover?: (stage: Stage) => void; 125 | disabled?: boolean; 126 | }) { 127 | return ( 128 |
129 | { 141 | if (props.onHover) { 142 | props.onHover(props.selected); 143 | } 144 | }} 145 | options={props.options} 146 | disabled={props.disabled} 147 | /> 148 | {!props.disabled && ( 149 | { 161 | if (props.onHover) { 162 | props.onHover(props.selected); 163 | } 164 | }} 165 | onClick={props.onRemove} 166 | /> 167 | )} 168 |
169 | ); 170 | } 171 | -------------------------------------------------------------------------------- /src/renderer/routes/stage_config/stage_preview.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Backend } from 'renderer/operations/backend'; 3 | import logo from '../../../../assets/logo_full.png'; 4 | import { StageInfo, Stage } from '../../operations/stage_info'; 5 | 6 | export function StagePreview(props: { stage: Stage }) { 7 | const info = new StageInfo(); 8 | 9 | const labelStyle = { 10 | border: '3px solid var(--main-button-border-color)', 11 | transform: 'skewX(-20deg)', 12 | boxShadow: '3px 3px 5px #333', 13 | backgroundColor: 'var(--main-button-bg-color)', 14 | color: 'var(--main-text-color)', 15 | fontSize: '2rem', 16 | textAlign: 'right' as const, 17 | position: 'relative' as const, 18 | display: 'inline-block' as const, 19 | paddingLeft: '50px', 20 | paddingRight: '5px', 21 | top: 15, 22 | left: -45, 23 | }; 24 | 25 | const detailsStyle = { 26 | border: '2px solid var(--main-button-border-color)', 27 | transform: 'skewX(-20deg)', 28 | boxShadow: '3px 3px 5px #333', 29 | backgroundColor: 'var(--main-button-bg-color)', 30 | color: 'var(--main-text-color)', 31 | fontSize: '1.25rem', 32 | textAlign: 'right' as const, 33 | position: 'relative' as const, 34 | paddingLeft: '50px', 35 | paddingRight: '5px', 36 | display: 'inline-block' as const, 37 | top: 15, 38 | left: -45, 39 | }; 40 | 41 | if (props.stage === null) { 42 | return ( 43 |
44 | Logo 45 |
46 | ); 47 | } 48 | 49 | return ( 50 |
51 |
52 |
{props.stage?.display_name}
53 |
54 | {/*
Top Blastzone: {props.stage.blastzones.top}

55 |
Side Blastzone: {props.stage.blastzones.side}

56 |
Bottom Blastzone: {props.stage.blastzones.bottom}

*/} 57 |
58 | Preview { 69 | currentTarget.onerror = null; // prevents looping 70 | console.warn('failed to load preview!'); 71 | currentTarget.src = logo; 72 | }} 73 | /> 74 |
75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /src/renderer/styles/infobox.css: -------------------------------------------------------------------------------- 1 | .info-container { 2 | position: relative; 3 | width: 100%; 4 | height: 100%; 5 | margin-top: 15px; 6 | } 7 | 8 | .visible-box { 9 | position: relative; 10 | top: 60%; 11 | left: 20%; 12 | width: 60%; 13 | height: 60%; 14 | border: 1px solid white; 15 | text-align: center; 16 | padding: 15px; 17 | color: white; 18 | font-size: large; 19 | } 20 | -------------------------------------------------------------------------------- /src/renderer/styles/opening.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --loading-time: 3s; 3 | } 4 | 5 | .overlay-opening { 6 | z-index: 998; 7 | background-color: black; 8 | position: absolute; 9 | top: 0px; 10 | right: 0px; 11 | bottom: 0px; 12 | left: 0px; 13 | animation: fade-out var(--loading-time) ease; 14 | animation-fill-mode: forwards; 15 | -webkit-animation: fade-out var(--loading-time) ease; 16 | -webkit-animation-fill-mode: forwards; 17 | } 18 | .loading-button { 19 | font-size: 2rem; 20 | font-weight: bold; 21 | color: rgb(112, 226, 112); 22 | position: absolute; 23 | bottom: 30px; 24 | right: 10px; 25 | background-color: black; 26 | border: none; 27 | box-shadow: none; 28 | } 29 | 30 | .loading { 31 | top: 220px; 32 | position: relative; 33 | overflow: hidden; 34 | } 35 | 36 | .loading-left { 37 | position: relative; 38 | display: flex; 39 | animation: slide-from-left var(--loading-time) ease-in-out; 40 | animation-fill-mode: forwards; 41 | -webkit-animation: slide-from-left var(--loading-time) ease-in-out; 42 | -webkit-animation-fill-mode: forwards; 43 | top: 10px; 44 | } 45 | 46 | .loading-right { 47 | position: relative; 48 | display: flex; 49 | animation: slide-from-right var(--loading-time) ease-in-out; 50 | -webkit-animation: slide-from-right var(--loading-time) ease-in-out; 51 | animation-fill-mode: forwards; 52 | -webkit-animation-fill-mode: forwards; 53 | top: -10px; 54 | } 55 | 56 | .hewdraw { 57 | font-size: 4rem; 58 | font-weight: bold; 59 | font-style: italic; 60 | color: silver; 61 | } 62 | 63 | .remix { 64 | font-size: 5rem; 65 | font-weight: bold; 66 | font-style: italic; 67 | color: green; 68 | } 69 | 70 | @keyframes slide-from-left { 71 | 0% { 72 | transform: translateX(-20%); 73 | -webkit-transform: translateX(-20%); 74 | opacity: 0; 75 | } 76 | 30%, 77 | 70% { 78 | transform: translateX(36%); 79 | -webkit-transform: translateX(36%); 80 | opacity: 1; 81 | } 82 | 100% { 83 | transform: translateX(100%); 84 | -webkit-transform: translateX(100%); 85 | opacity: 0; 86 | } 87 | } 88 | 89 | @keyframes slide-from-right { 90 | 0% { 91 | transform: translateX(100%); 92 | -webkit-transform: translateX(100%); 93 | opacity: 0; 94 | } 95 | 30%, 96 | 70% { 97 | transform: translateX(45%); 98 | -webkit-transform: translateX(45%); 99 | opacity: 1; 100 | } 101 | 100% { 102 | transform: translateX(-20%); 103 | -webkit-transform: translateX(-20%); 104 | opacity: 0; 105 | } 106 | } 107 | 108 | @keyframes fade-out { 109 | 0%, 110 | 85% { 111 | opacity: 1; 112 | } 113 | 100% { 114 | opacity: 0; 115 | visibility: hidden; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/renderer/styles/progress.css: -------------------------------------------------------------------------------- 1 | .progress-block { 2 | color: white; 3 | overflow-wrap: break-word; 4 | text-align: center; 5 | position: relative; 6 | } 7 | 8 | .progress-background { 9 | background: linear-gradient( 10 | 30deg, 11 | var(--main-button-bg-color), 12 | rgb(94, 93, 93) 13 | ); 14 | } 15 | 16 | .vertical-center { 17 | top: 250px; 18 | position: relative; 19 | } 20 | 21 | .progress-wrapper { 22 | position: relative; 23 | left: 15%; 24 | width: 70%; 25 | border: 3px solid var(--buttons-border-color); 26 | transform: skewX(20deg); 27 | } 28 | 29 | .overlay-progress { 30 | z-index: 999; 31 | background: linear-gradient( 32 | 30deg, 33 | var(--main-button-bg-color), 34 | rgb(94, 93, 93) 35 | ); 36 | position: absolute; 37 | top: 0px; 38 | right: 0px; 39 | bottom: 0px; 40 | left: 0px; 41 | overflow: hidden; 42 | } 43 | -------------------------------------------------------------------------------- /src/renderer/styles/sidebar.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --sidebar-button-width: 30px; 3 | --sidebar-button-height: 150px; 4 | } 5 | 6 | .button-container { 7 | position: relative; 8 | top: 50%; 9 | width: var(--sidebar-button-width); 10 | height: var(--sidebar-button-height); 11 | } 12 | 13 | .open-button { 14 | position: relative; 15 | transform: translateX( 16 | calc( 17 | -0.5 * var(--sidebar-button-height) + 0.5 * var(--sidebar-button-width) 18 | ) 19 | ) 20 | rotate(-90deg); 21 | height: var(--sidebar-button-width); 22 | width: var(--sidebar-button-height); 23 | } 24 | 25 | .sidebar-container { 26 | transition: 400ms linear; 27 | -webkit-transition: 400ms linear; 28 | position: absolute; 29 | right: 0px; 30 | top: 10%; 31 | height: 70%; 32 | overflow-x: hidden; 33 | overflow-y: hidden; 34 | } 35 | 36 | .wide { 37 | width: 65%; 38 | } 39 | 40 | .open-sidebar { 41 | display: relative; 42 | width: calc(100% - var(--sidebar-button-width) - 6px); 43 | transform: translateX(var(--sidebar-button-width)) translateY(-125px); 44 | border: 3px solid var(--main-button-border-color); 45 | height: 90%; 46 | padding: 0px; 47 | color: white; 48 | } 49 | 50 | .blur-back { 51 | backdrop-filter: blur(10px); 52 | } 53 | 54 | .opaque { 55 | background-color: rgb(68, 68, 68); 56 | opacity: 0.5; 57 | } 58 | -------------------------------------------------------------------------------- /switch/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hdr-launcher-react" 3 | version = "0.7.2" 4 | authors = ["techyCoder81"] 5 | edition = "2021" 6 | 7 | [package.metadata.skyline] 8 | titleid = "01006A800016E000" # Smash Ultimate 9 | 10 | [lib] 11 | crate-type = ["cdylib"] 12 | 13 | [features] 14 | updater = [] 15 | no-npm = [] 16 | 17 | [dependencies] 18 | skyline-config = { git = "https://github.com/skyline-rs/skyline-config" } 19 | skyline = { git = "https://github.com/ultimate-research/skyline-rs.git" } 20 | skyline-web = { git = "https://github.com/techyCoder81/skyline-web", features = ["json"] } 21 | serde = { version = "1.0.136", features = ["derive"] } 22 | serde_json = "1.0.79" 23 | arcropolis-api = { git = "https://github.com/Raytwo/arcropolis_api" } 24 | md5 = "0.7.0" 25 | walkdir = "2" 26 | anyhow = "1" 27 | zip = { version = "0.6.2", default-features = false, features = ["deflate"] } 28 | nx-request-handler = { git = "https://github.com/techyCoder81/nx-request-handler" } 29 | #nx-request-handler = { path = "../../nx-request-handler" } 30 | semver = "1.0.14" 31 | minreq = { version = "2", features = ["https-native", "json-using-serde"] } 32 | include_dir = "0.7.3" 33 | smashnet = "0.2.1" 34 | fs_extra = "1.2.0" 35 | 36 | [build-dependencies] 37 | prettify-js = "0.1.0" 38 | npm_rs = "0.2.1" 39 | fs_extra = "1.2.0" 40 | 41 | [patch.crates-io] 42 | skyline = { git = "https://github.com/ultimate-research/skyline-rs.git" } 43 | skyline-web = { git = "https://github.com/techyCoder81/skyline-web" } 44 | nnsdk = { git = "https://github.com/ultimate-research/nnsdk-rs" } 45 | native-tls = { git = "https://github.com/skyline-rs/rust-native-tls", branch="switch" } 46 | 47 | [patch."https://github.com/Raytwo/skyline-rs"] 48 | skyline = { git = "https://github.com/ultimate-research/skyline-rs.git" } 49 | 50 | [profile.dev] 51 | panic = "abort" 52 | 53 | [profile.release] 54 | panic = "abort" 55 | lto = true 56 | -------------------------------------------------------------------------------- /switch/build.rs: -------------------------------------------------------------------------------- 1 | #![feature(exit_status_error)] 2 | use std::process::{ExitStatusError}; 3 | use std::path::Path; 4 | use npm_rs::*; 5 | use fs_extra::dir::CopyOptions; 6 | use std::io::{Write, Read}; 7 | 8 | const JS_FILE_PATH: &str = "./web-build/renderer/renderer.js"; 9 | 10 | fn main() -> (){ 11 | println!("cargo:rerun-if-changed=build.rs"); 12 | println!("cargo:rerun-if-changed=../release/app/dist/renderer/assets-manifest.json"); 13 | 14 | #[cfg(not(feature = "no-npm"))] 15 | // do the npm run build 16 | NpmEnv::default() 17 | .set_path("../") 18 | .init_env() 19 | .install(None) 20 | .run("build") 21 | .exec().unwrap() 22 | .exit_ok().unwrap(); 23 | 24 | #[cfg(feature = "no-npm")] 25 | println!("Not using automatic npm install and build."); 26 | 27 | // clear the web build dir if necessary 28 | if Path::exists(Path::new("./web-build")) { 29 | std::fs::remove_dir_all("./web-build").unwrap(); 30 | } 31 | std::fs::create_dir_all("./web-build").unwrap(); 32 | 33 | 34 | let paths = std::fs::read_dir("../").unwrap(); 35 | for path in paths { 36 | println!("Path: {}", path.unwrap().path().display()); 37 | } 38 | let options = CopyOptions::new(); 39 | fs_extra::dir::copy("../release/app/dist/renderer/", "./web-build/", &options).unwrap(); 40 | 41 | // read and transform the js file 42 | let mut src_js = std::fs::File::open(JS_FILE_PATH).unwrap(); 43 | let mut data_js = String::new(); 44 | src_js.read_to_string(&mut data_js).unwrap(); 45 | let new_data_js = data_js 46 | .replace("const ", "var ") 47 | .replace("let ", "var ") 48 | .replace("let{", "var{") 49 | .replace("let[", "var[") 50 | .replace("const{", "var{") 51 | .replace("const[", "var[") 52 | .replace("() => e.default : () => e", "(() => e.default) : (() => e)") 53 | .replace("()=>e.default:()=>e", "(()=>e.default):(()=>e)") 54 | .replace("() =>(module['default'])", "(()=>(module['default']))") 55 | .replace("() =>(module)", "(() =>(module))") 56 | .replace("e=>Object.getPrototypeOf(e)", "(e=>Object.getPrototypeOf(e))") 57 | .replace(".Component{state=yr;", ".Component{") 58 | .replace("resetErrorBoundary=(...e)=>", "resetErrorBoundary(e)") 59 | //.replace("this.isNode() ? \"Emulator\" : \"Switch\"", "(this.isNode() ? \"Emulator\" : \"Switch\")") 60 | .replace("\"assets/", "\""); 61 | 62 | std::fs::remove_file(JS_FILE_PATH).unwrap(); 63 | let mut dest_js = std::fs::File::create(JS_FILE_PATH).unwrap(); 64 | 65 | // prettyprint and then write the file 66 | let (pretty, _) = prettify_js::prettyprint(&new_data_js); 67 | let formatted = pretty.replace(" ? .", "?.") 68 | .replace("props.onReset?.(", "props.onReset && props.onReset(") 69 | .replace("props.onError?.(", "props.onError && props.onError("); 70 | dest_js.write(formatted.as_bytes()).unwrap(); 71 | } 72 | 73 | 74 | /*use std::fs; 75 | use std::env; 76 | use std::path::Path; 77 | use std::fs::File; 78 | use std::io::{self, Read, Write}; 79 | use prettify_js::*; 80 | 81 | static JS_FILE_PATH: &str = "../.webpack/renderer/main_window/index.js"; 82 | static HTML_FILE_PATH: &str = "../.webpack/renderer/main_window/index.html"; 83 | static LOGO_FILE_PATH: &str = "../.webpack/renderer/assets/logo_full.png"; 84 | static MUSIC_FILE_PATH: &str = "../.webpack/renderer/assets/theme.wav"; 85 | 86 | fn main() -> std::io::Result<()> { 87 | println!("current dir: {}", env::current_dir()?.display()); 88 | // Tell Cargo that if the given file changes, to rerun this build script. 89 | println!("cargo:rerun-if-changed={}", JS_FILE_PATH); 90 | println!("cargo:rerun-if-changed={}", HTML_FILE_PATH); 91 | println!("cargo:rerun-if-changed=build.rs"); 92 | if Path::exists(Path::new("./web-build")) { 93 | fs::remove_dir_all("./web-build")?; 94 | } 95 | fs::create_dir_all("./web-build")?; 96 | 97 | // read and transform the html file 98 | let mut src_html = File::open(HTML_FILE_PATH)?; 99 | let mut data_html = String::new(); 100 | src_html.read_to_string(&mut data_html); 101 | let new_data_html = format!("{}", data_html 102 | .replace("../main_window/", "") 103 | .replace("./main_window/", "") 104 | .replace("/main_window", "") 105 | .replace("../assets/", "") 106 | .replace("./assets/", "") 107 | .replace("/assets", "") 108 | .replace("const ", "var ")); 109 | let mut dest_html = File::create("web-build/index.html")?; 110 | 111 | //write the file 112 | dest_html.write(new_data_html.as_bytes())?; 113 | 114 | // read and transform the js file 115 | let mut src_js = File::open(JS_FILE_PATH)?; 116 | let mut data_js = String::new(); 117 | src_js.read_to_string(&mut data_js); 118 | let new_data_js = format!("{}", data_js 119 | .replace("const ", "var ") 120 | .replace("() => e.default : () => e", "(() => e.default) : (() => e)") 121 | .replace("()=>e.default:()=>e", "(()=>e.default):(()=>e)")) 122 | .replace("() =>(module['default'])", "(()=>(module['default']))") 123 | .replace("() =>(module)", "(() =>(module))") 124 | .replace("\"assets/", "\""); 125 | let mut dest_js = File::create("web-build/index.js")?; 126 | 127 | // prettyprint and then write the file 128 | let (pretty, _) = prettify_js::prettyprint(&new_data_js); 129 | dest_js.write(pretty.as_bytes())?; 130 | 131 | // copy the logo image file 132 | fs::copy(LOGO_FILE_PATH, "web-build/logo_full.png")?; 133 | 134 | // copy the music file 135 | fs::copy(MUSIC_FILE_PATH, "web-build/theme.wav")?; 136 | 137 | Ok(()) 138 | } */ -------------------------------------------------------------------------------- /switch/src/stage_config.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | use std::fs::OpenOptions; 3 | use std::io::prelude::*; 4 | 5 | 6 | const FILE_PATH: &str = "sd:/ultimate/mods/hdr-stages/ui/param/database/ui_stage_db.prcxml"; 7 | const CONFIG_PATH: &str = "sd:/ultimate/hdr-config/"; 8 | const STAGING_FILE: &str = "sd:/ultimate/hdr-config/ui_stage_db.prcxml.temp"; 9 | const DEFAULT_FILE: &str = "sd:/ultimate/mods/hdr-stages/ui/param/database/ui_stage_db.default"; 10 | /* 11 | pub fn enable_stages(stage_names: &Vec) -> Result { 12 | return set_stages(stage_names, true); 13 | } 14 | 15 | pub fn disable_stages(stage_names: &Vec) -> Result { 16 | return set_stages(stage_names, false); 17 | } 18 | 19 | pub fn set_stages(stage_names: &Vec, enabled: bool) -> Result { 20 | let xml = read_file()?; 21 | 22 | // parse the xml raw string 23 | let mut out_lines = vec![]; 24 | let mut lines = xml.lines(); 25 | let mut line = lines.next(); 26 | while line.is_some() { 27 | let mut line_str = line.unwrap(); 28 | out_lines.push(line_str); 29 | if (line_str.contains("name_id")) { 30 | let name = line_str.trim().trim_start_matches("").trim_end_matches(""); 31 | if stage_names.contains(&name.to_string()) { 32 | while line.is_some() && !line_str.contains("can_select") { 33 | line = lines.next(); 34 | line_str = line.unwrap(); 35 | out_lines.push(line_str); 36 | } 37 | let len = out_lines.len(); 38 | out_lines[len - 1] = match enabled { 39 | true => " True", 40 | false => " False" 41 | } 42 | } 43 | } 44 | line = lines.next(); 45 | } 46 | 47 | let result = match std::fs::write(FILE_PATH, out_lines.join("\n")) { 48 | Ok(()) => Ok("File written successfully".to_string()), 49 | Err(e) => Err(format!("{:?}", e)) 50 | }; 51 | println!("Done writing file, result: {:?}", result); 52 | return result; 53 | } 54 | */ 55 | 56 | /// read the existing stage db xml file 57 | pub fn read_file() -> Result { 58 | let exists = Path::new(FILE_PATH).exists(); 59 | if !exists { 60 | return Err("ui_stage_db.prcxml file does not exist!".to_string()); 61 | } 62 | 63 | return match std::fs::read_to_string(FILE_PATH) { 64 | Ok(xml) => Ok(xml.trim().replace("\n", "")), 65 | Err(e) => Err(format!("{:?}", e)) 66 | }; 67 | } 68 | 69 | /// read the current temp file 70 | pub fn read_temp_file() -> Result { 71 | let exists = Path::new(STAGING_FILE).exists(); 72 | if !exists { 73 | return Err("temp file does not exist!".to_string()); 74 | } 75 | 76 | return match std::fs::read_to_string(STAGING_FILE) { 77 | Ok(xml) => Ok(xml.trim().replace("\n", "")), 78 | Err(e) => Err(format!("{:?}", e)) 79 | } 80 | } 81 | 82 | 83 | /// create a new stage xml temp file 84 | pub fn new_temp_file() -> Result { 85 | let exists = Path::new(STAGING_FILE).exists(); 86 | if exists { 87 | match std::fs::remove_file(STAGING_FILE) { 88 | Ok(()) => {}, 89 | Err(e) => return Err(format!("error overwriting: {:?}", e)) 90 | }; 91 | } 92 | 93 | match std::fs::create_dir_all(CONFIG_PATH) { 94 | Ok(()) => (), 95 | Err(e) => return Err(format!("error creating config paths: {:?}", e)) 96 | }; 97 | return match std::fs::write(STAGING_FILE, "") { 98 | Ok(()) => Ok("file created successfully.".to_string()), 99 | Err(e) => return Err(format!("error writing to new temp file: {:?}", e)) 100 | }; 101 | } 102 | 103 | /// append data to the existing stage xml temp file 104 | pub fn append_temp_line(data: &str) -> Result { 105 | let exists = Path::new(STAGING_FILE).exists(); 106 | if !exists { 107 | return Err("staging file does not already exist!".to_string()); 108 | } 109 | 110 | let mut file = OpenOptions::new() 111 | .write(true) 112 | .append(true) 113 | .open(STAGING_FILE) 114 | .unwrap(); 115 | 116 | if let Err(e) = write!(file, "\n{}", data) { 117 | return Err(format!("Couldn't write to staging file: {}", e)); 118 | } 119 | 120 | Ok("Appended to file successfully.".to_string()) 121 | } 122 | 123 | /// overwrite the stage file with the temp file's data 124 | pub fn overwrite_stage_file() -> Result { 125 | // ensure the temp file exists 126 | let exists = Path::new(STAGING_FILE).exists(); 127 | if !exists { 128 | return Err("staging file does not already exist!".to_string()); 129 | } 130 | 131 | // read the staged temp data 132 | let staged_xml = match std::fs::read_to_string(STAGING_FILE) { 133 | Ok(xml) => xml, 134 | Err(e) => return Err(format!("error overwriting: {:?}", e)) 135 | }; 136 | 137 | return write_stage_file(&staged_xml); 138 | } 139 | 140 | /// overwrite the stage file with the temp file's data 141 | pub fn reset_stage_file() -> Result { 142 | // ensure the default file exists 143 | let exists = Path::new(DEFAULT_FILE).exists(); 144 | if !exists { 145 | return Err("default file does not already exist!".to_string()); 146 | } 147 | 148 | // read the default data 149 | let default_xml = match std::fs::read_to_string(DEFAULT_FILE) { 150 | Ok(xml) => xml, 151 | Err(e) => return Err(format!("error resetting defaults: {:?}", e)) 152 | }; 153 | 154 | return write_stage_file(&default_xml); 155 | } 156 | 157 | pub fn write_stage_file(xml: &str) -> Result { 158 | // write that data to the real file 159 | return match std::fs::write(FILE_PATH, xml.trim().replace("\t", " ")) { 160 | Ok(()) => Ok("file overwritten successfully".to_string()), 161 | Err(e) => Err(format!("error overwriting: {:?}", e)) 162 | } 163 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "target": "es5", 5 | "module": "commonjs", 6 | "lib": ["dom", "es2015"], 7 | "jsx": "react-jsx", 8 | "strict": true, 9 | "sourceMap": true, 10 | "baseUrl": "./src", 11 | "moduleResolution": "node", 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "resolveJsonModule": true, 15 | "allowJs": true, 16 | "outDir": ".erb/dll", 17 | "downlevelIteration": true, 18 | }, 19 | "exclude": ["test", "release/build", "release/app/dist", ".erb/dll"] 20 | } 21 | --------------------------------------------------------------------------------