├── .eslintignore ├── .eslintrc-old.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── build.yml │ ├── latest.yml │ └── nexus.yml ├── .gitignore ├── .gitmodules ├── .vscode └── launch.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bench └── 1k_rand.txt ├── crowdin.yml ├── docs ├── configuration │ ├── config.md │ ├── credits.md │ ├── introduction.md │ ├── preferences.md │ ├── services.md │ └── shortcuts.md ├── development │ ├── addingpages.md │ ├── architecture.md │ ├── communication.md │ ├── database.md │ ├── extapi.md │ ├── gitworkflow.md │ ├── introduction.md │ ├── lang.md │ └── setup.md ├── extensions │ └── overview.md ├── introduction.md └── middleware.md ├── eslint.config.mjs ├── extensions └── core-ruffle │ ├── .gitignore │ ├── README.md │ ├── gulpfile.js │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── extension.ts │ ├── middleware │ │ ├── embed.ts │ │ └── standalone.ts │ ├── template │ │ └── basic.ts │ ├── types.ts │ └── util.ts │ ├── static │ ├── logo.svg │ └── templates │ │ ├── basic.css │ │ └── basic.mustache │ ├── tsconfig.json │ └── webpack.config.js ├── extern ├── elevate │ ├── Elevate.exe │ └── mklink.bat └── utils │ └── sourceBuild.py ├── gulpfile.extensions.js ├── gulpfile.js ├── gulpfile.util.js ├── icons ├── icon.icns ├── icon.ico └── icon.png ├── jest.config.ts ├── lang ├── cs-CZ.json ├── de-DE.json ├── el-GR.json ├── en.json ├── es-ES.json ├── et-EE.json ├── fi-FI.json ├── fr-FR.json ├── ga-IE.json ├── it-IT.json ├── ja-JP.json ├── mk-MK.json ├── nl-NL.json ├── pl-PL.json ├── pt-BR.json ├── ro-RO.json ├── ru-RU.json ├── tr-TR.json ├── vi-VN.json ├── zh-CN.json └── zh-TW.json ├── lefthook.yml ├── licenses ├── 7zip │ └── LICENSE.txt ├── open-iconic │ ├── ICON-LICENSE │ └── source.txt └── vscode │ └── LICENSE.txt ├── ormconfig.json ├── package-lock.json ├── package.json ├── secrets.json.example ├── src ├── back │ ├── ConfigFile.ts │ ├── Downloader.ts │ ├── Execs.ts │ ├── ExtConfigFile.ts │ ├── GameLauncher.ts │ ├── InstancedAbortController.ts │ ├── ManagedChildProcess.ts │ ├── MetaEdit.ts │ ├── PlaylistFile.ts │ ├── ServicesFile.ts │ ├── SocketServer.ts │ ├── SocketServerMiddleware.ts │ ├── Themes.ts │ ├── constants.ts │ ├── curate │ │ ├── format │ │ │ ├── parser.ts │ │ │ ├── stringifier.ts │ │ │ └── tokenizer.ts │ │ ├── fpfss.ts │ │ ├── parse.ts │ │ ├── read.ts │ │ ├── util.ts │ │ └── write.ts │ ├── dns.ts │ ├── download.ts │ ├── extensions │ │ ├── ApiEmitter.ts │ │ ├── ApiEvent.ts │ │ ├── ApiImplementation.ts │ │ ├── ExtensionService.ts │ │ ├── ExtensionUtils.ts │ │ ├── ExtensionsScanner.ts │ │ ├── NodeInterceptor.ts │ │ ├── types.ts │ │ └── util.ts │ ├── importGame.ts │ ├── index.ts │ ├── metadataImport.ts │ ├── middleware.ts │ ├── playlist.ts │ ├── responses.ts │ ├── rust.ts │ ├── sync.ts │ ├── types.ts │ └── util │ │ ├── EventAggregator.ts │ │ ├── EventQueue.ts │ │ ├── FileServer.ts │ │ ├── FolderWatcher.ts │ │ ├── LogFile.ts │ │ ├── SevenZip.ts │ │ ├── WrappedEventEmitter.ts │ │ ├── async.ts │ │ ├── charCode.ts │ │ ├── dialog.ts │ │ ├── elevate.ts │ │ ├── events.ts │ │ ├── extensions.ts │ │ ├── gameConfig.ts │ │ ├── lifecycle.ts │ │ ├── logging.ts │ │ ├── map.ts │ │ ├── misc.ts │ │ ├── search.ts │ │ ├── sql.ts │ │ ├── strings.ts │ │ └── uuid.ts ├── main │ ├── BrowserMode.ts │ ├── LogsWindow.ts │ ├── Main.ts │ ├── MainWindowPreload.ts │ ├── Util.ts │ ├── index.ts │ └── types.ts ├── renderer │ ├── Util.ts │ ├── changelog.ts │ ├── components │ │ ├── BoxList.tsx │ │ ├── CheckBox.tsx │ │ ├── ConfigBox.tsx │ │ ├── ConfigBoxButton.tsx │ │ ├── ConfigBoxCheckbox.tsx │ │ ├── ConfigBoxInput.tsx │ │ ├── ConfigBoxMultiSelect.tsx │ │ ├── ConfigBoxSelect.tsx │ │ ├── ConfigBoxSelectInput.tsx │ │ ├── ConfigFlashpointPathInput.tsx │ │ ├── ConfirmButton.tsx │ │ ├── ConfirmDialog.tsx │ │ ├── ConfirmElement.tsx │ │ ├── CreditsProfile.tsx │ │ ├── CreditsTooltip.tsx │ │ ├── CurateBox.tsx │ │ ├── CurateBoxAddApp.tsx │ │ ├── CurateBoxCheckBoxRow.tsx │ │ ├── CurateBoxInputRow.tsx │ │ ├── CurateBoxRow.tsx │ │ ├── CurateBoxWarnings.tsx │ │ ├── CuratePageLeftSidebar.tsx │ │ ├── Dialog.tsx │ │ ├── Dropdown.tsx │ │ ├── DropdownInput.tsx │ │ ├── DropdownInputField.tsx │ │ ├── EditableTextElement.tsx │ │ ├── FancyAnimation.tsx │ │ ├── FloatingContainer.tsx │ │ ├── Footer.tsx │ │ ├── FpfssEditGame.tsx │ │ ├── GameConfigDialog.tsx │ │ ├── GameDataBrowser.tsx │ │ ├── GameDataInfo.tsx │ │ ├── GameGrid.tsx │ │ ├── GameGridItem.tsx │ │ ├── GameImageSplit.tsx │ │ ├── GameItemContainer.tsx │ │ ├── GameList.tsx │ │ ├── GameListHeader.tsx │ │ ├── GameListItem.tsx │ │ ├── GameOrder.tsx │ │ ├── Header.tsx │ │ ├── HomePageBox.tsx │ │ ├── ImagePreview.tsx │ │ ├── InputField.tsx │ │ ├── LeftBrowseSidebar.tsx │ │ ├── LogData.tsx │ │ ├── MetaEditExporter.tsx │ │ ├── OpenIcon.tsx │ │ ├── PlaylistContent.tsx │ │ ├── PlaylistItem.tsx │ │ ├── ProgressComponents.tsx │ │ ├── RandomGames.tsx │ │ ├── ResizableSidebar.tsx │ │ ├── RightBrowseSidebar.tsx │ │ ├── RightBrowseSidebarAddApp.tsx │ │ ├── RightTagCategoriesSidebar.tsx │ │ ├── RightTagsSidebar.tsx │ │ ├── SearchBar.tsx │ │ ├── ServiceBox.tsx │ │ ├── SimpleButton.tsx │ │ ├── SizeProvider.tsx │ │ ├── Spinner.tsx │ │ ├── SplashScreen.tsx │ │ ├── TagAliasInputField.tsx │ │ ├── TagCategoriesList.tsx │ │ ├── TagCategoriesListHeader.tsx │ │ ├── TagCategoriesListItem.tsx │ │ ├── TagFilterGroupEditor.tsx │ │ ├── TagInputField.tsx │ │ ├── TagItemContainer.tsx │ │ ├── TagList.tsx │ │ ├── TagListHeader.tsx │ │ ├── TagListItem.tsx │ │ ├── TaskBar.tsx │ │ ├── TitleBar.tsx │ │ ├── app.tsx │ │ └── pages │ │ │ ├── AboutPage.tsx │ │ │ ├── BrowsePage.tsx │ │ │ ├── ConfigPage.tsx │ │ │ ├── CuratePage.tsx │ │ │ ├── DeveloperPage.tsx │ │ │ ├── Downloads.tsx │ │ │ ├── HomePage.tsx │ │ │ ├── IFramePage.tsx │ │ │ ├── LoadingPage.tsx │ │ │ ├── LogsPage.tsx │ │ │ ├── NotFoundPage.tsx │ │ │ ├── TagCategoriesPage.tsx │ │ │ └── TagsPage.tsx │ ├── containers │ │ ├── ConnectedApp.ts │ │ ├── ConnectedBrowsePage.ts │ │ ├── ConnectedConfigPage.ts │ │ ├── ConnectedCuratePage.ts │ │ ├── ConnectedFooter.ts │ │ ├── ConnectedHomePage.ts │ │ ├── ConnectedLeftBrowseSidebar.ts │ │ ├── ConnectedLogsPage.ts │ │ ├── ConnectedRightBrowseSidebar.ts │ │ ├── ConnectedRightTagsCategoriesSidebar.ts │ │ ├── ConnectedRightTagsSidebar.ts │ │ ├── ConnectedTagCategoriesPage.tsx │ │ ├── ConnectedTagsPage.ts │ │ ├── HeaderContainer.tsx │ │ ├── withConfirmDialog.tsx │ │ ├── withCurateState.ts │ │ ├── withFpfss.ts │ │ ├── withMainState.ts │ │ ├── withPreferences.tsx │ │ ├── withProgress.tsx │ │ ├── withSearch.ts │ │ ├── withTagCategories.ts │ │ ├── withTasks.ts │ │ └── withView.tsx │ ├── context-reducer │ │ ├── ContextReducerProvider.tsx │ │ ├── contextReducer.ts │ │ └── interfaces.ts │ ├── context │ │ ├── CurationContext.ts │ │ ├── PreferencesContext.tsx │ │ └── ProgressContext.ts │ ├── credits │ │ ├── CreditsFile.ts │ │ └── types.ts │ ├── curate │ │ ├── importCuration.ts │ │ └── util.ts │ ├── fpfss.ts │ ├── hooks │ │ ├── search.ts │ │ ├── useAppSelector.ts │ │ ├── useMouse.ts │ │ ├── usePreferences.ts │ │ ├── useStateRef.ts │ │ └── useThrottle.ts │ ├── index.tsx │ ├── interfaces.ts │ ├── logger.tsx │ ├── router.tsx │ ├── store │ │ ├── curate │ │ │ ├── middleware.ts │ │ │ └── slice.ts │ │ ├── fpfss │ │ │ └── slice.ts │ │ ├── listenerMiddleware.ts │ │ ├── main │ │ │ ├── middleware.ts │ │ │ └── slice.ts │ │ ├── reactKeybindCompat.tsx │ │ ├── search │ │ │ ├── middleware.ts │ │ │ └── slice.ts │ │ ├── store.ts │ │ ├── tagCategories │ │ │ └── slice.ts │ │ └── tasks │ │ │ └── slice.ts │ ├── upgrade │ │ ├── UpgradeFile.ts │ │ └── types.ts │ └── util │ │ ├── SevenZip.ts │ │ ├── async.ts │ │ ├── lang.ts │ │ ├── logging.ts │ │ └── queue.ts └── shared │ ├── BrowsePageLayout.ts │ ├── IPC.ts │ ├── Log │ ├── LogCommon.ts │ └── interface.ts │ ├── MetaEdit.ts │ ├── Paths.ts │ ├── Theme.ts │ ├── ThemeFile.ts │ ├── Util.ts │ ├── back │ ├── SocketClient.ts │ └── types.ts │ ├── config │ ├── interfaces.ts │ └── util.ts │ ├── constants.ts │ ├── curate │ ├── OLD_types.ts │ ├── defaultValues.ts │ ├── format │ │ ├── parser.ts │ │ ├── stringifier.ts │ │ └── tokenizer.ts │ ├── fpfss.ts │ ├── metaToMeta.ts │ ├── parse.ts │ ├── types.ts │ └── util.ts │ ├── eventResponseDebouncer.ts │ ├── extensions │ └── interfaces.ts │ ├── game │ └── util.ts │ ├── interfaces.ts │ ├── lang.ts │ ├── legacy │ ├── GameManager.ts │ ├── GameParser.ts │ ├── interfaces.ts │ ├── misc.ts │ └── types.ts │ ├── library │ └── util.ts │ ├── memoize.ts │ ├── order │ └── util.ts │ ├── preferences │ ├── PreferencesFile.ts │ └── util.ts │ ├── search │ └── util.ts │ ├── socket │ ├── SocketAPI.ts │ ├── SocketServer.ts │ ├── shared.ts │ └── types.ts │ ├── tsconfig.json │ ├── upgrade │ └── util.ts │ └── utils │ ├── Coerce.ts │ ├── Gate.ts │ ├── ObjectParser.ts │ ├── StringFormatter.ts │ ├── TaskProgress.ts │ ├── VariableString.ts │ ├── compare.ts │ ├── debounce.ts │ ├── misc.ts │ ├── sanitizeFilename.ts │ ├── throttle.ts │ └── uuid.ts ├── static └── window │ ├── flash_index.html │ ├── images │ ├── Logos │ │ ├── 404.png │ │ └── Extreme.png │ ├── cross.png │ ├── icon.png │ ├── logo-solid.svg │ ├── max.png │ └── min.png │ ├── index.html │ ├── logger.html │ ├── styles │ ├── core.css │ ├── fancy.css │ └── react-datepicker.css │ └── svg │ └── open-iconic.svg ├── swcrc.back.dev.json ├── swcrc.back.prod.json ├── tests ├── setup.ts ├── unit │ └── back │ │ ├── configuration.test.ts │ │ └── throttle.test.ts └── util │ ├── types.ts │ └── util.ts ├── tsconfig.backend.json ├── tsconfig.json ├── tsconfig.renderer.json ├── tslint.json ├── typings ├── flashpoint-launcher.d.ts ├── globals.d.ts └── react-virtualized-reactv17.d.ts ├── updateLangs.mjs └── website ├── .gitignore ├── babel.config.js ├── docusaurus.config.ts ├── package-lock.json ├── package.json ├── sidebars.ts ├── src ├── css │ └── core.css └── pages │ └── index.tsx ├── static ├── .nojeykll └── img │ ├── favicon.svg │ ├── icon.png │ ├── logo-solid.svg │ └── meta.png └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | *.json 2 | src/database/migration/* 3 | build/* 4 | typings/* 5 | gulpfile.* 6 | *webpack*.config.js -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. Windows 10] 28 | - Flashpoint Version [e.g. 7.1] 29 | - Log text (if relevant) 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build Status 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - '*' 7 | push: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | name: Build 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Checkout submodules 19 | shell: bash 20 | run: | 21 | auth_header="$(git config --local --get http.https://github.com/.extraheader)" 22 | git submodule sync --recursive 23 | git -c "http.extraheader=$auth_header" -c protocol.version=2 submodule update --init --force --recursive --depth=1 24 | - name: Use Node.js 20.x 25 | uses: actions/setup-node@v1 26 | with: 27 | node-version: 20.x 28 | - uses: actions/cache@v3 29 | id: cache 30 | with: 31 | path: | 32 | **/node_modules 33 | key: ${{ runner.os }}-${{ hashFiles('package.json') }} 34 | - name: Install Dependencies 35 | run: npm install --force 36 | if: steps.cache.outputs.cache-hit != 'true' 37 | env: 38 | CI: true 39 | - name: npm build 40 | run: npm run build 41 | env: 42 | CI: true 43 | -------------------------------------------------------------------------------- /.github/workflows/latest.yml: -------------------------------------------------------------------------------- 1 | name: Release Status 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - 'release/**' 8 | - 'hotfix/**' 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | runs-on: ${{ matrix.os }} 14 | 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, windows-2019, macOS-latest] 18 | 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: Checkout submodules 22 | shell: bash 23 | run: | 24 | auth_header="$(git config --local --get http.https://github.com/.extraheader)" 25 | git submodule sync --recursive 26 | git -c "http.extraheader=$auth_header" -c protocol.version=2 submodule update --init --force --recursive --depth=1 27 | - name: Use Node.js 20.x 28 | uses: actions/setup-node@v1 29 | with: 30 | node-version: 20.x 31 | - uses: actions/cache@v3 32 | id: cache 33 | with: 34 | path: | 35 | **/node_modules 36 | key: ${{ runner.os }}-${{ hashFiles('package.json') }} 37 | - name: Install Dependencies 38 | run: npm install --force 39 | if: steps.cache.outputs.cache-hit != 'true' 40 | env: 41 | CI: true 42 | - name: Install dmg-license 43 | if: matrix.os == 'macOS-latest' 44 | run: npm install dmg-license --force 45 | - name: Build and Release 46 | run: npm run release 47 | env: 48 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 49 | EP_DRAFT: true 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node/NPM 2 | node_modules 3 | npm-debug.log.* 4 | 5 | # Visual Studio Code 6 | /.vscode 7 | /.vs 8 | !/.vscode/launch.json 9 | 10 | # JetBrains 11 | /.idea 12 | 13 | # Build 14 | /build 15 | 16 | # Distribution 17 | /dist 18 | 19 | # Temp 20 | /temp 21 | /*.cpuprofile 22 | 23 | # Launcher files (created during testing) 24 | /config.json 25 | /preferences.json 26 | /extConfig.json 27 | /.installed 28 | /.version 29 | /version.txt 30 | /secret.txt 31 | /secret.dat 32 | /launcher.log 33 | /extern/bluezip/bluezip.db 34 | /flashpoint.sqlite 35 | /extensions/test 36 | /src/shared/version.ts 37 | Data 38 | 39 | # Jest related files 40 | /.coveralls.yml 41 | /coverage 42 | /tests/result 43 | 44 | # Mac 45 | .DS_Store 46 | 47 | secrets.json 48 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "extern/7zip-bin"] 2 | path = extern/7zip-bin 3 | url = https://github.com/develar/7zip-bin.git 4 | [submodule "docs/api"] 5 | path = docs/api 6 | url = https://github.com/FlashpointProject/launcher_ApiDocs.git 7 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Electron: Main", 6 | "type": "node", 7 | "request": "launch", 8 | "cwd": "${workspaceFolder}", 9 | "autoAttachChildProcesses": true, 10 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron", 11 | "windows": { 12 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd" 13 | }, 14 | "args" : [ 15 | "--remote-debugging-port=9223", 16 | "." 17 | ], 18 | "outputCapture": "std" 19 | }, 20 | { 21 | "name": "Electron: Renderer", 22 | "type": "chrome", 23 | "request": "attach", 24 | "port": 9223, 25 | "webRoot": "${workspaceFolder}", 26 | "timeout": 30000 27 | } 28 | ], 29 | "compounds": [ 30 | { 31 | "name": "Electron: All", 32 | "configurations": [ 33 | "Electron: Main", 34 | "Electron: Renderer" 35 | ] 36 | } 37 | ] 38 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | It's recommended to read the Documentation if you wish to contribute to Flashpoint Launcher development. 2 | 3 | You'll likely find help in the #launcher channel of the discord as well. 4 | 5 | Documentation: https://flashpointproject.github.io/launcher 6 | 7 | Discord: https://discordapp.com/invite/qhvAkhWXU5 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 - 2020, Jesper Gustafsson, Noah Loomans and Colin Berry 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 | 23 | Some elements of the Flashpoint Launcher are licensed from others, see below - 24 | 25 | Microsoft - Visual Studio Code - /licenses/vscode (MIT) 26 | Modifications of their Extensions code and util functions 27 | 28 | Iconic - Open Iconic - /licenses/open-iconic (MIT) 29 | Icons used in parts of the frontend 30 | 31 | 7zip - 7zip - /licenses/7zip (LGPL) 32 | (Un)Packing of archive files -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | files: 2 | - source: /lang/en.json 3 | translation: /lang/%locale%.json 4 | -------------------------------------------------------------------------------- /docs/configuration/credits.md: -------------------------------------------------------------------------------- 1 | # Credits 2 | 3 | Credits can be shown on the About page. If you're using the launcher in another project this can be a good way to credit people who have worked on it. 4 | 5 | Roles can be used as categories under which profiles will show. The category that the profile is under is determined by which in their roles list is first defined. 6 | 7 | E.G Profile roles in the order `"Two", "One", "Three"` will match `Two` first. 8 | 9 | ### Layout 10 | 11 | ```json title="/Data/credits.json" 12 | { 13 | "roles": [ 14 | { 15 | ... 16 | } 17 | ], 18 | "profiles": [ 19 | { 20 | ... 21 | } 22 | ] 23 | } 24 | ``` 25 | 26 | ### Roles 27 | 28 | Multiple roles can be assigned to multiple profiles. They can be given colors and whether they are used as a category or not is configurable. 29 | 30 | **Name** - The name of the role to show 31 | 32 | **Color** - The color of the role. This can be any valid CSS color format. 33 | 34 | **No Category** - (Optional) - Whether or not this should be a category. 35 | 36 | ```json 37 | { 38 | "name": "RoleName", 39 | "color": "#123456", 40 | "noCategory": true 41 | } 42 | ``` 43 | 44 | ### Profiles 45 | 46 | Profiles contain information on a user - Their title (name), what roles they have, a description on them and their icon to show. 47 | 48 | **Title** - The name of the person this profile is for. 49 | 50 | **Roles** - A list of their roles as strings matching role names. 51 | 52 | **Note** - A note / description of the person, usually their contribution. 53 | 54 | **Icon** - Injected into CSS to display the icon. `url("")`. 55 | This can be a url to a local file but can also be a base64 encoded image in the form of `data:image/;base64,` 56 | 57 | **Top Role** - (Optional) - Overrides the profiles roles list and uses the Top Role as the category. 58 | 59 | ```json 60 | { 61 | "title": "ThatOneGuy", 62 | "roles": ["list", "of", "roles"], 63 | "note": "What he did", 64 | "icon": "Icon", 65 | "topRole": "Administrator" 66 | } 67 | ``` -------------------------------------------------------------------------------- /docs/configuration/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | ## Overview 4 | 5 | The Configuration documentation is written from the perspective of someone wanting to use Flashpoint Launcher for their own project, and made using the knowledge of how it works for Flashpoint Archive. 6 | 7 | [**config.json**](config) - `config.json` defines the most basic operation settings, such as path to the root data folder and some immutable options like logs server url and Game of the Day remote server url. 8 | 9 | [**preferences.json**](preferences) - `preferences.json` defines user specific and data version specific settings like different folder paths, Browse page and Curate page settings. This page will explain how to set default preferences for the user, including when they delete their own preferences file. 10 | 11 | [**Services**](services) - `/Data/services.json` defines the background services, and required server process that Games will need. This is fairly flexible and should fit your needs most of the time. 12 | 13 | [**Shortcuts**](shortcuts) - Defined in `preferences.json` this covers all keyboard shortcuts in the Flashpoint Launcher. This is only relevant for the Curate page currently. 14 | 15 | [**Credits**](credits) - `/Data/credits.json` defines the user details that appear on the about page. This should be specific to the people working on your project. 16 | -------------------------------------------------------------------------------- /docs/configuration/shortcuts.md: -------------------------------------------------------------------------------- 1 | # Keyboard Shortcuts 2 | 3 | ## Hardcoded 4 | 5 | `CTRL + SHIFT + R` - Restart the Electron window, without restarting the backend. 6 | 7 | ## Configurable 8 | 9 | Keyboard shortcuts are configurable in `preferences.json`. 10 | 11 | Actions are defined as `section - action` (e.g. `curate - prev`) 12 | 13 | All shortcuts are defined in an array and can accept multiple binds. `ctrl` and `cmd` should both be used to support both Mac and Windows based keyboards. 14 | 15 | ```json title="/preferences.json" 16 | "shortcuts": { 17 | "curate": { 18 | "prev": [ 19 | "ctrl+arrowup", 20 | "cmd+arrowup" 21 | ] 22 | } 23 | } 24 | ``` 25 | 26 | ### List of Actions 27 | 28 | - `curate` - *Active on the Curate Page* 29 | - `prev` - *Show previous curation* 30 | - `next` - *Show next curation* 31 | - `load` - *Open dialog to load a curation archive* 32 | - `newCur` - *Create a new curation* 33 | - `deleteCurs` - *Delete all selected curations* 34 | - `exportCurs` - *Export all selected curations* 35 | - `exportDataPacks` - *Export data packs for all selected curations* 36 | - `importCurs` - *Imports all selected curations* 37 | - `refresh` - *Refreshes the content tree view for the curation* 38 | - `run` - *Runs the curation* 39 | - `runMad4fp` - *Runs the curation with a MAD4FP enabled server, if available* 40 | 41 | ## Default Configuration 42 | 43 | ```json title="/preferences.json" 44 | "shortcuts": { 45 | "curate": { 46 | "prev": [ 47 | "ctrl+arrowup", 48 | "cmd+arrowup" 49 | ], 50 | "next": [ 51 | "ctrl+arrowdown", 52 | "cmd+arrowdown" 53 | ], 54 | "load": [ 55 | "ctrl+o", 56 | "cmd+o" 57 | ], 58 | "newCur": [ 59 | "ctrl+n", 60 | "cmd+n" 61 | ], 62 | "deleteCurs": [ 63 | "ctrl+delete", 64 | "cmd+delete" 65 | ], 66 | "exportCurs": [ 67 | "ctrl+s", 68 | "cmd+s" 69 | ], 70 | "exportDataPacks": [ 71 | "ctrl+shift+s", 72 | "cmd+shift+s" 73 | ], 74 | "importCurs": [ 75 | "ctrl+i", 76 | "cmd+i" 77 | ], 78 | "refresh": [ 79 | "ctrl+r", 80 | "cmd+r" 81 | ], 82 | "run": [ 83 | "ctrl+t", 84 | "cmd+t" 85 | ], 86 | "runMad4fp": [ 87 | "ctrl+shift+t", 88 | "cmd+shift+t" 89 | ] 90 | } 91 | } 92 | ``` 93 | -------------------------------------------------------------------------------- /docs/development/architecture.md: -------------------------------------------------------------------------------- 1 | # Architecture 2 | 3 | ## Overview 4 | 5 | ```mermaid 6 | flowchart TD 7 | Main-.Forks.->Backend[Node Backend] 8 | Main<--IPC-->Front[Electron Frontend] 9 | Backend<--Websocket-->Front 10 | Backend---Database[Database API] 11 | Backend---Extensions 12 | ``` 13 | 14 | ## Startup Procedure 15 | 16 | ```mermaid 17 | sequenceDiagram 18 | Main Process->>Node Backend: Fork Process 19 | Main Process->>Node Backend: Sync via Window Messages 20 | Node Backend-->>Main Process: Signal Websocket is Ready 21 | Main Process->>Electron Frontend: Open Window w/ Connected Websocket 22 | Electron Frontend->>Node Backend: Prompt Initialization 23 | Node Backend-->>Electron Frontend: Current Load State + Config & Preferences 24 | Node Backend->>Electron Frontend: *Signals each LOADED event* 25 | Electron Frontend->>Node Backend: Request data when needed systems loaded 26 | ``` -------------------------------------------------------------------------------- /docs/development/database.md: -------------------------------------------------------------------------------- 1 | # Database API 2 | 3 | ## Overview 4 | 5 | The Database API is only available from the backend. The frontend must make a request (see [Front / Back Communication](communication)) to interact with the database. 6 | 7 | You can use the database through the `fpDatabase` object which is exported from `src/back/index.ts`. It must have been set up inside `initialize()` first but it's unlikely you'll ever access it before then. 8 | 9 | All the logic for the database API is contained with the `flashpoint-archive` Rust crate, and the `@fparchive/flashpoint-archive` node binding. For more details on how to modify this database API during development please see [Database API Development](setup#database-api-development) 10 | 11 | Most methods are fairly straight forward 12 | 13 | ```ts 14 | // Get metadata for a game 15 | const game = await fpDatabase.findGame(gameId); 16 | 17 | // Get a list of tag categories 18 | const cats = await fpDatabase.findTagCategories(); 19 | 20 | // Adding a new tag to a game 21 | game.tags.push("cool tag"); 22 | await fpDatabase.saveGame(game); 23 | ``` -------------------------------------------------------------------------------- /docs/development/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | ### Overview 4 | 5 | This section is here to explain the architecture of the Flashpoint Launcher at a technical level, and the common practices you'll probably use when making contributions. 6 | 7 | It's been written to document the techniques and systems that have been developed over time to create the launcher software. Try not to treat it as a hard and fast rule on what is accepted, but more of a helpful reference. 8 | 9 | The documentation assumes you have a basic understanding of Typescript, React and Nodejs. Whilst it will sometimes point out specific functions from these APIs and mention what they do, you are expected to either understand them already or be willing to look them up in their own documentation. 10 | 11 | 12 | ## Setup and Contributions 13 | 14 | See [Setup](setup) to set up your development environment. 15 | 16 | See [Git Workflow](gitworkflow) to learn how to start committing and merging your contributions. 17 | 18 | ## Future Considerations 19 | 20 | **React 17** - React 18 comes with breaking changes, so hasn't been worked on. A migratory pull request would be welcome, otherwise the plan is to wait for React 19 so we can reap the benefits of the React Compiler. 21 | 22 | **Electron 19** - Electron 19 is very outdated now but does the job fine.If there becomes a need for more modern Chromium functionality, then Electron will have to update and drop the Flash support in browser mode. 23 | 24 | ## Essential Reading 25 | 26 | Whilst some concepts are only used on occasion, there are some things you'll want to come back and reference frequently. 27 | 28 | - [Front / Back Communication](communication) - Almost everything you do will involve sending data between the frontend and backend, so this is vital to understand. 29 | - [Database API](database) - Interacting with the Database API is common place in the backend. -------------------------------------------------------------------------------- /docs/introduction.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ### Want to configure Flashpoint Launcher for your projects needs? 4 | 5 | See [Configuration](configuration/introduction) for a list of different ways you can configure the behaviour of the launcher for your needs. 6 | 7 | ### Want to contribute code? 8 | 9 | See [Development](development/introduction) to understand the structure of the application, common practices that make up the bulk of the codebase and how to apply them yourself. 10 | 11 | ### Want to write an extension? 12 | 13 | See [Extensions](extensions/overview) to find out what extensions can do and how you can write your own. 14 | 15 | :::note 16 | 17 | The documentation is fairly new, if you have any problems or suggestions for improvements please either: 18 | - Open a [GitHub](https://github.com/FlashpointProject/launcher) issue or 19 | - Message us in the `#launcher` [Discord](https://discordapp.com/invite/qhvAkhWXU5) channel 20 | 21 | ::: 22 | -------------------------------------------------------------------------------- /extensions/core-ruffle/.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | *.fplx -------------------------------------------------------------------------------- /extensions/core-ruffle/README.md: -------------------------------------------------------------------------------- 1 | # core-ruffle README 2 | 3 | This is the README for your extension "core-curation". After writing up a brief description, Flashpoint recommends including the following sections. 4 | 5 | ## Features 6 | 7 | Describe specific features of your extension including screenshots of your extension in action. Image paths are relative to this README file. 8 | 9 | For example if there is an image subfolder under your extension project workspace: 10 | 11 | \!\[feature X\]\(images/feature-x.png\) 12 | 13 | ## Requirements 14 | 15 | If you have any requirements or dependencies, add a section describing those and how to install and configure them. 16 | 17 | ## Extension Settings 18 | 19 | Include if your extension adds any Config page settings through the `contributes.configuration` extension point. 20 | 21 | ## Icon 22 | 23 | Add a Icon to your extension, this will be shown on the Config page. 24 | 25 | Add this to your package.json and move your icon to icon.png, or change the path to another image in your extension folder. 26 | * `"icon": "./icon.png"` 27 | 28 | ## Known Issues 29 | 30 | Calling out known issues can help limit users opening duplicate issues against your extension. 31 | 32 | ## Release Notes 33 | 34 | Users appreciate release notes as you update your extension. 35 | 36 | ### 1.0.0 37 | 38 | Initial release of ... 39 | 40 | ### 1.0.1 41 | 42 | Fixed issue #. 43 | 44 | ### 1.1.0 45 | 46 | Added features X, Y, and Z. 47 | 48 | ----------------------------------------------------------------------------------------------------------- 49 | 50 | ## Working with Markdown 51 | 52 | **Note:** You can author your README using Visual Studio Code. Here are some useful editor keyboard shortcuts: 53 | 54 | * Split the editor (`Cmd+\` on macOS or `Ctrl+\` on Windows and Linux) 55 | * Toggle preview (`Shift+CMD+V` on macOS or `Shift+Ctrl+V` on Windows and Linux) 56 | * Press `Ctrl+Space` (Windows, Linux) or `Cmd+Space` (macOS) to see a list of Markdown snippets 57 | 58 | ### For more information 59 | 60 | * [Visual Studio Code's Markdown Support](http://code.visualstudio.com/docs/languages/markdown) 61 | * [Markdown Syntax Reference](https://help.github.com/articles/markdown-basics/) 62 | 63 | **Enjoy!** 64 | -------------------------------------------------------------------------------- /extensions/core-ruffle/gulpfile.js: -------------------------------------------------------------------------------- 1 | const { series } = require('gulp'); 2 | const fs = require('fs'); 3 | const gulp = require('gulp'); 4 | const zip = require('gulp-zip'); 5 | const merge = require('merge-stream'); 6 | const esbuild = require('esbuild'); 7 | 8 | const filesToCopy = [ 9 | 'extension.js', 10 | 'package.json', 11 | 'icon.png', 12 | 'LICENSE.md', 13 | 'README.md' 14 | ]; 15 | 16 | function build(done) { 17 | esbuild.build({ 18 | bundle: true, 19 | entryPoints: ['./src/extension.ts'], 20 | outfile: './dist/extension.js', 21 | platform: 'node', 22 | external: ['flashpoint-launcher'], 23 | }) 24 | .catch(console.error) 25 | .finally(done); 26 | } 27 | 28 | async function watch() { 29 | const ctx = await esbuild.context({ 30 | bundle: true, 31 | entryPoints: ['./src/extension.ts'], 32 | outfile: './dist/extension.js', 33 | platform: 'node', 34 | external: ['flashpoint-launcher'], 35 | }); 36 | return ctx.watch(); 37 | } 38 | 39 | function clean(cb) { 40 | fs.rm('./package', { recursive: true }, (err) => { 41 | if (err) { console.log('Clean', err); } 42 | cb(); 43 | }); 44 | } 45 | 46 | function stage() { 47 | const streams = filesToCopy.map(file => { 48 | if (fs.existsSync(file)) { 49 | return gulp.src(file).pipe(gulp.dest('package/core-ruffle')); 50 | } 51 | }).filter(s => s !== undefined); 52 | return merge([ 53 | ...streams, 54 | gulp.src('out/**/*').pipe(gulp.dest('package/core-ruffle/out')), 55 | gulp.src('dist/**/*').pipe(gulp.dest('package/core-ruffle/dist')), 56 | gulp.src('static/**/*').pipe(gulp.dest('package/core-ruffle/static')), 57 | ]); 58 | } 59 | 60 | function package() { 61 | return gulp.src('package/**/*').pipe(zip('core-ruffle.fplx')).pipe(gulp.dest('.')); 62 | } 63 | 64 | exports.build = series(build); 65 | exports.watch = series(watch); 66 | exports.package = series(clean, stage, package); -------------------------------------------------------------------------------- /extensions/core-ruffle/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "core-ruffle", 3 | "displayName": "Ruffle", 4 | "description": "Required for Ruffle playback of Flash games", 5 | "version": "1.0.0", 6 | "main": "./dist/extension.js", 7 | "contributes": { 8 | "configuration": [ 9 | { 10 | "title": "Ruffle", 11 | "properties": { 12 | "com.ruffle.enabled": { 13 | "title": "Enabled (Supported Games)", 14 | "type": "boolean", 15 | "default": false, 16 | "description": "Enables Ruffle for games that have been marked as properly supported." 17 | }, 18 | "com.ruffle.enabled-all": { 19 | "title": "Enabled (Unsupported Games)", 20 | "type": "boolean", 21 | "default": false, 22 | "description": "Enables Ruffle Standalone for all games regardless of whether they have been checked as supported. Results my vary." 23 | }, 24 | "com.ruffle.graphics-mode": { 25 | "title": "Graphics Mode", 26 | "type": "string", 27 | "enum": [ 28 | "default", 29 | "vulkan", 30 | "metal", 31 | "dx12", 32 | "gl" 33 | ], 34 | "default": "default", 35 | "description": "Switches the graphics mode used when running games in Ruffle." 36 | } 37 | } 38 | } 39 | ] 40 | }, 41 | "scripts": { 42 | "build": "gulp build --color", 43 | "watch": "gulp watch --color", 44 | "package": "gulp package --color", 45 | "lint": "eslint src --ext ts" 46 | }, 47 | "dependencies": { 48 | "arch": "^2.2.0", 49 | "mustache": "^4.2.0" 50 | }, 51 | "devDependencies": { 52 | "@types/mustache": "^4.2.3", 53 | "@types/node": "18.x", 54 | "@typescript-eslint/eslint-plugin": "^5.21.0", 55 | "@typescript-eslint/parser": "^5.21.0", 56 | "esbuild": "0.20.2", 57 | "eslint": "^8.14.0", 58 | "gulp": "^4.0.2", 59 | "gulp-zip": "^5.0.2", 60 | "merge-stream": "^2.0.0", 61 | "typescript": "^4.6.4" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /extensions/core-ruffle/src/template/basic.ts: -------------------------------------------------------------------------------- 1 | import * as flashpoint from 'flashpoint-launcher'; 2 | import * as path from 'path'; 3 | import * as fs from 'fs'; 4 | import mustache from 'mustache'; 5 | 6 | export function buildBasicTemplate(game: flashpoint.Game, sourceUrl: string) { 7 | const templatesRoot = path.join(flashpoint.extensionPath, 'static', 'templates'); 8 | const data = fs.readFileSync(path.join(templatesRoot, 'basic.mustache'), 'utf8'); 9 | const styleData = fs.readFileSync(path.join(templatesRoot, 'basic.css'), 'utf8'); 10 | return mustache.render(data, { title: game.title, sourceUrl, styleData }); 11 | } 12 | -------------------------------------------------------------------------------- /extensions/core-ruffle/src/types.ts: -------------------------------------------------------------------------------- 1 | export type AssetFile = { 2 | name: string; 3 | url: string; 4 | publishedAt: string; 5 | }; 6 | -------------------------------------------------------------------------------- /extensions/core-ruffle/static/templates/basic.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Helvetica, Arial, sans-serif; 3 | } 4 | 5 | body { 6 | background-color: lightblue; 7 | } 8 | .page-content { 9 | display: flex; 10 | flex-direction: column; 11 | align-items: center; 12 | } 13 | #embed-container { 14 | display: flex; 15 | max-width: 80%; 16 | max-height: 80%; 17 | border: 3px solid black; 18 | border-radius: 2px; 19 | } 20 | .game-title { 21 | font-size: 2rem; 22 | font-weight: bold; 23 | text-align: center; 24 | margin-top: 1rem; 25 | margin-bottom: 1rem; 26 | } 27 | .button-row { 28 | margin-top: 1rem; 29 | display: flex; 30 | flex-direction: row; 31 | justify-content: left; 32 | } -------------------------------------------------------------------------------- /extensions/core-ruffle/static/templates/basic.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{title}} 5 | 8 | 9 | 10 |
11 |
{{title}}
12 |
13 |
14 | 17 |
18 | 19 | 34 | 35 |
36 | 37 | -------------------------------------------------------------------------------- /extensions/core-ruffle/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2020", 5 | "lib": [ 6 | "ES2020" 7 | ], 8 | "sourceMap": true, 9 | "rootDir": "src", 10 | "esModuleInterop": true, 11 | "strict": true /* enable all strict type-checking options */ 12 | /* Additional Checks */ 13 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 14 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 15 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 16 | }, 17 | "include": [ 18 | "../../typings/flashpoint-launcher.d.ts", 19 | "./src" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /extensions/core-ruffle/webpack.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | 'use strict'; 4 | 5 | const path = require('path'); 6 | 7 | /**@type {import('webpack').Configuration}*/ 8 | const config = { 9 | target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 10 | 11 | entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 12 | output: { 13 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 14 | path: path.resolve(__dirname, 'dist'), 15 | filename: 'extension.js', 16 | libraryTarget: 'commonjs2', 17 | devtoolModuleFilenameTemplate: '../[resource-path]' 18 | }, 19 | devtool: undefined, 20 | externals: { 21 | "flashpoint-launcher": "commonjs flashpoint-launcher", 22 | "sqlite3": "commonjs sqlite3" 23 | }, 24 | resolve: { 25 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 26 | extensions: ['.ts', '.js'] 27 | }, 28 | module: { 29 | rules: [ 30 | { 31 | test: /\.ts$/, 32 | exclude: /node_modules/, 33 | use: [ 34 | { 35 | loader: 'ts-loader' 36 | } 37 | ] 38 | } 39 | ] 40 | } 41 | }; 42 | module.exports = config; -------------------------------------------------------------------------------- /extern/elevate/Elevate.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlashpointProject/launcher/678880c6fd0917e172ec754b787cf62aadf7c798/extern/elevate/Elevate.exe -------------------------------------------------------------------------------- /extern/elevate/mklink.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | echo "%1" 3 | echo "%2" 4 | .\Elevate.exe -wait4exit cmd /c mklink /D %1 %2 -------------------------------------------------------------------------------- /extern/utils/sourceBuild.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import hashlib 3 | 4 | BLOCKSIZE = 65536 5 | 6 | def hash_file(file_path: str): 7 | shash = hashlib.sha256() 8 | with open(file_path, 'rb') as f: 9 | buf = f.read(BLOCKSIZE) 10 | while len(buf) > 0: 11 | shash.update(buf) 12 | buf = f.read(BLOCKSIZE) 13 | return shash.hexdigest() 14 | 15 | def hash_all_files(): 16 | with open('output.source', 'w') as outfile: 17 | for filename in glob.glob('./**/*.zip'): 18 | shash = hash_file(filename) 19 | outfile.write(shash.upper() + '\n') 20 | outfile.write(filename[2:] + '\n') 21 | 22 | 23 | hash_all_files() -------------------------------------------------------------------------------- /gulpfile.extensions.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | 3 | const glob = require('glob'); 4 | const path = require('path'); 5 | const { execute } = require('./gulpfile.util'); 6 | const { parallel } = require('gulp'); 7 | const extensionsPath = path.resolve('extensions'); 8 | const fs = require('fs'); 9 | 10 | const tsConfigPaths = glob.sync('**/tsconfig.json', { 11 | cwd: extensionsPath, 12 | ignore: ['**/out/**', '**/dist/**', '**/node_modules/**'] 13 | }); 14 | 15 | const targets = tsConfigPaths.map(target => { 16 | const name = path.dirname(target).replace(/\//g, '-'); 17 | const folder = path.join('./extensions', path.dirname(target)); 18 | const packageJsonPath = path.join(folder, 'package.json'); 19 | const json = JSON.parse(fs.readFileSync(packageJsonPath).toString()); 20 | 21 | const installTaskExec = `cd ${folder} && npm install`; 22 | const buildTaskExec = `cd ${folder} && ${json['scripts']['build']}`; 23 | const watchTaskExec = `cd ${folder} && ${json['scripts']['watch']}`; 24 | 25 | const installTask = (cb) => execute(installTaskExec, cb); 26 | const buildTask = (cb) => execute(buildTaskExec, cb); 27 | const watchTask = (cb) => execute(watchTaskExec, cb); 28 | 29 | Object.defineProperty(installTask, 'name', { value: `installExtension:${name}`}); 30 | Object.defineProperty(buildTask, 'name', { value: `buildExtension:${name}` }); 31 | Object.defineProperty(watchTask, 'name', { value: `watchExtension:${name}` }); 32 | 33 | return { 34 | installTask, 35 | buildTask, 36 | watchTask 37 | }; 38 | }); 39 | 40 | const installExtensions = targets.length > 0 ? parallel(targets.map(t => t.installTask)) : parallel([(done) => { done(); }]); 41 | const buildExtensions = targets.length > 0 ? parallel(targets.map(t => t.buildTask)) : parallel([(done) => { done(); }]); 42 | const watchExtensions = targets.length > 0 ? parallel(targets.map(t => t.watchTask)) : parallel([(done) => { done(); }]); 43 | 44 | exports.installExtensions = installExtensions; 45 | exports.buildExtensions = buildExtensions; 46 | exports.watchExtensions = watchExtensions; 47 | -------------------------------------------------------------------------------- /gulpfile.util.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const { exec } = require('child_process'); 3 | 4 | const execute = (command, callback) => { 5 | const child = exec(command); 6 | child.stderr.on('data', data => { process.stdout.write(data); }); 7 | child.stdout.on('data', data => { process.stdout.write(data); }); 8 | if (callback) { 9 | child.once('exit', () => { callback(); }); 10 | } 11 | }; 12 | 13 | exports.execute = execute; 14 | -------------------------------------------------------------------------------- /icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlashpointProject/launcher/678880c6fd0917e172ec754b787cf62aadf7c798/icons/icon.icns -------------------------------------------------------------------------------- /icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlashpointProject/launcher/678880c6fd0917e172ec754b787cf62aadf7c798/icons/icon.ico -------------------------------------------------------------------------------- /icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlashpointProject/launcher/678880c6fd0917e172ec754b787cf62aadf7c798/icons/icon.png -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | 2 | import type { Config } from '@jest/types'; 3 | 4 | // Sync object 5 | const config: Config.InitialOptions = { 6 | verbose: true, 7 | transform: { 8 | '^.+\\.tsx?$': 'ts-jest', 9 | }, 10 | runner: '@kayahr/jest-electron-runner/main', 11 | testEnvironment: 'node', 12 | setupFiles: ['/tests/setup.ts'], 13 | moduleNameMapper: { 14 | '^@shared(.*)$': '/src/shared/$1', 15 | '^@main(.*)$': '/src/main/$1', 16 | '^@renderer(.*)$': '/src/renderer/$1', 17 | '^@back(.*)$': '/src/back/$1', 18 | '^@tests(.*)$': '/tests/$1' 19 | }, 20 | testPathIgnorePatterns: [ 21 | '/build/' 22 | ] 23 | }; 24 | 25 | export default config; 26 | -------------------------------------------------------------------------------- /lefthook.yml: -------------------------------------------------------------------------------- 1 | # EXAMPLE USAGE: 2 | # 3 | # Refer for explanation to following link: 4 | # https://github.com/evilmartians/lefthook/blob/master/docs/configuration.md 5 | # 6 | # pre-push: 7 | # commands: 8 | # packages-audit: 9 | # tags: frontend security 10 | # run: yarn audit 11 | # gems-audit: 12 | # tags: backend security 13 | # run: bundle audit 14 | # 15 | # pre-commit: 16 | # parallel: true 17 | # commands: 18 | # eslint: 19 | # glob: "*.{js,ts,jsx,tsx}" 20 | # run: yarn eslint {staged_files} 21 | # rubocop: 22 | # tags: backend style 23 | # glob: "*.rb" 24 | # exclude: "application.rb|routes.rb" 25 | # run: bundle exec rubocop --force-exclusion {all_files} 26 | # govet: 27 | # tags: backend style 28 | # files: git ls-files -m 29 | # glob: "*.go" 30 | # run: go vet {files} 31 | # scripts: 32 | # "hello.js": 33 | # runner: node 34 | # "any.go": 35 | # runner: go run 36 | 37 | pre-push: 38 | commands: 39 | test: 40 | glob: "*.test.{js,ts,jsx,tsx}" 41 | run: npx jest {staged_files} 42 | 43 | pre-commit: 44 | commands: 45 | lint: 46 | glob: "*.{js,ts,jsx,tsx}" 47 | run: npx eslint {staged_files} -------------------------------------------------------------------------------- /licenses/open-iconic/ICON-LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Waybury 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /licenses/open-iconic/source.txt: -------------------------------------------------------------------------------- 1 | https://github.com/iconic/open-iconic 2 | -------------------------------------------------------------------------------- /licenses/vscode/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2015 - present Microsoft Corporation 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 | -------------------------------------------------------------------------------- /ormconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "sqlite", 3 | "host": "localhost", 4 | "port": 3306, 5 | "username": "flashpoint", 6 | "password": "Rw5up8YYxYsbSr", 7 | "database": "flashpoint.sqlite", 8 | "synchronize": false, 9 | "logging": false, 10 | "timezone": "Z", 11 | "entities": [ 12 | "build/database/entity/**/*.js" 13 | ], 14 | "migrations": [ 15 | "build/database/migration/**/*.js" 16 | ], 17 | "subscribers": [ 18 | "src/database/subscriber/**/*.ts" 19 | ], 20 | "cli": { 21 | "entitiesDir": "src/database/entity", 22 | "migrationsDir": "src/database/migration", 23 | "subscribersDir": "src/database/subscriber" 24 | } 25 | } -------------------------------------------------------------------------------- /secrets.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "CROWDIN_API_KEY": "12345" 3 | } -------------------------------------------------------------------------------- /src/back/Execs.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { ExecMapping } from '@shared/interfaces'; 3 | import { parseVarStr, readJsonFile } from '@shared/Util'; 4 | import * as Coerce from '@shared/utils/Coerce'; 5 | import { IObjectParserProp, ObjectParser } from '@shared/utils/ObjectParser'; 6 | 7 | const { str } = Coerce; 8 | 9 | /* Holds all Exec Mappings from execs file */ 10 | type ExecMappingFile = { 11 | execs: ExecMapping[]; 12 | } 13 | 14 | /** Relative path to exec mappings file */ 15 | const filePath = 'execs.json'; 16 | 17 | /** 18 | * Load exec mapping file 19 | * 20 | * @param jsonFolder Path to the folder containing execs.json 21 | * @param onError Callback for error 22 | */ 23 | export function loadExecMappingsFile(jsonFolder: string, onError?: (error: string) => void): Promise { 24 | return new Promise((resolve, reject) => { 25 | readJsonFile(path.join(jsonFolder, filePath), 'utf8') 26 | .then((json) => { resolve(parseExecMappingsFile(json, onError).execs); }) 27 | .catch(reject); 28 | }); 29 | } 30 | 31 | function parseExecMappingsFile(data: any, onError?: (error: string) => void) : ExecMappingFile { 32 | const parsed: ExecMappingFile = { 33 | execs: [], 34 | }; 35 | const parser = new ObjectParser({ 36 | input: data, 37 | onError: onError && ((e) => { onError(`Error while parsing Exec Mappings: ${e.toString()}`); }) 38 | }); 39 | parser.prop('execs').array(item => parsed.execs.push(parseExecMapping(item))); 40 | return parsed; 41 | } 42 | 43 | function parseExecMapping(parser: IObjectParserProp) : ExecMapping { 44 | const parsed: ExecMapping = { 45 | win32: '', 46 | linux: undefined, 47 | wine: undefined, 48 | darwin: undefined, 49 | darwine: undefined, 50 | }; 51 | parser.prop('win32', v => parsed.win32 = parseVarStr(str(v))); 52 | parser.prop('linux', v => parsed.linux = parseVarStr(str(v)), true); 53 | parser.prop('wine', v => parsed.wine = parseVarStr(str(v)), true); 54 | parser.prop('darwin', v => parsed.darwin = parseVarStr(str(v)), true); 55 | parser.prop('darwine', v => parsed.darwine = parseVarStr(str(v)), true); 56 | return parsed; 57 | } 58 | -------------------------------------------------------------------------------- /src/back/ExtConfigFile.ts: -------------------------------------------------------------------------------- 1 | import { AppExtConfigData } from '@shared/config/interfaces'; 2 | import { overwriteExtConfigData } from '@shared/config/util'; 3 | import { readJsonFile, stringifyJsonDataFile } from '@shared/Util'; 4 | import * as fs from 'fs'; 5 | 6 | export namespace ExtConfigFile { 7 | export function readFile(filePath: string): Promise { 8 | return new Promise((resolve, reject) => { 9 | readJsonFile(filePath, 'utf8') 10 | .then(json => resolve(parse(json))) 11 | .catch(reject); 12 | }); 13 | } 14 | 15 | export async function readOrCreateFile(filePath: string): Promise { 16 | let error: Error | undefined; 17 | let data: any; 18 | 19 | try { 20 | data = await readFile(filePath); 21 | } catch (e: any) { 22 | error = e; 23 | } 24 | 25 | if (error || !data) { 26 | data = {}; 27 | saveFile(filePath, data).catch(() => console.log('Failed to save default ext config file!')); 28 | } 29 | 30 | return data; 31 | } 32 | 33 | export function saveFile(filePath: string, data: AppExtConfigData): Promise { 34 | return new Promise((resolve, reject) => { 35 | // Convert config to json string 36 | const json: string = stringifyJsonDataFile(data); 37 | // Save the config file 38 | fs.writeFile(filePath, json, function(error) { 39 | if (error) { return reject(error); } 40 | else { return resolve(); } 41 | }); 42 | }); 43 | } 44 | 45 | function parse(json: any): AppExtConfigData { 46 | return overwriteExtConfigData({}, json); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/back/InstancedAbortController.ts: -------------------------------------------------------------------------------- 1 | export class InstancedAbortController { 2 | current?: AbortController; 3 | 4 | signal() { 5 | this.current = new AbortController(); 6 | return this.current.signal; 7 | } 8 | 9 | abort() { 10 | if (this.current) { 11 | this.current.abort(); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/back/SocketServerMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { BackRes, BackResParams, BackResReturnTypes } from '@shared/back/types'; 2 | 3 | export type ContextMiddlewareRes = { 4 | type: BackRes; 5 | args?: BackResParams; 6 | res?: BackResReturnTypes; 7 | }; 8 | 9 | export type Next = () => Promise | void 10 | 11 | export type MiddlewareRes = (context: ContextMiddlewareRes, next: Next) => Promise | void 12 | 13 | export type PipelineRes = { 14 | push: (...middlewares: MiddlewareRes[]) => void 15 | execute: (context: ContextMiddlewareRes) => Promise 16 | } 17 | 18 | export function genPipelineBackOut(...middlewares: MiddlewareRes[]): PipelineRes { 19 | const stack: MiddlewareRes[] = middlewares; 20 | 21 | const push: PipelineRes['push'] = (...middlewares) => { 22 | stack.push(...middlewares); 23 | }; 24 | 25 | const execute: PipelineRes['execute'] = async (context) => { 26 | let prevIndex = -1; 27 | 28 | const runner = async (index: number): Promise => { 29 | if (index === prevIndex) { 30 | throw new Error('next() called multiple times'); 31 | } 32 | 33 | prevIndex = index; 34 | 35 | const middleware = stack[index]; 36 | 37 | if (middleware) { 38 | await middleware(context, () => { 39 | return runner(index + 1); 40 | }); 41 | } 42 | }; 43 | 44 | await runner(0); 45 | }; 46 | 47 | return { push, execute }; 48 | } 49 | -------------------------------------------------------------------------------- /src/back/constants.ts: -------------------------------------------------------------------------------- 1 | /** Name of the file containing preferences. */ 2 | export const PREFERENCES_FILENAME = 'preferences.json'; 3 | 4 | /** Name of the file containing configs. */ 5 | export const CONFIG_FILENAME = 'config.json'; 6 | 7 | /** Name of the file containing extension configs. */ 8 | export const EXT_CONFIG_FILENAME = 'extConfig.json'; 9 | 10 | /** Source to use when logging messages regarding services. */ 11 | export const SERVICES_SOURCE = 'Background Services'; 12 | 13 | /** Wiki page for AV troubleshooting */ 14 | export const WIKI_AV_TROUBLESHOOTING = 'https://flashpointarchive.org/datahub/Troubleshooting_Antivirus_Interference'; 15 | 16 | /** Discord join link */ 17 | export const DISCORD_LINK = 'https://discord.gg/pbeUXW6B68'; 18 | -------------------------------------------------------------------------------- /src/back/curate/fpfss.ts: -------------------------------------------------------------------------------- 1 | import { FPFSS_INFO_FILENAME } from '@shared/curate/fpfss'; 2 | import { str } from '@shared/utils/Coerce'; 3 | import { ObjectParser } from '@shared/utils/ObjectParser'; 4 | import { CurationFpfssInfo } from 'flashpoint-launcher'; 5 | import * as fs from 'fs'; 6 | import * as path from 'path'; 7 | 8 | export async function getCurationFpfssInfo(folder: string): Promise { 9 | return fs.promises.readFile(path.join(folder, FPFSS_INFO_FILENAME), { encoding: 'utf-8' }) 10 | .then((dataStr) => { 11 | return parseCurationFpfssInfo(JSON.parse(dataStr)); 12 | }) 13 | .catch(() => { 14 | return null; 15 | }); 16 | } 17 | 18 | export async function saveCurationFpfssInfo(folder: string, info: CurationFpfssInfo) { 19 | const data = parseCurationFpfssInfo(info); 20 | return fs.promises.writeFile(path.join(folder, FPFSS_INFO_FILENAME), JSON.stringify(data, undefined, 2)); 21 | } 22 | 23 | export function parseCurationFpfssInfo(data: any): CurationFpfssInfo { 24 | const info: CurationFpfssInfo = { 25 | id: '' 26 | }; 27 | 28 | const parser = new ObjectParser({ 29 | input: data 30 | }); 31 | parser.prop('id', v => info.id = str(v)); 32 | 33 | return info; 34 | } 35 | -------------------------------------------------------------------------------- /src/back/dns.ts: -------------------------------------------------------------------------------- 1 | import * as https from 'node:https'; 2 | import * as dns from 'node:dns'; 3 | import * as dnsPacket from 'dns-packet'; 4 | import _axios from 'axios'; 5 | import { Agent } from 'node:https'; 6 | 7 | let id = 0; 8 | 9 | function getId(): number { 10 | id += 1; 11 | if (id >= 65535) { 12 | id = 0; 13 | } 14 | 15 | return id; 16 | } 17 | 18 | type DnsResult = { 19 | address: string; 20 | family: number; 21 | expiry: number; 22 | } 23 | 24 | const dnsCache = new Map; 25 | 26 | const lookup = ( 27 | hostname: string, 28 | options: dns.LookupOptions, 29 | callback: (err: NodeJS.ErrnoException | null, address: string | dns.LookupAddress[], family?: number 30 | ) => void): void => { 31 | // Check DNS cache first 32 | const cachedResult = dnsCache.get(hostname); 33 | if (cachedResult && cachedResult.expiry > Date.now()) { 34 | callback(null, cachedResult.address, cachedResult.family); 35 | return; 36 | } 37 | 38 | const buf = dnsPacket.encode({ 39 | type: 'query', 40 | id: getId(), 41 | flags: dnsPacket.RECURSION_DESIRED, 42 | questions: [{ 43 | type: 'A', 44 | name: hostname 45 | }] 46 | }); 47 | 48 | const reqOpts = { 49 | hostname: 'cloudflare-dns.com', 50 | port: 443, 51 | path: '/dns-query', 52 | method: 'POST', 53 | headers: { 54 | 'Content-Type': 'application/dns-message', 55 | 'Content-Length': Buffer.byteLength(buf) 56 | } 57 | } 58 | 59 | const request = https.request(reqOpts, (response) => { 60 | response.on('data', (d) => { 61 | const result = dnsPacket.decode(d); 62 | if (result.answers && result.answers.length > 0) { 63 | const answer: any = result.answers[0]; 64 | dnsCache.set(hostname, { 65 | address: answer.data, 66 | family: 4, 67 | expiry: Date.now() + (answer.ttl * 1000) 68 | }); 69 | callback(null, answer.data, 4); 70 | } else { 71 | callback(new Error('No answers in DNS response'), '', 0); 72 | } 73 | }) 74 | }) 75 | 76 | 77 | request.on('error', (e) => { 78 | console.error(e); 79 | callback(e, '', 0); 80 | }) 81 | request.write(buf) 82 | request.end() 83 | } 84 | 85 | const agent = new Agent({ 86 | lookup 87 | }); 88 | 89 | export const axios = _axios.create({ 90 | headers: { 91 | 'User-Agent': 'Flashpoint Launcher' 92 | } 93 | }) -------------------------------------------------------------------------------- /src/back/extensions/ApiEvent.ts: -------------------------------------------------------------------------------- 1 | import { Disposable } from '@back/util/lifecycle'; 2 | 3 | // An event instance, typed to the data of the event. Itself is a function to register listeners. 4 | export interface ApiEvent { 5 | (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable): Disposable; 6 | } 7 | -------------------------------------------------------------------------------- /src/back/extensions/ExtensionUtils.ts: -------------------------------------------------------------------------------- 1 | import { IExtension, IExtensionManifest } from '@shared/extensions/interfaces'; 2 | import { LogFunc } from '@shared/interfaces'; 3 | import { ILogEntry } from '@shared/Log/interface'; 4 | import * as path from 'path'; 5 | import { ExtensionLogFunc } from './types'; 6 | 7 | export function extensionString(ext: IExtension): string { 8 | return `ID - ${ext.id}\n` + 9 | `Type - ${ext.type}\n` + 10 | `Path - ${ext.extensionPath}`; 11 | } 12 | 13 | /** 14 | * Get modules entry point 15 | * 16 | * @param ext Extension to read module from 17 | * @returns Path to module entry point 18 | */ 19 | export function getExtensionEntry(ext: IExtension): string | undefined { 20 | if (ext.manifest.main) { 21 | const filePath = path.join(ext.extensionPath, ext.manifest.main); 22 | const relative = path.relative(ext.extensionPath, filePath); 23 | if (relative && !relative.startsWith('..') && !path.isAbsolute(relative)) { 24 | // Path is inside extension path 25 | return path.resolve(filePath); 26 | } else { 27 | // Don't allow imports outside extension path 28 | throw new Error('Extension is trying to import files outside its path!'); 29 | } 30 | } 31 | } 32 | 33 | /** 34 | * Creates an Extension log (Message format "[extension-name] ") 35 | * 36 | * @param extManifest Manifest of the Extension 37 | * @param message Message to fill in 38 | * @param func Log function to use (log.info, warn, error etc.) 39 | * @returns Complete Log Entry 40 | */ 41 | export function newExtLog(extManifest: IExtensionManifest, message: string, func: LogFunc): ILogEntry { 42 | return func('Extensions', `[${extManifest.displayName || extManifest.name}] ${message}`); 43 | } 44 | 45 | export function internalNewExtLog(name: string, message: string, func: LogFunc): ILogEntry { 46 | return func('Extensions', `[${name}] ${message}`); 47 | } 48 | 49 | /** 50 | * Creates an Extension Log Function 51 | * 52 | * @param extManifest Manifest of the Extension 53 | * @param addLog Function to push new log onto Logs page stack 54 | * @param func Log function to use (log.info, warn, error etc.) 55 | * @returns Function that logs an extensions message given just a message string 56 | */ 57 | export function extLogFactory(extManifest: IExtensionManifest, addLog: (entry: ILogEntry) => void, func: LogFunc): ExtensionLogFunc { 58 | return (message: string) => { 59 | addLog(newExtLog(extManifest, message, func)); 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /src/back/extensions/types.ts: -------------------------------------------------------------------------------- 1 | import { Disposable } from '@back/util/lifecycle'; 2 | import { ILogEntry } from '@shared/Log/interface'; 3 | import { Theme } from '@shared/ThemeFile'; 4 | import { LogoSet } from '@shared/extensions/interfaces'; 5 | import { IGameMiddleware } from 'flashpoint-launcher'; 6 | 7 | export type ExtensionData = { 8 | extId: string; 9 | enabled: boolean; 10 | subscriptions: Disposable; 11 | logs: ILogEntry[]; 12 | } 13 | 14 | export type ExtensionLogFunc = (message: string) => void 15 | 16 | export type ExtensionContext = { 17 | subscriptions: Disposable 18 | } 19 | 20 | export type ExtensionModule = { 21 | activate?: (context: ExtensionContext) => void | Promise; 22 | deactivate?: () => void | Promise; 23 | } 24 | 25 | export type RegisteredMiddleware = IGameMiddleware & { 26 | extId: string; 27 | } 28 | 29 | export type Registry = { 30 | commands: Map; 31 | logoSets: Map; 32 | themes: Map; 33 | middlewares: Map; 34 | } 35 | 36 | export interface ICommand { 37 | command: string; 38 | callback: (...any: any[]) => any; 39 | } 40 | 41 | export type Command = ICommand & Disposable; 42 | -------------------------------------------------------------------------------- /src/back/extensions/util.ts: -------------------------------------------------------------------------------- 1 | import { BackState } from '@back/types'; 2 | import { fixSlashes } from '@shared/Util'; 3 | import { parseVariableString } from '@shared/utils/VariableString'; 4 | import * as path from 'path'; 5 | 6 | export async function parseAppVar(extId: string, appPath: string, launchCommand: string, state: BackState) { 7 | const ext = await state.extensionsService.getExtension(extId); 8 | return parseVariableString(appPath, (name) => { 9 | switch (name) { 10 | case 'extPath': return path.resolve(ext ? ext.extensionPath : ''); 11 | case 'extDataURL': return `http://localhost:${state.fileServerPort}/extdata/${extId}/`; 12 | case 'os': return process.platform; 13 | case 'arch': return process.arch; 14 | case 'launchCommand': return launchCommand; 15 | case 'cwd': return fixSlashes(process.cwd()); 16 | case 'fpPath': return state.config ? path.resolve(fixSlashes(state.config.flashpointPath)) : ''; 17 | case 'proxy': return state.preferences.browserModeProxy || ''; 18 | default: { 19 | if (name.startsWith('extConf:')) { 20 | const key = name.substring(8); 21 | return state.extConfig[key]; 22 | } 23 | return ''; 24 | } 25 | } 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /src/back/middleware.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { ConfigSchema, Game, GameLaunchInfo, GameMiddlewareConfig, GameMiddlewareDefaultConfig, IGameMiddleware, middleware } from 'flashpoint-launcher'; 3 | 4 | const systemEnvSchema: ConfigSchema = [ 5 | { 6 | type: 'string', 7 | key: 'envVars', 8 | title: 'Environment Variables', 9 | description: 'A list of environment variables to append. E.G `TERM=xterm DOG="woof woof"`', 10 | optional: true, 11 | } 12 | ]; 13 | 14 | type SystemEnvConfig = { 15 | envVars: string; // Format e.g 'TERM=xterm DOG="woof woof"' 16 | } 17 | 18 | // Basic middleware to set environmental variables for a game launch 19 | export class SystemEnvMiddleware implements IGameMiddleware { 20 | id = 'system.middleware-env'; 21 | name = 'Environmental Variables'; 22 | extId = 'SYSTEM'; 23 | 24 | isValid(game: Game): boolean | Promise { 25 | return true; 26 | } 27 | isValidVersion(version: string): boolean | Promise { 28 | return true; 29 | } 30 | execute(gameLaunchInfo: GameLaunchInfo, middlewareConfig: GameMiddlewareConfig): GameLaunchInfo | Promise { 31 | const config: Partial = middlewareConfig.config; 32 | if (config.envVars) { 33 | const regex = /(\w+)=("[^"]+"|\S+)/g; 34 | let match; 35 | // Map config.envVars to key value pairs on env 36 | while ((match = regex.exec(config.envVars))) { 37 | const [_, key, value] = match; 38 | gameLaunchInfo.launchInfo.env[key] = value; 39 | } 40 | } 41 | throw new Error('Method not implemented.'); 42 | } 43 | getDefaultConfig(game: Game): GameMiddlewareDefaultConfig { 44 | return { 45 | version: 'latest', 46 | config: {} 47 | }; 48 | } 49 | getConfigSchema(version: string): ConfigSchema { 50 | return systemEnvSchema; 51 | } 52 | upgradeConfig(version: string, config: any) { 53 | return; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/back/rust.ts: -------------------------------------------------------------------------------- 1 | import { ContentTree } from '@shared/curate/types'; 2 | import * as path from 'path'; 3 | import * as fs from 'fs'; 4 | import { genContentTree as gct, copyFolder as cf } from '@fparchive/flashpoint-archive'; 5 | 6 | export async function genContentTree(folder: string): Promise { 7 | try { 8 | const tree = await gct(folder); 9 | return { 10 | root: tree, 11 | }; 12 | } catch (error) { 13 | log.error('Curate', `Error generating content tree: ${error}`); 14 | return { 15 | root: { 16 | name: '', 17 | expanded: true, 18 | nodeType: 'directory', 19 | children: [], 20 | count: 0 21 | } 22 | }; 23 | } 24 | } 25 | 26 | export async function copyFolder(src: string, dest: string): Promise { 27 | const rootFiles = await fs.promises.readdir(src); 28 | await Promise.all(rootFiles.map(async (f) => { 29 | await cf(path.resolve(path.join(src, f)), path.resolve(dest)); 30 | })); 31 | } 32 | -------------------------------------------------------------------------------- /src/back/util/LogFile.ts: -------------------------------------------------------------------------------- 1 | import { ILogEntry, LogLevel } from '@shared/Log/interface'; 2 | import { EventQueue } from './EventQueue'; 3 | import * as fs from 'fs'; 4 | 5 | /** 6 | * Saves Logged messages to a file 7 | */ 8 | export class LogFile { 9 | private _queue: EventQueue; 10 | private _active: boolean; 11 | 12 | constructor(private _filePath: string) { 13 | this._active = true; 14 | this._queue = new EventQueue(); 15 | } 16 | 17 | public saveLog(formedLog: ILogEntry): void { 18 | if (this._active) { 19 | this._queue.push(() => { 20 | const date = new Date(formedLog.timestamp); 21 | const formedText = `[${LogLevel[formedLog.logLevel].padEnd(5)}] [${date.toLocaleString('en-GB')}]: (${formedLog.source}) - ${formedLog.content}\n`; 22 | fs.appendFile(this._filePath, formedText, err => { 23 | if (err) { 24 | // Disable file logger to prevent looping 25 | this._active = false; 26 | log.error('Launcher', `Cannot save log file, disabling file save - ${err}`); 27 | } 28 | }); 29 | }); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/back/util/SevenZip.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | function get7zExec(isDev: boolean, exePath: string): string { 4 | const basePath = isDev ? process.cwd() : path.dirname(exePath); 5 | switch (process.platform) { 6 | case 'darwin': 7 | return path.join(basePath, '../extern/7zip-bin/mac', '7za'); 8 | case 'win32': 9 | return path.join(basePath, 'extern/7zip-bin/win', process.arch, '7za'); 10 | case 'linux': 11 | return path.join(basePath, 'extern/7zip-bin/linux', process.arch, '7za'); 12 | } 13 | return '7za'; 14 | } 15 | 16 | export const pathTo7zBack = get7zExec; 17 | -------------------------------------------------------------------------------- /src/back/util/WrappedEventEmitter.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | 3 | /** A wrapper around an event emitter. */ 4 | export class WrappedEventEmitter { 5 | /** Event emitter this is a wrapper around. */ 6 | protected _emitter: EventEmitter = new EventEmitter(); 7 | 8 | on(event: string, listener: () => void): this { 9 | this._emitter.on(event, listener); 10 | return this; 11 | } 12 | 13 | once(event: string, listener: () => void): this { 14 | this._emitter.once(event, listener); 15 | return this; 16 | } 17 | 18 | off(event: string, listener: () => void): this { 19 | this._emitter.off(event, listener); 20 | return this; 21 | } 22 | 23 | emit(event: string, ...args: any[]): boolean { 24 | return this._emitter.emit(event, ...args); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/back/util/async.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * No modifications, licensed from Visual Studio Code under the MIT license. See /licenses/vscode 3 | * A barrier that is initially closed and then becomes opened permanently. 4 | */ 5 | export class Barrier { 6 | 7 | private _isOpen: boolean; 8 | private readonly _promise: Promise; 9 | private _completePromise!: (v: boolean) => void; 10 | 11 | constructor() { 12 | this._isOpen = false; 13 | this._promise = new Promise((c) => { 14 | this._completePromise = c; 15 | }); 16 | } 17 | 18 | isOpen(): boolean { 19 | return this._isOpen; 20 | } 21 | 22 | open(): void { 23 | this._isOpen = true; 24 | this._completePromise(true); 25 | } 26 | 27 | wait(): Promise { 28 | return this._promise; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/back/util/dialog.ts: -------------------------------------------------------------------------------- 1 | import { BackState } from '@back/types'; 2 | import { BackOut } from '@shared/back/types'; 3 | import { DialogResponse, DialogStateTemplate } from 'flashpoint-launcher'; 4 | import { uuid } from './uuid'; 5 | import { BackClient } from '@back/SocketServer'; 6 | 7 | export async function createNewDialog(state: BackState, template: DialogStateTemplate, client?: BackClient): Promise { 8 | const code = uuid(); 9 | return new Promise((resolve) => { 10 | state.newDialogEvents.once(code, (dialogId: string) => { 11 | resolve(dialogId); 12 | }); 13 | if (client) { 14 | state.socketServer.send(client, BackOut.NEW_DIALOG, template, code); 15 | } else { 16 | state.socketServer.broadcast(BackOut.NEW_DIALOG, template, code); 17 | } 18 | }); 19 | } 20 | 21 | export async function awaitDialog(state: BackState, dialogId: string): Promise { 22 | return new Promise((resolve) => { 23 | state.resolveDialogEvents.once(dialogId, (dialog, buttonIdx) => { 24 | resolve({ 25 | dialog, 26 | buttonIdx 27 | }); 28 | }); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /src/back/util/elevate.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | export function getElevatePath(isDev: boolean, exePath: string) { 4 | const basePath = isDev ? process.cwd() : path.dirname(exePath); 5 | return path.join(basePath, 'extern/elevate/Elevate.exe'); 6 | } 7 | 8 | export function getMklinkBatPath(isDev: boolean, exePath: string) { 9 | const basePath = isDev ? process.cwd() : path.dirname(exePath); 10 | return path.join(basePath, 'extern/elevate/mklink.bat'); 11 | } 12 | -------------------------------------------------------------------------------- /src/back/util/events.ts: -------------------------------------------------------------------------------- 1 | import { ApiEmitter } from '@back/extensions/ApiEmitter'; 2 | import { CurationImportState } from '@back/importGame'; 3 | import { Game, GameData, Playlist, PlaylistGame, ServiceChange } from 'flashpoint-launcher'; 4 | 5 | export const onWillImportCuration: ApiEmitter = new ApiEmitter(); 6 | export const onDidInstallGameData = new ApiEmitter(); 7 | export const onWillUninstallGameData = new ApiEmitter(); 8 | export const onDidUninstallGameData = new ApiEmitter(); 9 | export const onDidUpdateGame = new ApiEmitter<{oldGame: Game, newGame: Game}>(); 10 | export const onDidRemoveGame = new ApiEmitter(); 11 | export const onDidUpdatePlaylist = new ApiEmitter<{oldPlaylist: Playlist, newPlaylist: Playlist}>(); 12 | export const onDidUpdatePlaylistGame = new ApiEmitter<{oldGame: PlaylistGame, newGame: PlaylistGame}>(); 13 | export const onDidRemovePlaylistGame = new ApiEmitter(); 14 | export const onServiceChange = new ApiEmitter(); 15 | -------------------------------------------------------------------------------- /src/back/util/extensions.ts: -------------------------------------------------------------------------------- 1 | import { IExtensionManifest } from '@shared/extensions/interfaces'; 2 | 3 | export const nullExtensionDescription = Object.freeze({ 4 | name: 'Null Extension Description', 5 | version: '0.0.0', 6 | author: 'flashpoint', 7 | launcherVersion: '', 8 | extensionLocation: '/FAKE/PATH/', 9 | isBuiltin: false, 10 | }); 11 | -------------------------------------------------------------------------------- /src/back/util/gameConfig.ts: -------------------------------------------------------------------------------- 1 | // import { GameConfig, IGameMiddleware } from 'flashpoint-launcher'; 2 | 3 | // export function loadGameConfig(config: RawGameConfig, registry: Map): GameConfig { 4 | // const data = JSON.parse(config.storedMiddleware); 5 | // const gc: GameConfig = { 6 | // id: config.id, 7 | // gameId: config.gameId, 8 | // name: config.name, 9 | // owner: config.owner, 10 | // middleware: data.middleware, 11 | // }; 12 | // for (const m of gc.middleware) { 13 | // const middleware = registry.get(m.middlewareId); 14 | // if (middleware === undefined) { 15 | // m.name = 'Not Loaded'; 16 | // } else { 17 | // m.name = middleware.name; 18 | // } 19 | // } 20 | // return gc; 21 | // } 22 | 23 | // export function storeGameConfig(config: GameConfig): RawGameConfig { 24 | // const data = { 25 | // middleware: [...config.middleware] 26 | // }; 27 | // for (const m of data.middleware) { 28 | // delete (m as any).name; 29 | // } 30 | // const rgc = new RawGameConfig(); 31 | // rgc.id = config.id; 32 | // rgc.gameId = config.gameId; 33 | // rgc.name = config.name; 34 | // rgc.owner = config.owner; 35 | // rgc.storedMiddleware = JSON.stringify(data); 36 | // return rgc; 37 | // } 38 | -------------------------------------------------------------------------------- /src/back/util/lifecycle.ts: -------------------------------------------------------------------------------- 1 | /** A self-nesting type that allows one time disposable with an optional callback */ 2 | export type Disposable = { 3 | /** Children to dispose of in the future */ 4 | toDispose: Disposable[]; 5 | /** Whether this is already disposed */ 6 | isDisposed: boolean; 7 | /** Callback to use when disposed */ 8 | onDispose?: () => void; 9 | } 10 | 11 | /** 12 | * Dispose of a disposable and all its children 13 | * 14 | * @param disposable Disposable to dispose of 15 | */ 16 | export function dispose(disposable: Disposable) { 17 | if (disposable.isDisposed) { 18 | return; 19 | } 20 | 21 | disposable.isDisposed = true; 22 | clearDisposable(disposable); 23 | if (disposable.onDispose) { 24 | disposable.onDispose(); 25 | } 26 | } 27 | 28 | /** 29 | * Dispose of all a disposable's children but not itself 30 | * 31 | * @param disposable Disposable to dispose the children of and then clear 32 | */ 33 | export function clearDisposable(disposable: Disposable) { 34 | disposable.toDispose.forEach(d => dispose(d)); 35 | disposable.toDispose = []; 36 | } 37 | 38 | /** 39 | * Register a disposable to its parent. They must not be the same. 40 | * 41 | * @param parent Parent disposable 42 | * @param child Child disposable to chain to parent 43 | */ 44 | export function registerDisposable(parent: Disposable, child: Disposable) { 45 | if (parent == child) { 46 | throw new Error('Cannot add disposable to itself!'); 47 | } 48 | if (parent.isDisposed) { 49 | throw new Error('Cannot add disposable to already disposed parent!'); 50 | } 51 | parent.toDispose.push(child); 52 | } 53 | 54 | /** 55 | * Creates Disposable data to fill a newly created Disposable type object 56 | * 57 | * @param onDispose Called when the returned Disposable is disposed 58 | * @returns Disposable 59 | */ 60 | export function newDisposable(onDispose?: () => void): Disposable { 61 | return { 62 | toDispose: [], 63 | isDisposed: false, 64 | onDispose: onDispose 65 | }; 66 | } 67 | -------------------------------------------------------------------------------- /src/back/util/logging.ts: -------------------------------------------------------------------------------- 1 | import { SocketServer } from '@back/SocketServer'; 2 | import { BackOut } from '@shared/back/types'; 3 | import { LogFunc } from '@shared/interfaces'; 4 | import { ILogEntry, LogLevel } from '@shared/Log/interface'; 5 | import { LogFile } from './LogFile'; 6 | import { ApiEmitter } from '@back/extensions/ApiEmitter'; 7 | 8 | export function logFactory(logLevel: LogLevel, socketServer: SocketServer, addLog: (message: ILogEntry) => number, logFile: LogFile, verbose: boolean, apiEvent: ApiEmitter): LogFunc { 9 | return function (source: string, content: string): ILogEntry { 10 | const levelName: string = LogLevel[logLevel] || '?????'; 11 | const formedLog: ILogEntry = { 12 | source: source, 13 | content: content, 14 | timestamp: Date.now(), 15 | logLevel: logLevel 16 | }; 17 | const index = addLog(formedLog); 18 | logFile.saveLog(formedLog); 19 | socketServer.broadcast(BackOut.LOG_ENTRY_ADDED, formedLog, index); 20 | apiEvent.fire(formedLog); 21 | if (verbose) { console.log(`${levelName.padEnd(5)} - ${Date.now()} - ${content}`); } 22 | return formedLog; 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/back/util/strings.ts: -------------------------------------------------------------------------------- 1 | import { CharCode } from './charCode'; 2 | 3 | export function isLowerAsciiLetter(code: number): boolean { 4 | return code >= CharCode.a && code <= CharCode.z; 5 | } 6 | 7 | export function isUpperAsciiLetter(code: number): boolean { 8 | return code >= CharCode.A && code <= CharCode.Z; 9 | } 10 | 11 | export function compareSubstring(a: string, b: string, aStart = 0, aEnd: number = a.length, bStart = 0, bEnd: number = b.length): number { 12 | for (; aStart < aEnd && bStart < bEnd; aStart++, bStart++) { 13 | const codeA = a.charCodeAt(aStart); 14 | const codeB = b.charCodeAt(bStart); 15 | if (codeA < codeB) { 16 | return -1; 17 | } else if (codeA > codeB) { 18 | return 1; 19 | } 20 | } 21 | const aLen = aEnd - aStart; 22 | const bLen = bEnd - bStart; 23 | if (aLen < bLen) { 24 | return -1; 25 | } else if (aLen > bLen) { 26 | return 1; 27 | } 28 | return 0; 29 | } 30 | 31 | export function compareSubstringIgnoreCase(a: string, b: string, aStart = 0, aEnd: number = a.length, bStart = 0, bEnd: number = b.length): number { 32 | 33 | for (; aStart < aEnd && bStart < bEnd; aStart++, bStart++) { 34 | 35 | const codeA = a.charCodeAt(aStart); 36 | const codeB = b.charCodeAt(bStart); 37 | 38 | if (codeA === codeB) { 39 | // equal 40 | continue; 41 | } 42 | 43 | const diff = codeA - codeB; 44 | if (diff === 32 && isUpperAsciiLetter(codeB)) { // codeB =[65-90] && codeA =[97-122] 45 | continue; 46 | 47 | } else if (diff === -32 && isUpperAsciiLetter(codeA)) { // codeB =[97-122] && codeA =[65-90] 48 | continue; 49 | } 50 | 51 | if (isLowerAsciiLetter(codeA) && isLowerAsciiLetter(codeB)) { 52 | // 53 | return diff; 54 | 55 | } else { 56 | return compareSubstring(a.toLowerCase(), b.toLowerCase(), aStart, aEnd, bStart, bEnd); 57 | } 58 | } 59 | 60 | const aLen = aEnd - aStart; 61 | const bLen = bEnd - bStart; 62 | 63 | if (aLen < bLen) { 64 | return -1; 65 | } else if (aLen > bLen) { 66 | return 1; 67 | } 68 | 69 | return 0; 70 | } 71 | -------------------------------------------------------------------------------- /src/back/util/uuid.ts: -------------------------------------------------------------------------------- 1 | import { randomBytes } from 'crypto'; 2 | import * as guid from 'uuid/v4'; 3 | 4 | /** 5 | * Wrapper function over uuid's v4 method that attempts to source 6 | * entropy using the window Crypto instance rather than through 7 | * Node.JS. 8 | */ 9 | export function uuid() { 10 | return guid({ random: bufferToNumbers(randomBytes(16)) }); 11 | } 12 | 13 | function bufferToNumbers(buffer: Buffer): number[] { 14 | const array: number[] = []; 15 | for (let i = 0; i < buffer.length; i++) { 16 | array[i] = buffer[i]; 17 | } 18 | return array; 19 | } 20 | -------------------------------------------------------------------------------- /src/main/Util.ts: -------------------------------------------------------------------------------- 1 | import * as child_process from 'child_process'; 2 | import { app } from 'electron'; 3 | import * as path from 'path'; 4 | import * as util from 'util'; 5 | 6 | const execFile = util.promisify(child_process.execFile); 7 | 8 | /** 9 | * Check if an application is installed 10 | * 11 | * @param binaryName The command you would use the run an application command 12 | * @param argument An argument to pass the command. This argument should not cause any side effects. By default --version 13 | */ 14 | export async function isInstalled(binaryName: string, argument = '--version'): Promise { 15 | try { 16 | await execFile(binaryName, [argument]); 17 | } catch (e) { 18 | return false; 19 | } 20 | return true; 21 | } 22 | 23 | /** 24 | * If Electron is in development mode (or in release mode) 25 | * (This is copied straight out of the npm package 'electron-is-dev') 26 | */ 27 | export const isDev: boolean = (function() { 28 | const getFromEnv = parseInt(process.env.ELECTRON_IS_DEV || '', 10) === 1; 29 | const isEnvSet = 'ELECTRON_IS_DEV' in process.env; 30 | return isEnvSet ? getFromEnv : (process.defaultApp || /node_modules[\\/]electron[\\/]/.test(process.execPath)); 31 | }()); 32 | 33 | /** 34 | * Get the path of the folder containing the config and preferences files. 35 | */ 36 | export function getMainFolderPath(): string { 37 | return isDev 38 | ? process.cwd() // Dev 39 | : process.platform == 'darwin' 40 | ? path.resolve(path.dirname(app.getPath('exe')), '../../..') 41 | : path.dirname(app.getPath('exe')); // Portable 42 | } 43 | -------------------------------------------------------------------------------- /src/main/index.ts: -------------------------------------------------------------------------------- 1 | import * as remoteMain from '@electron/remote/main'; 2 | import * as Coerce from '@shared/utils/Coerce'; 3 | import { startBrowserMode } from './BrowserMode'; 4 | import { startLogger } from './LogsWindow'; 5 | import { main } from './Main'; 6 | import { Init } from './types'; 7 | 8 | remoteMain.initialize(); 9 | 10 | const init = getArgs(); 11 | 12 | if (init.args['logger']) { 13 | startLogger(init); 14 | } else if (init.args['browser_mode']) { 15 | startBrowserMode(init); 16 | } else { 17 | main(init); 18 | } 19 | 20 | function getArgs(): Init { 21 | const init: Init = { 22 | args: {}, 23 | rest: '', 24 | protocol: undefined 25 | }; 26 | 27 | const args = process.argv.slice(2); 28 | init.protocol = args.find((arg) => arg.startsWith('flashpoint://')); 29 | let lastArgIndex = -1; 30 | for (let i = 0; i < args.length; i++) { 31 | const arg = args[i]; 32 | const eqIndex = arg.indexOf('='); 33 | if (eqIndex >= 0) { 34 | const name = arg.substring(0, eqIndex); 35 | const value = arg.substring(eqIndex + 1); 36 | switch (name) { 37 | // String value 38 | case 'connect-remote': 39 | case 'plugin': 40 | init.args[name] = value; 41 | lastArgIndex = i; 42 | break; 43 | // Boolean value 44 | case 'logger': 45 | case 'host-remote': 46 | case 'back-only': 47 | case 'browser_mode': 48 | init.args[name] = Coerce.strToBool(value); 49 | lastArgIndex = i; 50 | break; 51 | case 'browser_url': 52 | init.args[name] = Coerce.str(value); 53 | lastArgIndex = i; 54 | break; 55 | // Numerical value 56 | case 'width': 57 | case 'height': 58 | init.args[name] = Coerce.num(value); 59 | lastArgIndex = i; 60 | break; 61 | case 'verbose': 62 | init.args[name] = Coerce.strToBool(value); 63 | break; 64 | } 65 | } 66 | } 67 | 68 | init.rest = args.slice(lastArgIndex + 1).join(' '); 69 | 70 | console.log(init); // @DEBUG 71 | 72 | return init; 73 | } 74 | -------------------------------------------------------------------------------- /src/main/types.ts: -------------------------------------------------------------------------------- 1 | export type InitArgs = Partial<{ 2 | // Main mode 3 | 'connect-remote': string; 4 | 'host-remote': boolean; 5 | 'back-only': boolean; 6 | 7 | // Browser mode 8 | /** If the application should start in "logger mode" */ 9 | 'logger': boolean; 10 | /** If the application should start in "browser mode". */ 11 | 'browser_mode': boolean; 12 | /** URL that browser mode should open */ 13 | 'browser_url': string; 14 | /** Desired width of the window. */ 15 | 'width': number; 16 | /** Desired height of the window. */ 17 | 'height': number; 18 | /** Filename of the flash plugin file to use (without the file extension). */ 19 | 'plugin': string; 20 | /** Whether to enable verbose printing */ 21 | 'verbose': boolean; 22 | }>; 23 | 24 | export type Init = { 25 | args: InitArgs; 26 | rest: string; 27 | protocol?: string; 28 | } 29 | -------------------------------------------------------------------------------- /src/renderer/components/CheckBox.tsx: -------------------------------------------------------------------------------- 1 | import { Omit } from '@shared/interfaces'; 2 | import * as React from 'react'; 3 | import { useCallback } from 'react'; 4 | import {FancyAnimation} from '@renderer/components/FancyAnimation'; 5 | 6 | /** Props for an input element. */ 7 | type InputProps = React.DetailedHTMLProps, HTMLInputElement>; 8 | 9 | export type CheckBoxProps = Omit & { 10 | /** Called when the checkbox becomes checked or unchecked. This is called right after "onChange". */ 11 | onToggle?: (isChecked: boolean) => void; 12 | }; 13 | 14 | // Basic checkbox element. Wrapper around the element. 15 | export function CheckBox(props: CheckBoxProps) { 16 | const { onToggle, onChange, ...rest } = props; 17 | // Hooks 18 | const onChangeCallback = useCallback(() => { 19 | if (onToggle) { onToggle(!props.checked); } 20 | }, [props.checked, onToggle, onChange]); 21 | // Render 22 | return ( 23 |
26 | ( 28 |
29 | )} 30 | normalRender={() => ( 31 |
32 | )}/> 33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/renderer/components/ConfigBox.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export type ConfigBoxProps = { 4 | title: string; 5 | description: string; 6 | swapChildren?: boolean; 7 | // eslint-disable-next-line react/no-unused-prop-types 8 | contentClassName?: string; 9 | bottomChildren?: JSX.Element | JSX.Element[]; 10 | } 11 | 12 | export function ConfigBoxInner(props: React.PropsWithChildren) { 13 | return ( 14 |
15 |
16 |

17 | {props.title} 18 |

19 |
20 | {props.description} 21 |
22 |
23 |
24 | { props.swapChildren &&

{props.description}

} 25 | { !props.swapChildren ? props.children : props.bottomChildren } 26 |
27 |
28 | ); 29 | } 30 | 31 | export function ConfigBox(props: React.PropsWithChildren) { 32 | return ( 33 |
34 |
35 |

{props.title}

36 |
37 | { props.swapChildren &&

{props.description}

} 38 | { props.swapChildren ? props.bottomChildren : props.children } 39 |
40 |
41 |
42 | { !props.swapChildren &&

{props.description}

} 43 | { props.swapChildren ? props.children : props.bottomChildren } 44 |
45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/renderer/components/ConfigBoxButton.tsx: -------------------------------------------------------------------------------- 1 | import { ConfigBox, ConfigBoxProps } from './ConfigBox'; 2 | import { SimpleButton, SimpleButtonProps } from './SimpleButton'; 3 | 4 | export type ConfigBoxButtonProps = ConfigBoxProps & SimpleButtonProps; 5 | 6 | export function ConfigBoxButton(props: ConfigBoxButtonProps) { 7 | return ( 8 | 11 |
12 | 15 |
16 |
17 | ); 18 | } 19 | 20 | export function ConfigBoxInnerButton(props: ConfigBoxButtonProps) { 21 | return ( 22 |
23 |
24 |

25 | {props.title} 26 |

27 |
28 | {props.description} 29 |
30 |
31 | 34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/renderer/components/ConfigBoxCheckbox.tsx: -------------------------------------------------------------------------------- 1 | import { CheckBox, CheckBoxProps } from './CheckBox'; 2 | import { ConfigBox, ConfigBoxProps } from './ConfigBox'; 3 | 4 | export type ConfigBoxCheckboxProps = ConfigBoxProps & CheckBoxProps; 5 | 6 | export function ConfigBoxCheckbox(props: ConfigBoxCheckboxProps) { 7 | return ( 8 | 11 |
12 | 13 |
14 |
15 | ); 16 | } 17 | 18 | export function ConfigBoxInnerCheckbox(props: ConfigBoxCheckboxProps) { 19 | return ( 20 |
21 |
22 |

23 | {props.title} 24 |

25 |
26 | {props.description} 27 |
28 |
29 | 30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/renderer/components/ConfigBoxInput.tsx: -------------------------------------------------------------------------------- 1 | import { ConfigBox, ConfigBoxProps } from './ConfigBox'; 2 | import { InputField, InputFieldProps } from './InputField'; 3 | 4 | export type ConfigBoxInputProps = ConfigBoxProps & InputFieldProps; 5 | 6 | export function ConfigBoxInput(props: ConfigBoxInputProps) { 7 | return ( 8 | 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/renderer/components/ConfigBoxMultiSelect.tsx: -------------------------------------------------------------------------------- 1 | import { ConfigBox, ConfigBoxProps } from './ConfigBox'; 2 | import { SelectItem } from './ConfigBoxSelect'; 3 | import { Dropdown } from './Dropdown'; 4 | 5 | export type ConfigBoxMultiSelectProps = ConfigBoxProps & { 6 | text: string; 7 | onChange: (item: T) => void; 8 | items: MultiSelectItem[]; 9 | }; 10 | 11 | export type MultiSelectItem = SelectItem & { 12 | checked: boolean; 13 | } 14 | 15 | export function ConfigBoxMultiSelect(props: ConfigBoxMultiSelectProps) { 16 | return ( 17 | 21 |
22 | 24 | {renderMultiSelectItems(props.items, props.onChange)} 25 | 26 |
27 |
28 | ); 29 | } 30 | 31 | function renderMultiSelectItems(items: MultiSelectItem[], onChange: (item: T) => void): JSX.Element[] { 32 | return items.map((item, idx) => ( 33 | 49 | )); 50 | } 51 | -------------------------------------------------------------------------------- /src/renderer/components/ConfigBoxSelect.tsx: -------------------------------------------------------------------------------- 1 | import { memoizeOne } from '@shared/memoize'; 2 | import * as React from 'react'; 3 | import { ConfigBox, ConfigBoxProps } from './ConfigBox'; 4 | 5 | export type SelectItem = { 6 | value: T; 7 | display?: string; 8 | } 9 | 10 | export type ConfigBoxSelectProps = ConfigBoxProps & { 11 | value: T; 12 | onChange: (event: React.ChangeEvent) => void; 13 | items: SelectItem[]; 14 | }; 15 | 16 | export function ConfigBoxSelect(props: ConfigBoxSelectProps) { 17 | return ( 18 | 21 |
22 | 28 |
29 |
30 | ); 31 | } 32 | 33 | const renderSelectItemsMemo = memoizeOne((selectItems: SelectItem[]): JSX.Element[] => { 34 | return selectItems.map((item, idx)=> ( 35 | 40 | )); 41 | }); 42 | -------------------------------------------------------------------------------- /src/renderer/components/ConfigBoxSelectInput.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { ConfigBox, ConfigBoxProps } from './ConfigBox'; 3 | import { DropdownInputField } from './DropdownInputField'; 4 | 5 | export type ConfigBoxSelectInputProps = ConfigBoxProps & { 6 | text: string; 7 | placeholder: string; 8 | onChange: (value: string) => void; 9 | onItemSelect: (value: string, index: number) => void; 10 | editable: boolean; 11 | items: string[]; 12 | }; 13 | 14 | export function ConfigBoxSelectInput(props: ConfigBoxSelectInputProps) { 15 | const [inputRef, setInputRef] = React.useState(null); 16 | const inputRefFunc = (ref: HTMLInputElement | HTMLTextAreaElement | null)=> { setInputRef(ref); }; 17 | 18 | return ( 19 | 22 | props.onChange(event.target.value)} 26 | editable={props.editable} 27 | items={props.items} 28 | onItemSelect={(text, index) => onItemSelect(text, index, inputRef, props.onItemSelect)} 29 | inputRef={inputRefFunc} /> 30 | 31 | ); 32 | } 33 | 34 | function onItemSelect(value: string, index: number, inputRef: HTMLInputElement | HTMLTextAreaElement | null, onChange: (value: string, index: number) => void) { 35 | if (inputRef) { 36 | inputRef.focus(); 37 | } 38 | onChange(value, index); 39 | } 40 | -------------------------------------------------------------------------------- /src/renderer/components/ConfigFlashpointPathInput.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export type ConfigFlashpointPathInputProps = { 4 | /** Initial value of the input field. */ 5 | input?: string; 6 | /** If the current input is valid. */ 7 | isValid?: boolean; 8 | /** Text to display on the button */ 9 | buttonText?: string; 10 | /** Called when the value of the input field is changed. */ 11 | onInputChange?: (input: string) => void; 12 | }; 13 | 14 | /** Text input element made specifically for setting the Flashpoint path at the config page. */ 15 | export class ConfigFlashpointPathInput extends React.Component { 16 | componentDidMount() { 17 | if (this.props.onInputChange) { this.props.onInputChange(this.props.input || ''); } 18 | } 19 | 20 | render() { 21 | const { input, isValid } = this.props; 22 | let className = 'flashpoint-path__input'; 23 | if (isValid !== undefined) { 24 | className += isValid ? ' flashpoint-path__input--valid' : ' flashpoint-path__input--invalid'; 25 | } 26 | return ( 27 | <> 28 |
29 | 33 |
34 | 39 | 40 | ); 41 | } 42 | 43 | onInputChange = (event: React.ChangeEvent): void => { 44 | this.setInput(event.target.value); 45 | }; 46 | 47 | onBrowseClick = (): void => { 48 | // Synchronously show a "open dialog" (this makes the main window "frozen" while this is open) 49 | const filePaths = window.Shared.showOpenDialogSync({ 50 | title: 'Select the FlashPoint root directory', 51 | properties: ['openDirectory'], 52 | }); 53 | if (filePaths) { this.setInput(filePaths[0]); } 54 | }; 55 | 56 | setInput(input: string): void { 57 | if (this.props.onInputChange) { this.props.onInputChange(input || ''); } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/renderer/components/ConfirmDialog.tsx: -------------------------------------------------------------------------------- 1 | import { SimpleButton } from './SimpleButton'; 2 | 3 | export type ConfirmDialogProps = { 4 | message: string; 5 | buttons: string[]; 6 | // eslint-disable-next-line react/no-unused-prop-types 7 | cancelId?: number; 8 | onResult: (result: number) => void; 9 | } 10 | 11 | export function ConfirmDialog(props: ConfirmDialogProps) { 12 | return ( 13 |
14 |
{props.message}
15 |
16 | { props.buttons.map((text, index) => { 17 | return ( 18 | props.onResult(index)} /> 22 | ); 23 | }) } 24 |
25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/renderer/components/ConfirmElement.tsx: -------------------------------------------------------------------------------- 1 | import { withConfirmDialog, WithConfirmDialogProps } from '@renderer/containers/withConfirmDialog'; 2 | import { LangContext } from '@renderer/util/lang'; 3 | import { Subtract } from '@shared/interfaces'; 4 | import * as React from 'react'; 5 | 6 | export type ConfirmElementArgs = { 7 | /** Calls the "onConfirm" callback (does not change or reset "activationCounter") */ 8 | confirm: () => void; 9 | /** Extra props passed by the parent. */ 10 | extra: T; 11 | }; 12 | 13 | type ConfirmElementComponentProps = { 14 | /** Function that renders the element (render prop). */ 15 | render?: (args: ConfirmElementArgs) => JSX.Element | undefined; 16 | /** Confirmation Message */ 17 | message: string; 18 | /** Called when confirmed. */ 19 | onConfirm?: () => void; 20 | } & (T extends undefined ? { 21 | /** Extra props to pass through to the child. */ 22 | extra?: undefined; 23 | } : { 24 | /** Extra props to pass through to the child. */ 25 | extra: T; 26 | }) & WithConfirmDialogProps; 27 | 28 | // Wrapper component around the "useConfirm" hook. 29 | function ConfirmElementComponent(props: ConfirmElementComponentProps) { 30 | const { onConfirm, message, render, extra } = props; 31 | const strings = React.useContext(LangContext); 32 | const confirm = React.useCallback(async () => { 33 | if (onConfirm) { 34 | const res = await props.openConfirmDialog(message, [strings.misc.yes, strings.misc.no], 1, 0); 35 | if (res === 0) { 36 | onConfirm(); 37 | } 38 | } 39 | }, [onConfirm]); 40 | // Render 41 | return render && render({ 42 | confirm: confirm, 43 | extra: extra as any 44 | }) || (<>); 45 | } 46 | 47 | export type ConfirmElementProps = Subtract, WithConfirmDialogProps>; 48 | export const ConfirmElement = withConfirmDialog(ConfirmElementComponent) as unknown as (props: ConfirmElementProps) => JSX.Element; 49 | -------------------------------------------------------------------------------- /src/renderer/components/CreditsProfile.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useCallback, useEffect, useRef } from 'react'; 3 | import { CreditsDataProfile } from '../credits/types'; 4 | 5 | export type CreditsIconProps = { 6 | /** Credits profile of the person to display. */ 7 | profile: CreditsDataProfile; 8 | /** Called when the mouse enters the element. */ 9 | onMouseEnter: (event: React.MouseEvent, profile: CreditsDataProfile) => void; 10 | /** Called when the mouse leaves the element. */ 11 | onMouseLeave: () => void; 12 | }; 13 | 14 | // Displays an icon from a credits profile. 15 | export function CreditsIcon(props: CreditsIconProps) { 16 | const ref = useRef(null); 17 | 18 | const onMouseEnter = useCallback((event: React.MouseEvent) => { 19 | if (props.onMouseEnter) { props.onMouseEnter(event, props.profile); } 20 | }, [props.onMouseEnter, props.profile]); 21 | 22 | const onMouseLeave = useCallback(() => { 23 | if (props.onMouseLeave) { props.onMouseLeave(); } 24 | }, [props.onMouseLeave]); 25 | 26 | // (Delay decoding the icon, this allows the browser to spread the work across multiple frames) 27 | useEffect(() => { 28 | let timeout = window.setTimeout(() => { 29 | timeout = -1; 30 | if (!ref.current) { throw new Error('CreditsIcon could not set profile image. Image element is missing.'); } 31 | if (props.profile.icon) { 32 | ref.current.style.backgroundImage = `url("${props.profile.icon}")`; 33 | } 34 | }, 0); 35 | return () => { 36 | if (timeout >= 0) { window.clearTimeout(timeout); } 37 | }; 38 | }, [props.profile]); 39 | 40 | // Render 41 | return ( 42 |
47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/renderer/components/CurateBoxCheckBoxRow.tsx: -------------------------------------------------------------------------------- 1 | import { CheckBox } from '@renderer/components/CheckBox'; 2 | import { CurateBoxRow } from '@renderer/components/CurateBoxRow'; 3 | import { CurationMeta } from '@shared/curate/types'; 4 | import * as React from 'react'; 5 | import { Dispatch } from 'redux'; 6 | import { useDispatch } from 'react-redux'; 7 | import { editCurationMeta } from '@renderer/store/curate/slice'; 8 | 9 | export type CurateBoxCheckBoxProps = { 10 | title: string; 11 | checked: boolean | undefined; 12 | property: keyof CurationMeta; 13 | curationFolder: string; 14 | disabled: boolean; 15 | } 16 | 17 | export function CurateBoxCheckBox(props: CurateBoxCheckBoxProps) { 18 | const dispatch = useDispatch(); 19 | const onChange = useOnCheckboxToggle(props.property, props.curationFolder, dispatch); 20 | 21 | return ( 22 | 23 | 27 | 28 | ); 29 | } 30 | 31 | function useOnCheckboxToggle(property: keyof CurationMeta, folder: string | undefined, dispatch: Dispatch) { 32 | return React.useCallback((checked: boolean) => { 33 | if (folder !== undefined) { 34 | dispatch(editCurationMeta({ 35 | folder, 36 | property, 37 | value: checked, 38 | })); 39 | } 40 | }, [dispatch, folder]); 41 | } 42 | -------------------------------------------------------------------------------- /src/renderer/components/CurateBoxRow.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | type CurateRowProps = { 4 | children?: React.ReactNode; 5 | /** Title of the row. */ 6 | title?: string; 7 | /** CSS class(es) of this component's top level element. */ 8 | className?: string; 9 | }; 10 | 11 | // A row inside a curate box. 12 | export function CurateBoxRow(props: CurateRowProps) { 13 | return ( 14 | 15 | {props.title ? (props.title + ':') : ''} 16 | {props.children} 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/renderer/components/CurateBoxWarnings.tsx: -------------------------------------------------------------------------------- 1 | import { LangContainer } from '@shared/lang'; 2 | import { CurationWarnings } from 'flashpoint-launcher'; 3 | import * as React from 'react'; 4 | import { useMemo } from 'react'; 5 | import { LangContext } from '../util/lang'; 6 | 7 | export type CurateBoxWarningsProps = { 8 | /** Warnings to display. */ 9 | warnings: CurationWarnings; 10 | }; 11 | 12 | // The part of a Curation Box that displays all the warnings (if any). 13 | export function CurateBoxWarnings(props: CurateBoxWarningsProps) { 14 | const strings = React.useContext(LangContext).curate; 15 | const { warnings } = props; 16 | // Count the number of warnings 17 | const warningCount = props.warnings.writtenWarnings.length; 18 | // Converts warnings into a single string 19 | const warningsStrings = useMemo(() => { 20 | return warnings.writtenWarnings.map(s => `- ${strings[s as keyof LangContainer['curate']] || s}\n`); 21 | }, [warnings]); 22 | // Render warnings 23 | const warningElements = useMemo(() => ( 24 | warningsStrings.length > 0 ? ( 25 | 27 | {`${warningsStrings.join('')}`} 28 | 29 | ) : ( undefined ) 30 | ), [warningsStrings]); 31 | // Misc. 32 | const isEmpty = warningCount === 0; 33 | // Render 34 | return ( 35 | <> 36 | {/* Warnings */} 37 |
38 |
39 | {strings.warnings}: {warningCount} 40 |
41 |
42 |           {warningElements}
43 |         
44 |
45 | {/* Divider */} 46 | { !isEmpty ?
: undefined } 47 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /src/renderer/components/Dropdown.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useCallback, useEffect, useRef, useState } from 'react'; 3 | import { checkIfAncestor } from '../Util'; 4 | 5 | export type DropdownProps = { 6 | /** Extra class name to add to dropdown frame */ 7 | className?: string; 8 | headerClassName?: string; 9 | /** Element(s) to show in the drop-down element (only visible when expanded). */ 10 | children: React.ReactNode; 11 | /** Text to show in the text field (always visible). */ 12 | text: string; 13 | form?: boolean; 14 | }; 15 | 16 | // A text element, with a drop-down element that can be shown/hidden. 17 | export function Dropdown(props: DropdownProps) { 18 | // Hooks 19 | const [expanded, setExpanded] = useState(false); 20 | const contentRef = useRef(null); 21 | useEffect(() => { // ("Hide" the drop-downs content if the user clicks outside the content element) 22 | if (expanded) { 23 | const onGlobalMouseDown = (event: MouseEvent) => { 24 | if (!event.defaultPrevented) { 25 | if (!checkIfAncestor(event.target as HTMLElement | null, contentRef.current)) { 26 | setExpanded(false); 27 | } 28 | } 29 | }; 30 | document.addEventListener('mousedown', onGlobalMouseDown); 31 | return () => { document.removeEventListener('mousedown', onGlobalMouseDown); }; 32 | } 33 | }, [expanded, contentRef]); 34 | const onMouseDown = useCallback((event: React.MouseEvent) => { 35 | if (event.button === 0) { // (Left mouse button) 36 | setExpanded(!expanded); 37 | } 38 | }, [expanded]); 39 | 40 | const baseClass = props.form ? 'simple-dropdown-form' : 'simple-dropdown'; 41 | 42 | // Render 43 | return ( 44 |
45 |
49 |
50 | { props.text } 51 |
52 |
53 |
54 |
setExpanded(false)} 57 | ref={contentRef}> 58 | { props.children } 59 |
60 |
61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /src/renderer/components/FancyAnimation.tsx: -------------------------------------------------------------------------------- 1 | import { withPreferences, WithPreferencesProps } from '@renderer/containers/withPreferences'; 2 | import { ReactElement } from 'react'; 3 | 4 | type OwnProps = { 5 | fancyRender: (() => ReactElement) | ReactElement; 6 | normalRender: (() => ReactElement) | ReactElement; 7 | }; 8 | 9 | type FancyAnimationProps = OwnProps & WithPreferencesProps; 10 | 11 | function _FancyAnimation(props: FancyAnimationProps) { 12 | if (props.preferencesData.fancyAnimations) { 13 | if (typeof props.fancyRender == 'function') { 14 | return props.fancyRender(); 15 | } else { 16 | return props.fancyRender; 17 | } 18 | } else { 19 | if (typeof props.normalRender == 'function') { 20 | return props.normalRender(); 21 | } else { 22 | return props.normalRender; 23 | } 24 | } 25 | } 26 | 27 | export const FancyAnimation = withPreferences(_FancyAnimation); 28 | -------------------------------------------------------------------------------- /src/renderer/components/FloatingContainer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | type FloatingContainerProps = { 4 | floatingClassName?: string 5 | children: JSX.Element | JSX.Element[]; 6 | onClick?: () => void; 7 | } & React.HTMLProps; 8 | 9 | export class FloatingContainer extends React.Component { 10 | render() { 11 | return ( 12 |
15 |
16 | {this.props.children} 17 |
18 |
19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/renderer/components/FpfssEditGame.tsx: -------------------------------------------------------------------------------- 1 | import { withConfirmDialog } from '@renderer/containers/withConfirmDialog'; 2 | import { withPreferences } from '@renderer/containers/withPreferences'; 3 | import { withSearch } from '@renderer/containers/withSearch'; 4 | import { RightBrowseSidebar, RightBrowseSidebarProps } from './RightBrowseSidebar'; 5 | 6 | export type FpfssEditGameProps = RightBrowseSidebarProps; 7 | 8 | function FpfssEditGame(props: FpfssEditGameProps) { 9 | return ( 10 | 13 | ); 14 | } 15 | 16 | export const ConnectedFpfssEditGame = withConfirmDialog(withSearch(withPreferences(FpfssEditGame))); 17 | -------------------------------------------------------------------------------- /src/renderer/components/HomePageBox.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { OpenIcon } from './OpenIcon'; 3 | 4 | export type HomePageBoxProps = { 5 | minimized: boolean; 6 | cssKey: string; 7 | title: string; 8 | onToggleMinimize: () => void; 9 | } 10 | 11 | export function HomePageBox(props: React.PropsWithChildren) { 12 | return ( 13 |
14 |
15 |
{props.title}
16 |
18 | { props.minimized ? ( 19 | 20 | ) : ( 21 | 22 | )} 23 |
24 |
25 | { !props.minimized && ( 26 |
    27 | {props.children} 28 |
29 | )} 30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/renderer/components/SimpleButton.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Omit } from '@shared/interfaces'; 3 | 4 | /** Props for an input element. */ 5 | type InputProps = React.DetailedHTMLProps, HTMLInputElement>; 6 | 7 | export type SimpleButtonProps = Omit; 8 | 9 | // A normal button, but with the "simple-button" css class added. 10 | export function SimpleButton(props: SimpleButtonProps) { 11 | const { className, ...rest } = props; 12 | return ( 13 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/renderer/components/SizeProvider.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useEffect, useRef } from 'react'; 3 | 4 | export type SizeProviderProps = { 5 | /** Children of the wrapping
element. */ 6 | children?: React.ReactNode; 7 | /** Value to set the "--width" CSS variable to. */ 8 | width: string | number; 9 | /** Value to set the "--height" CSS variable to. */ 10 | height: string | number; 11 | }; 12 | 13 | // Sets and updates the "--width" and "--height" CSS variables to match the prop values. 14 | export function SizeProvider(props: SizeProviderProps) { 15 | const ref = useRef(null); 16 | // Update "--width" 17 | useEffect(() => { 18 | updateStyle(ref.current, '--width', props.width); 19 | }, [ref.current, props.width]); 20 | // Update "--height" 21 | useEffect(() => { 22 | updateStyle(ref.current, '--height', props.height); 23 | }, [ref.current, props.height]); 24 | // Render 25 | return ( 26 |
27 | {props.children} 28 |
29 | ); 30 | } 31 | 32 | /** 33 | * Update the a style property of the style attribute of an element. 34 | * 35 | * @param element Element to update the style attribute of. 36 | * @param prop Name of the style property. 37 | * @param value Value of the style property. 38 | */ 39 | function updateStyle(element: HTMLElement | null, prop: string, value: string | number): void { 40 | if (!element) { throw new Error('Can not update CSS variables. Element not found.'); } 41 | element.style.setProperty(prop, value+''); 42 | } 43 | -------------------------------------------------------------------------------- /src/renderer/components/Spinner.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Source: https://loading.io/css/ 3 | * License: CC0 4 | */ 5 | export function Spinner() { 6 | return ( 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/renderer/components/SplashScreen.tsx: -------------------------------------------------------------------------------- 1 | import { BackInit } from '@shared/back/types'; 2 | 3 | export type SplashScreenProps = { 4 | quitting: boolean, 5 | loadedAll: boolean; 6 | loaded: { [key in BackInit]: boolean; }; 7 | } 8 | 9 | export function SplashScreen(props: SplashScreenProps) { 10 | const extraClass = (props.loadedAll && !props.quitting) 11 | ? ' splash-screen--fade-out' 12 | : ''; 13 | 14 | return ( 15 |
16 |
17 |
18 |
19 |
20 |
21 | { props.quitting ? 'Closing Down' : 'Loading' } 22 |
23 | { !props.loaded[BackInit.DATABASE] ? ( 24 |
25 | Database 26 |
27 | ) : undefined } 28 | { !props.loaded[BackInit.PLAYLISTS] ? ( 29 |
30 | Playlists 31 |
32 | ) : undefined } 33 | { !props.loaded[BackInit.CURATE] ? ( 34 |
35 | Curations 36 |
37 | ) : undefined } 38 | { !props.loaded[BackInit.SERVICES] ? ( 39 |
40 | Services 41 |
42 | ) : undefined } 43 | { !props.loaded[BackInit.EXTENSIONS] ? ( 44 |
45 | Extensions 46 |
47 | ) : undefined } 48 | { !props.loaded[BackInit.EXEC_MAPPINGS] ? ( 49 |
50 | Exec Mappings 51 |
52 | ) : undefined } 53 |
54 |
55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /src/renderer/components/TagCategoriesListHeader.tsx: -------------------------------------------------------------------------------- 1 | import { LangContext } from '@renderer/util/lang'; 2 | import * as React from 'react'; 3 | import { useMemo } from 'react'; 4 | 5 | export type TagCategoriesListHeaderProps = {}; 6 | 7 | /** 8 | * Header on top of the GameList. 9 | * It contains the resizable columns that decide how wide each column is. 10 | */ 11 | export function TagCategoriesListHeader() { 12 | const strings = React.useContext(LangContext); 13 | return useMemo(() => ( 14 |
15 | 16 |
17 | 18 | 19 |
20 |
21 |
22 | ), []); 23 | } 24 | 25 | type ColumnProps = { 26 | /** Name of the modifier. */ 27 | modifier: string; 28 | /** Displayed title of the column. */ 29 | title?: string; 30 | /** If the divider should be hidden (defaults to false). */ 31 | hideDivider?: boolean; 32 | }; 33 | 34 | function Column(props: ColumnProps) { 35 | const className = 'tag-list-header-column'; 36 | const showDivider = !props.hideDivider; 37 | // Render 38 | return ( 39 |
40 | { showDivider ? ( 41 |
42 | ) : undefined } 43 |
{props.title || ''}
44 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/renderer/components/TagItemContainer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | /** All props of a DIV element (except for "ref"). */ 4 | type HTMLDivProps = React.HTMLAttributes; 5 | 6 | export type TagItemContainerProps = HTMLDivProps & { 7 | /** Reference to the underlying DIV element. */ 8 | realRef?: JSX.IntrinsicElements['div']['ref']; 9 | onTagSelect?: (event: React.MouseEvent, tagId: number | undefined) => void; 10 | /** 11 | * Find the tag ID of an element (or sub-element) of a game. 12 | * 13 | * @param element Element or sub-element of a game. 14 | * @returns The tag's ID (or undefined if no tag was found). 15 | */ 16 | findTagId: (element: EventTarget) => number | undefined; 17 | // Overrides onTagSelect 18 | onClick?: (event: React.MouseEvent) => void; 19 | }; 20 | 21 | /** 22 | * A DIV element with additional props that listens for "game item" events that bubbles up. 23 | * This is more efficient than listening for events on each "game item" individually. 24 | */ 25 | export class TagItemContainer extends React.Component { 26 | render() { 27 | return ( 28 |
32 | {this.props.children} 33 |
34 | ); 35 | } 36 | 37 | onClick = (event: React.MouseEvent) => { 38 | if (this.props.onClick) { this.props.onClick(event); } 39 | if (this.props.onTagSelect) { 40 | this.props.onTagSelect(event, this.findTagId(event.target)); 41 | } 42 | }; 43 | 44 | /** 45 | * Find a tag ID given an event target 46 | * 47 | * @param target Event target 48 | */ 49 | findTagId(target: EventTarget): number | undefined { 50 | return this.props.findTagId(target); 51 | } 52 | } 53 | 54 | // Create a shallow copy of the props object, but without all non-div element props. 55 | function filterDivProps(props: TagItemContainerProps): JSX.IntrinsicElements['div'] { 56 | const rest: HTMLDivProps & { 57 | // These need to be explicitly specified: the compiler doesn't infer them correctly. 58 | realRef?: any; 59 | onTagSelect?: any; 60 | findTagId?: any; 61 | } = Object.assign({}, props); 62 | delete rest.realRef; 63 | delete rest.onTagSelect; 64 | delete rest.findTagId; 65 | return rest; 66 | } 67 | -------------------------------------------------------------------------------- /src/renderer/components/TagListHeader.tsx: -------------------------------------------------------------------------------- 1 | import { LangContext } from '@renderer/util/lang'; 2 | import * as React from 'react'; 3 | import { useMemo } from 'react'; 4 | 5 | export type TagListHeaderProps = {}; 6 | 7 | /** 8 | * Header on top of the GameList. 9 | * It contains the resizable columns that decide how wide each column is. 10 | */ 11 | export function TagListHeader() { 12 | const strings = React.useContext(LangContext); 13 | return useMemo(() => ( 14 |
15 | 16 |
17 | 18 | 19 | 20 |
21 |
22 |
23 | ), []); 24 | } 25 | 26 | type ColumnProps = { 27 | /** Name of the modifier. */ 28 | modifier: string; 29 | /** Displayed title of the column. */ 30 | title?: string; 31 | /** If the divider should be hidden (defaults to false). */ 32 | hideDivider?: boolean; 33 | }; 34 | 35 | function Column(props: ColumnProps) { 36 | const className = 'tag-list-header-column'; 37 | const showDivider = !props.hideDivider; 38 | // Render 39 | return ( 40 |
41 | { showDivider ? ( 42 |
43 | ) : undefined } 44 |
{props.title || ''}
45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/renderer/components/TitleBar.tsx: -------------------------------------------------------------------------------- 1 | import { VERSION } from '@shared/version'; 2 | 3 | export type TitleBarProps = { 4 | /** Title to display. */ 5 | title?: string; 6 | }; 7 | 8 | // Title bar of the window (the top-most part of the window). 9 | export function TitleBar(props: TitleBarProps) { 10 | return ( 11 |
12 |
13 |

{props.title || ''}

14 |

{VERSION}

15 |
16 |
19 |
22 |
25 |
26 |
27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/renderer/components/pages/Downloads.tsx: -------------------------------------------------------------------------------- 1 | import { useAppSelector } from '@renderer/hooks/useAppSelector'; 2 | import { ProgressBar } from '../ProgressComponents'; 3 | 4 | export function DownloadsPage() { 5 | const { main } = useAppSelector((state) => state); 6 | const downloaderState = main.downloaderState; 7 | const tasks = Object.values(downloaderState.tasks); 8 | 9 | return ( 10 |
11 |
12 |
13 | {`Task completion: ${tasks.filter(t => t.status !== 'waiting' && t.status !== 'in_progress').length} / ${tasks.length}`} 14 |
15 |
16 | {`Failures: ${tasks.filter(t => t.status === 'failure').length}`} 17 |
18 |
19 |
20 | { downloaderState.workers.map((worker, idx) => { 21 | const progressPercent = ((worker.step - 1) / worker.totalSteps) + worker.stepProgress; 22 | const isDone = worker.text === 'Done'; 23 | return ( 24 |
25 | 36 |
37 | ); 38 | })} 39 |
40 |
41 | ) 42 | } -------------------------------------------------------------------------------- /src/renderer/components/pages/IFramePage.tsx: -------------------------------------------------------------------------------- 1 | export type IFramePageProps = { 2 | url: string; 3 | } 4 | 5 | export function IFramePage(props: IFramePageProps) { 6 | return ( 7 |
8 | 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/renderer/components/pages/LoadingPage.tsx: -------------------------------------------------------------------------------- 1 | export function LoadingPage() { 2 | return
; 3 | } 4 | -------------------------------------------------------------------------------- /src/renderer/components/pages/NotFoundPage.tsx: -------------------------------------------------------------------------------- 1 | import { Paths } from '@shared/Paths'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | /** Page shown when the current URL does not point to an existing page. */ 5 | export function NotFoundPage() { 6 | return ( 7 |
8 |

You appear to have gotten lost :(

9 | Back to home 10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/renderer/containers/ConnectedApp.ts: -------------------------------------------------------------------------------- 1 | import { withCurate } from '@renderer/containers/withCurateState'; 2 | import { withFpfss } from '@renderer/containers/withFpfss'; 3 | import { withSearch } from '@renderer/containers/withSearch'; 4 | import { withView } from '@renderer/containers/withView'; 5 | import { withShortcut } from '@renderer/store/reactKeybindCompat'; 6 | import { withRouter } from 'react-router'; 7 | import { App } from '../components/app'; 8 | import { withMainState } from './withMainState'; 9 | import { withPreferences } from './withPreferences'; 10 | import { withTagCategories } from './withTagCategories'; 11 | import { withTasks } from './withTasks'; 12 | 13 | export default withView(withFpfss(withSearch(withShortcut(withCurate(withTasks(withRouter(withMainState(withTagCategories(withPreferences(App)))))))))); 14 | -------------------------------------------------------------------------------- /src/renderer/containers/ConnectedBrowsePage.ts: -------------------------------------------------------------------------------- 1 | import { BrowsePage, BrowsePageProps } from '../components/pages/BrowsePage'; 2 | import { withPreferences, WithPreferencesProps } from './withPreferences'; 3 | import { withTagCategories, WithTagCategoriesProps } from './withTagCategories'; 4 | import { withSearch, WithSearchProps } from '@renderer/containers/withSearch'; 5 | import { Subtract } from '@shared/interfaces'; 6 | import { withView, WithViewProps } from '@renderer/containers/withView'; 7 | 8 | export type ConnectedBrowsePageProps = Subtract; 9 | 10 | export default withView(withSearch(withTagCategories(withPreferences( 11 | BrowsePage 12 | )))); 13 | -------------------------------------------------------------------------------- /src/renderer/containers/ConnectedConfigPage.ts: -------------------------------------------------------------------------------- 1 | import { Subtract } from '@shared/interfaces'; 2 | import { ConfigPage, ConfigPageProps } from '../components/pages/ConfigPage'; 3 | import { withPreferences, WithPreferencesProps } from './withPreferences'; 4 | import { withTagCategories, WithTagCategoriesProps } from './withTagCategories'; 5 | import { withSearch, WithSearchProps } from '@renderer/containers/withSearch'; 6 | 7 | export type ConnectedConfigPageProps = Subtract; 8 | 9 | export const ConnectedConfigPage = withSearch(withTagCategories(withPreferences(ConfigPage))); 10 | -------------------------------------------------------------------------------- /src/renderer/containers/ConnectedCuratePage.ts: -------------------------------------------------------------------------------- 1 | import { withCurate, WithCurateProps } from '@renderer/containers/withCurateState'; 2 | import { withShortcut, WithShortcutProps } from '@renderer/store/reactKeybindCompat'; 3 | import { Subtract } from '@shared/interfaces'; 4 | import { CuratePage, CuratePageProps } from '../components/pages/CuratePage'; 5 | import { withConfirmDialog, WithConfirmDialogProps } from './withConfirmDialog'; 6 | import { withMainState, WithMainStateProps } from './withMainState'; 7 | import { withPreferences, WithPreferencesProps } from './withPreferences'; 8 | import { withTagCategories, WithTagCategoriesProps } from './withTagCategories'; 9 | import { withTasks, WithTasksProps } from './withTasks'; 10 | 11 | export type ConnectedCuratePageProps = Subtract; 12 | 13 | export const ConnectedCuratePage = withShortcut(withTasks(withTagCategories(withPreferences(withMainState(withCurate(withConfirmDialog(CuratePage))))))); 14 | -------------------------------------------------------------------------------- /src/renderer/containers/ConnectedFooter.ts: -------------------------------------------------------------------------------- 1 | import { withRouter } from 'react-router-dom'; 2 | import { Footer } from '../components/Footer'; 3 | import { withMainState } from './withMainState'; 4 | import { withPreferences } from './withPreferences'; 5 | import { withView } from '@renderer/containers/withView'; 6 | 7 | export const ConnectedFooter = withView(withRouter(withMainState(withPreferences(Footer)))); 8 | -------------------------------------------------------------------------------- /src/renderer/containers/ConnectedHomePage.ts: -------------------------------------------------------------------------------- 1 | import { Subtract } from '@shared/interfaces'; 2 | import { HomePage, HomePageProps } from '../components/pages/HomePage'; 3 | import { withPreferences, WithPreferencesProps } from './withPreferences'; 4 | import { withSearch, WithSearchProps } from './withSearch'; 5 | import { withMainState, WithMainStateProps } from './withMainState'; 6 | 7 | export type ConnectedHomePageProps = Subtract; 8 | 9 | export default withMainState(withSearch(withPreferences(HomePage))); 10 | -------------------------------------------------------------------------------- /src/renderer/containers/ConnectedLeftBrowseSidebar.ts: -------------------------------------------------------------------------------- 1 | import { LeftBrowseSidebar } from '../components/LeftBrowseSidebar'; 2 | import { withPreferences } from './withPreferences'; 3 | 4 | export const ConnectedLeftBrowseSidebar = withPreferences(LeftBrowseSidebar); 5 | -------------------------------------------------------------------------------- /src/renderer/containers/ConnectedLogsPage.ts: -------------------------------------------------------------------------------- 1 | import { LogsPage } from '../components/pages/LogsPage'; 2 | import { withPreferences } from './withPreferences'; 3 | 4 | export const ConnectedLogsPage = withPreferences(LogsPage); 5 | -------------------------------------------------------------------------------- /src/renderer/containers/ConnectedRightBrowseSidebar.ts: -------------------------------------------------------------------------------- 1 | import { RightBrowseSidebar } from '../components/RightBrowseSidebar'; 2 | import { withConfirmDialog } from './withConfirmDialog'; 3 | import { withPreferences } from './withPreferences'; 4 | import { withSearch } from './withSearch'; 5 | 6 | export const ConnectedRightBrowseSidebar = withConfirmDialog(withPreferences(RightBrowseSidebar)); 7 | -------------------------------------------------------------------------------- /src/renderer/containers/ConnectedRightTagsCategoriesSidebar.ts: -------------------------------------------------------------------------------- 1 | import { RightTagCategoriesSidebar } from '@renderer/components/RightTagCategoriesSidebar'; 2 | import { withPreferences } from './withPreferences'; 3 | 4 | 5 | export const ConnectedRightTagCategoriesSidebar = withPreferences(RightTagCategoriesSidebar); 6 | -------------------------------------------------------------------------------- /src/renderer/containers/ConnectedRightTagsSidebar.ts: -------------------------------------------------------------------------------- 1 | import { RightTagsSidebar } from '@renderer/components/RightTagsSidebar'; 2 | import { withPreferences } from './withPreferences'; 3 | 4 | export const ConnectedRightTagsSidebar = withPreferences(RightTagsSidebar); 5 | -------------------------------------------------------------------------------- /src/renderer/containers/ConnectedTagCategoriesPage.tsx: -------------------------------------------------------------------------------- 1 | import { TagCategoriesPage, TagCategoriesPageProps } from '@renderer/components/pages/TagCategoriesPage'; 2 | import { Subtract } from '@shared/interfaces'; 3 | import { withPreferences, WithPreferencesProps } from './withPreferences'; 4 | import { withTagCategories, WithTagCategoriesProps } from './withTagCategories'; 5 | 6 | export type ConnectedTagCategoriesPageProps = Subtract; 7 | 8 | export const ConnectedTagCategoriesPage = withPreferences(withTagCategories(TagCategoriesPage)); 9 | -------------------------------------------------------------------------------- /src/renderer/containers/ConnectedTagsPage.ts: -------------------------------------------------------------------------------- 1 | import { Subtract } from '@shared/interfaces'; 2 | import { TagsPage, TagsPageProps } from '../components/pages/TagsPage'; 3 | import { withPreferences, WithPreferencesProps } from './withPreferences'; 4 | import { withTagCategories, WithTagCategoriesProps } from './withTagCategories'; 5 | 6 | export type ConnectedTagsPageProps = Subtract; 7 | 8 | export const ConnectedTagsPage = withPreferences(withTagCategories(TagsPage)); 9 | -------------------------------------------------------------------------------- /src/renderer/containers/HeaderContainer.tsx: -------------------------------------------------------------------------------- 1 | import { withRouter } from 'react-router'; 2 | import { Header } from '../components/Header'; 3 | import { withPreferences } from './withPreferences'; 4 | import { withTagCategories } from './withTagCategories'; 5 | import { withSearch } from '@renderer/containers/withSearch'; 6 | import { withView } from '@renderer/containers/withView'; 7 | import { withMainState } from '@renderer/containers/withMainState'; 8 | import { withConfirmDialog } from '@renderer/containers/withConfirmDialog'; 9 | 10 | export default withConfirmDialog(withMainState(withView(withSearch(withRouter(withTagCategories(withPreferences(Header))))))); 11 | -------------------------------------------------------------------------------- /src/renderer/containers/withCurateState.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { bindActionCreators, Dispatch } from 'redux'; 3 | import { RootState } from '@renderer/store/store'; 4 | import { curateActions } from '@renderer/store/curate/slice'; 5 | 6 | const mapStateToProps = (state: RootState) => ({ 7 | curate: state.curate, 8 | }); 9 | 10 | function mapDispatchToProps(dispatch: Dispatch) { 11 | return { 12 | curateActions: bindActionCreators(curateActions, dispatch) 13 | }; 14 | } 15 | 16 | export type WithCurateProps = ReturnType & ReturnType; 17 | 18 | export const withCurate = connect( 19 | mapStateToProps, 20 | mapDispatchToProps, 21 | null, 22 | { getDisplayName: name => 'withCurate('+name+')' } 23 | ); 24 | -------------------------------------------------------------------------------- /src/renderer/containers/withFpfss.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { bindActionCreators, Dispatch } from 'redux'; 3 | import { RootState } from '@renderer/store/store'; 4 | import { fpfssActions } from '@renderer/store/fpfss/slice'; 5 | 6 | const mapStateToProps = (state: RootState) => ({ 7 | fpfss: state.fpfss, 8 | }); 9 | 10 | function mapDispatchToProps(dispatch: Dispatch) { 11 | return { 12 | fpfssActions: bindActionCreators(fpfssActions, dispatch), 13 | }; 14 | } 15 | 16 | export type WithFpfssProps = ReturnType & ReturnType; 17 | 18 | export const withFpfss = connect( 19 | mapStateToProps, 20 | mapDispatchToProps, 21 | null, 22 | { getDisplayName: name => 'withFpfss('+name+')' } 23 | ); 24 | -------------------------------------------------------------------------------- /src/renderer/containers/withMainState.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { bindActionCreators, Dispatch } from 'redux'; 3 | import { RootState } from '@renderer/store/store'; 4 | import { mainActions, MainState } from '@renderer/store/main/slice'; 5 | 6 | export type WithMainStateProps = ReturnType & ReturnType; 7 | 8 | const mapStateToProps = (state: RootState) => ({ 9 | main: state.main, 10 | }); 11 | 12 | function mapDispatchToProps(dispatch: Dispatch) { 13 | return { 14 | dispatch, 15 | setMainState: (state: Partial) => dispatch(mainActions.setMainState(state)), 16 | mainActions: bindActionCreators(mainActions, dispatch), 17 | }; 18 | } 19 | 20 | export const withMainState = connect( 21 | mapStateToProps, 22 | mapDispatchToProps, 23 | null, 24 | { getDisplayName: name => 'withMainState('+name+')' } 25 | ); 26 | -------------------------------------------------------------------------------- /src/renderer/containers/withPreferences.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useContext } from 'react'; 3 | import { Subtract } from '@shared/interfaces'; 4 | import { AppPreferencesData } from 'flashpoint-launcher'; 5 | import { PreferencesContext } from '../context/PreferencesContext'; 6 | 7 | export type WithPreferencesProps = { 8 | /** Current preference data. */ 9 | preferencesData: Readonly; 10 | }; 11 | 12 | export function withPreferences

(Component: React.ComponentType

) { 13 | return function WithPreferences(props: Subtract) { 14 | const preferences = useContext(PreferencesContext); 15 | return ( 16 | 19 | ); 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/renderer/containers/withSearch.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { bindActionCreators, Dispatch } from 'redux'; 3 | import { RootState, store } from '@renderer/store/store'; 4 | import { forceSearch, ForceSearchAction, searchActions } from '@renderer/store/search/slice'; 5 | 6 | const mapStateToProps = (state: RootState) => ({ 7 | search: state.search, 8 | }); 9 | 10 | function mapDispatchToProps(dispatch: Dispatch) { 11 | return { 12 | searchActions: { 13 | ...bindActionCreators(searchActions, dispatch), 14 | forceSearch: (action: ForceSearchAction) => { store.dispatch(forceSearch(action)); }, 15 | } 16 | }; 17 | } 18 | 19 | export type WithSearchProps = ReturnType & ReturnType; 20 | 21 | export const withSearch = connect( 22 | mapStateToProps, 23 | mapDispatchToProps, 24 | null, 25 | { getDisplayName: name => 'withSearch('+name+')' } 26 | ); 27 | -------------------------------------------------------------------------------- /src/renderer/containers/withTagCategories.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { Dispatch } from 'redux'; 3 | import { RootState } from '@renderer/store/store'; 4 | import { setTagCategories } from '@renderer/store/tagCategories/slice'; 5 | import { TagCategory } from 'flashpoint-launcher'; 6 | 7 | const mapStateToProps = (state: RootState) => ({ 8 | tagCategories: state.tagCategories, 9 | }); 10 | 11 | function mapDispatchToProps(dispatch: Dispatch) { 12 | return { 13 | setTagCategories: (tagCats: TagCategory[]) => dispatch(setTagCategories(tagCats)), 14 | }; 15 | } 16 | 17 | export type WithTagCategoriesProps = ReturnType & ReturnType; 18 | 19 | export const withTagCategories = connect( 20 | mapStateToProps, 21 | mapDispatchToProps, 22 | null, 23 | { getDisplayName: name => 'withTagCategories('+name+')' } 24 | ); 25 | -------------------------------------------------------------------------------- /src/renderer/containers/withTasks.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { Dispatch } from 'redux'; 3 | import { RootState } from '@renderer/store/store'; 4 | import { addTask, setTask } from '@renderer/store/tasks/slice'; 5 | import { Task } from '@shared/interfaces'; 6 | 7 | const mapStateToProps = (state: RootState) => ({ 8 | tasks: state.tasks, 9 | }); 10 | 11 | function mapDispatchToProps(dispatch: Dispatch) { 12 | return { 13 | addTask: (task: Task) => dispatch(addTask(task)), 14 | setTask: (task: Partial) => dispatch(setTask(task)), 15 | }; 16 | } 17 | 18 | export type WithTasksProps = ReturnType & ReturnType; 19 | 20 | export const withTasks = connect( 21 | mapStateToProps, 22 | mapDispatchToProps, 23 | null, 24 | { getDisplayName: name => 'withTasks('+name+')' } 25 | ); 26 | -------------------------------------------------------------------------------- /src/renderer/containers/withView.tsx: -------------------------------------------------------------------------------- 1 | import { ResultsView } from '@renderer/store/search/slice'; 2 | import * as React from 'react'; 3 | import { useLocation } from 'react-router-dom'; 4 | import { getViewName } from '@renderer/Util'; 5 | import { useAppSelector } from '@renderer/hooks/useAppSelector'; 6 | import { NotFoundPage } from '@renderer/components/pages/NotFoundPage'; 7 | 8 | export type WithViewProps = { 9 | currentView: ResultsView; 10 | currentViewName: string; 11 | }; 12 | 13 | export function withView(Component: React.ComponentType) { 14 | return function WrappedComponent(props: Omit) { 15 | const location = useLocation(); 16 | const viewName = getViewName(location.pathname); 17 | const search = useAppSelector((state) => state.search); 18 | const view = search.views[viewName]; 19 | if (view) { 20 | return ; 25 | } else { 26 | return ; 27 | } 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /src/renderer/context-reducer/ContextReducerProvider.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useMemo, useReducer } from 'react'; 3 | import { ReducerContext, ReducerContextValue } from './interfaces'; 4 | 5 | /** Short-hand type. */ 6 | type AnyReducer = React.Reducer; 7 | 8 | export type ContextReducerProviderProps = { 9 | children?: React.ReactNode; 10 | /** Reducer context to provide. */ 11 | context: ReducerContext; 12 | }; 13 | 14 | // Stores the state of a Context and provides a dispatcher for changing its value. 15 | export function ContextReducerProvider(props: ContextReducerProviderProps) { 16 | const { Provider } = props.context.context; 17 | // Reducer that stores the state 18 | const [state, dispatch]: ReducerContextValue = useReducer( 19 | props.context.reducer, 20 | props.context.initialState 21 | ); 22 | // Context value (reducer state and dispatcher) 23 | const contextValue = useMemo((): ReducerContextValue => { 24 | return [state, dispatch]; 25 | }, [state, dispatch]); 26 | // Render 27 | return ( 28 | 29 | {props.children} 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/renderer/context-reducer/contextReducer.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { ReducerContext, ReducerContextValue } from './interfaces'; 3 | 4 | /** 5 | * Create a Reducer Context. 6 | * 7 | * @param reducer Reducer that will manage the state. 8 | * @param initialState Initial state of the context (and the default value in case no context is available). 9 | */ 10 | export function createContextReducer>( 11 | reducer: R, 12 | initialState: ReducerContextValue[0] 13 | ): ReducerContext { 14 | // Default value of the context 15 | // (Value returned when attempting to get the value of a context that has no accessible provider) 16 | const defaultValue: ReducerContextValue = [ 17 | initialState, 18 | function noProviderDispatch() { console.error('Failed to dispatch. The context does not have a provider.'); } 19 | ]; 20 | // Create the context this is wrapping 21 | const context = React.createContext(defaultValue); 22 | // Create and return the reducer context object 23 | return { 24 | context, 25 | reducer, 26 | initialState 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/renderer/context-reducer/interfaces.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | /** 4 | * Value of a Reducer Context. 5 | * It contains both the state and dispatcher for the reducer used. 6 | * The first value is the state, the second is the dispatcher. 7 | */ 8 | export type ReducerContextValue> = [ 9 | React.ReducerState, 10 | React.Dispatch> 11 | ]; 12 | 13 | /** 14 | * A "wrapped" React Context that can be used together with a Context Reducer Provider. 15 | * This makes it capable of providing the state and dispatcher for a reducer. 16 | */ 17 | export type ReducerContext> = { 18 | /** React Context that this is wrapping. */ 19 | context: React.Context>; 20 | /** Reducer used to modify the state. */ 21 | reducer: R; 22 | /** Initial state of the context. */ 23 | initialState: ReducerContextValue[0]; 24 | }; 25 | 26 | /** Generic reducer action with a type and a payload. */ 27 | export type ReducerAction = { 28 | /** Identifier of what type of action this is. */ 29 | type: T; 30 | /** Arguments or data passed along the action. */ 31 | payload: P; 32 | }; 33 | -------------------------------------------------------------------------------- /src/renderer/context/PreferencesContext.tsx: -------------------------------------------------------------------------------- 1 | import { deepCopy } from '@shared/Util'; 2 | import { AppPreferencesData } from 'flashpoint-launcher'; 3 | import * as React from 'react'; 4 | 5 | export const PreferencesContext = React.createContext({} as any); 6 | 7 | type PreferencesContextProviderProps = { 8 | children?: React.ReactNode; 9 | } 10 | 11 | export function PreferencesContextProvider(props: PreferencesContextProviderProps) { 12 | // Note: This assumes that the preferences has been loaded before this is created. 13 | const [state, setState] = React.useState(() => deepCopy(window.Shared.preferences.data)); 14 | 15 | // Update when preferences change 16 | React.useEffect(() => { 17 | const listener = () => { setState(window.Shared.preferences.data); }; 18 | window.Shared.preferences.onUpdate = listener; 19 | return () => { window.Shared.preferences.onUpdate = undefined; }; 20 | }); 21 | 22 | return ( 23 | 24 | {props.children} 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/renderer/credits/CreditsFile.ts: -------------------------------------------------------------------------------- 1 | import * as Coerce from '@shared/utils/Coerce'; 2 | import { IObjectParserProp, ObjectParser } from '@shared/utils/ObjectParser'; 3 | import { CreditsData, CreditsDataProfile, CreditsDataRole } from './types'; 4 | 5 | const { str } = Coerce; 6 | 7 | export namespace CreditsFile { 8 | export function parseCreditsData(data: any, onError?: (error: string) => void): CreditsData { 9 | const parsed: CreditsData = { 10 | profiles: [], 11 | roles: [] 12 | }; 13 | const parser = new ObjectParser({ 14 | input: data, 15 | onError: onError && (e => onError(`Error while parsing Credits: ${e.toString()}`)) 16 | }); 17 | parser.prop('profiles').array(item => parsed.profiles.push(parseProfile(item))); 18 | parser.prop('roles').array(item => parsed.roles.push(parseRole(item))); 19 | return parsed; 20 | } 21 | 22 | function parseRole(parser: IObjectParserProp): CreditsDataRole { 23 | const parsed: CreditsDataRole = { 24 | name: '', 25 | color: '', 26 | description: '', 27 | noCategory: false 28 | }; 29 | parser.prop('name', v => parsed.name = str(v)); 30 | parser.prop('color', v => parsed.color = str(v), true); // @TODO Validate Colors? 31 | parser.prop('description', v => parsed.description = str(v), true); 32 | parser.prop('noCategory', v => parsed.noCategory = !!v, true); 33 | return parsed; 34 | } 35 | 36 | function parseProfile(parser: IObjectParserProp): CreditsDataProfile { 37 | const parsed: CreditsDataProfile = { 38 | title: '', 39 | roles: [], 40 | note: undefined, 41 | icon: undefined, 42 | topRole: undefined 43 | }; 44 | parser.prop('title', v => parsed.title = str(v)); 45 | parser.prop('icon', v => parsed.icon = str(v), true); 46 | parser.prop('note', v => parsed.note = str(v), true); 47 | parser.prop('roles', true).arrayRaw(role => parsed.roles.push(str(role))); 48 | parser.prop('topRole', v => parsed.topRole = str(v), true); 49 | return parsed; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/renderer/credits/types.ts: -------------------------------------------------------------------------------- 1 | /** Data contained inside the Credits file. */ 2 | export type CreditsData = { 3 | /** Order for roles to appear in */ 4 | roles: CreditsDataRole[]; 5 | /** Profiles of each person in the credits. */ 6 | profiles: CreditsDataProfile[]; 7 | }; 8 | 9 | export type CreditsBlock = { 10 | role: CreditsDataRole; 11 | profiles: CreditsDataProfile[]; 12 | } 13 | 14 | export type CreditsDataRole = { 15 | /** Role name */ 16 | name: string; 17 | /** Hex color code of Role */ 18 | color?: string; 19 | /** Description of role */ 20 | description?: string; 21 | /** Do not categorize this role */ 22 | noCategory?: boolean; 23 | } 24 | 25 | export type CreditsDataProfile = { 26 | /** Title of the profile (their displayed name). */ 27 | title: string; 28 | /** Roles of the profile (in the Discord server). */ 29 | roles: string[]; 30 | /** Note about the profile (additional text to display). */ 31 | note?: string; 32 | /** Icon of the profile (Base64 encoded image). */ 33 | icon?: string; 34 | /** Role name to use as a category (override) */ 35 | topRole?: string; 36 | }; 37 | -------------------------------------------------------------------------------- /src/renderer/curate/util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Log function for the 'Curation' heading 3 | * 4 | * @param content Log content 5 | */ 6 | export function curationLog(content: string): void { 7 | log.info('Curation', content); 8 | } 9 | -------------------------------------------------------------------------------- /src/renderer/hooks/search.ts: -------------------------------------------------------------------------------- 1 | import { useLocation } from 'react-router-dom'; 2 | import { getViewName } from '@renderer/Util'; 3 | import { useAppSelector } from '@renderer/hooks/useAppSelector'; 4 | 5 | export function useView() { 6 | const location = useLocation(); 7 | const viewName = getViewName(location.pathname); 8 | const search = useAppSelector((state) => state.search); 9 | return search.views[viewName]; 10 | } 11 | -------------------------------------------------------------------------------- /src/renderer/hooks/useAppSelector.ts: -------------------------------------------------------------------------------- 1 | import { RootState } from '@renderer/store/store'; 2 | import { useSelector } from 'react-redux'; 3 | 4 | export function useAppSelector(func: (state: RootState) => T) { 5 | return useSelector((state: RootState) => func(state)); 6 | } 7 | -------------------------------------------------------------------------------- /src/renderer/hooks/usePreferences.ts: -------------------------------------------------------------------------------- 1 | import { PreferencesContext } from "@renderer/context/PreferencesContext"; 2 | import { AppPreferencesData } from "flashpoint-launcher"; 3 | import { useContext } from "react"; 4 | 5 | export function usePreferences(): AppPreferencesData { 6 | const preferences = useContext(PreferencesContext); 7 | return preferences; 8 | } -------------------------------------------------------------------------------- /src/renderer/hooks/useStateRef.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch, Reducer, useReducer } from 'react'; 2 | 3 | export type StateRef = { 4 | /** Current value (when this was created). */ 5 | value: T; 6 | /** Reference to the most recent value. */ 7 | ref: Ref; 8 | } 9 | 10 | type Ref = { 11 | current: T; 12 | } 13 | 14 | /** 15 | * Stores state and a ref to the most recent state. 16 | * Useful when you need the most recent state in callbacks without having to recreate them. 17 | * 18 | * @param initial Initial state 19 | */ 20 | export function useStateRef(initial: T): [StateRef, Dispatch] { 21 | return useReducer, T>, T>(reducer, initial, initializer); 22 | } 23 | 24 | function reducer(prevState: StateRef, action: T): StateRef { 25 | prevState.ref.current = action; 26 | 27 | return { 28 | value: action, 29 | ref: prevState.ref, 30 | }; 31 | } 32 | 33 | function initializer(arg: T): StateRef { 34 | return { 35 | value: arg, 36 | ref: { current: arg }, 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /src/renderer/hooks/useThrottle.ts: -------------------------------------------------------------------------------- 1 | import { AnyFunction } from '@shared/interfaces'; 2 | import * as React from 'react'; 3 | 4 | type ThrottleRef = { 5 | /** Timeout of the currently throttled call. */ 6 | timeout: NodeJS.Timeout | number | undefined; 7 | /** Callback to call once the time is out. */ 8 | callback: AnyFunction; 9 | /** Function to call in order to later call "callback". */ 10 | fn: (fn: AnyFunction) => void; 11 | } 12 | 13 | export function useThrottle(time: number): ThrottleRef['fn'] { 14 | const ref = React.useRef(undefined as any); 15 | 16 | if (!ref.current) { 17 | ref.current = { 18 | timeout: undefined, 19 | callback: () => {}, 20 | fn: (callback: AnyFunction) => { 21 | if (ref.current.timeout !== undefined) { return; } 22 | callback(); 23 | 24 | ref.current.timeout = setTimeout(function() { 25 | ref.current.timeout = undefined; 26 | }, time); 27 | }, 28 | }; 29 | } 30 | 31 | return ref.current.fn; 32 | } 33 | 34 | export function useDelayedThrottle(time: number): ThrottleRef['fn'] { 35 | const ref = React.useRef(undefined as any); 36 | 37 | if (!ref.current) { 38 | ref.current = { 39 | timeout: undefined, 40 | callback: () => {}, 41 | fn: (callback: AnyFunction) => { 42 | ref.current.callback = callback; 43 | 44 | if (ref.current.timeout !== undefined) { return; } 45 | 46 | ref.current.timeout = setTimeout(function() { 47 | ref.current.timeout = undefined; 48 | ref.current.callback(); 49 | }, time); 50 | }, 51 | }; 52 | } 53 | 54 | return ref.current.fn; 55 | } 56 | -------------------------------------------------------------------------------- /src/renderer/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { ViewGame } from 'flashpoint-launcher'; 2 | 3 | export type ViewGameSet = Record 4 | 5 | /** State of a single "stage" in the upgrade system (each individual downloadable upgrade is called a "stage"). */ 6 | export type UpgradeStageState = { 7 | /** If the stage was already installed when the launcher started up (this value is only meaningful if the stage checks are done). */ 8 | alreadyInstalled: boolean; 9 | /** If the checks has been performed (this is to check if the stage has already been installed). */ 10 | checksDone: boolean; 11 | /** Whether the stage is up to date (checked via hash comparison) */ 12 | upToDate: boolean; 13 | /** If the stage is currently being downloaded or installed. */ 14 | isInstalling: boolean; 15 | /** If the stage was installed during this session (this is so the user can be told to restart the application). */ 16 | isInstallationComplete: boolean; 17 | /** Current progress note of the installation (visible text meant to inform the user about the current progress of the download or install). */ 18 | installProgressNote: string; 19 | } 20 | 21 | /** Update the range of pages that are visible in the visible and buffered area. */ 22 | export type UpdateView = (start: number, count: number) => void; 23 | -------------------------------------------------------------------------------- /src/renderer/logger.tsx: -------------------------------------------------------------------------------- 1 | import { BackOut } from '@shared/back/types'; 2 | import { LogLevel } from '@shared/Log/interface'; 3 | import { ConnectedRouter } from 'connected-react-router'; 4 | import { createMemoryHistory } from 'history'; 5 | import * as ReactDOM from 'react-dom'; 6 | import { Provider } from 'react-redux'; 7 | import { ConnectedLogsPage } from './containers/ConnectedLogsPage'; 8 | import { PreferencesContextProvider } from './context/PreferencesContext'; 9 | import { LangContext } from './util/lang'; 10 | import { logFactory } from './util/logging'; 11 | import store from '@renderer/store/store'; 12 | 13 | (async () => { 14 | window.log = { 15 | trace: logFactory(LogLevel.TRACE, window.Shared.back), 16 | debug: logFactory(LogLevel.DEBUG, window.Shared.back), 17 | info: logFactory(LogLevel.INFO, window.Shared.back), 18 | warn: logFactory(LogLevel.WARN, window.Shared.back), 19 | error: logFactory(LogLevel.ERROR, window.Shared.back) 20 | }; 21 | // Toggle DevTools when CTRL+SHIFT+I is pressed 22 | window.addEventListener('keypress', (event) => { 23 | if (event.ctrlKey && event.shiftKey && event.code === 'KeyI') { 24 | window.Shared.toggleDevtools(); 25 | event.preventDefault(); 26 | } 27 | }); 28 | 29 | // Wait for the preferences and config to initialize 30 | await window.Shared.waitUntilInitialized(); 31 | 32 | // Create history 33 | const history = createMemoryHistory(); 34 | 35 | // Connect to backend 36 | window.Shared.back.register(BackOut.LOG_ENTRY_ADDED, (event, entry, index) => { 37 | window.Shared.log.entries[index - window.Shared.log.offset] = entry; 38 | }); 39 | 40 | // Render the application 41 | ReactDOM.render(( 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | ), document.getElementById('root')); 52 | })(); 53 | -------------------------------------------------------------------------------- /src/renderer/store/curate/middleware.ts: -------------------------------------------------------------------------------- 1 | import { BackIn } from '@shared/back/types'; 2 | import { startAppListening } from '@renderer/store/listenerMiddleware'; 3 | import { 4 | addPlatform, 5 | addTag, BaseCurateAction, changeGroup, createAddApp, 6 | editAddApp, 7 | editCurationMeta, regenUuid, 8 | removeAddApp, 9 | removePlatform, 10 | removeTag, setPrimaryPlatform, setWarnings 11 | } from '@renderer/store/curate/slice'; 12 | import { isAnyOf, PayloadAction } from '@reduxjs/toolkit'; 13 | import store from '@renderer/store/store'; 14 | 15 | export function addCurationMiddleware() { 16 | // Update warnings when curation changes 17 | startAppListening({ 18 | matcher: isAnyOf(editAddApp, createAddApp, removeAddApp, editCurationMeta, addTag, 19 | removeTag, addPlatform, removePlatform, setPrimaryPlatform, regenUuid, changeGroup), 20 | effect: async(action: PayloadAction, listenerApi)=> { 21 | const { curate } = listenerApi.getState(); 22 | const curation = curate.curations.find(c => c.folder === action.payload.folder); 23 | if (curation) { 24 | console.log(curation.folder); 25 | window.Shared.back.request(BackIn.CURATE_GEN_WARNINGS, { 26 | ...curation, 27 | contents: undefined, // Strip content tree since it's unused and huge 28 | }) 29 | .then((warnings) => { 30 | // Set new warnings 31 | store.dispatch(setWarnings({ 32 | folder: curation.folder, 33 | warnings, 34 | })); 35 | }); 36 | } 37 | } 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /src/renderer/store/fpfss/slice.ts: -------------------------------------------------------------------------------- 1 | import { FpfssState, FpfssUser } from '@shared/back/types'; 2 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 3 | import { Game } from 'flashpoint-launcher'; 4 | 5 | const initialState: FpfssState = { 6 | user: null, 7 | editingGame: null, 8 | }; 9 | 10 | const fpfssSlice = createSlice({ 11 | name: 'fpfss', 12 | initialState, 13 | reducers: { 14 | setUser(state: FpfssState, { payload }: PayloadAction) { 15 | state.user = payload; 16 | }, 17 | setGame(state: FpfssState, { payload }: PayloadAction) { 18 | state.editingGame = payload; 19 | }, 20 | applyGameDelta(state: FpfssState, { payload }: PayloadAction>) { 21 | if (state.editingGame) { 22 | state.editingGame = { 23 | ...state.editingGame, 24 | ...payload 25 | }; 26 | } 27 | } 28 | }, 29 | }); 30 | 31 | export const { actions: fpfssActions } = fpfssSlice; 32 | export const { 33 | setUser, 34 | setGame, 35 | applyGameDelta 36 | } = fpfssSlice.actions; 37 | export default fpfssSlice.reducer; 38 | -------------------------------------------------------------------------------- /src/renderer/store/listenerMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { createListenerMiddleware, addListener } from '@reduxjs/toolkit'; 2 | import type { TypedStartListening, TypedAddListener } from '@reduxjs/toolkit'; 3 | 4 | import type { RootState, AppDispatch } from './store'; 5 | 6 | export const listenerMiddleware = createListenerMiddleware(); 7 | 8 | export type AppStartListening = TypedStartListening; 9 | 10 | export const startAppListening = 11 | listenerMiddleware.startListening as AppStartListening; 12 | 13 | export const addAppListener = addListener as TypedAddListener< 14 | RootState, 15 | AppDispatch 16 | >; 17 | -------------------------------------------------------------------------------- /src/renderer/store/main/middleware.ts: -------------------------------------------------------------------------------- 1 | import { isAnyOf, PayloadAction } from '@reduxjs/toolkit'; 2 | import { startAppListening } from '@renderer/store/listenerMiddleware'; 3 | import { BackIn } from '@shared/back/types'; 4 | import { removePlaylistGame, RemovePlaylistGameAction, resolveDialog, ResolveDialogActionData } from './slice'; 5 | import store, { history } from '../store'; 6 | import { selectGame, selectPlaylist } from '../search/slice'; 7 | import { useView } from '@renderer/hooks/search'; 8 | import { getViewName } from '@renderer/Util'; 9 | 10 | export function addMainMiddleware() { 11 | // Send dialog state to event handlers after reducer has finished 12 | startAppListening({ 13 | matcher: isAnyOf(resolveDialog), 14 | effect: async(action: PayloadAction, listenerApi)=> { 15 | const { main } = listenerApi.getState(); 16 | if (main.lastResolvedDialog) { 17 | const dialog = main.lastResolvedDialog; 18 | window.Shared.back.send(BackIn.DIALOG_RESPONSE, dialog, action.payload.button); 19 | window.Shared.dialogResEvent.emit(dialog.id, dialog, action.payload.button); 20 | } 21 | } 22 | }); 23 | 24 | startAppListening({ 25 | matcher: isAnyOf(removePlaylistGame), 26 | effect: async(action: PayloadAction, listenerApi)=> { 27 | const { main } = listenerApi.getState(); 28 | const playlist = main.playlists.find(p => p.id === action.payload.playlistId); 29 | if (playlist) { 30 | const viewId = getViewName(history.location.pathname); 31 | store.dispatch(selectPlaylist({ 32 | view: viewId, 33 | playlist 34 | })); 35 | store.dispatch(selectGame({ 36 | view: viewId, 37 | game: undefined 38 | })); 39 | } 40 | } 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /src/renderer/store/reactKeybindCompat.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentType } from 'react'; 2 | import { IShortcutProviderRenderProps, useShortcut } from 'react-keybind'; 3 | 4 | export type WithShortcutProps = { 5 | shortcut: IShortcutProviderRenderProps; 6 | } 7 | 8 | export function withShortcut(Component: ComponentType) { 9 | return function WrappedComponent(props: Omit) { 10 | const shortcut = useShortcut(); 11 | return ; 12 | } 13 | } -------------------------------------------------------------------------------- /src/renderer/store/store.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import searchReducer from './search/slice'; 3 | import { listenerMiddleware } from './listenerMiddleware'; 4 | import { addSearchMiddleware } from './search/middleware'; 5 | import tagCategoriesReducer from './tagCategories/slice'; 6 | import mainReducer from './main/slice'; 7 | import fpfssReducer from './fpfss/slice'; 8 | import tasksReducer from './tasks/slice'; 9 | import curateReducer from './curate/slice'; 10 | import { addCurationMiddleware } from './curate/middleware'; 11 | import { createMemoryHistory } from 'history'; 12 | import { connectRouter, routerMiddleware } from 'connected-react-router'; 13 | import { addMainMiddleware } from './main/middleware'; 14 | 15 | export const history = createMemoryHistory(); 16 | 17 | // Initialize all store middleware 18 | addSearchMiddleware(); 19 | addCurationMiddleware(); 20 | addMainMiddleware(); 21 | 22 | // Create store 23 | export const store = configureStore({ 24 | reducer: { 25 | router: connectRouter(history) as any, 26 | curate: curateReducer, 27 | fpfss: fpfssReducer, 28 | main: mainReducer, 29 | search: searchReducer, 30 | tagCategories: tagCategoriesReducer, 31 | tasks: tasksReducer, 32 | }, 33 | middleware: (getDefaultMiddleware) => { 34 | const middleware = getDefaultMiddleware(); 35 | middleware.push(listenerMiddleware.middleware); 36 | middleware.push(routerMiddleware(history) as any); 37 | return middleware; 38 | } 39 | }); 40 | 41 | // Create typings for the store 42 | export type RootState = ReturnType; 43 | export type AppDispatch = typeof store.dispatch; 44 | export default store; 45 | -------------------------------------------------------------------------------- /src/renderer/store/tagCategories/slice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | import { TagCategory } from 'flashpoint-launcher'; 3 | 4 | const initialState: TagCategory[] = []; 5 | 6 | const tagCategoriesSlice = createSlice({ 7 | name: 'tagCategories', 8 | initialState, 9 | reducers: { 10 | setTagCategories(state: TagCategory[], { payload }: PayloadAction) { 11 | return payload; 12 | } 13 | }, 14 | }); 15 | 16 | export const { actions: tagCategoriesActions } = tagCategoriesSlice; 17 | export const { setTagCategories } = tagCategoriesSlice.actions; 18 | 19 | export default tagCategoriesSlice.reducer; 20 | 21 | -------------------------------------------------------------------------------- /src/renderer/store/tasks/slice.ts: -------------------------------------------------------------------------------- 1 | import { Task } from '@shared/interfaces'; 2 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 3 | 4 | const initialState: Task[] = []; 5 | 6 | const tasksSlice = createSlice({ 7 | name: 'tasks', 8 | initialState, 9 | reducers: { 10 | addTask(state: Task[], { payload }: PayloadAction) { 11 | const taskIdx = state.findIndex(t => t.id === payload.id); 12 | if (taskIdx > -1) { 13 | log.error('Launcher', 'Illegal Action: addTask - ID Collision'); 14 | return; 15 | } 16 | state.push(payload); 17 | }, 18 | setTask(state: Task[], { payload }: PayloadAction>) { 19 | if (payload.id) { 20 | const taskIdx = state.findIndex(t => t.id === payload.id); 21 | if (taskIdx > -1) { 22 | state[taskIdx] = { 23 | ...state[taskIdx], 24 | ...payload, 25 | }; 26 | } 27 | } 28 | } 29 | }, 30 | }); 31 | 32 | export const { actions: tasksActions } = tasksSlice; 33 | export const { addTask, setTask } = tasksSlice.actions; 34 | export default tasksSlice.reducer; 35 | -------------------------------------------------------------------------------- /src/renderer/upgrade/types.ts: -------------------------------------------------------------------------------- 1 | import { UpgradeStageState } from '../interfaces'; 2 | 3 | export type UpgradeStage = { 4 | id: string; 5 | title: string; 6 | description: string; 7 | /** Paths of files that should exist if the stage is "installed" (paths are relative to the flashpoint root) */ 8 | verify_files: string[]; 9 | /** SHA256 sums of the verifiable files */ 10 | verify_sha256: string[]; 11 | /** URLs from where the stage can be downloaded (only one will be downloaded, the other are "backups") */ 12 | sources: string[]; 13 | /** SHA256 sum of the source file */ 14 | sources_sha256: string; 15 | /** Paths to delete from Install Path before installation / extraction */ 16 | deletePaths: string[]; 17 | /** Paths to ignore from the Install Path when installing / extracting */ 18 | keepPaths: string[]; 19 | /** State of this upgrade stage */ 20 | state: UpgradeStageState; 21 | }; 22 | -------------------------------------------------------------------------------- /src/renderer/util/SevenZip.ts: -------------------------------------------------------------------------------- 1 | import * as remote from '@electron/remote'; 2 | import * as path from 'path'; 3 | 4 | function get7zExec(): string { 5 | const basePath = window.Shared.isDev ? process.cwd() : path.dirname(remote.app.getPath('exe')); 6 | switch (process.platform) { 7 | case 'darwin': 8 | return path.join(basePath, 'extern/7zip-bin/mac', '7za'); 9 | case 'win32': 10 | return path.join(basePath, 'extern/7zip-bin/win', process.arch, '7za'); 11 | case 'linux': 12 | return path.join(basePath, 'extern/7zip-bin/linux', process.arch, '7za'); 13 | } 14 | return '7za'; 15 | } 16 | 17 | export const pathTo7z = get7zExec(); 18 | -------------------------------------------------------------------------------- /src/renderer/util/async.ts: -------------------------------------------------------------------------------- 1 | import { Game } from 'flashpoint-launcher'; 2 | import { BackIn } from '@shared/back/types'; 3 | 4 | type DoAsyncFunction = (done: () => void) => void; 5 | 6 | /** 7 | * Call several functions in parallel, then resolve the promise once all are done 8 | * 9 | * @param calls Functions to call in parallel, they are considered "done" once the "done" callback parameter has been called 10 | */ 11 | export function doAsyncParallel(calls: Array): Promise { 12 | return new Promise((resolve, reject) => { 13 | try { 14 | let callsLeft = calls.length; 15 | for (let i = 0; i < calls.length; i++) { 16 | let hasCalledDone = false; 17 | calls[i](() => { 18 | if (!hasCalledDone) { 19 | hasCalledDone = true; 20 | callsLeft -= 1; 21 | if (callsLeft <= 0) { resolve(); } 22 | } 23 | else { console.warn('You should not call the same done() functions multiple times.'); } 24 | }); 25 | } 26 | } catch (e) { reject(e); } 27 | }); 28 | } 29 | 30 | export async function idToGame(gameId: string): Promise { 31 | return window.Shared.back.request(BackIn.GET_GAME, gameId); 32 | } 33 | -------------------------------------------------------------------------------- /src/renderer/util/lang.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { getDefaultLocalization } from '@shared/lang'; 3 | 4 | export const LangContext = React.createContext(getDefaultLocalization()); 5 | -------------------------------------------------------------------------------- /src/renderer/util/logging.ts: -------------------------------------------------------------------------------- 1 | import { SocketClient } from '@shared/back/SocketClient'; 2 | import { BackIn } from '@shared/back/types'; 3 | import { LogFunc } from '@shared/interfaces'; 4 | import { LogLevel } from '@shared/Log/interface'; 5 | 6 | export function logFactory(logLevel: LogLevel, socketServer: SocketClient): LogFunc { 7 | return function (source: string, content: string) { 8 | socketServer.send(BackIn.ADD_LOG, { 9 | source: source, 10 | content: content, 11 | logLevel: logLevel 12 | }); 13 | // @TODO : Log Frontend Console 14 | // @TODO : Send this from back somehow instead 15 | return { 16 | source: source, 17 | content: content, 18 | timestamp: Date.now(), 19 | logLevel: logLevel 20 | }; 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/util/queue.ts: -------------------------------------------------------------------------------- 1 | type QueueOneWrapper Promise> = (...args: Parameters) => void 2 | 3 | /** 4 | * Ensures that the wrapped function can not be called until its returned promise is pending. 5 | * If the wrapper is called before that and there is no call queued, 6 | * the call will be queued and executed when the promise resolves or rejects. 7 | * If there is a call queued then it will replace the currently queued call (hence "queueOne"). 8 | * 9 | * @param fn Function to wrap. 10 | * @returns Wrapper function. 11 | */ 12 | export function queueOne Promise>(fn: T): QueueOneWrapper { 13 | /** If it is waiting for the wrapped function to resolve / reject. */ 14 | let isBusy = false; 15 | /** The most recent arguments this was called with while busy. */ 16 | let queued: Parameters | undefined; 17 | 18 | const callback: QueueOneWrapper = (...args) => { 19 | if (isBusy) { 20 | queued = args; 21 | } else { 22 | isBusy = true; 23 | 24 | fn(...args) 25 | .catch(error => { 26 | console.error('queueOne - Error thrown in wrapped function!', error); 27 | }) 28 | .finally(() => { 29 | isBusy = false; 30 | 31 | if (queued) { 32 | const next = queued; 33 | queued = undefined; 34 | callback(...next); 35 | } 36 | }); 37 | } 38 | }; 39 | 40 | return callback; 41 | } 42 | -------------------------------------------------------------------------------- /src/shared/BrowsePageLayout.ts: -------------------------------------------------------------------------------- 1 | /** Modes for displaying the game collection at the BrowsePage */ 2 | export enum BrowsePageLayout { 3 | /** Games are in a vertical list, one game per row */ 4 | list = 0, 5 | /** Games are in a table-like grid, each cell is a game */ 6 | grid = 1, 7 | } 8 | 9 | export enum ScreenshotPreviewMode { 10 | OFF = 0, 11 | ON = 1, 12 | ALWAYS = 2 13 | } 14 | 15 | /** BrowsePageLayout in string form */ 16 | export type BrowsePageLayoutString = 'list' | 'grid'; 17 | 18 | /** 19 | * Convert a BrowsePageLayout value to a string (returns undefined if value is invalid) 20 | * 21 | * @param layout Browse page layout type (list or grid) 22 | */ 23 | export function stringifyBrowsePageLayout(layout: BrowsePageLayout): BrowsePageLayoutString|undefined { 24 | switch (layout) { 25 | case BrowsePageLayout.list: return 'list'; 26 | case BrowsePageLayout.grid: return 'grid'; 27 | } 28 | return undefined; 29 | } 30 | 31 | /** 32 | * Convert a string to a BrowsePageLayout value (returns undefined if string is invalid) 33 | * 34 | * @param str 'list' or 'grid' 35 | */ 36 | export function parseBrowsePageLayout(str: string): BrowsePageLayout|undefined { 37 | switch (str) { 38 | case 'list': return BrowsePageLayout.list; 39 | case 'grid': return BrowsePageLayout.grid; 40 | } 41 | return undefined; 42 | } 43 | -------------------------------------------------------------------------------- /src/shared/IPC.ts: -------------------------------------------------------------------------------- 1 | /** Channel to send the "initialize renderer" message over. */ 2 | export const InitRendererChannel = 'renderer-init'; 3 | 4 | /** Message contents for the "initialize renderer" message. */ 5 | export type InitRendererData = { 6 | isBackRemote: boolean; 7 | installed: boolean; 8 | version: number; 9 | host: string; 10 | secret: string; 11 | url?: string; 12 | } 13 | 14 | export const FlashInitChannel = 'renderer-flash-init'; 15 | 16 | export type FlashInitData = { 17 | entry: string; 18 | } 19 | -------------------------------------------------------------------------------- /src/shared/Log/interface.ts: -------------------------------------------------------------------------------- 1 | export enum LogLevel { 2 | TRACE, 3 | DEBUG, 4 | INFO, 5 | WARN, 6 | ERROR, 7 | SILENT, 8 | } 9 | 10 | /** A log entry _before_ it is added to the main log */ 11 | export type ILogPreEntry = { 12 | /** Name of the source of the log entry (name of what added the log entry) */ 13 | source: string; 14 | /** Content of the log entry */ 15 | content: string; 16 | } 17 | 18 | /** A log entry from the main log */ 19 | export type ILogEntry = ILogPreEntry & { 20 | /** Timestamp of when the entry was added to the main's log */ 21 | timestamp: number; 22 | /** Level of the log, 0-5, Trace, Info, Warn, Error, Fatal */ 23 | logLevel: LogLevel; 24 | } 25 | -------------------------------------------------------------------------------- /src/shared/Paths.ts: -------------------------------------------------------------------------------- 1 | /** URL Paths of the different pages */ 2 | export enum Paths { 3 | HOME = '/', 4 | BROWSE = '/browse', 5 | TAGS = '/tags', 6 | CATEGORIES = '/categories', 7 | LOGS = '/logs', 8 | MANUAL = '/manual', 9 | CONFIG = '/config', 10 | ABOUT = '/about', 11 | CURATE = '/curate', 12 | DEVELOPER = '/developer', 13 | LOADING = '/loading', 14 | DOWNLOADS = '/downloads', 15 | } 16 | -------------------------------------------------------------------------------- /src/shared/Theme.ts: -------------------------------------------------------------------------------- 1 | import { ITheme } from './ThemeFile'; 2 | import { getFileServerURL } from './Util'; 3 | 4 | /** 5 | * Element attribute used exclusively on the "global" theme element. 6 | * This is to make it searchable in the DOM tree. 7 | * (Custom HTML element attributes should start with "data-") 8 | */ 9 | const globalThemeAttribute = 'data-theme'; 10 | 11 | /** 12 | * Set the theme data of the "global" theme style element. 13 | * 14 | * @param theme Theme to apply on top of the default 15 | */ 16 | export function setTheme(theme: ITheme | undefined): void { 17 | let element = findThemeGlobal(); 18 | if (!element) { 19 | element = createThemeElement(); 20 | element.setAttribute(globalThemeAttribute, 'true'); 21 | if (document.head) { document.head.appendChild(element); } 22 | } 23 | if (theme) { element.setAttribute('href', `${getFileServerURL()}/Themes/${theme.id}/${theme.entryPath}`); } 24 | else { element.removeAttribute('href'); } 25 | } 26 | 27 | /** Find the "global" theme style element. */ 28 | function findThemeGlobal(): HTMLElement | undefined { 29 | // Go through all children of 30 | if (document.head) { 31 | const children = document.head.children; 32 | for (let i = children.length; i >= 0; i--) { 33 | const child = children.item(i) as HTMLElement; 34 | if (child) { 35 | // Check if the child has the unique "global theme element" attribute 36 | const attribute = child.getAttribute(globalThemeAttribute); 37 | if (attribute) { return child; } 38 | } 39 | } 40 | } 41 | } 42 | 43 | /** Create an element that themes can be "applied" to. */ 44 | function createThemeElement(): HTMLElement { 45 | const element = document.createElement('link'); 46 | element.setAttribute('type', 'text/css'); 47 | element.setAttribute('rel', 'stylesheet'); 48 | return element; 49 | } 50 | -------------------------------------------------------------------------------- /src/shared/config/interfaces.ts: -------------------------------------------------------------------------------- 1 | export { AppConfigData } from 'flashpoint-launcher'; 2 | 3 | export type AppExtConfigData = { 4 | [key: string]: any; 5 | } 6 | -------------------------------------------------------------------------------- /src/shared/constants.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | /** Title of the main window. */ 4 | export const APP_TITLE = 'Flashpoint Launcher'; 5 | 6 | // Flashpoint libraries 7 | export const ARCADE = 'arcade'; 8 | export const THEATRE = 'theatre'; 9 | 10 | // Names of the image sub-folders 11 | export const LOGOS = 'Logos'; 12 | export const SCREENSHOTS = 'Screenshots'; 13 | 14 | /** Games to fetch in each block */ 15 | export const VIEW_PAGE_SIZE = 250; 16 | 17 | export const CURATIONS_FOLDER = 'Curations'; // @TODO Replace this with a configurable path (in the config, just like the other paths) 18 | export const CURATIONS_FOLDER_EXTRACTING = path.join(CURATIONS_FOLDER, 'Extracting'); 19 | export const CURATIONS_FOLDER_WORKING = path.join(CURATIONS_FOLDER, 'Working'); 20 | export const CURATIONS_FOLDER_TEMP = path.join(CURATIONS_FOLDER, '.temp'); 21 | export const CURATIONS_FOLDER_EXPORTED = path.join(CURATIONS_FOLDER, 'Exported'); 22 | 23 | 24 | /** Valid curation meta filenames (case insensitive). */ 25 | export const CURATION_META_FILENAMES = ['meta.txt', 'meta.yaml', 'meta.yml']; 26 | -------------------------------------------------------------------------------- /src/shared/curate/defaultValues.ts: -------------------------------------------------------------------------------- 1 | /** Container of all default values for curation game meta. */ 2 | export type GameMetaDefaults = { 3 | /** Default application paths (ordered after each platform). */ 4 | appPaths: { [platform: string]: string; }; 5 | language: string; 6 | platform: string; 7 | playMode: string; 8 | status: string; 9 | library: string; 10 | }; 11 | -------------------------------------------------------------------------------- /src/shared/curate/fpfss.ts: -------------------------------------------------------------------------------- 1 | export const FPFSS_INFO_FILENAME = 'fpfss.info'; 2 | -------------------------------------------------------------------------------- /src/shared/curate/parse.ts: -------------------------------------------------------------------------------- 1 | import { EditAddAppCurationMeta, EditCurationMeta } from './OLD_types'; 2 | 3 | /** Return value type of the parseCurationMeta function. */ 4 | export type ParsedCurationMeta = { 5 | /** Meta data of the game. */ 6 | game: EditCurationMeta; 7 | /** Meta data of the additional applications. */ 8 | addApps: EditAddAppCurationMeta[]; 9 | }; 10 | 11 | export function generateExtrasAddApp(folderName: string) : EditAddAppCurationMeta { 12 | return { 13 | heading: 'Extras', 14 | applicationPath: ':extras:', 15 | launchCommand: folderName 16 | }; 17 | } 18 | 19 | export function generateMessageAddApp(message: string) : EditAddAppCurationMeta { 20 | return { 21 | heading: 'Message', 22 | applicationPath: ':message:', 23 | launchCommand: message 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/shared/curate/types.ts: -------------------------------------------------------------------------------- 1 | import { Platform, Tag } from 'flashpoint-launcher'; 2 | 3 | export type ContentTree = { 4 | // Root node - 'content' folder 5 | root: ContentTreeNode; 6 | } 7 | 8 | export type ContentTreeNode = { 9 | name: string; 10 | /** Frontend - Whether this is expanded in the content tree view */ 11 | expanded: boolean; 12 | /** File size (if type is file) */ 13 | size?: number; 14 | nodeType: 'file' | 'directory' | string; 15 | /** Immediate items below this node */ 16 | children: ContentTreeNode[]; 17 | /** Number of items below this node */ 18 | count: number; 19 | } 20 | 21 | export type CurationMeta = Partial<{ 22 | // Game fields 23 | title: string; 24 | alternateTitles: string; 25 | series: string; 26 | developer: string; 27 | publisher: string; 28 | status: string; 29 | extreme: boolean; 30 | tags: Tag[]; 31 | source: string; 32 | launchCommand: string; 33 | library: string; 34 | notes: string; 35 | curationNotes: string; 36 | primaryPlatform: string; 37 | platforms: Platform[]; 38 | applicationPath: string; 39 | playMode: string; 40 | releaseDate: string; 41 | version: string; 42 | originalDescription: string; 43 | language: string; 44 | mountParameters: string; 45 | ruffleSupport: string; 46 | }> 47 | 48 | 49 | 50 | export type AddAppCurationMeta = Partial<{ 51 | heading: string; 52 | applicationPath: string; 53 | launchCommand: string; 54 | }> 55 | 56 | export type AddAppCuration = {key: string} & AddAppCurationMeta; 57 | 58 | export type PlatformAppPathSuggestions = Record; 59 | 60 | export type PlatformAppPath = { 61 | appPath: string; 62 | count: number; 63 | } 64 | -------------------------------------------------------------------------------- /src/shared/eventResponseDebouncer.ts: -------------------------------------------------------------------------------- 1 | export type EventResponseDebouncer = { 2 | dispatch: (fired: Promise, cb: (event: T) => any) => void; 3 | invalidate: () => void; 4 | } 5 | 6 | export function eventResponseDebouncerFactory(): EventResponseDebouncer { 7 | let lastId = 0; 8 | 9 | return { 10 | dispatch: async (fired, cb) => { 11 | lastId++; 12 | const thisId = lastId; 13 | const resp = await fired; 14 | // Only fire callback if this was the last request 15 | if (thisId === lastId) { 16 | cb(resp); 17 | } 18 | }, 19 | invalidate: () => { 20 | lastId++; 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/shared/game/util.ts: -------------------------------------------------------------------------------- 1 | import { newGame } from '@shared/utils/misc'; 2 | import { AdditionalApp, Game } from 'flashpoint-launcher'; 3 | 4 | export namespace ModelUtils { 5 | export function createGame(): Game { 6 | const game = newGame(); 7 | Object.assign(game, { 8 | id: '', 9 | title: '', 10 | alternateTitles: '', 11 | series: '', 12 | developer: '', 13 | publisher: '', 14 | platform: '', 15 | dateAdded: new Date().toISOString(), 16 | dateModified: new Date().toISOString(), 17 | broken: false, 18 | extreme: false, 19 | playMode: '', 20 | status: '', 21 | notes: '', 22 | tags: [], 23 | tagsStr: '', 24 | source: '', 25 | applicationPath: '', 26 | launchCommand: '', 27 | releaseDate: '', 28 | version: '', 29 | originalDescription: '', 30 | language: '', 31 | library: '', 32 | orderTitle: '', 33 | addApps: [], 34 | placeholder: false, 35 | activeDataOnDisk: false 36 | }); 37 | return game; 38 | } 39 | 40 | export function createAddApp(game: Game): AdditionalApp { 41 | return { 42 | id: '', 43 | parentGameId: game.id, 44 | applicationPath: '', 45 | autoRunBefore: false, 46 | launchCommand: '', 47 | name: '', 48 | waitForExit: false, 49 | }; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/shared/legacy/misc.ts: -------------------------------------------------------------------------------- 1 | import { Legacy_ErrorCopy } from './types'; 2 | 3 | /** 4 | * Copy properties from an error to a new object. 5 | * 6 | * @param error Error to copy 7 | */ 8 | export function Legacy_errorCopy(error: any): Legacy_ErrorCopy { 9 | if (typeof error !== 'object' || error === null) { error = {}; } 10 | const copy: Legacy_ErrorCopy = { 11 | message: error.message+'', 12 | name: error.name+'', 13 | }; 14 | // @TODO These properties are not standard, and perhaps they have different types in different environments. 15 | // So do some testing and add some extra checks mby? 16 | if (typeof error.columnNumber === 'number') { copy.columnNumber = error.columnNumber; } 17 | if (typeof error.fileName === 'string') { copy.fileName = error.fileName; } 18 | if (typeof error.lineNumber === 'number') { copy.lineNumber = error.lineNumber; } 19 | if (typeof error.stack === 'string') { copy.stack = error.stack; } 20 | return copy; 21 | } 22 | -------------------------------------------------------------------------------- /src/shared/legacy/types.ts: -------------------------------------------------------------------------------- 1 | 2 | export type Legacy_ErrorCopy = { 3 | columnNumber?: number; 4 | fileName?: string; 5 | lineNumber?: number; 6 | message: string; 7 | name: string; 8 | stack?: string; 9 | } 10 | 11 | 12 | export type Legacy_LoadPlatformError = Legacy_ErrorCopy & { 13 | /** File path of the platform file the error is related to. */ 14 | filePath: string; 15 | } 16 | -------------------------------------------------------------------------------- /src/shared/library/util.ts: -------------------------------------------------------------------------------- 1 | import { GameOrderBy, GameOrderReverse } from 'flashpoint-launcher'; 2 | import { LangContainer } from '../lang'; 3 | 4 | /** 5 | * Get the title of a library item from a language sub-container (or return the item's route if none was found). 6 | * 7 | * @param library Library ID 8 | * @param lang Language sub-container to look for title in. 9 | */ 10 | export function getLibraryItemTitle(library: string, lang?: LangContainer['libraries']): string { 11 | return lang && lang[library] || library; 12 | } 13 | 14 | export type ViewQuery = { 15 | /** Query string. */ 16 | text: string; 17 | /** The field to order the games by. */ 18 | orderBy: GameOrderBy; 19 | /** The way to order the games. */ 20 | orderReverse: GameOrderReverse; 21 | /** Playlist to search */ 22 | playlistId?: string; 23 | /** If extreme games are included. */ 24 | extreme: boolean; 25 | } 26 | -------------------------------------------------------------------------------- /src/shared/memoize.ts: -------------------------------------------------------------------------------- 1 | import { AnyFunction, ArgumentTypesOf, ReturnTypeOf } from './interfaces'; 2 | import { shallowStrictEquals } from './Util'; 3 | 4 | /** Callable object, A is the arguments, R is the return value */ 5 | interface Callable extends Function { 6 | (...args: A): R; 7 | } 8 | 9 | /** A short-hand for a callable function that has the same argument and return types as a "normal" function */ 10 | type CallableWrap = Callable, ReturnTypeOf>; 11 | 12 | type EqualsCheck = (newArgs: T, prevArgs: T) => boolean; 13 | 14 | /** 15 | * Memoize a function with a cache size of one (only store the last return value) 16 | * Note: This does not make copies of the arguments when caching them, you have to do that yourself beforehand 17 | * 18 | * @param func Function to memoize 19 | * @param equalsFunc Function that compares the the new and previous arguments 20 | * @returns Memoized function 21 | */ 22 | export function memoizeOne(func: T, equalsFunc: EqualsCheck> = defaultEqualsFunc): CallableWrap { 23 | let prevArgs: ArgumentTypesOf; 24 | let prevReturn: ReturnTypeOf; 25 | let firstCall = true; 26 | 27 | const memo: CallableWrap = (...args) => { 28 | // Figure out if the function has to be called or if the previous return value should be used 29 | let doRefresh = false; 30 | if (firstCall || !equalsFunc(args, prevArgs)) { 31 | doRefresh = true; 32 | } 33 | firstCall = false; 34 | // Refresh return value if necessary 35 | if (doRefresh) { 36 | prevArgs = Object.assign([], args); 37 | prevReturn = func(...args); 38 | } 39 | return prevReturn; 40 | }; 41 | 42 | return memo; 43 | } 44 | 45 | /** 46 | * Default function used to compare arguments 47 | * 48 | * @param newArgs First to compare 49 | * @param prevArgs Second to compare 50 | */ 51 | function defaultEqualsFunc(newArgs: T, prevArgs: T): boolean { 52 | return newArgs.length === prevArgs.length && 53 | shallowStrictEquals(newArgs, prevArgs); 54 | } 55 | -------------------------------------------------------------------------------- /src/shared/order/util.ts: -------------------------------------------------------------------------------- 1 | import { GameOrderBy, GameOrderReverse } from 'flashpoint-launcher'; 2 | 3 | /** An array with all valid values of GameOrderBy */ 4 | export const gameOrderByOptions: GameOrderBy[] = [ 'title', 'developer', 'publisher', 'series', 'platform', 'dateAdded', 'dateModified', 'releaseDate', 'lastPlayed', 'playtime' ]; 5 | 6 | /** An array with all valid values of GameOrderReverse */ 7 | export const gameOrderReverseOptions: GameOrderReverse[] = [ 'ASC', 'DESC' ]; 8 | -------------------------------------------------------------------------------- /src/shared/socket/types.ts: -------------------------------------------------------------------------------- 1 | export type SocketTemplate any; }> = { 2 | [key in keyof U]: U[key]; 3 | } 4 | 5 | /** Data of a websocket request message. */ 6 | export type SocketRequestData = { 7 | /** Unique ID of the message (undefined if not tracked). */ 8 | id?: number; 9 | /** Type of message (determines what callback to use). */ 10 | type: any; 11 | /** Arguments to call the callback with. */ 12 | args: any[]; 13 | } 14 | 15 | /** Data of a websocket response message. */ 16 | export type SocketResponseData = SocketResponseData_Error | SocketResponseData_Result; 17 | 18 | export type SocketResponseData_Error = { 19 | /** Unique ID of the message. */ 20 | id: number; 21 | /** Arguments to call the callback with. */ 22 | error: any; 23 | } 24 | 25 | export type SocketResponseData_Result = { 26 | /** Unique ID of the message. */ 27 | id: number; 28 | /** Type of message (determines what callback to use). */ 29 | result: T; 30 | } 31 | 32 | export function isErrorResponse(variable: SocketResponseData): variable is SocketResponseData_Error { 33 | return Object.prototype.hasOwnProperty.call(variable, 'error'); 34 | } 35 | 36 | /** Minimal WebSocket interface. */ 37 | export interface BaseSocket { 38 | onclose: CB; 39 | onerror: CB; 40 | onmessage: CB; 41 | onopen: CB; 42 | send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void; 43 | close(code?: number, reason?: string): void; 44 | readyState: number; 45 | } 46 | type CB = ((ev: any) => any) | null; 47 | -------------------------------------------------------------------------------- /src/shared/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "../../build" 4 | }, 5 | "extends": "../../tsconfig.json", 6 | "include": [ 7 | "../../typings/**/*", 8 | "./**/*", "../database/entity/Game.ts", "../database/entity/AdditionalApp.ts" 9 | , "../renderer/util/curate.ts", "../renderer/changelog.ts" ] 10 | } 11 | -------------------------------------------------------------------------------- /src/shared/upgrade/util.ts: -------------------------------------------------------------------------------- 1 | import { LangContainer } from '../lang'; 2 | 3 | /** 4 | * Returns the localized string for an upgrade (Or the same string, if none is found) 5 | * 6 | * @param str String ID 7 | * @param lang Language container 8 | */ 9 | export function getUpgradeString(str: string, lang?: LangContainer['upgrades']) { 10 | return lang && lang[str] || str; 11 | } 12 | -------------------------------------------------------------------------------- /src/shared/utils/Coerce.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Coerce a value to a string. 3 | * If the value is undefined, an empty string is returned instead. 4 | * 5 | * @param value Value to coerce. 6 | */ 7 | export function str(value: any): string { 8 | return (value === undefined) 9 | ? '' 10 | : value + ''; 11 | } 12 | 13 | /** 14 | * Coerce a value to a string. 15 | * If the value is undefined, an empty string is returned instead. 16 | * 17 | * @param value Value to coerce. 18 | */ 19 | export function strArray(value: any[]): string[] { 20 | return value.map(val => str(val)); 21 | } 22 | 23 | /** 24 | * Coerce a value to a number. 25 | * If the coerced value is NaN, 0 will be returned instead. 26 | * 27 | * @param value Value to coerce. 28 | */ 29 | export function num(value: any): number { 30 | return (value * 1) || 0; 31 | } 32 | 33 | /** 34 | * Convert a string to a boolean (case insensitive). 35 | * 36 | * @param str String to convert ("true" and "yes" is true, "false" and "no" is false). 37 | * @param defaultVal Value returned if the string is neither true nor false. 38 | */ 39 | export function strToBool(str: string, defaultVal = false): boolean { 40 | const lowerStr = str.toLowerCase(); 41 | if (lowerStr === 'yes' || lowerStr === 'true') { return true; } 42 | if (lowerStr === 'no' || lowerStr === 'false') { return false; } 43 | return defaultVal; 44 | } 45 | -------------------------------------------------------------------------------- /src/shared/utils/Gate.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | 3 | export class Gate { 4 | public isOpen: boolean; 5 | private _listeners: EventEmitter; 6 | 7 | constructor() { 8 | this.isOpen = false; 9 | this._listeners = new EventEmitter(); 10 | } 11 | 12 | async wait(): Promise { 13 | if (!this.isOpen) { 14 | return new Promise((resolve) => { 15 | this._listeners.once('done', resolve); 16 | }); 17 | } 18 | } 19 | 20 | open() { 21 | this.isOpen = true; 22 | this._listeners.emit('done'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/shared/utils/StringFormatter.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { VariableStringOptions, splitVariableString } from './VariableString'; 3 | 4 | const opts: VariableStringOptions = { 5 | openChar: '{', 6 | closeChar: '}', 7 | }; 8 | 9 | type Arg = string | JSX.Element; 10 | 11 | /** 12 | * Format a string by replacing all instanced of "{N}" (where N is an integer) with the argument with the same index 13 | * (minus one, so the 2nd argument is mapped to the number 0). 14 | * 15 | * @param str String to format. 16 | * @param args Arguments to replace "{N}" instances with. 17 | */ 18 | export function formatString(str: string, ...args: T): any[] | string { 19 | let onlyStrings = true; 20 | const map = splitVariableString(str, opts).map((val, index) => { 21 | if (index % 2 === 1) { 22 | const i = parseInt(val, 10); 23 | if (i >= 0 && i < args.length) { 24 | const arg = args[i]; 25 | if (React.isValidElement(arg)) { 26 | onlyStrings = false; 27 | return React.Children.toArray(arg).map(component => Object.assign({ key: index.toString() }, component)); 28 | } 29 | return arg; 30 | } else { throw new Error(`Failed to format string. Index out of bounds (index: "${i}", string: "${str}").`); } 31 | } else { 32 | return val; 33 | } 34 | }); 35 | return onlyStrings 36 | ? map.join('') 37 | : map; 38 | } 39 | -------------------------------------------------------------------------------- /src/shared/utils/compare.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Check if two arrays are shallowly strictly equal. 3 | * 4 | * @param a Array to compare. 5 | * @param b Array to compare. 6 | */ 7 | export function arrayShallowStrictEquals(a: unknown[], b: unknown[]): boolean { 8 | if (a === b) { return true; } 9 | 10 | if (a.length !== b.length) { return false; } 11 | 12 | for (let i = 0; i < a.length; i++) { 13 | if (a[i] !== b[i]) { return false; } 14 | } 15 | 16 | return true; 17 | } 18 | -------------------------------------------------------------------------------- /src/shared/utils/debounce.ts: -------------------------------------------------------------------------------- 1 | import { AnyFunction, ArgumentTypesOf } from '../interfaces'; 2 | 3 | /** A callable object that has the same argument types as T (and void as the return type). */ 4 | interface CallableCopy extends Function { 5 | (...args: ArgumentTypesOf): void; 6 | } 7 | 8 | /** 9 | * Executes a callback after a `time` millisecond timer, resetting the existing timer (cancelling its callback) with each call 10 | * 11 | * @param callback Called when the timer ends 12 | * @param time Time in milliseconds before calling 13 | */ 14 | export function debounce(callback: T, time:number): CallableCopy { 15 | // Store timeout 16 | let timeout: ReturnType | undefined; 17 | // Function that receives and records the events 18 | const debouncer: CallableCopy = function(...args) { 19 | // Reset timer for release 20 | if (timeout != undefined) { clearTimeout(timeout); } 21 | // Release event after some time 22 | timeout = setTimeout(function() { 23 | timeout = undefined; 24 | callback(...args); 25 | }, time); 26 | }; 27 | 28 | return debouncer; 29 | } 30 | -------------------------------------------------------------------------------- /src/shared/utils/misc.ts: -------------------------------------------------------------------------------- 1 | import { Game, GameData } from 'flashpoint-launcher'; 2 | 3 | export function chunkArray(array: T[], chunkSize: number): T[][] { 4 | const chunks: T[][] = []; 5 | 6 | for (let i = 0; i < array.length; i += chunkSize) { 7 | chunks.push(array.slice(i, i + chunkSize)); 8 | } 9 | 10 | return chunks; 11 | } 12 | 13 | export function newGame(): Game { 14 | return { 15 | id: '', 16 | library: '', 17 | title: '', 18 | alternateTitles: '', 19 | series: '', 20 | developer: '', 21 | publisher: '', 22 | primaryPlatform: '', 23 | platforms: [], 24 | dateAdded: (new Date()).toISOString(), 25 | dateModified: (new Date()).toISOString(), 26 | detailedPlatforms: [], 27 | playMode: '', 28 | status: '', 29 | notes: '', 30 | tags: [], 31 | detailedTags: [], 32 | source: '', 33 | legacyApplicationPath: '', 34 | legacyLaunchCommand: '', 35 | releaseDate: '', 36 | version: '', 37 | originalDescription: '', 38 | language: '', 39 | activeDataId: 0, 40 | activeDataOnDisk: false, 41 | lastPlayed: (new Date()).toISOString(), 42 | playtime: 0, 43 | playCounter: 0, 44 | activeGameConfigId: 0, 45 | activeGameConfigOwner: '', 46 | archiveState: 0, 47 | gameData: [], 48 | addApps: [], 49 | ruffleSupport: '', 50 | }; 51 | } 52 | 53 | export function getGameDataFilename(data: GameData) { 54 | const cleanDate = data.dateAdded.includes('T') ? data.dateAdded : `${data.dateAdded} +0000 UTC`; 55 | return `${data.gameId}-${(new Date(cleanDate)).getTime()}.zip`; 56 | } 57 | 58 | export function mapRuffleSupportString(rs: string) { 59 | switch (rs) { 60 | case '': 61 | return 'None'; 62 | case 'standalone': 63 | return 'Standalone'; 64 | default: 65 | return 'Broken Value'; 66 | } 67 | } -------------------------------------------------------------------------------- /static/window/flash_index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 31 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /static/window/images/Logos/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlashpointProject/launcher/678880c6fd0917e172ec754b787cf62aadf7c798/static/window/images/Logos/404.png -------------------------------------------------------------------------------- /static/window/images/Logos/Extreme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlashpointProject/launcher/678880c6fd0917e172ec754b787cf62aadf7c798/static/window/images/Logos/Extreme.png -------------------------------------------------------------------------------- /static/window/images/cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlashpointProject/launcher/678880c6fd0917e172ec754b787cf62aadf7c798/static/window/images/cross.png -------------------------------------------------------------------------------- /static/window/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlashpointProject/launcher/678880c6fd0917e172ec754b787cf62aadf7c798/static/window/images/icon.png -------------------------------------------------------------------------------- /static/window/images/logo-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /static/window/images/max.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlashpointProject/launcher/678880c6fd0917e172ec754b787cf62aadf7c798/static/window/images/max.png -------------------------------------------------------------------------------- /static/window/images/min.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlashpointProject/launcher/678880c6fd0917e172ec754b787cf62aadf7c798/static/window/images/min.png -------------------------------------------------------------------------------- /static/window/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /static/window/logger.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /swcrc.back.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": [ 3 | "node_modules/**", 4 | "src/renderer/**", 5 | "tests/**" 6 | ], 7 | "module": { 8 | "type": "commonjs", 9 | "strict": true, 10 | "noInterop": true 11 | }, 12 | "jsc": { 13 | "target": "esnext", 14 | "baseUrl": "./src", 15 | "paths": { 16 | "@shared/*": [ "./shared/*" ], 17 | "@main/*": [ "./main/*" ], 18 | "@back/*": [ "./back/*" ], 19 | "@renderer/*": [ "./renderer/*" ] 20 | }, 21 | "parser": { 22 | "syntax": "typescript", 23 | "decorators": true 24 | }, 25 | "transform": { 26 | "decoratorMetadata": true 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /swcrc.back.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": [ 3 | "node_modules/**", 4 | "src/renderer/**", 5 | "tests/**" 6 | ], 7 | "module": { 8 | "type": "commonjs", 9 | "strict": true, 10 | "noInterop": true 11 | }, 12 | "jsc": { 13 | "target": "esnext", 14 | "baseUrl": "./src", 15 | "paths": { 16 | "@shared/*": [ "./shared/*" ], 17 | "@main/*": [ "./main/*" ], 18 | "@back/*": [ "./back/*" ], 19 | "@renderer/*": [ "./renderer/*" ] 20 | }, 21 | "parser": { 22 | "syntax": "typescript", 23 | "decorators": true 24 | }, 25 | "transform": { 26 | "decoratorMetadata": true 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/setup.ts: -------------------------------------------------------------------------------- 1 | global.log = { 2 | trace: (a, b) => { return { source: '', content: '', timestamp: 0, logLevel: 0 }; }, 3 | debug: (a, b) => { return { source: '', content: '', timestamp: 0, logLevel: 0 }; }, 4 | info: (a, b) => { return { source: '', content: '', timestamp: 0, logLevel: 0 }; }, 5 | warn: (a, b) => { return { source: '', content: '', timestamp: 0, logLevel: 0 }; }, 6 | error: (a, b) => { return { source: '', content: '', timestamp: 0, logLevel: 0 }; } 7 | }; 8 | -------------------------------------------------------------------------------- /tests/unit/back/throttle.test.ts: -------------------------------------------------------------------------------- 1 | import { delayedThrottle, delayedThrottleAsync, throttle } from '@shared/utils/throttle'; 2 | 3 | describe('Throttle Utils', () => { 4 | beforeAll(async () => { 5 | jest.useFakeTimers(); 6 | jest.spyOn(global, 'setTimeout'); 7 | }); 8 | 9 | it('throttle', () => { 10 | const cb = jest.fn(); 11 | const throttledFunc = throttle(cb, 100); 12 | // First call 13 | throttledFunc(); 14 | expect(cb).toBeCalledTimes(1); 15 | 16 | // Second call - Too early, throttle 17 | throttledFunc(); 18 | expect(cb).toBeCalledTimes(1); 19 | 20 | // Throttle expired, will now run again 21 | jest.runAllTimers(); 22 | throttledFunc(); 23 | expect(cb).toBeCalledTimes(2); 24 | }); 25 | 26 | const delayedThrottleTestFactory = (delayedThrottle: (callback: () => any, time:number) => any) => { 27 | return () => { 28 | const cb = jest.fn(); 29 | const delayedThrottleFunc = delayedThrottle(cb, 100); 30 | 31 | // First call, wait 100ms to run 32 | delayedThrottleFunc(cb); 33 | expect(cb).toBeCalledTimes(0); 34 | jest.runAllTimers(); 35 | expect(cb).toBeCalledTimes(1); 36 | 37 | // 2 calls at once, only latest should call, first ignored 38 | delayedThrottleFunc(cb); 39 | delayedThrottleFunc(cb); 40 | expect(cb).toBeCalledTimes(1); 41 | jest.runAllTimers(); 42 | expect(cb).toBeCalledTimes(2); 43 | }; 44 | }; 45 | 46 | it('delayedThrottle', delayedThrottleTestFactory(delayedThrottle)); 47 | 48 | it('delayedThrottleAsync', delayedThrottleTestFactory(delayedThrottleAsync)); 49 | }); 50 | -------------------------------------------------------------------------------- /tests/util/types.ts: -------------------------------------------------------------------------------- 1 | export interface IDataFactory { 2 | run(options?: Options, flags?: Flags[]): Promise | O; 3 | runMany(iterations: number, options?: Options, flags?: Flags[]): Promise> | Array; 4 | } 5 | -------------------------------------------------------------------------------- /tests/util/util.ts: -------------------------------------------------------------------------------- 1 | export function syncRunManyFactory(run: (options?: Options, flags?: Flags) => Promise | Output) { 2 | return async (iterations: number, options?: Options, flags?: Flags): Promise> => { 3 | const results = []; 4 | for (let i = 0; i < iterations; i++) { 5 | results.push(await run(options, flags)); 6 | } 7 | return results; 8 | }; 9 | } 10 | 11 | export function asyncRunManyFactory(run: (flags?: Flags, options?: Options) => Promise | Output) { 12 | return async (iterations: number, flags?: Flags, options?: Options): Promise> => { 13 | const promises = []; 14 | for (let i = 0; i < iterations; i++) { 15 | promises.push(run(flags, options)); 16 | } 17 | return Promise.all(promises); 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.backend.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": [ 4 | "./src/renderer", 5 | "./tests" 6 | ] 7 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "noImplicitAny": true, 5 | "sourceMap": true, 6 | "outDir": "build", 7 | "baseUrl": "./src", 8 | "target": "esnext", 9 | "lib": ["esnext", "dom"], 10 | "strict": true, 11 | "skipLibCheck": true, 12 | "jsx": "react-jsx", 13 | "emitDecoratorMetadata": true, 14 | "experimentalDecorators": true, 15 | "strictPropertyInitialization": false, 16 | "strictNullChecks": true, 17 | "paths": { 18 | "@shared/*": ["./shared/*"], 19 | "@main/*": ["./main/*"], 20 | "@back/*": ["./back/*"], 21 | "@renderer/*": ["./renderer/*"] 22 | }, 23 | "plugins": [ 24 | { 25 | "name": "empty", 26 | "transform": "ts-transform-paths" 27 | } 28 | ] 29 | }, 30 | "exclude": ["node_modules", "dist", "build", "./src/shared"], 31 | "include": ["./typings/**/*", "./src", "./tests"] 32 | } 33 | -------------------------------------------------------------------------------- /tsconfig.renderer.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "jsx": "react", 5 | }, 6 | "exclude": [ 7 | "./src/main", 8 | "./src/back", 9 | "./tests" 10 | ] 11 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-redundant-jsdoc": false, 4 | "no-void-expression": false, 5 | "promise-function-async": false, 6 | "typedef": [ 7 | true, 8 | "property-declaration" 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /typings/globals.d.ts: -------------------------------------------------------------------------------- 1 | import { LogFunc, IMainWindowExternal } from '../src/shared/interfaces'; 2 | 3 | /** Custom modifications made by this project */ 4 | type LogFuncs = { 5 | trace: LogFunc; 6 | debug: LogFunc; 7 | info: LogFunc; 8 | warn: LogFunc; 9 | error: LogFunc; 10 | } 11 | 12 | declare global { 13 | interface Window { 14 | Shared: IMainWindowExternal; 15 | log: LogFuncs; 16 | } 17 | namespace NodeJS { 18 | interface Global { 19 | log: LogFuncs; 20 | } 21 | } 22 | let log: LogFuncs; 23 | } 24 | 25 | /** Add missing declarations ("polyfill" type information) */ 26 | declare global { 27 | interface Clipboard { 28 | writeText(newClipText: string): Promise; 29 | // Add any other methods you need here. 30 | } 31 | interface NavigatorClipboard { 32 | // Only available in a secure context. 33 | readonly clipboard?: Clipboard; 34 | } 35 | interface Navigator extends NavigatorClipboard {} 36 | } 37 | -------------------------------------------------------------------------------- /typings/react-virtualized-reactv17.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'react-virtualized-reactv17' { 2 | import * as lib from 'react-virtualized'; 3 | export = lib; 4 | } 5 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /website/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "docusaurus": "docusaurus", 4 | "start": "docusaurus start", 5 | "build": "docusaurus build", 6 | "swizzle": "docusaurus swizzle", 7 | "deploy": "docusaurus deploy", 8 | "version": "docusaurus docs:version", 9 | "serve": "docusaurus serve" 10 | }, 11 | "dependencies": { 12 | "@docusaurus/core": "3.2.1", 13 | "@docusaurus/preset-classic": "3.2.1", 14 | "@docusaurus/theme-mermaid": "3.2.1", 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0", 17 | "typescript": "5.4.3" 18 | }, 19 | "browserslist": { 20 | "production": [ 21 | ">0.5%", 22 | "not dead", 23 | "not op_mini all" 24 | ], 25 | "development": [ 26 | "last 3 chrome version", 27 | "last 3 firefox version", 28 | "last 5 safari version" 29 | ] 30 | }, 31 | "devDependencies": { 32 | "@docusaurus/tsconfig": "^3.2.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /website/sidebars.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsdoc/check-alignment */ 2 | import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; 3 | 4 | /** 5 | * Creating a sidebar enables you to: 6 | - create an ordered group of docs 7 | - render a sidebar for each doc of that group 8 | - provide next/previous navigation 9 | 10 | The sidebars can be generated from the filesystem, or explicitly defined here. 11 | 12 | Create as many sidebars as you want. 13 | */ 14 | const sidebars: SidebarsConfig = { 15 | docs: [ 16 | { 17 | type: 'doc', 18 | id: 'introduction', 19 | label: 'Getting Started' 20 | }, { 21 | type: 'category', 22 | label: 'Configuration', 23 | items: [ 24 | 'configuration/introduction', 25 | 'configuration/config', 26 | 'configuration/preferences', 27 | 'configuration/services', 28 | 'configuration/shortcuts', 29 | 'configuration/credits', 30 | ] 31 | }, { 32 | type: 'category', 33 | label: 'Development', 34 | items: [ 35 | 'development/introduction', 36 | 'development/setup', 37 | 'development/gitworkflow', 38 | 'development/architecture', 39 | { 40 | type: 'category', 41 | label: 'Common Practices', 42 | items: [ 43 | 'development/communication', 44 | 'development/database', 45 | 'development/addingpages', 46 | 'development/lang', 47 | 'development/extapi', 48 | ] 49 | } 50 | ] 51 | }, { 52 | type: 'category', 53 | label: 'Extensions', 54 | items: [ 55 | 'extensions/overview', 56 | ] 57 | } 58 | ], 59 | }; 60 | 61 | export default sidebars; 62 | -------------------------------------------------------------------------------- /website/src/css/core.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --ifm-color-primary: #e24962; 3 | } 4 | 5 | .hero--primary { 6 | --ifm-hero-background-color: #DD0428; 7 | } -------------------------------------------------------------------------------- /website/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import Link from '@docusaurus/Link'; 3 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 4 | import Layout from '@theme/Layout'; 5 | import Heading from '@theme/Heading'; 6 | 7 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 8 | import React from 'react'; 9 | 10 | function HomepageHeader() { 11 | const {siteConfig} = useDocusaurusContext(); 12 | return ( 13 |
14 |
15 | 16 | {siteConfig.title} 17 | 18 |

{siteConfig.tagline}

19 |
20 | 23 | Getting Started 24 | 25 |



26 | 29 | Configuration 30 | 31 |



32 | 35 | Development 36 | 37 |



38 | 41 | Extensions 42 | 43 |
44 |
45 |
46 | ); 47 | } 48 | 49 | export default function Home(): JSX.Element { 50 | return ( 51 | 54 | 55 |
56 |
57 |
58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /website/static/.nojeykll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlashpointProject/launcher/678880c6fd0917e172ec754b787cf62aadf7c798/website/static/.nojeykll -------------------------------------------------------------------------------- /website/static/img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlashpointProject/launcher/678880c6fd0917e172ec754b787cf62aadf7c798/website/static/img/icon.png -------------------------------------------------------------------------------- /website/static/img/logo-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /website/static/img/meta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlashpointProject/launcher/678880c6fd0917e172ec754b787cf62aadf7c798/website/static/img/meta.png -------------------------------------------------------------------------------- /website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is not used in compilation. It is here just for a nice editor experience. 3 | "extends": "@docusaurus/tsconfig", 4 | "compilerOptions": { 5 | "baseUrl": ".", 6 | } 7 | } 8 | --------------------------------------------------------------------------------