├── .eslintrc.cjs ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yaml │ ├── config.yml │ └── feature_request.yaml └── workflows │ └── pull_request_checks.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── COPYRIGHT.txt ├── LICENSE.txt ├── LOGO_LICENSE.txt ├── README.md ├── SECURITY.md ├── appIcon.png ├── build ├── entitlements.mac.plist ├── icon.icns ├── icon.ico ├── icon.png ├── installerHeaderIcon.ico ├── installerIcon.ico └── license_en.txt ├── docs └── images │ └── mac_screen_project_view_dark.png ├── e2e ├── SettingsView.spec.ts └── mainWindow.spec.ts ├── electron-builder-config.cjs ├── index.html ├── package-lock.json ├── package.json ├── playwright.config.ts ├── postcss.config.js ├── src ├── assets │ ├── icons │ │ ├── darwin │ │ │ ├── appIcon.png │ │ │ ├── icon.png │ │ │ ├── trayIconTemplate.png │ │ │ ├── trayIconTemplate@16x.png │ │ │ ├── trayIconTemplate@2x.png │ │ │ ├── trayIconTemplate@4x.png │ │ │ └── trayIconTemplate@8x.png │ │ └── default │ │ │ ├── appIcon.png │ │ │ ├── icon.png │ │ │ ├── trayIcon.png │ │ │ ├── trayIcon@16x.png │ │ │ ├── trayIcon@2x.png │ │ │ ├── trayIcon@4x.png │ │ │ └── trayIcon@8x.png │ ├── menu_icons │ │ ├── bin-dark.png │ │ ├── bin-light.png │ │ ├── checkbox-checked-dark.png │ │ ├── checkbox-checked-light.png │ │ ├── checkbox-dark.png │ │ ├── checkbox-light.png │ │ ├── file-export-dark.png │ │ ├── file-export-light.png │ │ ├── file-save-dark.png │ │ ├── file-save-light.png │ │ ├── folder-dark.png │ │ ├── folder-light.png │ │ ├── godot-dark.png │ │ ├── godot-light.png │ │ ├── open-folder-dark.png │ │ └── open-folder-light.png │ ├── project_resources │ │ ├── default_gitignore │ │ ├── icon.png │ │ └── icon.svg │ ├── templates │ │ ├── editor_settings.template.mst │ │ └── project.godot_v5.template.mst │ └── utils │ │ └── win_elevate_symlink.vbs ├── electron │ ├── app.ts │ ├── autoUpdater.ts │ ├── checks.ts │ ├── commands │ │ ├── addProject.ts │ │ ├── createProject.ts │ │ ├── installRelease.ts │ │ ├── installedTools.ts │ │ ├── menuCommands.ts │ │ ├── projects.ts │ │ ├── releases.ts │ │ ├── removeRelease.ts │ │ ├── setProjectEditor.ts │ │ ├── shellFolders.ts │ │ └── userPreferences.ts │ ├── constants.ts │ ├── helpers │ │ ├── menu.helper.ts │ │ ├── tray.helper.ts │ │ └── tray.test.ts │ ├── main.ts │ ├── pathResolver.test.ts │ ├── pathResolver.ts │ ├── preload.cts │ ├── tsconfig.json │ ├── types │ │ ├── github.d.ts │ │ └── types.d.ts │ ├── utils.test.ts │ ├── utils.ts │ └── utils │ │ ├── fs.utils.ts │ │ ├── git.utils.test.ts │ │ ├── git.utils.ts │ │ ├── github.utils.test.ts │ │ ├── github.utils.ts │ │ ├── godot.utils.darwin.ts │ │ ├── godot.utils.linux.ts │ │ ├── godot.utils.test.ts │ │ ├── godot.utils.ts │ │ ├── godot.utils.windows.ts │ │ ├── godotProject.utils.test.ts │ │ ├── godotProject.utils.ts │ │ ├── platform.utils.test.ts │ │ ├── platform.utils.ts │ │ ├── prefs.util.test.ts │ │ ├── prefs.utils.ts │ │ ├── projects.utils.ts │ │ ├── release.utils.test.ts │ │ ├── releaseSorting.utils.ts │ │ ├── releases.utils.ts │ │ └── vscode.utils.ts └── ui │ ├── App.css │ ├── App.tsx │ ├── assets │ ├── Nunito_Sans │ │ ├── NunitoSans-Italic-VariableFont_YTLC,opsz,wdth,wght.ttf │ │ ├── NunitoSans-VariableFont_YTLC,opsz,wdth,wght.ttf │ │ ├── OFL.txt │ │ └── README.txt │ ├── icon.png │ ├── icons │ │ ├── Discord-Symbol-Blurple.svg │ │ ├── Discord-Symbol-White.svg │ │ └── godot_icon_color.svg │ └── logo.png │ ├── components │ ├── alert.component.tsx │ ├── closeButton.component.tsx │ ├── confirm.component.tsx │ ├── installReleaseTable.tsx │ ├── installedReleasesTable.tsx │ ├── selectInstalledRelease.component.tsx │ ├── settings │ │ ├── AutoStartSetting.component.tsx │ │ ├── EditorLocation.component.tsx │ │ ├── checkForUpdates.component.tsx │ │ ├── gitToolSettings.component.tsx │ │ ├── projectLaunchAction.component.tsx │ │ ├── projectsLocation.component.tsx │ │ └── vsCodeToolSettings.component.tsx │ └── welcomeSteps │ │ ├── CustomizeBehaviorStep.tsx │ │ ├── SetLocationStep.tsx │ │ ├── StartStep.tsx │ │ ├── WindowsStep.tsx │ │ ├── currentSettingsStep.tsx │ │ ├── linuxStep.tsx │ │ ├── macosStep.tsx │ │ └── welcomeStep.tsx │ ├── constants.ts │ ├── hooks │ ├── useAlerts.tsx │ ├── useApp.tsx │ ├── useAppNavigation.tsx │ ├── usePreferences.tsx │ ├── useProjects.tsx │ ├── useRelease.tsx │ └── useTheme.tsx │ ├── index.css │ ├── main.tsx │ ├── releaseStoring.utils.ts │ ├── styles │ └── buttons.css │ ├── views │ ├── help.view.tsx │ ├── installs.view.tsx │ ├── projects.view.tsx │ ├── settings.view.tsx │ ├── subViews │ │ ├── createProject.subview.tsx │ │ └── installEditor.subview.tsx │ └── welcome.view.tsx │ └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json ├── types.d.ts ├── vite.config.ts └── vitest.config.ts /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | "eslint:recommended", 6 | "plugin:@typescript-eslint/recommended", 7 | "plugin:react-hooks/recommended", 8 | ], 9 | ignorePatterns: [ 10 | "dist/**", 11 | "dist-react/**", 12 | "dist-electron/**", 13 | "*config.js", 14 | "*.cts", 15 | ".eslintrc.cjs", 16 | "*.spec.ts", 17 | "*.test.ts", 18 | ], 19 | parser: "@typescript-eslint/parser", 20 | plugins: ["react-refresh"], 21 | rules: { 22 | "react-refresh/only-export-components": [ 23 | "warn", 24 | { allowConstantExport: true }, 25 | ], 26 | "linebreak-style": ["error", "unix"], 27 | "no-console": "warn", 28 | quotes: ["error", "single"], 29 | semi: ["error", "always"], 30 | indent: ["error", 4], 31 | 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yaml: -------------------------------------------------------------------------------- 1 | name: "🐛 Bug Report" 2 | description: Create a report to help us improve Godot Launcher 3 | title: "[BUG] " 4 | labels: ["bug"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | When submitting an issue or bug report, please follow these guidelines to help us address it more efficiently: 10 | 11 | 1. **Descriptive Title** – Keep it clear and concise. 12 | 2. **One Bug Per Issue** – Submit multiple reports separately if needed. 13 | 3. **Update First** – Confirm you’re using the latest version. 14 | 4. **Search Before Posting** – Check [open issues](https://github.com/godotlauncher/launcher/issues) and [closed issues](https://github.com/godotlauncher/launcher/issues?q=is%3Aissue+is%3Aclosed) first. 15 | 16 | - type: textarea 17 | id: description 18 | attributes: 19 | label: "Describe the bug" 20 | description: A clear and concise description of what the bug is. 21 | validations: 22 | required: true 23 | 24 | - type: textarea 25 | id: steps 26 | attributes: 27 | label: "To Reproduce" 28 | description: Describe the steps to reproduce the issue. 29 | placeholder: | 30 | 1. Go to '...' 31 | 2. Click on '...' 32 | 3. See error 33 | validations: 34 | required: true 35 | 36 | - type: textarea 37 | id: expected 38 | attributes: 39 | label: "Expected behavior" 40 | description: What did you expect to happen? 41 | validations: 42 | required: true 43 | 44 | - type: textarea 45 | id: screenshots 46 | attributes: 47 | label: "Screenshots" 48 | description: If applicable, add screenshots to help explain the problem. 49 | placeholder: "Drag and drop screenshots here or paste image URLs" 50 | 51 | - type: input 52 | id: os 53 | attributes: 54 | label: "Operating System" 55 | placeholder: "e.g. Windows 11, macOS Ventura, Ubuntu 22.04" 56 | validations: 57 | required: true 58 | 59 | - type: input 60 | id: launcher_version 61 | attributes: 62 | label: "Godot Launcher Version" 63 | placeholder: "e.g. 1.1.0" 64 | validations: 65 | required: true 66 | 67 | - type: dropdown 68 | id: latest_tested 69 | attributes: 70 | label: "Have you tested with the latest version?" 71 | options: 72 | - "Yes" 73 | - "No" 74 | validations: 75 | required: true 76 | 77 | - type: dropdown 78 | id: regression 79 | attributes: 80 | label: "Did this work in a previous version?" 81 | description: Select yes if this bug didn't exist before. 82 | options: 83 | - "Yes" 84 | - "No" 85 | - "Not sure" 86 | validations: 87 | required: true 88 | 89 | - type: input 90 | id: previous_version 91 | attributes: 92 | label: "If yes, which version did not have this bug?" 93 | placeholder: "e.g. 1.0.0" 94 | validations: 95 | required: false 96 | 97 | - type: checkboxes 98 | id: log_consent 99 | attributes: 100 | label: "If more info is needed..." 101 | description: Check if you're open to sharing logs if asked during debugging. 102 | options: 103 | - label: I'm willing to share logs if needed 104 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Community Support on Discord 4 | url: https://discord.gg/Ju9jkFJGvz 5 | about: Please ask and answer questions here. 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yaml: -------------------------------------------------------------------------------- 1 | name: "✨ Feature Request" 2 | description: Suggest an idea to improve Godot Launcher 3 | title: "[Feature] " 4 | labels: ["enhancement"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Got an idea to improve Godot Launcher? Awesome — fill this out to help us understand and prioritize it. 10 | 11 | - type: textarea 12 | id: problem 13 | attributes: 14 | label: "Is your feature request related to a problem?" 15 | description: Describe the problem or frustration that this feature would solve. 16 | placeholder: "Ex. I'm always frustrated when..." 17 | validations: 18 | required: true 19 | 20 | - type: textarea 21 | id: solution 22 | attributes: 23 | label: "Describe the solution you'd like" 24 | description: What would you like to see added or improved? 25 | placeholder: "A clear and concise description of what you want to happen." 26 | validations: 27 | required: true 28 | 29 | - type: textarea 30 | id: alternatives 31 | attributes: 32 | label: "Describe alternatives you've considered" 33 | description: Have you thought of any other ways this could be solved or similar tools that do it differently? 34 | placeholder: "I currently do X manually... / I've seen Y do it like this..." 35 | validations: 36 | required: false 37 | 38 | - type: dropdown 39 | id: scope 40 | attributes: 41 | label: "Feature area" 42 | description: What part of the launcher does this affect? 43 | options: 44 | - Project management 45 | - Godot version handling 46 | - Editor settings 47 | - UI/UX or visuals 48 | - System tray or background behavior 49 | - Other (please explain above) 50 | multiple: true 51 | 52 | - type: textarea 53 | id: context 54 | attributes: 55 | label: "Additional context or mockups" 56 | description: Include screenshots, sketches, or any extra explanation that could help clarify your idea. 57 | placeholder: "Images, references, or use cases..." 58 | -------------------------------------------------------------------------------- /.github/workflows/pull_request_checks.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request Checks 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | lint-and-build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | # Checkout the repository 14 | - name: Checkout code 15 | uses: actions/checkout@v3 16 | 17 | # Set up Node.js 18 | - name: Set up Node.js 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: '22' # Adjust the version as needed 22 | 23 | # Install dependencies 24 | - name: Install dependencies 25 | run: npm install 26 | 27 | # Run lint 28 | - name: Run lint 29 | run: npm run lint 30 | 31 | # Run build:sources 32 | - name: Run build:sources 33 | run: npm run build:sources 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | # project 11 | node_modules 12 | dist 13 | dist-ssr 14 | dist-react 15 | dist-electron 16 | *.local 17 | .env 18 | *.env 19 | build.local 20 | build.local/* 21 | *.p12 22 | 23 | # Editor directories and files 24 | .vscode 25 | .cursor 26 | .vscode/* 27 | .cursor/* 28 | !.vscode/extensions.json 29 | .idea 30 | .DS_Store 31 | *.suo 32 | *.ntvs* 33 | *.njsproj 34 | *.sln 35 | *.sw? 36 | 37 | # Playwright 38 | /test-results/ 39 | /playwright-report/ 40 | /blob-report/ 41 | /playwright/.cache/ 42 | 43 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### Changelog 2 | 3 | All notable changes to this project will be documented in this file. Dates are displayed in UTC. 4 | 5 | Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). 6 | 7 | #### [v1.3.0](https://github.com/godotlauncher/launcher/compare/v1.2.0...v1.3.0) 8 | 9 | - Feat: Add an error when trying to add a project with a name that already exist [`#13`](https://github.com/godotlauncher/launcher/pull/13) 10 | - Feature/project sorting [`#11`](https://github.com/godotlauncher/launcher/pull/11) 11 | - Refactor icon usage across components to utilize Lucide icons [`#10`](https://github.com/godotlauncher/launcher/pull/10) 12 | - Feat: Update copyright and licensing information for Lucide Icons in documentation [`411e119`](https://github.com/godotlauncher/launcher/commit/411e11927dccd903b04cb64b464859f6f599bb88) 13 | 14 | #### [v1.2.0](https://github.com/godotlauncher/launcher/compare/v1.1.0...v1.2.0) 15 | 16 | > 20 April 2025 17 | 18 | - Release/1.2.0 [`#9`](https://github.com/godotlauncher/launcher/pull/9) 19 | - Fix: Update tooltip position for project release indicators [`#8`](https://github.com/godotlauncher/launcher/pull/8) 20 | - Feat: Add Windowed Mode to Per Project [`#7`](https://github.com/godotlauncher/launcher/pull/7) 21 | 22 | #### [v1.1.0](https://github.com/godotlauncher/launcher/compare/v1.0.0...v1.1.0) 23 | 24 | > 2 April 2025 25 | 26 | - Feature: Hide uid files in vscode [`#1`](https://github.com/godotlauncher/launcher/pull/1) 27 | 28 | #### v1.0.0 29 | 30 | > 31 March 2025 31 | 32 | - Initial Release [`d965222`](https://github.com/godotlauncher/launcher/commit/d965222356663a106bdd08dda0b832681dcdfe41) 33 | -------------------------------------------------------------------------------- /COPYRIGHT.txt: -------------------------------------------------------------------------------- 1 | Copyright 2 | ========= 3 | 4 | Godot Launcher 5 | -------------- 6 | 7 | Author: Mario Debono 8 | License: MIT 9 | Full license details: https://godotlauncher.org/license 10 | 11 | The Godot Launcher project is distributed under the MIT license. See the LICENSE file for the full text of the license. 12 | 13 | Godot Launcher Logo 14 | ------------------- 15 | 16 | Author: Jean Paul Grech 17 | License: Creative Commons Attribution 4.0 International (CC BY 4.0) 18 | Full license details: https://godotlauncher.org/license 19 | 20 | The Godot Launcher logo is licensed under CC BY 4.0. For attribution requirements, see the link above. 21 | 22 | Third-Party Assets and Libraries 23 | ================================ 24 | 25 | Nunito Sans 26 | ----------- 27 | 28 | Authors: Vernon Adams, Jacques Le Bailly, et al. 29 | License: SIL Open Font License (OFL) 30 | Source: https://fonts.google.com/specimen/Nunito+Sans 31 | 32 | Nunito Sans is used for UI text in Godot Launcher. 33 | 34 | Material Icons 35 | -------------- 36 | 37 | Author: Google 38 | License: Apache License 2.0 39 | Source: https://github.com/google/material-design-icons 40 | 41 | Material Icons are used for menu icons in the Godot Launcher interface. 42 | 43 | Lucide Icons 44 | ------------ 45 | 46 | Authors: Lucide Contributors 47 | License: ISC License 48 | Source: https://lucide.dev 49 | License Text: https://github.com/lucide-icons/lucide/blob/main/LICENSE 50 | 51 | Lucide Icons are used throughout the Godot Launcher interface for a unified and clean visual style. 52 | 53 | 54 | Godot Engine Logo 55 | ----------------- 56 | 57 | Author: Andrea Calabró 58 | License: Creative Commons Attribution 4.0 International (CC BY 4.0 International) 59 | Source: https://creativecommons.org/licenses/by/4.0/ 60 | 61 | The Godot Engine logo is a registered trademark of its respective owners. 62 | Godot Launcher is an independent project and is not affiliated with, sponsored by, or endorsed by the official Godot Engine team. 63 | 64 | ------------------------------------------------------------ 65 | 66 | For questions regarding licenses and attributions, please open an issue on the official GitHub repository. 67 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024-present Mario Debono 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. -------------------------------------------------------------------------------- /LOGO_LICENSE.txt: -------------------------------------------------------------------------------- 1 | Godot Launcher Logo 2 | Copyright (c) 2025 Jean Paul Grech 3 | 4 | This work is licensed under the Creative Commons Attribution 4.0 International 5 | license (CC BY 4.0 International): https://creativecommons.org/licenses/by/4.0/ 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Godot Launcher 2 | 3 | **Godot Launcher** is a streamlined, open-source tool designed to simplify and accelerate your Godot game development workflow. It automates Git initialization, configures VSCode instantly, manages multiple Godot versions seamlessly, and keeps project settings isolated. Quickly prototype ideas, access projects effortlessly from the system tray, and stay up-to-date with automatic updates. 4 | 5 | Spend less time configuring your environment and more time creating amazing games. 6 | 7 | ✅ **Free, Open Source, and Community-Driven.**\ 8 | ✅ **Cross-platform support, available on Windows, MacOS, and Linux.**\ 9 | ✅ **Designed for modern Godot workflows, focusing on widely used versions.** 10 | 11 | ![Screen shot of the project view with menu open](docs/images/mac_screen_project_view_dark.png) 12 | 13 | ## How to Get Godot Launcher 14 | 15 | The best way to get the latest installer is from [the Godot Launcher website](https://godotlauncher.org/download). 16 | 17 | ## Features 18 | 19 | ### **Quick Project Setup with Git and VSCode** 20 | 21 | - **Instant Git Initialization:** Start a new project with Git automatically initialized, complete with an initial commit—no extra steps required. 22 | - **VSCode Configuration:** Essential paths and settings for VSCode are ready out of the box. Say goodbye to manual editor setups and get to coding faster. 23 | 24 | ### **Effortless Godot Version Management** 25 | 26 | - **Install and Switch Versions:** Seamlessly download and manage multiple Godot Editor versions. Experiment with the latest pre-releases or fall back to stable editions in seconds. 27 | - **Fast Prototyping:** Quickly spin up new ideas using different editor builds. Testing features and previews is a breeze. 28 | - **Modern Godot Support:** The launcher focuses on the most commonly used versions of Godot. Since Godot 3.x adoption is steadily decreasing, custom configurations for it have not been included yet. 29 | 30 | ### **Per-Project Editor Settings** 31 | 32 | - **Isolated Configuration:** Each project maintains its own editor preferences, so you never have to worry about conflicting settings across multiple projects. 33 | - **Easy Import/Export:** Share your editor configurations with teammates or import settings that make sense for different workflows. 34 | 35 | ### **Quick Edit from System Tray** 36 | 37 | - **Seamless Workflow:** Minimize the launcher to your system tray to keep it out of your way while you work. 38 | - **Instant Access:** Jump back into any project with a single click—no need to relaunch or sift through folders. 39 | 40 | ### **Automatic Updates** 41 | 42 | - **Stay Current:** Receive notifications whenever a new version of the launcher is available. 43 | - **One-Click Upgrade:** Simply restart the launcher to apply the update. 44 | 45 | ### **Cross-Platform Availability** 46 | 47 | - **Windows:** Fully supported. 48 | - **Mac & Linux:** Coming soon to ensure you can use the same streamlined workflow on any machine. 49 | 50 | ### **Free and Open Source** 51 | 52 | - **Forever Free:** Godot Launcher is—and always will be—free for everyone. 53 | - **Community-Driven:** Contribute code, suggest features, or report issues. Join a passionate community dedicated to making Godot development smoother for all. 54 | 55 | ## Community 56 | 57 | Join our **[Godot Launcher Discord server](https://discord.gg/Ju9jkFJGvz)** to connect with other users and contributors. Ask questions, share feedback, and stay updated on new releases. 58 | 59 | If you’re interested in contributing to Godot Launcher, see the [Contributing](#contributing) section below. 60 | 61 | ## Contributing 62 | 63 | > [!IMPORTANT]\ 64 | > Before opening a new bug report or feature request, please check the [open issues](https://github.com/godotlauncher/launcher/issues) and [closed issues](https://github.com/godotlauncher/launcher/issues?q=is%3Aissue%20state%3Aclosed) first to see if it has already been reported. 65 | 66 | We warmly welcome contributions from the community! For detailed guidelines on submitting pull requests, best practices, and more, please see our [contribution guide](CONTRIBUTING.md). 67 | 68 | ### **Feature Proposals** 69 | 70 | For major changes or new features, please open an issue and clearly mark the title as a proposal [here](https://github.com/godotlauncher/launcher/issues). 71 | 72 | ### **Local Development** 73 | 74 | 1. **Fork & Clone** this repository to your local machine. 75 | 2. Ensure you have **Node.js 22+** installed. 76 | 3. Run `npm install` to install all dependencies. 77 | 4. Launch the app with `npm run dev`. 78 | 79 | Once you have everything running, feel free to open pull requests with your improvements, fixes, or new features. 80 | 81 | **We appreciate all contributions!** 82 | 83 | ## Documentation 84 | 85 | Visit the **[official Godot Launcher documentation](https://docs.godotlauncher.org)** for detailed guides, FAQs, and troubleshooting. This documentation is also open-source and maintained by the community in its own [GitHub repository](https://github.com/godotlauncher/launcher-docs). 86 | 87 | ## License 88 | 89 | Godot Launcher is licensed under the [MIT License](./LICENSE.txt). A copy of the license is provided in the repository for your convenience. By contributing to or using this project, you agree to the terms stated therein. 90 | 91 | This project also includes third-party assets and libraries, which are licensed under their respective terms. For details, please refer to [`COPYRIGHT.txt`](./COPYRIGHT.txt). 92 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # 📄 Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | If you discover a security vulnerability in **Godot Launcher**, please **do not** open a public issue. 6 | 7 | Instead, report it responsibly by emailing: 8 | 9 | 📧 `security@godotlauncher.org` 10 | 11 | Please include: 12 | 13 | - A detailed description of the vulnerability. 14 | - Steps to reproduce the issue. 15 | - Potential impact. 16 | - Any suggested mitigation or patch (optional but appreciated). 17 | 18 | We aim to respond within **72 hours** and provide a fix within **7–14 days**, depending on severity. 19 | 20 | --- 21 | 22 | ## Supported Versions 23 | 24 | | Version | Supported | 25 | |----------------|--------------------| 26 | | Latest release | ✅ | 27 | | Older versions | ❌ (not maintained) | 28 | 29 | We only patch the **latest stable release**. 30 | 31 | --- 32 | 33 | ## Scope 34 | 35 | This policy applies to: 36 | 37 | - The Godot Launcher application 38 | - Godot version manager code 39 | - Editor settings handling logic 40 | - Update system 41 | - All scripts in the [main GitHub repository](https://github.com/godotlauncher/launcher) 42 | 43 | It does **not** apply to: 44 | - External tools like Git or VSCode 45 | - The Godot engine itself (report those to [Godot's issue tracker](https://github.com/godotengine/godot/issues)) 46 | 47 | --- 48 | 49 | ## Disclosure Process 50 | 51 | 1. Vulnerability reported privately 52 | 2. Acknowledgement from maintainer 53 | 3. Investigation and patch creation 54 | 4. Coordinated disclosure (if needed) 55 | 5. Public security advisory on GitHub 56 | 57 | --- 58 | 59 | ## Hall of Fame 60 | 61 | We may credit responsible disclosures in our release notes or a `SECURITY_CREDITS.md` file, if permission is granted. 62 | -------------------------------------------------------------------------------- /appIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotlauncher/launcher/7edbc83db18128c20b2e04d61c89d7eb1725878a/appIcon.png -------------------------------------------------------------------------------- /build/entitlements.mac.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | com.apple.security.cs.allow-jit 7 | 8 | 9 | 10 | com.apple.security.cs.allow-unsigned-executable-memory 11 | 12 | 13 | 14 | com.apple.security.network.client 15 | 16 | 17 | 18 | 20 | 21 | 22 | 24 | 25 | -------------------------------------------------------------------------------- /build/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotlauncher/launcher/7edbc83db18128c20b2e04d61c89d7eb1725878a/build/icon.icns -------------------------------------------------------------------------------- /build/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotlauncher/launcher/7edbc83db18128c20b2e04d61c89d7eb1725878a/build/icon.ico -------------------------------------------------------------------------------- /build/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotlauncher/launcher/7edbc83db18128c20b2e04d61c89d7eb1725878a/build/icon.png -------------------------------------------------------------------------------- /build/installerHeaderIcon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotlauncher/launcher/7edbc83db18128c20b2e04d61c89d7eb1725878a/build/installerHeaderIcon.ico -------------------------------------------------------------------------------- /build/installerIcon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotlauncher/launcher/7edbc83db18128c20b2e04d61c89d7eb1725878a/build/installerIcon.ico -------------------------------------------------------------------------------- /build/license_en.txt: -------------------------------------------------------------------------------- 1 | GODOT LAUNCHER END-USER LICENSE AGREEMENT (EULA) 2 | 3 | Last Updated: 2025-03-09 4 | 5 | Godot Launcher ("the Software") is free and open-source software distributed under the terms of the MIT License. By downloading, installing, or using Godot Launcher, you agree to be bound by the following terms and conditions. 6 | 7 | 1. MIT License 8 | 9 | MIT License 10 | 11 | Copyright (c) 2025 Mario Debono 12 | 13 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED WITHOUT ANY WARRANTY OR GUARANTEE OF ANY KIND. THERE IS NO GUARANTEE THAT THE SOFTWARE WILL WORK AS EXPECTED OR BE SUITABLE FOR YOUR PARTICULAR USE. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | 19 | 2. Third-Party Usage & Dependencies 20 | 21 | Godot Launcher includes third-party assets and libraries that are licensed under their respective terms. By using this software, you agree to comply with the licenses governing these third-party components. 22 | 23 | 3. Trademarks & Attribution 24 | 25 | - Godot Launcher Logo: The Godot Launcher logo and branding are © 2025 Jean Paul Grech. Licensed under the Creative Commons Attribution 4.0 International (CC BY 4.0). See https://godotlauncher.org/license for details. 26 | - Godot Engine: Godot Engine and associated logos are trademarks owned by their respective copyright holders. Godot Launcher is an independent project and is not affiliated with, sponsored by, or endorsed by the official Godot Engine team. 27 | - Third-party Icons, Fonts, and Assets: Godot Launcher includes third-party assets that remain the intellectual property of their respective copyright holders and are used in compliance with their respective licenses. These include: 28 | - Nunito Sans (SIL Open Font License) – https://fonts.google.com/specimen/Nunito+Sans 29 | - Material Icons (Apache License 2.0) – https://github.com/google/material-design-icons 30 | - Lucide Icons (ISC License) – https://lucide.dev / https://github.com/lucide-icons/lucide/blob/main/LICENSE 31 | - Godot Engine Logo (MIT, subject to Godot’s trademark policies) – https://godotengine.org/license 32 | 33 | 34 | For a complete list of third-party attributions and licenses, see https://github.com/godotlauncher/launcher/blob/main/COPYRIGHT.txt 35 | 36 | 4. Update Checks & Data Collection 37 | Godot Launcher may perform periodic update checks to ensure users have access to the latest features and security improvements. These checks do not collect personally identifiable information. If future versions introduce optional telemetry, users will be given the choice to opt in or out. 38 | 39 | 5. Branding & Trademark Restrictions 40 | The name “Godot Launcher” and its associated branding are the property of the project maintainers. Any modified versions of the software must be clearly distinguished from the original and may not use the name “Godot Launcher” or its official logo without prior permission. 41 | 42 | 6. Resale Restrictions 43 | Redistribution of Godot Launcher as part of a paid product or service is permitted only if the software has been substantially modified and clear attribution is provided. Selling the software in an unmodified or minimally modified state without prior permission is prohibited. 44 | 45 | 7. Disclaimer of Warranty 46 | 47 | - No Warranties: Godot Launcher is provided without any warranties or guarantees, whether express or implied. This includes, but is not limited to, warranties of merchantability, fitness for a particular purpose, or non-infringement. 48 | - Limitation of Liability: Under no circumstances shall the authors or copyright holders be liable for any claims, damages, or other liability arising from your use or inability to use the Software. 49 | 50 | 8. Privacy & Data Handling 51 | 52 | - No Automatic Data Collection: Godot Launcher does not automatically collect or transmit any personal or analytics data. 53 | - Voluntary Sharing: If you voluntarily share logs or troubleshooting data, you are responsible for removing sensitive information prior to sharing. 54 | 55 | 9. Contact & Support 56 | 57 | - Community & Contributions: To report issues, request new features, or contribute code, please visit the official repository or community channels. 58 | - Support: Consult the official online documentation or join the official Discord community at https://discord.gg/Ju9jkFJGvz for additional assistance. 59 | 60 | By installing, copying, or otherwise using Godot Launcher, you confirm that you have read, understand, and agree to comply with all terms and conditions specified in this EULA. 61 | -------------------------------------------------------------------------------- /docs/images/mac_screen_project_view_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotlauncher/launcher/7edbc83db18128c20b2e04d61c89d7eb1725878a/docs/images/mac_screen_project_view_dark.png -------------------------------------------------------------------------------- /e2e/SettingsView.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect, _electron } from '@playwright/test'; 2 | import fs from 'fs/promises'; 3 | 4 | 5 | let electronApp: Awaited>; 6 | let mainPage: Awaited>; 7 | 8 | async function waitForPreloadScript() { 9 | return new Promise((resolve) => { 10 | const interval = setInterval(async () => { 11 | const electronBridge = await mainPage.evaluate(() => { 12 | return (window as Window & { electron?: any; }).electron; 13 | }); 14 | 15 | if (electronBridge) { 16 | clearInterval(interval); 17 | resolve(true); 18 | } 19 | 20 | }, 100); 21 | }); 22 | } 23 | 24 | test.beforeEach(async () => { 25 | electronApp = await _electron.launch({ 26 | args: ['.'], 27 | env: { NODE_ENV: 'development' }, 28 | }); 29 | mainPage = await electronApp.firstWindow(); 30 | await waitForPreloadScript(); 31 | 32 | await mainPage.getByTestId('btnSettings').click(); 33 | const settingsView = await mainPage.getByTestId('settingsTitle'); 34 | expect(settingsView).toHaveCount(1); 35 | expect(settingsView.isVisible()).toBeTruthy(); 36 | await mainPage.getByTestId("tabAppearance").click(); 37 | 38 | }); 39 | 40 | test.afterEach(async () => { 41 | await electronApp.close(); 42 | }); 43 | 44 | 45 | test('Can set theme light', async () => { 46 | 47 | await mainPage.getByTestId('themeLight').click(); 48 | const theme = await mainPage.evaluate(() => window.localStorage.getItem('theme')); 49 | expect(theme).toBe('light'); 50 | 51 | }); 52 | 53 | test('Can set theme dark', async () => { 54 | 55 | await mainPage.getByTestId('themeDark').click(); 56 | const theme = await mainPage.evaluate(() => window.localStorage.getItem('theme')); 57 | expect(theme).toBe('dark'); 58 | 59 | }); 60 | 61 | test('Can set theme auto', async () => { 62 | 63 | await mainPage.getByTestId('themeAuto').click(); 64 | const theme = await mainPage.evaluate(() => window.localStorage.getItem('theme')); 65 | expect(theme).toBe('auto'); 66 | 67 | }); 68 | 69 | 70 | -------------------------------------------------------------------------------- /e2e/mainWindow.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect, _electron } from '@playwright/test'; 2 | import fs from 'fs/promises'; 3 | 4 | 5 | let electronApp: Awaited>; 6 | let mainPage: Awaited>; 7 | 8 | async function waitForPreloadScript() { 9 | return new Promise((resolve) => { 10 | const interval = setInterval(async () => { 11 | const electronBridge = await mainPage.evaluate(() => { 12 | return (window as Window & { electron?: any; }).electron; 13 | }); 14 | 15 | if (electronBridge) { 16 | clearInterval(interval); 17 | resolve(true); 18 | } 19 | 20 | }, 100); 21 | }); 22 | } 23 | 24 | test.beforeEach(async () => { 25 | electronApp = await _electron.launch({ 26 | args: ['.'], 27 | env: { NODE_ENV: 'development' }, 28 | }); 29 | mainPage = await electronApp.firstWindow(); 30 | await waitForPreloadScript(); 31 | }); 32 | 33 | test.afterEach(async () => { 34 | await electronApp.close(); 35 | }); 36 | 37 | 38 | 39 | test('Has the correct title', async () => { 40 | const { version } = JSON.parse(await fs.readFile('./package.json', 'utf-8')); 41 | 42 | const title = await mainPage.title(); 43 | expect(title).toBe('Godot Launcher ' + version); 44 | }); 45 | 46 | test('Can view projects', async () => { 47 | 48 | await mainPage.getByTestId('btnProjects').click(); 49 | const projectsView = await mainPage.getByTestId('projectsTitle'); 50 | expect(projectsView).toHaveCount(1); 51 | 52 | }); 53 | 54 | test('Can view installs', async () => { 55 | 56 | await mainPage.getByTestId('btnInstalls').click(); 57 | const installsView = await mainPage.getByTestId('installsTitle'); 58 | expect(installsView).toHaveCount(1); 59 | 60 | }); 61 | 62 | test('Can view settings', async () => { 63 | 64 | await mainPage.getByTestId('btnSettings').click(); 65 | const settingsView = await mainPage.getByTestId('settingsTitle'); 66 | expect(settingsView).toHaveCount(1); 67 | 68 | }); 69 | 70 | -------------------------------------------------------------------------------- /electron-builder-config.cjs: -------------------------------------------------------------------------------- 1 | const dotenv = require("dotenv"); 2 | dotenv.config(); 3 | 4 | module.exports = 5 | { 6 | "appId": "org.godotlauncher.app", 7 | "productName": "Godot Launcher", 8 | "artifactName": "Godot_Launcher-${version}-${os}.${arch}.${ext}", 9 | 10 | "files": [ 11 | "dist-electron", 12 | "dist-react" 13 | ], 14 | "extraResources": [ 15 | "dist-electron/preload.cjs", 16 | "src/assets/**" 17 | ], 18 | "mac": { 19 | "icon": "build/icon.icns", 20 | "category": "public.app-category.developer-tools", 21 | 22 | "target": { 23 | "target": "default", 24 | "arch": [ 25 | "universal", 26 | "arm64", 27 | "x64" 28 | ] 29 | }, 30 | "type": "distribution", 31 | "hardenedRuntime": true, 32 | "gatekeeperAssess": false, 33 | "entitlements": "build/entitlements.mac.plist", 34 | "entitlementsInherit": "build/entitlements.mac.plist" 35 | }, 36 | "linux": { 37 | "icon": "build/icon.png", 38 | "target": [ 39 | { 40 | "target": "AppImage", 41 | "arch": [ 42 | "x64", 43 | "arm64" 44 | ] 45 | }, 46 | // { 47 | // "target": "deb", 48 | // "arch": [ 49 | // "x64", 50 | // "arm64" 51 | // ] 52 | // }, 53 | // { 54 | // "target": "rpm", 55 | // "arch": [ 56 | // "x64", 57 | // "arm64" 58 | // ] 59 | // } 60 | ], 61 | "category": "Development" 62 | }, 63 | "win": { 64 | "icon": "build/icon.ico", 65 | "executableName": "GodotLauncher", 66 | "artifactName": "Godot_Launcher-${version}-${os}.${ext}", 67 | "azureSignOptions": { 68 | 69 | "endpoint": process.env.WIN_SIGN_ENDPOINT, 70 | "certificateProfileName": process.env.WIN_SIGN_CERTIFICATE_PROFILE_NAME, 71 | "codeSigningAccountName": process.env.WIN_SIGN_CODE_SIGNING_ACCOUNT_NAME, 72 | }, 73 | 74 | "target": [ 75 | { 76 | "target": "nsis", 77 | "arch": [ 78 | "x64", 79 | "ia32" 80 | ] 81 | } 82 | ] 83 | }, 84 | "appImage": { 85 | "license": "build/license_en.txt", 86 | }, 87 | "nsis": { 88 | "oneClick": false, 89 | "allowToChangeInstallationDirectory": true, 90 | "runAfterFinish": true, 91 | "license": "build/license_en.txt", 92 | "installerHeaderIcon": "build/icon.ico", 93 | "installerIcon": "build/icon.ico", 94 | }, 95 | "publish": { 96 | "provider": "github", 97 | "owner": "godotlauncher", 98 | "repo": "launcher", 99 | "releaseType": "release", 100 | "vPrefixedTagName": true, 101 | } 102 | }; -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | Godot Launcher 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "godot-launcher", 3 | "private": true, 4 | "version": "1.3.0", 5 | "description": "Godot Launcher is a companion app for managing Godot projects with per-project editor settings.", 6 | "keywords": [ 7 | "godot", 8 | "launcher", 9 | "per-project editor settings" 10 | ], 11 | "type": "module", 12 | "main": "dist-electron/main.js", 13 | "author": { 14 | "name": "Mario Debono - godotlauncher.org", 15 | "email": "mario@godotlauncher.org", 16 | "url": "https://godotlauncher.org" 17 | }, 18 | "license": "MIT", 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/godotlauncher/launcher.git" 22 | }, 23 | "bugs": { 24 | "url": "https://github.com/godotlauncher/launcher/issues" 25 | }, 26 | "homepage": "https://godotlauncher.org", 27 | "engines": { 28 | "node": ">=22.14.0" 29 | }, 30 | "scripts": { 31 | "dev": "npm-run-all --parallel dev:electron dev:react", 32 | "dev:electron": "npm run transpile:electron && cross-env NODE_ENV=development electron .", 33 | "dev:react": "vite", 34 | "build": "tsc && vite build", 35 | "transpile:electron": "tsc --project src/electron/tsconfig.json", 36 | "lint": "eslint .", 37 | "lint:fix": "eslint . --fix", 38 | "preview": "vite preview", 39 | "build:sources": "npm run build && npm run transpile:electron ", 40 | "dist:mac": "npm run build:sources && electron-builder --config electron-builder-config.cjs --mac", 41 | "dist:linux": "npm run build:sources && electron-builder --config electron-builder-config.cjs --linux", 42 | "dist:win": "npm run build:sources && electron-builder --config electron-builder-config.cjs --win ", 43 | "dist:win:publish": "npm run build:sources && electron-builder --config electron-builder-config.cjs --win --publish always", 44 | "dist": "npm run build:sources && electron-builder --config electron-builder-config.cjs --mac --win --linux", 45 | "test:e2e": "playwright test", 46 | "test:unit": "vitest src", 47 | "app:dir": "electron-builder --dir", 48 | "app:dist": "electron-builder", 49 | "postinstall": "electron-builder install-app-deps", 50 | "changelog": "auto-changelog --ignore-commit-pattern \"[c|C]hore|changelog|update changelog|updated changelog|update CHANGELOG\"" 51 | }, 52 | "dependencies": { 53 | "clsx": "^2.1.1", 54 | "decompress": "^4.2.1", 55 | "electron-log": "^5.3.0", 56 | "electron-updater": "^6.3.9", 57 | "javascript-time-ago": "^2.5.11", 58 | "lucide-react": "^0.507.0", 59 | "mustache": "^4.2.0", 60 | "react": "^18.2.0", 61 | "react-dom": "^18.2.0", 62 | "which": "^5.0.0" 63 | }, 64 | "devDependencies": { 65 | "@babel/helpers": "^7.26.10", 66 | "@playwright/test": "^1.50.1", 67 | "@tailwindcss/typography": "^0.5.16", 68 | "@types/decompress": "^4.2.7", 69 | "@types/mustache": "^4.2.5", 70 | "@types/node": "^22.13.4", 71 | "@types/react": "^18.2.66", 72 | "@types/react-dom": "^18.2.22", 73 | "@types/which": "^3.0.4", 74 | "@typescript-eslint/eslint-plugin": "^7.2.0", 75 | "@typescript-eslint/parser": "^7.2.0", 76 | "@vitejs/plugin-react": "^4.2.1", 77 | "@vitest/coverage-v8": "^3.0.5", 78 | "auto-changelog": "^2.5.0", 79 | "autoprefixer": "^10.4.20", 80 | "cross-env": "^7.0.3", 81 | "daisyui": "^4.12.23", 82 | "electron": "^34.0.2", 83 | "electron-builder": "^25.1.8", 84 | "eslint": "^8.57.0", 85 | "eslint-plugin-react-hooks": "^4.6.0", 86 | "eslint-plugin-react-refresh": "^0.4.6", 87 | "npm-run-all": "^4.1.5", 88 | "postcss": "^8.5.1", 89 | "tailwindcss": "^3.4.17", 90 | "typescript": "^5.2.2", 91 | "vite": "^6.2.1", 92 | "vitest": "^3.0.5" 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/test'; 2 | 3 | /** 4 | * Read environment variables from file. 5 | * https://github.com/motdotla/dotenv 6 | */ 7 | // import dotenv from 'dotenv'; 8 | // import path from 'path'; 9 | // dotenv.config({ path: path.resolve(__dirname, '.env') }); 10 | 11 | /** 12 | * See https://playwright.dev/docs/test-configuration. 13 | */ 14 | export default defineConfig({ 15 | testDir: './e2e', 16 | /* Run tests in files in parallel */ 17 | fullyParallel: true, 18 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 19 | forbidOnly: !!process.env.CI, 20 | /* Retry on CI only */ 21 | retries: process.env.CI ? 2 : 0, 22 | /* Opt out of parallel tests on CI. */ 23 | workers: process.env.CI ? 1 : undefined, 24 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 25 | reporter: 'html', 26 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 27 | use: { 28 | /* Base URL to use in actions like `await page.goto('/')`. */ 29 | // baseURL: 'http://127.0.0.1:3000', 30 | 31 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 32 | trace: 'on-first-retry', 33 | }, 34 | 35 | /* Configure projects for major browsers */ 36 | projects: [ 37 | { 38 | name: 'chromium', 39 | use: { ...devices['Desktop Chrome'] }, 40 | }, 41 | 42 | // { 43 | // name: 'firefox', 44 | // use: { ...devices['Desktop Firefox'] }, 45 | // }, 46 | 47 | // { 48 | // name: 'webkit', 49 | // use: { ...devices['Desktop Safari'] }, 50 | // }, 51 | 52 | /* Test against mobile viewports. */ 53 | // { 54 | // name: 'Mobile Chrome', 55 | // use: { ...devices['Pixel 5'] }, 56 | // }, 57 | // { 58 | // name: 'Mobile Safari', 59 | // use: { ...devices['iPhone 12'] }, 60 | // }, 61 | 62 | /* Test against branded browsers. */ 63 | // { 64 | // name: 'Microsoft Edge', 65 | // use: { ...devices['Desktop Edge'], channel: 'msedge' }, 66 | // }, 67 | // { 68 | // name: 'Google Chrome', 69 | // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, 70 | // }, 71 | ], 72 | 73 | /* Run your local dev server before starting the tests */ 74 | webServer: { 75 | command: 'npm run dev:react', 76 | url: 'http://localhost:5123', 77 | reuseExistingServer: !process.env.CI, 78 | }, 79 | }); 80 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /src/assets/icons/darwin/appIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotlauncher/launcher/7edbc83db18128c20b2e04d61c89d7eb1725878a/src/assets/icons/darwin/appIcon.png -------------------------------------------------------------------------------- /src/assets/icons/darwin/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotlauncher/launcher/7edbc83db18128c20b2e04d61c89d7eb1725878a/src/assets/icons/darwin/icon.png -------------------------------------------------------------------------------- /src/assets/icons/darwin/trayIconTemplate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotlauncher/launcher/7edbc83db18128c20b2e04d61c89d7eb1725878a/src/assets/icons/darwin/trayIconTemplate.png -------------------------------------------------------------------------------- /src/assets/icons/darwin/trayIconTemplate@16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotlauncher/launcher/7edbc83db18128c20b2e04d61c89d7eb1725878a/src/assets/icons/darwin/trayIconTemplate@16x.png -------------------------------------------------------------------------------- /src/assets/icons/darwin/trayIconTemplate@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotlauncher/launcher/7edbc83db18128c20b2e04d61c89d7eb1725878a/src/assets/icons/darwin/trayIconTemplate@2x.png -------------------------------------------------------------------------------- /src/assets/icons/darwin/trayIconTemplate@4x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotlauncher/launcher/7edbc83db18128c20b2e04d61c89d7eb1725878a/src/assets/icons/darwin/trayIconTemplate@4x.png -------------------------------------------------------------------------------- /src/assets/icons/darwin/trayIconTemplate@8x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotlauncher/launcher/7edbc83db18128c20b2e04d61c89d7eb1725878a/src/assets/icons/darwin/trayIconTemplate@8x.png -------------------------------------------------------------------------------- /src/assets/icons/default/appIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotlauncher/launcher/7edbc83db18128c20b2e04d61c89d7eb1725878a/src/assets/icons/default/appIcon.png -------------------------------------------------------------------------------- /src/assets/icons/default/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotlauncher/launcher/7edbc83db18128c20b2e04d61c89d7eb1725878a/src/assets/icons/default/icon.png -------------------------------------------------------------------------------- /src/assets/icons/default/trayIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotlauncher/launcher/7edbc83db18128c20b2e04d61c89d7eb1725878a/src/assets/icons/default/trayIcon.png -------------------------------------------------------------------------------- /src/assets/icons/default/trayIcon@16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotlauncher/launcher/7edbc83db18128c20b2e04d61c89d7eb1725878a/src/assets/icons/default/trayIcon@16x.png -------------------------------------------------------------------------------- /src/assets/icons/default/trayIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotlauncher/launcher/7edbc83db18128c20b2e04d61c89d7eb1725878a/src/assets/icons/default/trayIcon@2x.png -------------------------------------------------------------------------------- /src/assets/icons/default/trayIcon@4x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotlauncher/launcher/7edbc83db18128c20b2e04d61c89d7eb1725878a/src/assets/icons/default/trayIcon@4x.png -------------------------------------------------------------------------------- /src/assets/icons/default/trayIcon@8x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotlauncher/launcher/7edbc83db18128c20b2e04d61c89d7eb1725878a/src/assets/icons/default/trayIcon@8x.png -------------------------------------------------------------------------------- /src/assets/menu_icons/bin-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotlauncher/launcher/7edbc83db18128c20b2e04d61c89d7eb1725878a/src/assets/menu_icons/bin-dark.png -------------------------------------------------------------------------------- /src/assets/menu_icons/bin-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotlauncher/launcher/7edbc83db18128c20b2e04d61c89d7eb1725878a/src/assets/menu_icons/bin-light.png -------------------------------------------------------------------------------- /src/assets/menu_icons/checkbox-checked-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotlauncher/launcher/7edbc83db18128c20b2e04d61c89d7eb1725878a/src/assets/menu_icons/checkbox-checked-dark.png -------------------------------------------------------------------------------- /src/assets/menu_icons/checkbox-checked-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotlauncher/launcher/7edbc83db18128c20b2e04d61c89d7eb1725878a/src/assets/menu_icons/checkbox-checked-light.png -------------------------------------------------------------------------------- /src/assets/menu_icons/checkbox-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotlauncher/launcher/7edbc83db18128c20b2e04d61c89d7eb1725878a/src/assets/menu_icons/checkbox-dark.png -------------------------------------------------------------------------------- /src/assets/menu_icons/checkbox-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotlauncher/launcher/7edbc83db18128c20b2e04d61c89d7eb1725878a/src/assets/menu_icons/checkbox-light.png -------------------------------------------------------------------------------- /src/assets/menu_icons/file-export-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotlauncher/launcher/7edbc83db18128c20b2e04d61c89d7eb1725878a/src/assets/menu_icons/file-export-dark.png -------------------------------------------------------------------------------- /src/assets/menu_icons/file-export-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotlauncher/launcher/7edbc83db18128c20b2e04d61c89d7eb1725878a/src/assets/menu_icons/file-export-light.png -------------------------------------------------------------------------------- /src/assets/menu_icons/file-save-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotlauncher/launcher/7edbc83db18128c20b2e04d61c89d7eb1725878a/src/assets/menu_icons/file-save-dark.png -------------------------------------------------------------------------------- /src/assets/menu_icons/file-save-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotlauncher/launcher/7edbc83db18128c20b2e04d61c89d7eb1725878a/src/assets/menu_icons/file-save-light.png -------------------------------------------------------------------------------- /src/assets/menu_icons/folder-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotlauncher/launcher/7edbc83db18128c20b2e04d61c89d7eb1725878a/src/assets/menu_icons/folder-dark.png -------------------------------------------------------------------------------- /src/assets/menu_icons/folder-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotlauncher/launcher/7edbc83db18128c20b2e04d61c89d7eb1725878a/src/assets/menu_icons/folder-light.png -------------------------------------------------------------------------------- /src/assets/menu_icons/godot-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotlauncher/launcher/7edbc83db18128c20b2e04d61c89d7eb1725878a/src/assets/menu_icons/godot-dark.png -------------------------------------------------------------------------------- /src/assets/menu_icons/godot-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotlauncher/launcher/7edbc83db18128c20b2e04d61c89d7eb1725878a/src/assets/menu_icons/godot-light.png -------------------------------------------------------------------------------- /src/assets/menu_icons/open-folder-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotlauncher/launcher/7edbc83db18128c20b2e04d61c89d7eb1725878a/src/assets/menu_icons/open-folder-dark.png -------------------------------------------------------------------------------- /src/assets/menu_icons/open-folder-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotlauncher/launcher/7edbc83db18128c20b2e04d61c89d7eb1725878a/src/assets/menu_icons/open-folder-light.png -------------------------------------------------------------------------------- /src/assets/project_resources/default_gitignore: -------------------------------------------------------------------------------- 1 | # Godot 4+ specific ignores 2 | .godot/ 3 | 4 | # Godot-specific ignores 5 | .import/ 6 | export.cfg 7 | export_presets.cfg 8 | 9 | # Imported translations (automatically generated from CSV files) 10 | *.translation 11 | 12 | # Mono-specific ignores 13 | .mono/ 14 | data_*/ 15 | mono_crash.*.json 16 | 17 | # VSCode-Specific ignores 18 | .vscode/ 19 | 20 | # other 21 | build/ 22 | builds/ 23 | -------------------------------------------------------------------------------- /src/assets/project_resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotlauncher/launcher/7edbc83db18128c20b2e04d61c89d7eb1725878a/src/assets/project_resources/icon.png -------------------------------------------------------------------------------- /src/assets/project_resources/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/templates/editor_settings.template.mst: -------------------------------------------------------------------------------- 1 | [gd_resource type="EditorSettings" format={{{editorConfigFormat}}}] 2 | 3 | [resource] 4 | text_editor/external/use_external_editor = {{{useExternalEditor}}} 5 | text_editor/external/exec_path = "{{{execPath}}}" 6 | text_editor/external/exec_flags = "{{execFlags}}" 7 | {{#isMono}} 8 | dotnet/editor/external_editor = 4 9 | dotnet/editor/custom_exec_path_args = "{file}" 10 | {{/isMono}} 11 | -------------------------------------------------------------------------------- /src/assets/templates/project.godot_v5.template.mst: -------------------------------------------------------------------------------- 1 | config_version=5 2 | 3 | [application] 4 | config/name="{{{projectName}}}" 5 | config/features=PackedStringArray("{{{editorVersion}}}") 6 | config/icon="res://assets/icon.svg" 7 | 8 | {{#mobile}} 9 | [rendering] 10 | renderer/rendering_method="mobile" 11 | {{/mobile}} 12 | 13 | {{#compatible}} 14 | [rendering] 15 | renderer/rendering_method="gl_compatibility" 16 | renderer/rendering_method.mobile="gl_compatibility" 17 | {{/compatible}} -------------------------------------------------------------------------------- /src/assets/utils/win_elevate_symlink.vbs: -------------------------------------------------------------------------------- 1 | ' Usage: 2 | ' wscript elevate_symlink.vbs "target1|path1|file" "target2|path2|dir" ... 3 | 4 | Set shell = CreateObject("Shell.Application") 5 | Set args = WScript.Arguments 6 | 7 | If args.Count = 0 Then 8 | WScript.Echo "Usage: wscript elevate_symlink.vbs ""target|path|type"" ..." 9 | WScript.Quit 1 10 | End If 11 | 12 | cmd = "" 13 | 14 | For i = 0 To args.Count - 1 15 | parts = Split(args(i), "|") 16 | If UBound(parts) < 1 Then 17 | WScript.Echo "Invalid argument: " & args(i) 18 | WScript.Quit 1 19 | End If 20 | 21 | target = parts(0) 22 | path = parts(1) 23 | argType = "file" 24 | If UBound(parts) >= 2 Then 25 | argType = LCase(Trim(parts(2))) 26 | End If 27 | 28 | Select Case argType 29 | Case "dir" 30 | mkSwitch = "/D" 31 | Case "junction" 32 | mkSwitch = "/J" 33 | Case "hard" 34 | mkSwitch = "/H" 35 | Case Else 36 | mkSwitch = "" 37 | End Select 38 | 39 | cmd = cmd & "mklink " & mkSwitch & " """ & path & """ """ & target & """" & " & " 40 | Next 41 | 42 | ' Remove trailing ampersand 43 | If Right(cmd, 2) = " &" Then 44 | cmd = Left(cmd, Len(cmd) - 3) 45 | End If 46 | 47 | shell.ShellExecute "cmd.exe", "/c " & cmd, "", "runas", 1 48 | -------------------------------------------------------------------------------- /src/electron/autoUpdater.ts: -------------------------------------------------------------------------------- 1 | import electronUpdater, { UpdateCheckResult } from 'electron-updater'; 2 | import logger from 'electron-log'; 3 | 4 | import { setInterval } from 'timers'; 5 | import { ipcWebContentsSend } from './utils.js'; 6 | import { app, BrowserWindow, WebContents } from 'electron'; 7 | 8 | let interval: NodeJS.Timeout; 9 | 10 | let webContents: WebContents; 11 | const { autoUpdater } = electronUpdater; 12 | 13 | export async function startAutoUpdateChecks(intervalMs: number = (60 * 60 * 1000)) { 14 | if (!interval || !interval.hasRef()) { 15 | 16 | logger.info('Starting auto update check'); 17 | // run as soon as it starts 18 | await checkForUpdates(); 19 | 20 | interval = setInterval(async () => { 21 | await checkForUpdates(); 22 | }, intervalMs); 23 | 24 | interval.ref(); 25 | } 26 | } 27 | 28 | export function installUpdateAndRestart() { 29 | logger.info('Installing update and restarting app'); 30 | autoUpdater.autoRunAppAfterInstall = true; 31 | autoUpdater.quitAndInstall(true, true); 32 | app.quit(); 33 | } 34 | 35 | 36 | export function stopAutoUpdateChecks() { 37 | if (interval && interval.hasRef()) { 38 | clearInterval(interval); 39 | interval.unref(); 40 | logger.log('Stopped auto update checks'); 41 | } 42 | } 43 | 44 | export async function checkForUpdates() { 45 | logger.info('Checking for updates...'); 46 | ipcWebContentsSend('app-updates', webContents, { 47 | available: false, 48 | downloaded: false, 49 | type: 'checking', 50 | message: 'Checking for updates...', 51 | }); 52 | 53 | let result: UpdateCheckResult | null = null; 54 | try { 55 | result = await autoUpdater.checkForUpdates(); 56 | 57 | } catch (e) { 58 | logger.error('Error checking for updates', e); 59 | } 60 | 61 | if (result) { 62 | logger.info(`New version available: ${result?.updateInfo.version}`); 63 | } 64 | else { 65 | logger.info('No updates available'); 66 | } 67 | 68 | const hasNewVersion = result !== null && result.updateInfo.version !== autoUpdater.currentVersion.version; 69 | const newVersion = result?.updateInfo.version; 70 | ipcWebContentsSend('app-updates', webContents, { 71 | available: hasNewVersion, 72 | downloaded: false, 73 | type: hasNewVersion ? 'available' : 'none', 74 | version: newVersion, 75 | message: hasNewVersion ? `New version available: ${newVersion}` : 'No updates available', 76 | }); 77 | } 78 | 79 | export async function setupAutoUpdate( 80 | mainWindow: BrowserWindow, 81 | checkForUpdates: boolean = true, 82 | intervalMs: number = (60 * 60 * 1000), 83 | autoDownload: boolean = false, 84 | installOnQuit: boolean = true 85 | ) { 86 | 87 | logger.info(`Starting auto updates, enabled: ${checkForUpdates}; autoDownload: ${autoDownload}; installOnQuit: ${installOnQuit}`); 88 | 89 | webContents = mainWindow.webContents; 90 | 91 | autoUpdater.logger = logger; 92 | autoUpdater.autoDownload = autoDownload; 93 | autoUpdater.autoInstallOnAppQuit = installOnQuit; 94 | 95 | if (checkForUpdates) { 96 | await startAutoUpdateChecks(intervalMs); 97 | } 98 | 99 | autoUpdater.on('update-available', async (info) => { 100 | ipcWebContentsSend('app-updates', webContents, { 101 | available: true, 102 | downloaded: false, 103 | type: 'available', 104 | version: info.version, 105 | message: `New version available: ${info.version}`, 106 | }); 107 | 108 | logger.info('Downloading update...'); 109 | const download = await autoUpdater.downloadUpdate(); 110 | logger.log('Update downloaded'); 111 | download.forEach(logger.log); 112 | }); 113 | 114 | autoUpdater.on('download-progress', (progress) => { 115 | logger.info(`Download progress: ${progress.percent}`); 116 | ipcWebContentsSend('app-updates', webContents, { 117 | available: true, 118 | downloaded: false, 119 | type: 'downloading', 120 | message: `Downloading update: ${Math.round(progress.percent)}%`, 121 | }); 122 | }); 123 | 124 | autoUpdater.on('checking-for-update', () => { 125 | logger.info('Checking for update...'); 126 | ipcWebContentsSend('app-updates', webContents, { 127 | available: false, 128 | downloaded: false, 129 | type: 'checking', 130 | message: 'Checking for updates...', 131 | }); 132 | }); 133 | 134 | autoUpdater.on('update-downloaded', (event) => { 135 | logger.info(`Update downloaded: ${event.version}`); 136 | event.files.forEach(logger.log); 137 | 138 | ipcWebContentsSend('app-updates', webContents, { 139 | available: true, 140 | downloaded: true, 141 | type: 'ready', 142 | version: event.version, 143 | message: 'Update downloaded, restart to install.', 144 | 145 | }); 146 | }); 147 | 148 | } 149 | 150 | -------------------------------------------------------------------------------- /src/electron/checks.ts: -------------------------------------------------------------------------------- 1 | import logger from 'electron-log'; 2 | import * as fs from 'node:fs'; 3 | import * as path from 'node:path'; 4 | 5 | import { INSTALLED_RELEASES_FILENAME, PROJECTS_FILENAME } from './constants.js'; 6 | import { SetProjectEditorRelease } from './utils/godot.utils.js'; 7 | import { getDefaultDirs } from './utils/platform.utils.js'; 8 | import { 9 | getStoredProjectsList, 10 | storeProjectsList, 11 | } from './utils/projects.utils.js'; 12 | import { 13 | getStoredInstalledReleases, 14 | saveStoredInstalledReleases, 15 | } from './utils/releases.utils.js'; 16 | 17 | export async function checkAndUpdateReleases(): Promise { 18 | logger.info('Checking and updating releases'); 19 | 20 | const { configDir } = getDefaultDirs(); 21 | 22 | // get releases 23 | const releasesFile = path.resolve(configDir, INSTALLED_RELEASES_FILENAME); 24 | const releases = await getStoredInstalledReleases(releasesFile); 25 | 26 | // check that release path exist 27 | for (const release of releases) { 28 | if (!fs.existsSync(release.editor_path)) { 29 | logger.warn(`Release '${release.version}' has an invalid path`); 30 | release.valid = false; 31 | } else { 32 | release.valid = true; 33 | } 34 | } 35 | 36 | // remove invalid releases and save 37 | return await saveStoredInstalledReleases( 38 | releasesFile, 39 | releases.filter((r) => r.valid) 40 | ); 41 | } 42 | 43 | export async function checkAndUpdateProjects(): Promise { 44 | logger.info('Checking and updating projects'); 45 | 46 | const { configDir } = getDefaultDirs(); 47 | // get projects 48 | const projectsFile = path.resolve(configDir, PROJECTS_FILENAME); 49 | const projects = await getStoredProjectsList(projectsFile); 50 | 51 | // check that project path exist and has a project.godot file 52 | for (let i = 0; i < projects.length; i++) { 53 | const project = projects[i]; 54 | projects[i] = await checkProjectValid(project); 55 | } 56 | 57 | // update the projects file 58 | return await storeProjectsList(projectsFile, projects); 59 | } 60 | 61 | export async function checkProjectValid( 62 | project: ProjectDetails 63 | ): Promise { 64 | logger.info(`Checking project '${project.name}'`); 65 | 66 | // check project path 67 | if (!fs.existsSync(path.resolve(project.path, 'project.godot'))) { 68 | logger.warn(`Project '${project.name}' has an invalid path`); 69 | project.valid = false; 70 | } else { 71 | project.valid = true; 72 | } 73 | 74 | // check release 75 | if (!fs.existsSync(project.release.editor_path)) { 76 | logger.warn(`Project '${project.name}' has an invalid release path`); 77 | project.valid = false; 78 | project.release.valid = false; 79 | } else { 80 | if (!fs.existsSync(project.launch_path)) { 81 | logger.warn(`Restoring launch path for Project '${project.name}'`); 82 | // await setEditorSymlink(path.dirname(project.launch_path), project.release.editor_path); 83 | await SetProjectEditorRelease( 84 | path.dirname(project.launch_path), 85 | project.release 86 | ); 87 | } 88 | project.release.valid = true; 89 | } 90 | 91 | return project; 92 | } 93 | -------------------------------------------------------------------------------- /src/electron/commands/installedTools.ts: -------------------------------------------------------------------------------- 1 | import { findExecutable, getCommandVersion } from '../utils/platform.utils.js'; 2 | 3 | import { getVSCodeInstallPath } from '../utils/vscode.utils.js'; 4 | import { getUserPreferences } from './userPreferences.js'; 5 | 6 | export async function getInstalledTools(): Promise { 7 | 8 | const installedTools: InstalledTool[] = []; 9 | 10 | // check if git is installed 11 | const gitPath = await findExecutable('git'); 12 | 13 | if (gitPath) { 14 | 15 | const gitVersion = gitPath ? await getCommandVersion('git') : ''; 16 | installedTools.push({ 17 | name: 'Git', 18 | version: gitVersion, 19 | path: gitPath, 20 | }); 21 | 22 | } 23 | 24 | // check if vscode is installed 25 | const { vs_code_path } = await getUserPreferences(); 26 | const vscodePath = await getVSCodeInstallPath(vs_code_path); 27 | 28 | if (vscodePath) { 29 | 30 | installedTools.push({ 31 | name: 'VSCode', 32 | version: '', 33 | path: vscodePath ?? '', 34 | }); 35 | 36 | } 37 | 38 | return installedTools; 39 | } 40 | -------------------------------------------------------------------------------- /src/electron/commands/releases.ts: -------------------------------------------------------------------------------- 1 | import * as os from 'os'; 2 | import * as path from 'node:path'; 3 | 4 | import { getDefaultDirs } from '../utils/platform.utils.js'; 5 | import { getStoredAvailableReleases, getStoredInstalledReleases, storeAvailableReleases } from '../utils/releases.utils.js'; 6 | import { CACHE_LENGTH, MIN_VERSION } from '../constants.js'; 7 | import { getReleases } from '../utils/github.utils.js'; 8 | import { sortByPublishDate } from '../utils/releaseSorting.utils.js'; 9 | import { spawn } from 'child_process'; 10 | 11 | export async function getInstalledReleases(): Promise { 12 | const { installedReleasesCachePath: installedReleasesPath } = getDefaultDirs(); 13 | 14 | const installedReleases = await getStoredInstalledReleases( 15 | installedReleasesPath 16 | ); 17 | return installedReleases; 18 | } 19 | 20 | export async function getAvailableReleases(): Promise { 21 | const { releaseCachePath } = getDefaultDirs(); 22 | 23 | let releases = await getStoredAvailableReleases(releaseCachePath); 24 | 25 | if (releases.lastUpdated + CACHE_LENGTH < Date.now()) { 26 | const newReleases = await getReleases('RELEASES', releases.lastPublishDate, MIN_VERSION, 1, 100); 27 | 28 | const allReleases = newReleases.releases.concat(releases.releases).sort(sortByPublishDate); 29 | 30 | 31 | releases = await storeAvailableReleases( 32 | releaseCachePath, 33 | newReleases.lastPublishDate, 34 | allReleases 35 | ); 36 | } 37 | 38 | return releases.releases; 39 | } 40 | 41 | export async function getAvailablePrereleases(): Promise { 42 | const { prereleaseCachePath } = getDefaultDirs(); 43 | 44 | let releases = await getStoredAvailableReleases(prereleaseCachePath); 45 | 46 | if (releases.lastUpdated + CACHE_LENGTH < Date.now()) { 47 | const newReleases = await getReleases('BUILDS', releases.lastPublishDate, MIN_VERSION, 1, 100); 48 | 49 | const allReleases = newReleases.releases.concat(releases.releases).sort(sortByPublishDate); 50 | 51 | releases = await storeAvailableReleases( 52 | prereleaseCachePath, 53 | newReleases.lastPublishDate, 54 | allReleases 55 | ); 56 | } 57 | 58 | return releases.releases; 59 | } 60 | 61 | export async function openProjectManager(release: InstalledRelease): Promise { 62 | 63 | let launchPath = release.editor_path; 64 | if (os.platform() === 'darwin') { 65 | launchPath = path.resolve(release.editor_path, 'Contents', 'MacOS', 'Godot'); 66 | } 67 | 68 | const editor = spawn(launchPath, ['-p'], { detached: true, stdio: 'ignore' }); 69 | editor.unref(); 70 | 71 | } -------------------------------------------------------------------------------- /src/electron/commands/removeRelease.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'node:fs'; 2 | import logger from 'electron-log'; 3 | 4 | import { getDefaultDirs } from '../utils/platform.utils.js'; 5 | import { removeStoredInstalledRelease, removeProjectEditorUsingRelease } from '../utils/releases.utils.js'; 6 | import { checkAndUpdateProjects } from '../checks.js'; 7 | 8 | 9 | export async function removeRelease(release: InstalledRelease): Promise { 10 | const installedReleasesCachePath = getDefaultDirs().installedReleasesCachePath; 11 | try { 12 | logger.info(`Removing release '${release.version}'`); 13 | const releases = await removeStoredInstalledRelease(installedReleasesCachePath, release); 14 | 15 | await removeProjectEditorUsingRelease(release); 16 | 17 | // delete release folder 18 | if (fs.existsSync(release.install_path)) { 19 | await fs.promises.rm(release.install_path, { 20 | recursive: true, 21 | force: true, 22 | }); 23 | } 24 | 25 | await checkAndUpdateProjects(); 26 | 27 | return { 28 | success: true, 29 | version: release.version, 30 | mono: release.mono, 31 | releases, 32 | }; 33 | } catch (error) { 34 | return { 35 | success: false, 36 | error: (error as Error).message, 37 | version: release.version, 38 | mono: release.mono, 39 | releases: [], 40 | }; 41 | } 42 | } -------------------------------------------------------------------------------- /src/electron/commands/setProjectEditor.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'node:fs'; 2 | import * as path from 'node:path'; 3 | import logger from 'electron-log'; 4 | 5 | import { getDefaultDirs } from '../utils/platform.utils.js'; 6 | import { getUserPreferences } from './userPreferences.js'; 7 | import { getStoredProjectsList, storeProjectsList } from '../utils/projects.utils.js'; 8 | import { DEFAULT_PROJECT_DEFINITION, getProjectDefinition, SetProjectEditorRelease } from '../utils/godot.utils.js'; 9 | import { EDITOR_CONFIG_DIRNAME, PROJECTS_FILENAME, TEMPLATE_DIR_NAME } from '../constants.js'; 10 | import { createNewEditorSettings } from '../utils/godotProject.utils.js'; 11 | import { getAssetPath } from '../pathResolver.js'; 12 | import { getInstalledTools } from './installedTools.js'; 13 | import { addOrUpdateVSCodeRecommendedExtensions, addVSCodeNETLaunchConfig } from '../utils/vscode.utils.js'; 14 | 15 | 16 | export async function setProjectEditor(project: ProjectDetails, newRelease: InstalledRelease): Promise { 17 | 18 | const { configDir } = getDefaultDirs(); 19 | const projectListPath = path.resolve(configDir, PROJECTS_FILENAME); 20 | const allProjects = await getStoredProjectsList(projectListPath); 21 | 22 | // check if the project is already using the release 23 | if (project.release.version === newRelease.version && project.release.mono == newRelease.mono) { 24 | logger.warn(`Project already using the selected release, ${newRelease.version} - ${newRelease.mono ? 'mono' : ''}`); 25 | return { 26 | success: true, 27 | projects: allProjects, 28 | }; 29 | } 30 | 31 | const { install_location: installLocation } = await getUserPreferences(); 32 | 33 | const projectIndex = allProjects.findIndex(p => p.path === project.path); 34 | if (projectIndex === -1) { 35 | return { 36 | success: false, 37 | error: 'Project not found', 38 | }; 39 | } 40 | 41 | // check the config version and compare with the release version 42 | // cannot use release with config version lower than the already existing one 43 | 44 | const config = getProjectDefinition(newRelease.version_number, DEFAULT_PROJECT_DEFINITION); 45 | 46 | if (!config) { 47 | return { 48 | success: false, 49 | error: 'Invalid editor version', 50 | }; 51 | } 52 | 53 | // only allow same major version 54 | if (parseInt(project.version_number.toString()) != parseInt(newRelease.version_number.toString())) { 55 | return { 56 | success: false, 57 | error: 'Cannot use a different major version of the editor\nTo protect your project see official migration guide on https://godotengine.org', 58 | }; 59 | } 60 | 61 | const projectEditorPath = path.resolve(installLocation, EDITOR_CONFIG_DIRNAME, project.name); 62 | 63 | const newLaunchPath = await SetProjectEditorRelease(projectEditorPath, newRelease, project.release); 64 | const newEditorSettingsFile = path.resolve(path.dirname(project.launch_path), 'editor_data', config.editorConfigFilename(newRelease.version_number)); 65 | 66 | const tools = await getInstalledTools(); 67 | const vsCodeTool = tools.find(t => t.name === 'VSCode'); 68 | 69 | let shouldReportOnSettings = false; 70 | let settingsCreated = false; 71 | if (project.withVSCode && vsCodeTool) { 72 | 73 | // update vscode launch.json file with new launch path 74 | await addVSCodeNETLaunchConfig(project.path, newLaunchPath); 75 | await addOrUpdateVSCodeRecommendedExtensions(project.path, newRelease.mono); 76 | 77 | const needsNewEditorSettings: boolean = fs.existsSync(newEditorSettingsFile) ? false : true; 78 | 79 | shouldReportOnSettings = true; 80 | 81 | if (needsNewEditorSettings) { 82 | const templatesDir = path.resolve(getAssetPath(), TEMPLATE_DIR_NAME); 83 | 84 | // create the new editor settings file 85 | await createNewEditorSettings( 86 | templatesDir, 87 | newLaunchPath, 88 | config.editorConfigFilename(newRelease.version_number), 89 | config.editorConfigFormat, 90 | true, 91 | vsCodeTool.path, 92 | '{project} --goto {file}:{line}:{col}', 93 | newRelease.mono, 94 | ); 95 | 96 | settingsCreated = true; 97 | } 98 | else { 99 | logger.warn('Editor settings file already exists, no changes made to editor settings'); 100 | settingsCreated = false; 101 | } 102 | } 103 | 104 | // set the project release 105 | project.release = newRelease; 106 | project.version = newRelease.version; 107 | project.version_number = newRelease.version_number; 108 | project.release = newRelease; 109 | project.launch_path = newLaunchPath; 110 | project.editor_settings_path = path.resolve(path.dirname(newEditorSettingsFile)); 111 | project.editor_settings_file = newEditorSettingsFile; 112 | project.valid = true; 113 | project.release.valid = true; 114 | 115 | allProjects[projectIndex] = project; 116 | const updatedProjects = await storeProjectsList(projectListPath, allProjects); 117 | 118 | return { 119 | success: true, 120 | projects: updatedProjects, 121 | additionalInfo: { 122 | settingsCreated, 123 | shouldReportOnSettings, 124 | } 125 | }; 126 | } -------------------------------------------------------------------------------- /src/electron/commands/shellFolders.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'node:fs'; 2 | import * as path from 'node:path'; 3 | 4 | import { dialog, Menu, shell } from 'electron'; 5 | 6 | export async function openShellFolder(pathToOpen: string): Promise { 7 | 8 | const menu = Menu.buildFromTemplate([ 9 | { 10 | label: 'Open in Terminal', 11 | click: () => { 12 | shell.openPath(pathToOpen); 13 | } 14 | } 15 | ]); 16 | menu.popup(); 17 | // shell.openPath(pathToOpen); 18 | } 19 | 20 | export async function openFileDialog( 21 | defaultPath: string, 22 | title: string = 'Select File', 23 | filters: Electron.FileFilter[] = [{ name: 'Any File', extensions: ['*.*'] }] 24 | ): Promise { 25 | return await dialog.showOpenDialog({ 26 | defaultPath: path.resolve(defaultPath), 27 | filters, 28 | title, 29 | properties: ['openFile'], 30 | }); 31 | } 32 | 33 | export async function openDirectoryDialog( 34 | defaultPath: string, 35 | title: string = 'Select Folder', 36 | filters: Electron.FileFilter[] = [] 37 | ): Promise { 38 | defaultPath = path.resolve(defaultPath + path.sep); 39 | 40 | if (!fs.existsSync(defaultPath)) { 41 | fs.promises.mkdir(defaultPath, { recursive: true }); 42 | } 43 | 44 | return await dialog.showOpenDialog({ 45 | defaultPath, 46 | filters, 47 | title, 48 | properties: ['openDirectory', 'createDirectory', 'promptToCreate'], 49 | }); 50 | } 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/electron/commands/userPreferences.ts: -------------------------------------------------------------------------------- 1 | import { getDefaultDirs } from '../utils/platform.utils.js'; 2 | import { getDefaultPrefs, readPrefsFromDisk, writePrefsToDisk } from '../utils/prefs.utils.js'; 3 | 4 | 5 | export async function getUserPreferences(): Promise { 6 | 7 | const { prefsPath } = getDefaultDirs(); 8 | 9 | const prefs = await readPrefsFromDisk(prefsPath, await getDefaultPrefs()); 10 | return prefs; 11 | } 12 | 13 | export async function setUserPreferences( 14 | prefs: UserPreferences 15 | ): Promise { 16 | 17 | const { prefsPath } = getDefaultDirs(); 18 | 19 | await writePrefsToDisk(prefsPath, prefs); 20 | return prefs; 21 | } -------------------------------------------------------------------------------- /src/electron/constants.ts: -------------------------------------------------------------------------------- 1 | export const APP_INTERNAL_NAME: string = 'gd-launcher'; 2 | export const MIN_VERSION: number = 4.0; 3 | 4 | export const CACHE_LENGTH = 1000 * 60 * 10; // 10 minutes 5 | 6 | export const TEMPLATE_DIR_NAME = 'templates'; 7 | export const PROJECT_RESOURCES_DIRNAME = 'project_resources'; 8 | 9 | export const PROJECTS_FILENAME = 'projects.json'; 10 | export const PREFS_FILENAME = 'prefs.json'; 11 | export const RELEASES_FILENAME = 'releases.json'; 12 | export const INSTALLED_RELEASES_FILENAME = 'installed-releases.json'; 13 | export const PRERELEASES_FILENAME = 'prereleases.json'; 14 | export const EDITOR_CONFIG_DIRNAME = '.editor_config'; 15 | 16 | export const EDITOR_SETTINGS_TEMPLATE_FILENAME = 'editor_settings.template.mst'; -------------------------------------------------------------------------------- /src/electron/helpers/menu.helper.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow, Menu, shell } from 'electron'; 2 | import { isDev } from '../utils.js'; 3 | import { getPrefsPath } from '../utils/prefs.utils.js'; 4 | 5 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 6 | export function createMenu(mainWindow: BrowserWindow) { 7 | Menu.setApplicationMenu( 8 | Menu.buildFromTemplate([ 9 | { 10 | label: process.platform === 'darwin' ? undefined : 'App', 11 | type: 'submenu', 12 | submenu: [ 13 | { 14 | label: 'About', 15 | role: 'about', 16 | }, 17 | { 18 | type: 'separator', 19 | }, 20 | { 21 | label: 'Close', 22 | role: 'close', 23 | }, 24 | { 25 | type: 'separator', 26 | }, 27 | { 28 | label: 'Quit', 29 | role: 'quit', 30 | }, 31 | ], 32 | }, 33 | { 34 | label: 'Developer', 35 | submenu: [ 36 | { 37 | label: 'Reload', 38 | role: 'reload', 39 | }, 40 | { 41 | label: 'Toggle Developer Tools', 42 | role: 'toggleDevTools', 43 | visible: isDev(), 44 | }, 45 | { 46 | type: 'separator', 47 | }, 48 | { 49 | label: 'Open Config Folder', 50 | click: async () => { 51 | const prefsPath = await getPrefsPath(); 52 | shell.showItemInFolder(prefsPath); 53 | }, 54 | }, 55 | ], 56 | }, 57 | ]) 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /src/electron/helpers/tray.helper.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path'; 2 | 3 | import { app, BrowserWindow, Menu, Tray } from 'electron'; 4 | import { getAssetPath } from '../pathResolver.js'; 5 | import { getStoredProjectsList } from '../utils/projects.utils.js'; 6 | import { getConfigDir } from '../utils/prefs.utils.js'; 7 | import { PROJECTS_FILENAME } from '../constants.js'; 8 | import { launchProject } from '../commands/projects.js'; 9 | 10 | let tray: Tray; 11 | let mainWindow: BrowserWindow; 12 | 13 | export async function createTray(window: BrowserWindow): Promise { 14 | 15 | mainWindow = window; 16 | 17 | tray = new Tray(path.resolve(getAssetPath(), 'icons', 18 | process.platform === 'darwin' ? 'darwin/trayIconTemplate.png' : 'default/trayIcon.png') 19 | ); 20 | 21 | tray.setToolTip('Godot Launcher'); 22 | 23 | if (process.platform === 'darwin') { 24 | tray.on('click', async () => { 25 | await popMenu(tray, mainWindow); 26 | }); 27 | tray.on('right-click', async () => { 28 | await popMenu(tray, mainWindow); 29 | }); 30 | } 31 | 32 | if (process.platform === 'win32') { 33 | 34 | tray.on('click', async () => { 35 | mainWindow.show(); 36 | if (app.dock) { 37 | app.dock.show(); 38 | } 39 | }); 40 | 41 | tray.on('right-click', async () => { 42 | await popMenu(tray, mainWindow); 43 | }); 44 | } 45 | if (process.platform === 'linux') { 46 | await updateLinuxTray(); 47 | } 48 | 49 | return tray; 50 | 51 | } 52 | 53 | export async function updateLinuxTray(): Promise { 54 | tray.setContextMenu(await updateMenu(tray, mainWindow)); 55 | } 56 | 57 | export async function updateMenu(tray: Tray, mainWindow: BrowserWindow): Promise { 58 | const projectListFIle = path.resolve(await getConfigDir(), PROJECTS_FILENAME); 59 | 60 | const projects = await getStoredProjectsList(projectListFIle); 61 | const filteredProjects = projects.filter(p => p.valid && p.last_opened != null && p.last_opened.getTime() > 0) 62 | .sort((a, b) => b.last_opened!.getTime() - a.last_opened!.getTime()); 63 | 64 | const last3 = filteredProjects.slice(0, 3); 65 | 66 | let quickLaunchMenu: Array<(Electron.MenuItemConstructorOptions)> = []; 67 | 68 | if (last3.length > 0) { 69 | quickLaunchMenu = [ 70 | { 71 | label: 'Recent Projects', 72 | enabled: false 73 | } 74 | ]; 75 | 76 | last3.forEach(p => { 77 | quickLaunchMenu.push({ 78 | label: p.name, 79 | click: async () => { 80 | await launchProject(p); 81 | } 82 | }); 83 | }); 84 | 85 | quickLaunchMenu.push({ 86 | type: 'separator' 87 | }); 88 | } 89 | 90 | 91 | const menu = Menu.buildFromTemplate([ 92 | ...quickLaunchMenu, 93 | { 94 | label: 'Show Godot Launcher', 95 | click: () => { 96 | mainWindow.show(); 97 | if (app.dock) { 98 | app.dock.show(); 99 | } 100 | }, 101 | }, 102 | { type: 'separator' }, 103 | { 104 | label: 'Quit', 105 | click: () => { 106 | app.quit(); 107 | } 108 | } 109 | ]); 110 | return menu; 111 | } 112 | 113 | 114 | async function popMenu(tray: Tray, mainWindow: BrowserWindow): Promise { 115 | 116 | const menu = await updateMenu(tray, mainWindow); 117 | 118 | tray.popUpContextMenu(menu); 119 | 120 | } -------------------------------------------------------------------------------- /src/electron/pathResolver.test.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path'; 2 | 3 | import { describe, expect, it, vi } from 'vitest'; 4 | import { getUIPath, getAssetPath, getPreloadPath } from './pathResolver'; 5 | 6 | vi.mock('electron-updater', () => ({ 7 | default: { 8 | autoUpdater: { 9 | on: vi.fn(), 10 | logger: null, 11 | channel: null, 12 | checkForUpdates: vi.fn(), 13 | checkForUpdatesAndNotify: vi.fn(), 14 | downloadUpdate: vi.fn(), 15 | quitAndInstall: vi.fn(), 16 | setFeedURL: vi.fn(), 17 | addAuthHeader: vi.fn(), 18 | isUpdaterActive: vi.fn(), 19 | currentVersion: '1.0.0' 20 | } 21 | }, 22 | UpdateCheckResult: {} 23 | })); 24 | 25 | vi.mock('electron', () => ({ 26 | Menu: { 27 | setApplicationMenu: vi.fn() 28 | }, app: { 29 | getAppPath: vi.fn(() => '/app/path'), 30 | isPackaged: false, 31 | getName: vi.fn(), 32 | getVersion: vi.fn(() => '1.0.0'), 33 | getLocale: vi.fn(), 34 | getPath: vi.fn(), 35 | on: vi.fn(), 36 | whenReady: vi.fn(), 37 | quit: vi.fn(), 38 | requestSingleInstanceLock: vi.fn(() => true), 39 | dock: { 40 | show: vi.fn(), 41 | hide: vi.fn() 42 | } 43 | }, 44 | BrowserWindow: vi.fn(), 45 | shell: { 46 | showItemInFolder: vi.fn(), 47 | openExternal: vi.fn() 48 | }, 49 | dialog: { 50 | showOpenDialog: vi.fn(), 51 | showMessageBox: vi.fn() 52 | } 53 | })); 54 | 55 | describe('Path Resolver', () => { 56 | it('should get UI path', () => { 57 | const uiPath = getUIPath(); 58 | 59 | expect(uiPath).toBe(path.join('/app/path', '/dist-react/index.html')); 60 | }); 61 | 62 | it('should get asset path for dev', () => { 63 | vi.stubEnv('NODE_ENV', 'development'); 64 | 65 | const assetPath = getAssetPath(); 66 | expect(assetPath).toBe(path.join('/app/path', 'src/assets')); 67 | }); 68 | 69 | it('should get asset path for prod', () => { 70 | vi.stubEnv('NODE_ENV', 'production'); 71 | 72 | const assetPath = getAssetPath(); 73 | expect(assetPath).toBe(path.join('/app', 'src/assets')); 74 | }); 75 | 76 | it('should get preload path for dev', () => { 77 | vi.stubEnv('NODE_ENV', 'development'); 78 | 79 | const preloadPath = getPreloadPath(); 80 | expect(preloadPath).toBe(path.join('/app/path', 'dist-electron/preload.cjs')); 81 | }); 82 | 83 | it('should get preload path for prod', () => { 84 | vi.stubEnv('NODE_ENV', 'production'); 85 | 86 | const preloadPath = getPreloadPath(); 87 | expect(preloadPath).toBe(path.join('/app', 'dist-electron/preload.cjs')); 88 | }); 89 | 90 | }); 91 | -------------------------------------------------------------------------------- /src/electron/pathResolver.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { app } from 'electron'; 3 | 4 | import { isDev } from './utils.js'; 5 | 6 | /** 7 | * Retrieves the path to the preload script. 8 | * 9 | * This function constructs the path to the preload script based on the application's 10 | * current path and whether the application is running in development mode or production mode. 11 | * 12 | * @returns {string} The full path to the preload script. 13 | */ 14 | export function getPreloadPath() { 15 | return path.join( 16 | app.getAppPath(), 17 | isDev() ? '.' : '..', 18 | 'dist-electron/preload.cjs'); 19 | } 20 | 21 | 22 | /** 23 | * Retrieves the file path to the UI's index.html file. 24 | * 25 | * This function constructs the path by joining the application's root path 26 | * with the relative path to the `index.html` file located in the `dist-react` directory. 27 | * 28 | * @returns {string} The full file path to the `index.html` file. 29 | */ 30 | export function getUIPath() { 31 | return path.join(app.getAppPath(), '/dist-react/index.html'); 32 | } 33 | 34 | /** 35 | * Retrieves the path to the assets directory. 36 | * 37 | * This function constructs the path to the assets directory based on the application's 38 | * current path and whether the application is running in development mode or production mode. 39 | * 40 | * @returns {string} The full path to the assets directory. 41 | */ 42 | export function getAssetPath() { 43 | return path.join(app.getAppPath(), isDev() ? '.' : '..', '/src/assets'); 44 | } 45 | -------------------------------------------------------------------------------- /src/electron/preload.cts: -------------------------------------------------------------------------------- 1 | const electron = require("electron"); 2 | 3 | electron.contextBridge.exposeInMainWorld("electron", { 4 | // ##### user-preferences ##### 5 | 6 | getUserPreferences: () => ipcInvoke("get-user-preferences"), 7 | setUserPreferences: (prefs: UserPreferences) => 8 | ipcInvoke("set-user-preferences", prefs), 9 | setAutoStart: (autoStart: boolean, hidden: boolean) => 10 | ipcInvoke("set-auto-start", autoStart, hidden), 11 | setAutoCheckUpdates: (enabled: boolean) => 12 | ipcInvoke("set-auto-check-updates", enabled), 13 | 14 | // ##### releases ##### 15 | 16 | getAvailableReleases: () => ipcInvoke("get-available-releases"), 17 | getAvailablePrereleases: () => ipcInvoke("get-available-prereleases"), 18 | getInstalledReleases: () => ipcInvoke("get-installed-releases"), 19 | installRelease: (release: ReleaseSummary, mono: boolean) => 20 | ipcInvoke("install-release", release, mono), 21 | removeRelease: (release: InstalledRelease) => 22 | ipcInvoke("remove-release", release), 23 | 24 | openEditorProjectManager: (release: InstalledRelease) => 25 | ipcInvoke("open-editor-project-manager", release), 26 | checkAllReleasesValid: () => ipcInvoke("check-all-releases-valid"), 27 | 28 | // ##### dialogs ##### 29 | openDirectoryDialog: ( 30 | defaultPath: string, 31 | title: string = "Select Folder", 32 | filters?: Electron.FileFilter[], 33 | properties?: Electron.OpenDialogOptions["properties"] 34 | ) => 35 | ipcInvoke("open-directory-dialog", defaultPath, title, filters, properties), 36 | openFileDialog: ( 37 | defaultPath: string, 38 | title: string = "Select File", 39 | filters?: Electron.FileFilter[], 40 | properties?: Electron.OpenDialogOptions["properties"] 41 | ) => ipcInvoke("open-file-dialog", defaultPath, title, filters, properties), 42 | 43 | openShellFolder: (pathToOpen: string) => 44 | ipcInvoke("shell-open-folder", pathToOpen), 45 | 46 | showProjectMenu: (project: ProjectDetails) => 47 | ipcInvoke("show-project-menu", project), 48 | showReleaseMenu: (release: InstalledRelease) => 49 | ipcInvoke("show-release-menu", release), 50 | 51 | openExternal: (url: string) => ipcInvoke("open-external", url), 52 | 53 | // ##### projects ##### 54 | 55 | createProject: ( 56 | name: string, 57 | release: InstalledRelease, 58 | renderer: RendererType[5], 59 | withVSCode: boolean, 60 | withGit: boolean 61 | ) => 62 | ipcInvoke("create-project", name, release, renderer, withVSCode, withGit), 63 | 64 | getProjectsDetails: () => ipcInvoke("get-projects-details"), 65 | removeProject: (project: ProjectDetails) => 66 | ipcInvoke("remove-project", project), 67 | addProject: (projectPath: string) => ipcInvoke("add-project", projectPath), 68 | setProjectEditor: (project: ProjectDetails, release: InstalledRelease) => 69 | ipcInvoke("set-project-editor", project, release), 70 | 71 | launchProject: (project: ProjectDetails) => 72 | ipcInvoke("launch-project", project), 73 | 74 | checkProjectValid: (project: ProjectDetails) => 75 | ipcInvoke("check-project-valid", project), 76 | checkAllProjectsValid: () => ipcInvoke("check-all-projects-valid"), 77 | 78 | // ##### tools ##### 79 | 80 | getInstalledTools: () => ipcInvoke("get-installed-tools"), 81 | 82 | subscribeProjects: (callback) => ipcOn("projects-updated", callback), 83 | subscribeReleases: (callback) => ipcOn("releases-updated", callback), 84 | 85 | subscribeAppUpdates: (callback) => ipcOn("app-updates", callback), 86 | 87 | relaunchApp: () => ipcInvoke("relaunch-app"), 88 | installUpdateAndRestart: () => ipcInvoke("install-update-and-restart"), 89 | 90 | getPlatform: () => ipcInvoke("get-platform"), 91 | getAppVersion: () => ipcInvoke("get-app-version"), 92 | checkForUpdates: () => ipcInvoke("check-updates"), 93 | } satisfies Window["electron"]); 94 | 95 | function ipcInvoke( 96 | key: Channel, 97 | ...args: any[] 98 | ): EventChannelMapping[Channel] { 99 | return electron.ipcRenderer.invoke(key, ...args); 100 | } 101 | 102 | function ipcOn( 103 | key: Key, 104 | callback: (payload: EventChannelMapping[Key]) => void 105 | ) { 106 | const cb = (_: Electron.IpcRendererEvent, payload: any) => callback(payload); 107 | electron.ipcRenderer.on(key, cb); 108 | return () => electron.ipcRenderer.off(key, cb); 109 | } 110 | 111 | function ipcSend( 112 | key: Key, 113 | payload: EventChannelMapping[Key] 114 | ) { 115 | electron.ipcRenderer.send(key, payload); 116 | } 117 | -------------------------------------------------------------------------------- /src/electron/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "target": "ESNext", 5 | "module": "NodeNext", 6 | "outDir": "../../dist-electron", 7 | "skipLibCheck": true, 8 | "types": [ 9 | "../../types", 10 | ], 11 | }, 12 | "exclude": [ 13 | "**/*.test.ts" 14 | ] 15 | } -------------------------------------------------------------------------------- /src/electron/types/github.d.ts: -------------------------------------------------------------------------------- 1 | interface ReleaseAsset { 2 | id: number; 3 | name: string; 4 | content_type: string; 5 | size: number; 6 | state: 'uploaded' | 'open'; 7 | url: string; 8 | node_id: string; 9 | download_count: number; 10 | label: string | null; 11 | uploader: User | null; 12 | browser_download_url: string; 13 | created_at: string; 14 | updated_at: string; 15 | } 16 | 17 | interface ReactionRollup { 18 | url: string; 19 | total_count: number; 20 | '+1': number; 21 | '-1': number; 22 | laugh: number; 23 | confused: number; 24 | heart: number; 25 | hooray: number; 26 | eyes: number; 27 | rocket: number; 28 | } 29 | 30 | interface User { 31 | avatar_url: string; 32 | events_url: string; 33 | followers_url: string; 34 | following_url: string; 35 | gists_url: string; 36 | gravatar_id: string | null; 37 | html_url: string; 38 | id: number; 39 | node_id: string; 40 | login: string; 41 | organizations_url: string; 42 | received_events_url: string; 43 | repos_url: string; 44 | site_admin: boolean; 45 | starred_url: string; 46 | subscriptions_url: string; 47 | type: 'User'; 48 | url: string; 49 | starred_at: string | null; 50 | user_view_type: string; 51 | } 52 | 53 | interface Release { 54 | assets_url: string; 55 | upload_url: string; 56 | tarball_url: string | null; 57 | zipball_url: string | null; 58 | created_at: string; 59 | published_at: string | null; 60 | draft: boolean; 61 | id: number; 62 | node_id: string; 63 | author: User; 64 | html_url: string; 65 | name: string | null; 66 | prerelease: boolean; 67 | tag_name: string; 68 | target_commitish: string; 69 | assets: ReleaseAsset[]; 70 | url: string; 71 | body_html?: string; 72 | body_text?: string; 73 | mentions_count?: number; 74 | discussion_url?: string; 75 | reactions: ReactionRollup; 76 | } 77 | -------------------------------------------------------------------------------- /src/electron/types/types.d.ts: -------------------------------------------------------------------------------- 1 | 2 | type ProjectConfig = { 3 | configVersion: keyof RendererType, 4 | defaultRenderer: RendererType[keyof RendererType], 5 | resources: { src: string, dst: string; }[], 6 | projectFilename: string; 7 | editorConfigFilename: (editor_version: number) => string; 8 | editorConfigFormat: number; 9 | }; 10 | 11 | 12 | 13 | type ProjectDefinition = Map; -------------------------------------------------------------------------------- /src/electron/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { vi, describe, it, test, expect } from 'vitest'; 2 | import { validateEventFrame } from './utils'; 3 | 4 | // Mock imported modules 5 | vi.mock('electron-updater', () => ({ 6 | default: { 7 | autoUpdater: { 8 | on: vi.fn(), 9 | logger: null, 10 | channel: null, 11 | checkForUpdates: vi.fn(), 12 | checkForUpdatesAndNotify: vi.fn(), 13 | downloadUpdate: vi.fn(), 14 | quitAndInstall: vi.fn(), 15 | setFeedURL: vi.fn(), 16 | addAuthHeader: vi.fn(), 17 | isUpdaterActive: vi.fn(), 18 | currentVersion: '1.0.0' 19 | } 20 | }, 21 | UpdateCheckResult: {} 22 | })); 23 | 24 | vi.mock('electron', () => ({ 25 | Menu: { 26 | setApplicationMenu: vi.fn() 27 | }, 28 | ipcMain: { 29 | on: vi.fn(), 30 | handle: vi.fn() 31 | }, 32 | app: { 33 | isPackaged: false, 34 | getName: vi.fn(), 35 | getVersion: vi.fn(() => '1.0.0'), 36 | getLocale: vi.fn(), 37 | getPath: vi.fn(), 38 | on: vi.fn(), 39 | whenReady: vi.fn(), 40 | quit: vi.fn(), 41 | getAppPath: vi.fn(), 42 | requestSingleInstanceLock: vi.fn(() => true), 43 | dock: { 44 | show: vi.fn(), 45 | hide: vi.fn() 46 | } 47 | }, 48 | BrowserWindow: vi.fn(), 49 | shell: { 50 | showItemInFolder: vi.fn(), 51 | openExternal: vi.fn() 52 | }, 53 | dialog: { 54 | showOpenDialog: vi.fn(), 55 | showMessageBox: vi.fn() 56 | }, 57 | nativeImage: { 58 | createFromPath: vi.fn(() => ({ 59 | resize: vi.fn(() => ({})) 60 | })) 61 | }, 62 | nativeTheme: { 63 | shouldUseDarkColors: false 64 | }, 65 | WebFrameMain: class { 66 | url: string = ''; 67 | constructor(url: string = '') { 68 | this.url = url; 69 | } 70 | } 71 | })); 72 | 73 | describe("Utils", () => { 74 | describe("Validation", async () => { 75 | it("should error without a frame", () => { 76 | expect(() => validateEventFrame(null)).toThrowError(/Invalid Frame/i); 77 | }); 78 | 79 | it("should return undefined if dev and localhost:5123", () => { 80 | vi.stubEnv("NODE_ENV", 'development'); 81 | const frame = { 82 | url: "http://localhost:5123" 83 | }; 84 | 85 | expect(validateEventFrame(frame as any)).toBeUndefined(); 86 | }); 87 | 88 | it("should throw if not dev and localhost:5123", () => { 89 | vi.stubEnv("NODE_ENV", 'production'); 90 | const frame = { 91 | url: "http://localhost:5123" 92 | }; 93 | 94 | expect(() => validateEventFrame(frame as any)).toThrow(); 95 | }); 96 | 97 | it("should throw if dev and not localhost:5123", () => { 98 | vi.stubEnv("NODE_ENV", 'development'); 99 | const frame = { 100 | url: "http://localhost:123" 101 | }; 102 | 103 | expect(() => validateEventFrame(frame as any)).toThrow(); 104 | }); 105 | 106 | it("it should throw if not dev and path is not file://", () => { 107 | vi.stubEnv("NODE_ENV", 'production'); 108 | const frame = { 109 | url: "file://localhost:5123" 110 | }; 111 | 112 | expect(() => validateEventFrame(frame as any)).toThrow(); 113 | }); 114 | 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /src/electron/utils.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | 3 | import { 4 | ipcMain, 5 | IpcMainInvokeEvent, 6 | nativeImage, 7 | NativeImage, 8 | nativeTheme, 9 | WebContents, 10 | WebFrameMain, 11 | } from 'electron'; 12 | import { pathToFileURL } from 'url'; 13 | import { getMainWindow } from './main.js'; 14 | import { getAssetPath, getUIPath } from './pathResolver.js'; 15 | 16 | export function isDev(): boolean { 17 | return process.env.NODE_ENV === 'development'; 18 | } 19 | 20 | export function getThemedMenuIcon(iconName: string): NativeImage { 21 | const theme = nativeTheme.shouldUseDarkColors ? 'dark' : 'light'; 22 | const iconPath = path.join( 23 | getAssetPath(), 24 | 'menu_icons', 25 | `${iconName}-${theme}.png` 26 | ); 27 | const image = nativeImage.createFromPath(iconPath); 28 | return image.resize({ width: 24, height: 24 }); 29 | } 30 | 31 | export function ipcMainHandler( 32 | key: Channel, 33 | handler: ( 34 | event: IpcMainInvokeEvent, 35 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 36 | ...args: any[] 37 | ) => EventChannelMapping[Channel] 38 | ) { 39 | ipcMain.handle(key, (event, ...args) => { 40 | validateEventFrame(event.senderFrame); 41 | return handler(event, ...args); 42 | }); 43 | } 44 | 45 | export function validateEventFrame(frame: WebFrameMain | null) { 46 | if (!frame) { 47 | throw new Error('Invalid frame'); 48 | } 49 | if (isDev() && new URL(frame.url).host === 'localhost:5123') { 50 | return; 51 | } 52 | 53 | if (!frame.url.startsWith(pathToFileURL(getUIPath()).toString())) { 54 | throw new Error('Invalid frame'); 55 | } 56 | } 57 | 58 | export function ipcMainOn( 59 | key: Key, 60 | handler: (payload: EventChannelMapping[Key]) => void 61 | ) { 62 | ipcMain.on(key, (event, payload) => { 63 | validateEventFrame(event.senderFrame); 64 | return handler(payload); 65 | }); 66 | } 67 | 68 | export function ipcWebContentsSend( 69 | key: Key, 70 | webContents: WebContents, 71 | payload: EventChannelMapping[Key] 72 | ) { 73 | webContents.send(key, payload); 74 | } 75 | 76 | export function ipcSendToMainWindowSync( 77 | key: Key, 78 | payload: EventChannelMapping[Key] 79 | ) { 80 | getMainWindow().webContents.send(key, payload); 81 | } 82 | -------------------------------------------------------------------------------- /src/electron/utils/fs.utils.ts: -------------------------------------------------------------------------------- 1 | import logger from 'electron-log'; 2 | import { execFileSync } from 'node:child_process'; 3 | import fs from 'node:fs'; 4 | import path from 'path'; 5 | import { getAssetPath } from '../pathResolver.js'; 6 | 7 | export type SymlinkOptions = { 8 | // Renamed for clarity from 'links' 9 | target: string; // What the symlink points to 10 | path: string; // Where the symlink is created 11 | type: 'file' | 'dir' | 'junction' | 'hard'; 12 | }; 13 | 14 | function createElevatedSymlink(links: SymlinkOptions[]) { 15 | const assetsPath = getAssetPath(); 16 | const scriptPath = path.resolve( 17 | assetsPath, 18 | 'utils', 19 | 'win_elevate_symlink.vbs' 20 | ); 21 | 22 | // Each link definition must be a separate argument to the VBScript. 23 | // VBScript expects "target|path|type" 24 | const scriptArgs = links.map( 25 | (linkObj) => `${linkObj.target}|${linkObj.path}|${linkObj.type}` 26 | ); 27 | 28 | logger.debug('Running VBScript with args:', scriptArgs); 29 | 30 | execFileSync('wscript.exe', [scriptPath, ...scriptArgs]); 31 | } 32 | 33 | async function testSymlink(linkPath: string) { 34 | try { 35 | const linkStats = await fs.promises.lstat(linkPath); // Use lstat to check the link itself 36 | // Check if it's a symbolic link, or a file/directory (could be a hard link) 37 | if ( 38 | linkStats.isSymbolicLink() || 39 | linkStats.isFile() || 40 | linkStats.isDirectory() 41 | ) { 42 | logger.log(`Link at '${linkPath}' appears to be valid or present.`); 43 | } else { 44 | logger.error( 45 | `Link at '${linkPath}' exists, but is not a symbolic link, file, or directory.` 46 | ); 47 | throw new Error( 48 | `Link at '${linkPath}' exists, but is not a recognized valid type.` 49 | ); 50 | } 51 | } catch (err) { 52 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 53 | if ((err as any).code === 'ENOENT') { 54 | logger.error(`Link '${linkPath}' was not created or does not exist.`); 55 | } else { 56 | logger.error(`Error testing link '${linkPath}': ${(err as Error).message}`); 57 | } 58 | throw err; 59 | } 60 | } 61 | 62 | export async function trySymlinkOrElevateAsync( 63 | linksToCreate: SymlinkOptions[] 64 | ) { 65 | try { 66 | // Try to create the symlinks normally 67 | for (const { target, path: currentPath, type } of linksToCreate) { 68 | logger.debug('Creating symlink:', { target, path: currentPath, type }); 69 | await fs.promises.symlink(target, currentPath, type); 70 | } 71 | // If all succeed non-elevated, test them 72 | for (const link of linksToCreate) { 73 | await testSymlink(link.path); 74 | } 75 | } catch (err) { 76 | logger.error('Error creating symlink:', (err as Error).message); 77 | // Check for Windows, permission error, and try elevated symlink 78 | if (process.platform === 'win32' && (err as Error).message?.includes('EPERM')) { 79 | logger.info( 80 | 'Attempting to create symlink(s) with elevation via VBScript.' 81 | ); 82 | // Map linksToCreate to the structure expected by createElevatedSymlink 83 | // createElevatedSymlink expects 'link' for where the symlink is created, 84 | // and 'target' for what it points to. 85 | const mappedLinks: SymlinkOptions[] = linksToCreate.map((l) => ({ 86 | target: l.target, // Stays as target 87 | path: l.path, // l.path (where link is created) maps to 'link' 88 | type: l.type, 89 | })); 90 | createElevatedSymlink(mappedLinks); 91 | // After attempting elevated creation, test all links 92 | logger.info('Testing links after elevated attempt...'); 93 | // for (const link of linksToCreate) { 94 | // await testSymlink(link.path); 95 | // } 96 | } else { 97 | throw err; // Re-throw if not a Windows EPERM error or other issue 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/electron/utils/git.utils.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, vi } from 'vitest'; 2 | import { gitConfig, gitConfigGetUser, gitExists } from './git.utils'; 3 | 4 | import { exec } from "child_process"; 5 | 6 | const execMock = vi.mocked(exec); 7 | 8 | describe('git.utils', () => { 9 | it('should check if git exists', async () => { 10 | const result = await gitExists(); 11 | expect(result).toBe(true); 12 | }); 13 | 14 | it('should get git config', async () => { 15 | const result = await gitConfig(); 16 | expect(result).not.toBe(""); 17 | }); 18 | 19 | it('should get git user', async () => { 20 | const result = await gitConfigGetUser(); 21 | expect(result).not.toBeNull(); 22 | }); 23 | 24 | 25 | }); 26 | -------------------------------------------------------------------------------- /src/electron/utils/git.utils.ts: -------------------------------------------------------------------------------- 1 | import { exec } from 'child_process'; 2 | import logger from 'electron-log'; 3 | 4 | export async function gitExists(): Promise { 5 | return new Promise((resolve) => { 6 | exec('git --version', (error) => { 7 | if (error) { 8 | logger.error(error); 9 | resolve(false); 10 | } else { 11 | resolve(true); 12 | } 13 | }); 14 | }); 15 | } 16 | 17 | export async function gitConfigSetUser(name: string, email: string): Promise { 18 | return new Promise((resolve) => { 19 | exec(`git config user.name "${name}" && git config user.email "${email}"`, (error) => { 20 | if (error) { 21 | logger.error(error); 22 | resolve(false); 23 | } else { 24 | resolve(true); 25 | } 26 | }); 27 | }); 28 | } 29 | 30 | export async function gitConfigGetUser(): Promise<{ name: string, email: string; }> { 31 | return new Promise((resolve) => { 32 | exec('git config --global user.name && git config --global user.email', (error, stdout) => { 33 | if (error) { 34 | logger.error(error); 35 | resolve({ name: '', email: '' }); 36 | } else { 37 | const [name, email] = stdout.split('\n'); 38 | resolve({ name, email }); 39 | } 40 | }); 41 | }); 42 | } 43 | 44 | export async function gitConfigSetAutoCrlf(autoCrlf: boolean): Promise { 45 | return new Promise((resolve) => { 46 | exec(`git config core.autocrlf ${autoCrlf ? 'true' : 'false'}`, (error) => { 47 | if (error) { 48 | logger.error(error); 49 | resolve(false); 50 | } else { 51 | resolve(true); 52 | } 53 | }); 54 | }); 55 | } 56 | 57 | 58 | export async function gitConfig(): Promise { 59 | return new Promise((resolve) => { 60 | exec('git config --list', (error, stdout) => { 61 | if (error) { 62 | logger.error(error); 63 | resolve(''); 64 | } else { 65 | resolve(stdout); 66 | } 67 | }); 68 | }); 69 | } 70 | 71 | export async function gitInit(dir: string): Promise { 72 | return new Promise((resolve) => { 73 | exec(`git init ${dir}`, (error) => { 74 | if (error) { 75 | logger.error(error); 76 | resolve(false); 77 | } else { 78 | resolve(true); 79 | } 80 | }); 81 | }); 82 | } 83 | 84 | export async function gitAddAndCommit(dir: string): Promise { 85 | return new Promise((resolve) => { 86 | exec(`git -C ${dir} add . && git -C ${dir} commit -m "Initial commit" && git -C ${dir} branch -m main`, (error) => { 87 | if (error) { 88 | logger.error(error); 89 | resolve(false); 90 | } else { 91 | resolve(true); 92 | } 93 | }); 94 | }); 95 | } 96 | -------------------------------------------------------------------------------- /src/electron/utils/github.utils.ts: -------------------------------------------------------------------------------- 1 | import logger from 'electron-log'; 2 | 3 | import { createAssetSummary, sortReleases } from './releases.utils.js'; 4 | 5 | /** 6 | * Fetches a list of releases from the GitHub repository for Godot Engine. 7 | * 8 | * @param type - The type of releases to fetch. Can be 'RELEASES' for official releases or 'BUILDS' for build releases. 9 | * @param fromPage - The page number to start fetching from. Defaults to 1. 10 | * @param perPage - The number of releases to fetch per page. Defaults to 100. 11 | * @returns A promise that resolves to an array of ReleaseSummary objects. 12 | * @throws Will throw an error if the fetch operation fails. 13 | */ 14 | export async function getReleases(type: 'RELEASES' | 'BUILDS', since: Date = new Date(), minVersion: number = 3.0, fromPage: number = 1, perPage: number = 100): Promise { 15 | let allReleases: ReleaseSummary[] = []; 16 | 17 | let repo: 'godot' | 'godot-builds' = 'godot'; 18 | 19 | switch (type) { 20 | case 'RELEASES': 21 | repo = 'godot'; 22 | break; 23 | case 'BUILDS': 24 | repo = 'godot-builds'; 25 | break; 26 | } 27 | 28 | let latestPublishedDate = new Date(0); 29 | 30 | const LOOP_LIMIT = 100; 31 | let iterations = 0; 32 | 33 | while (iterations++ < LOOP_LIMIT) { 34 | const url = `https://api.github.com/repos/godotengine/${repo}/releases?page=${fromPage++}&per_page=${perPage}`; 35 | logger.debug(`Fetching releases from ${url}`); 36 | const releases = await fetch(url); 37 | 38 | if (releases.status !== 200) { 39 | logger.error('Failed to fetch releases'); 40 | let message: string = ''; 41 | try { 42 | message = await releases.text(); 43 | logger.error(message); 44 | } 45 | catch { 46 | logger.error('Failed to read response'); 47 | } 48 | 49 | throw new Error(`Failed to fetch releases: ${releases.status}; ${message}`); 50 | 51 | } 52 | 53 | const json = await releases.json() as Release[]; 54 | 55 | // get a list of release versions and the available artifact name and url 56 | const releasesList: ReleaseSummary[] = json 57 | .filter(release => { 58 | 59 | if (release == null) { 60 | return false; 61 | } 62 | 63 | // check if name is empty string or whitespace, fall back to tag name 64 | // if no tag name, skip the release 65 | if (!release.name || release.name === '' || /^\s*$/.test(release.name!)) { 66 | 67 | if (release.tag_name != null && release.tag_name !== '' && !/^\s*$/.test(release.tag_name)) { 68 | release.name = release.tag_name; 69 | } else { 70 | return false; 71 | 72 | } 73 | } 74 | const valid = (release != null && release.name != null && release.draft === false); 75 | 76 | // if the release is not valid or the release date is before the specified date, skip it 77 | if (!valid || (valid && new Date(release.published_at || 0) <= since)) { 78 | return false; 79 | } 80 | 81 | // extract version number from release name 82 | // extract the first float from the left of the string 83 | 84 | // should match version number from release name 85 | const version = parseFloat(release.name!.match(/(\d+\.\d+)/)![0]); 86 | if (version >= minVersion) { 87 | return true; 88 | } 89 | else { return false; } 90 | 91 | }) 92 | .map((release: Release) => { 93 | return { 94 | tag: release.tag_name, 95 | version: release.tag_name, 96 | version_number: parseFloat(release.name!.match(/(\d+\.\d+)/)![0]), 97 | name: release.name!, 98 | published_at: release.published_at, 99 | draft: release.draft, 100 | prerelease: release.prerelease, 101 | assets: release.assets?.map(createAssetSummary) 102 | }; 103 | }); 104 | 105 | 106 | allReleases = allReleases.concat(releasesList); 107 | 108 | if (json.length < perPage || releasesList.length === 0) { 109 | break; 110 | } 111 | } 112 | if (allReleases.length > 0) { 113 | latestPublishedDate = new Date(allReleases[0].published_at || Date.now()); 114 | } 115 | else { 116 | latestPublishedDate = since; 117 | } 118 | allReleases.sort(sortReleases); 119 | 120 | return { releases: allReleases, lastPublishDate: latestPublishedDate }; 121 | } 122 | 123 | -------------------------------------------------------------------------------- /src/electron/utils/godot.utils.darwin.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'node:fs'; 2 | import * as path from 'node:path'; 3 | import logger from 'electron-log'; 4 | // import { ProjectDetails } from "../types/types.js"; // Removed as ProjectDetails should be globally available 5 | 6 | export async function removeProjectEditorDarwin( 7 | project: ProjectDetails 8 | ): Promise { 9 | // remove editor files 10 | if (fs.existsSync(project.launch_path)) { 11 | await fs.promises.rm(project.launch_path, { 12 | recursive: true, 13 | force: true, 14 | }); 15 | } 16 | } 17 | 18 | export async function setProjectEditorReleaseDarwin( 19 | projectEditorPath: string, 20 | release: InstalledRelease, 21 | previousRelease?: InstalledRelease 22 | ): Promise { 23 | // remove previous editor 24 | if (previousRelease) { 25 | const appPath = path.resolve( 26 | projectEditorPath, 27 | previousRelease.mono ? 'Godot_mono.app' : 'Godot.app' 28 | ); 29 | logger.debug(`Previous editor at ${appPath}`); 30 | if (fs.existsSync(appPath)) { 31 | logger.debug(`Removing previous editor at ${appPath}`); 32 | await fs.promises.rm(appPath, { recursive: true }); 33 | } 34 | } 35 | 36 | // create new editor 37 | const srcEditorPath = path.resolve(release.editor_path); 38 | const dstEditorPath = path.resolve( 39 | projectEditorPath, 40 | path.basename(release.editor_path) 41 | ); 42 | 43 | logger.debug(`Copying editor from ${srcEditorPath} to ${dstEditorPath}`); 44 | if (fs.existsSync(srcEditorPath)) { 45 | await fs.promises.cp(srcEditorPath, dstEditorPath, { recursive: true }); 46 | logger.debug(`Copied editor from ${srcEditorPath} to ${dstEditorPath}`); 47 | } 48 | 49 | return dstEditorPath; 50 | } 51 | -------------------------------------------------------------------------------- /src/electron/utils/godot.utils.linux.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'node:fs'; 2 | import * as path from 'node:path'; 3 | // import { ProjectDetails } from "../types/types.js"; // Removed as ProjectDetails should be globally available 4 | 5 | export async function removeProjectEditorLinux( 6 | project: ProjectDetails 7 | ): Promise { 8 | // remove editor files 9 | if (fs.existsSync(project.launch_path)) { 10 | const baseFileName = path.basename(project.launch_path); 11 | const projectEditorPath = path.dirname(project.launch_path); 12 | 13 | const binPath = path.resolve(projectEditorPath, baseFileName); 14 | if (fs.existsSync(binPath)) { 15 | await fs.promises.unlink(binPath); 16 | } 17 | 18 | if (project.release.mono) { 19 | const sharpDir = path.resolve(projectEditorPath, 'GodotSharp'); 20 | if (fs.existsSync(sharpDir)) { 21 | // On Linux, GodotSharp is a symlink to a directory, 22 | await fs.promises.unlink(sharpDir); 23 | } 24 | } 25 | } 26 | } 27 | 28 | export async function setProjectEditorReleaseLinux( 29 | projectEditorPath: string, 30 | release: InstalledRelease, 31 | previousRelease?: InstalledRelease 32 | ): Promise { 33 | // remove previous editor 34 | if (previousRelease) { 35 | const baseFileName = path.basename(previousRelease.editor_path); 36 | const binPath = path.resolve(projectEditorPath, baseFileName); 37 | if (fs.existsSync(binPath)) { 38 | await fs.promises.unlink(binPath); 39 | } 40 | 41 | if (previousRelease.mono) { 42 | const sharpDir = path.resolve(projectEditorPath, 'GodotSharp'); 43 | if (fs.existsSync(sharpDir)) { 44 | await fs.promises.unlink(sharpDir); 45 | } 46 | } 47 | } 48 | 49 | // create new editor 50 | const baseFileName = path.basename(release.editor_path); 51 | const srcBinPath = path.resolve(release.editor_path); 52 | const dstBinPath = path.resolve(projectEditorPath, baseFileName); 53 | 54 | if (!fs.existsSync(dstBinPath)) { 55 | if (fs.existsSync(srcBinPath)) { 56 | await fs.promises.link(srcBinPath, dstBinPath); 57 | } 58 | } 59 | 60 | if (release.mono) { 61 | const srcSharpDir = path.resolve(release.install_path, 'GodotSharp'); 62 | const dstSharpDir = path.resolve(projectEditorPath, 'GodotSharp'); 63 | 64 | if (!fs.existsSync(dstSharpDir)) { 65 | if (fs.existsSync(srcSharpDir)) { 66 | await fs.promises.symlink(srcSharpDir, dstSharpDir, 'dir'); 67 | } 68 | } 69 | } 70 | 71 | return dstBinPath; 72 | } 73 | -------------------------------------------------------------------------------- /src/electron/utils/godotProject.utils.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | 3 | describe("decode project.godot file", () => { 4 | test("Should decode project.godot file", () => { 5 | const projectFile = 6 | `config_version=5 7 | 8 | [application] 9 | config/name="test" 10 | config/features=PackedStringArray("4.4") 11 | config/icon="res://icon.svg" 12 | 13 | [rendering] 14 | renderer/rendering_method="mobile" 15 | `; 16 | 17 | 18 | let sections: Map> = new Map>(); 19 | 20 | let current_section: string = "ROOT"; 21 | sections.set("ROOT", new Map()); 22 | 23 | projectFile.replaceAll("\r", "").split("\n").forEach((line) => { 24 | if (line.trim().startsWith(";") || line.trim().startsWith("#") || line.trim().length === 0) { 25 | return; 26 | } 27 | if (line.trim().startsWith("[")) { 28 | const section = line.trim().replaceAll("[", "").replaceAll("]", "").trim(); 29 | current_section = section; 30 | if (!sections.has(section)) { 31 | sections.set(section, new Map()); 32 | } 33 | } 34 | else { 35 | const [key, value] = line.split("="); 36 | if (key && value) { 37 | sections.get(current_section)?.set(key.trim(), value.trim()); 38 | } 39 | } 40 | }); 41 | 42 | sections.get("application")?.set("config/name", "\"test2\""); 43 | sections.get("application")?.set("config/name", "\"test\""); 44 | 45 | // serialize sections to text 46 | let serialized = ""; 47 | sections.forEach((section, section_name) => { 48 | if (section_name !== "ROOT") { 49 | serialized += `\n[${section_name}]\n`; 50 | } 51 | section.forEach((value, key) => { 52 | serialized += `${key}=${value}\n`; 53 | }); 54 | }); 55 | 56 | // match without whitespaces 57 | expect(serialized.replace(/\s/g, '')).toMatch(projectFile.replace(/\s/g, '')); 58 | 59 | }); 60 | 61 | }); 62 | -------------------------------------------------------------------------------- /src/electron/utils/platform.utils.test.ts: -------------------------------------------------------------------------------- 1 | import * as os from "node:os"; 2 | import path from "node:path"; 3 | import { beforeEach, describe, expect, it, vi } from "vitest"; 4 | import { APP_INTERNAL_NAME } from "../constants"; 5 | import { getDefaultDirs } from "./platform.utils"; 6 | 7 | // Mock electron-updater 8 | vi.mock('electron-updater', () => ({ 9 | default: { 10 | autoUpdater: { 11 | on: vi.fn(), 12 | logger: null, 13 | channel: null, 14 | checkForUpdates: vi.fn(), 15 | checkForUpdatesAndNotify: vi.fn(), 16 | downloadUpdate: vi.fn(), 17 | quitAndInstall: vi.fn(), 18 | setFeedURL: vi.fn(), 19 | addAuthHeader: vi.fn(), 20 | isUpdaterActive: vi.fn(), 21 | currentVersion: '1.0.0' 22 | } 23 | }, 24 | UpdateCheckResult: {} 25 | })); 26 | 27 | // Mock electron 28 | vi.mock('electron', () => ({ 29 | Menu: { 30 | setApplicationMenu: vi.fn() 31 | }, 32 | app: { 33 | getAppPath: vi.fn(() => '/app/path'), 34 | isPackaged: false, 35 | getName: vi.fn(), 36 | getVersion: vi.fn(() => '1.0.0'), 37 | getLocale: vi.fn(), 38 | getPath: vi.fn(), 39 | on: vi.fn(), 40 | whenReady: vi.fn(), 41 | quit: vi.fn(), 42 | requestSingleInstanceLock: vi.fn(() => true), 43 | dock: { 44 | show: vi.fn(), 45 | hide: vi.fn() 46 | } 47 | }, 48 | BrowserWindow: vi.fn(), 49 | shell: { 50 | showItemInFolder: vi.fn(), 51 | openExternal: vi.fn() 52 | }, 53 | dialog: { 54 | showOpenDialog: vi.fn(), 55 | showMessageBox: vi.fn() 56 | } 57 | })); 58 | 59 | vi.mock("node:os", () => ({ 60 | // Provide default mocks, these will be overridden in beforeEach 61 | homedir: vi.fn(() => "/home/user"), 62 | platform: vi.fn(() => "linux"), 63 | })); 64 | 65 | describe("platform.utils", () => { 66 | describe("Windows paths", () => { 67 | beforeEach(() => { 68 | (os.platform as any).mockReturnValue('win32'); 69 | (os.homedir as any).mockReturnValue('c:\\Users\\User'); 70 | }); 71 | 72 | it("should get default paths for Windows", () => { 73 | const dirs = getDefaultDirs(); 74 | const expectedHomeDir = 'c:\\Users\\User'; 75 | expect(dirs).toEqual({ 76 | configDir: path.win32.join(expectedHomeDir, `.${APP_INTERNAL_NAME}`), 77 | dataDir: path.win32.join(expectedHomeDir, 'Godot', 'Editors'), 78 | prefsPath: path.win32.join(expectedHomeDir, `.${APP_INTERNAL_NAME}`, 'prefs.json'), 79 | installedReleasesCachePath: path.win32.join(expectedHomeDir, `.${APP_INTERNAL_NAME}`, 'installed-releases.json'), 80 | prereleaseCachePath: path.win32.join(expectedHomeDir, `.${APP_INTERNAL_NAME}`, 'prereleases.json'), 81 | projectDir: path.win32.join(expectedHomeDir, 'Godot', 'Projects'), 82 | releaseCachePath: path.win32.join(expectedHomeDir, `.${APP_INTERNAL_NAME}`, 'releases.json') 83 | }); 84 | }); 85 | }); 86 | 87 | describe("Linux paths", () => { 88 | beforeEach(() => { 89 | (os.platform as any).mockReturnValue('linux'); 90 | (os.homedir as any).mockReturnValue('/home/user'); 91 | }); 92 | 93 | it("should get default paths for Linux", () => { 94 | const dirs = getDefaultDirs(); 95 | const expectedHomeDir = '/home/user'; 96 | expect(dirs).toEqual({ 97 | configDir: path.posix.join(expectedHomeDir, `.${APP_INTERNAL_NAME}`), 98 | dataDir: path.posix.join(expectedHomeDir, 'Godot', 'Editors'), 99 | prefsPath: path.posix.join(expectedHomeDir, `.${APP_INTERNAL_NAME}`, 'prefs.json'), 100 | installedReleasesCachePath: path.posix.join(expectedHomeDir, `.${APP_INTERNAL_NAME}`, 'installed-releases.json'), 101 | prereleaseCachePath: path.posix.join(expectedHomeDir, `.${APP_INTERNAL_NAME}`, 'prereleases.json'), 102 | projectDir: path.posix.join(expectedHomeDir, 'Godot', 'Projects'), 103 | releaseCachePath: path.posix.join(expectedHomeDir, `.${APP_INTERNAL_NAME}`, 'releases.json') 104 | }); 105 | }); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /src/electron/utils/platform.utils.ts: -------------------------------------------------------------------------------- 1 | import { app } from 'electron'; 2 | import logger from 'electron-log'; 3 | import * as os from 'node:os'; 4 | import * as path from 'node:path'; 5 | import which from 'which'; 6 | 7 | import { exec } from 'child_process'; 8 | import { getUserPreferences, setUserPreferences } from '../commands/userPreferences.js'; 9 | import { APP_INTERNAL_NAME, INSTALLED_RELEASES_FILENAME, PREFS_FILENAME, PRERELEASES_FILENAME, RELEASES_FILENAME } from '../constants.js'; 10 | import { isDev } from '../utils.js'; 11 | 12 | 13 | /** 14 | * Returns default data/config directories for an app, based on the given platform. 15 | * 16 | * - dataDir : Large user-specific data (e.g. downloaded Godot binaries). 17 | * - configDir: Small user-specific configs/preferences. 18 | * 19 | * @param platform A string from `os.platform()` e.g. "win32", "darwin", "linux" 20 | * @returns An object containing `dataDir` and `configDir`. 21 | */ 22 | export function getDefaultDirs(): { 23 | dataDir: string; 24 | configDir: string, 25 | projectDir: string, 26 | prefsPath: string; 27 | releaseCachePath: string, 28 | installedReleasesCachePath: string; 29 | prereleaseCachePath: string; 30 | } { 31 | // Select the correct path module based on the platform 32 | // this is to make the function testable on all platforms 33 | // by mocking the os.platform() function and the path module 34 | 35 | const platform = os.platform(); 36 | const pathModule = platform === 'win32' ? path.win32 : path.posix; 37 | const homedir = os.homedir(); 38 | 39 | const configDir = pathModule.resolve(homedir, `.${APP_INTERNAL_NAME}`); 40 | const dataDir = pathModule.resolve(homedir, 'Godot', 'Editors'); 41 | const projectDir = pathModule.resolve(homedir, 'Godot', 'Projects'); 42 | const prefsPath = pathModule.resolve(configDir, PREFS_FILENAME); 43 | const releaseCachePath = pathModule.resolve(configDir, RELEASES_FILENAME); 44 | const prereleaseCachePath = pathModule.resolve(configDir, PRERELEASES_FILENAME); 45 | const installedReleasesCachePath = pathModule.resolve(configDir, INSTALLED_RELEASES_FILENAME); 46 | 47 | return { 48 | prefsPath, 49 | dataDir, 50 | configDir, 51 | projectDir, 52 | releaseCachePath, 53 | prereleaseCachePath, 54 | installedReleasesCachePath 55 | }; 56 | } 57 | 58 | 59 | export async function findExecutable(command: string): Promise { 60 | 61 | logger.debug(`Searching for ${command} executable...`); 62 | 63 | const commandPath = await which(command, { nothrow: true }); 64 | return commandPath; 65 | } 66 | 67 | export async function getCommandVersion(commandPath: string): Promise { 68 | 69 | if (!commandPath) { 70 | return ''; 71 | } 72 | 73 | return new Promise((resolve) => { 74 | exec(`${commandPath} --version`, (error, stdout) => { 75 | if (error) { 76 | resolve(''); 77 | } else { 78 | resolve(stdout.split('\n')[0].trim()); 79 | } 80 | }); 81 | }); 82 | } 83 | 84 | export async function setAutoStart(autoStart: boolean, hidden: boolean): Promise { 85 | // ensure save in prefs 86 | const prefs = await getUserPreferences(); 87 | await setUserPreferences({ ...prefs, auto_start: autoStart, start_in_tray: hidden }); 88 | 89 | if (isDev()) { 90 | app.setLoginItemSettings({ 91 | openAtLogin: false, 92 | }); 93 | } 94 | else { 95 | logger.info(`Setting auto start to ${autoStart} and hidden to ${hidden}`); 96 | app.setLoginItemSettings({ 97 | openAtLogin: autoStart, 98 | args: [ 99 | ...(hidden ? ['--hidden'] : []) 100 | ] 101 | }); 102 | } 103 | return { 104 | success: true 105 | }; 106 | } -------------------------------------------------------------------------------- /src/electron/utils/prefs.utils.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'node:fs'; 2 | import * as os from 'node:os'; 3 | import * as path from 'node:path'; 4 | 5 | import { startAutoUpdateChecks, stopAutoUpdateChecks } from '../autoUpdater.js'; 6 | import { getUserPreferences, setUserPreferences } from '../commands/userPreferences.js'; 7 | import { getDefaultDirs } from './platform.utils.js'; 8 | 9 | const loadedPrefs: UserPreferences | null = null; 10 | 11 | export async function getPrefsPath(): Promise { 12 | const defaultPaths = getDefaultDirs(); 13 | return defaultPaths.prefsPath; 14 | } 15 | 16 | export async function getConfigDir(): Promise { 17 | const defaultPaths = getDefaultDirs(); 18 | return defaultPaths.configDir; 19 | } 20 | 21 | export async function getDefaultPrefs(): Promise { 22 | const defaultPrefs = getDefaultDirs(); 23 | const platform = os.platform(); 24 | const pathModule = platform === 'win32' ? path.win32 : path.posix; 25 | 26 | return { 27 | prefs_version: 2, 28 | install_location: pathModule.resolve(defaultPrefs.dataDir), 29 | config_location: pathModule.resolve(defaultPrefs.configDir), 30 | projects_location: pathModule.resolve(defaultPrefs.projectDir), 31 | post_launch_action: 'close_to_tray', 32 | auto_check_updates: true, 33 | auto_start: true, 34 | start_in_tray: true, 35 | confirm_project_remove: true, 36 | first_run: true, 37 | vs_code_path: '', 38 | 39 | }; 40 | } 41 | 42 | export async function readPrefsFromDisk(prefsPath: string, defaultPrefs: UserPreferences): Promise { 43 | if (!fs.existsSync(prefsPath)) { 44 | // load defaults 45 | return defaultPrefs; 46 | } 47 | 48 | // Read prefs from disk 49 | const prefsData = await fs.promises.readFile(prefsPath, 'utf-8'); 50 | const prefs = JSON.parse(prefsData); 51 | return { ...defaultPrefs, ...prefs }; 52 | } 53 | 54 | export async function writePrefsToDisk(prefsPath: string, prefs: UserPreferences): Promise { 55 | // Write prefs to disk 56 | const prefsData = JSON.stringify(prefs, null, 4); 57 | await fs.promises.writeFile(prefsPath, prefsData, 'utf-8'); 58 | } 59 | 60 | export async function getLoadedPrefs(): Promise { 61 | 62 | if (loadedPrefs) { 63 | return loadedPrefs; 64 | } 65 | 66 | const prefsPath = await getPrefsPath(); 67 | const defaultPrefs = await getDefaultPrefs(); 68 | return readPrefsFromDisk(prefsPath, defaultPrefs); 69 | } 70 | 71 | export async function setAutoCheckUpdates(enabled: boolean): Promise { 72 | // ensure save in prefs 73 | const prefs = await getUserPreferences(); 74 | await setUserPreferences({ ...prefs, auto_check_updates: enabled }); 75 | 76 | if (enabled) { 77 | startAutoUpdateChecks(); 78 | } 79 | else { 80 | stopAutoUpdateChecks(); 81 | } 82 | 83 | return enabled; 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/electron/utils/projects.utils.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'node:fs'; 2 | import logger from 'electron-log'; 3 | 4 | /** 5 | * Stores a list of projects to a JSON file. 6 | * 7 | * @param storeDir - The file path where project data will be stored 8 | * @param projects - The array of ProjectDetails to store 9 | * @returns A Promise that resolves to the same projects array that was provided 10 | */ 11 | export async function storeProjectsList(storeDir: string, projects: ProjectDetails[]): Promise { 12 | 13 | const data = JSON.stringify(projects, null, 4); 14 | await fs.promises.writeFile(storeDir, data, 'utf-8'); 15 | return projects; 16 | } 17 | 18 | /** 19 | * Retrieves a list of stored projects from a JSON file. 20 | * 21 | * @param storeDir - The file path where project data is stored 22 | * @returns A Promise that resolves to an array of ProjectDetails 23 | * @throws Will throw an error if the file cannot be read 24 | * @throws Will throw an error if the file cannot be parsed 25 | * @throws Will throw an error if the file does not exist 26 | */ 27 | export async function getStoredProjectsList(storeDir: string): Promise { 28 | 29 | try { 30 | if (fs.existsSync(storeDir)) { 31 | const storedProjects = await fs.promises.readFile(storeDir, 'utf-8'); 32 | const parsed = JSON.parse(storedProjects) as ProjectDetails[]; 33 | return parsed.map(p => { 34 | p.last_opened = p.last_opened ? new Date(p.last_opened) : null; 35 | return p; 36 | }); 37 | 38 | } 39 | } catch (error) { 40 | logger.error('Failed to read stored project list', error); 41 | } 42 | 43 | return []; 44 | } 45 | 46 | /** 47 | * Removes a project from the stored projects list based on its path. 48 | * 49 | * @param storeDir - The directory path where project data is stored 50 | * @param projectPath - The path of the project to be removed from the list 51 | * @returns A Promise that resolves to the updated array of ProjectDetails after removal 52 | */ 53 | export async function removeProjectFromList(storeDir: string, projectPath: string): Promise { 54 | 55 | const projects = await getStoredProjectsList(storeDir); 56 | const updatedProjects = projects.filter(p => p.path !== projectPath); 57 | 58 | return storeProjectsList(storeDir, updatedProjects); 59 | } 60 | 61 | /** 62 | * Adds a project to the stored projects list. 63 | * 64 | * If the project already exists in the list (identified by its path), it will be replaced with the new project details. 65 | * Otherwise, the project will be added to the end of the list. 66 | * The list is then sorted by the last opened date, and stored. 67 | * 68 | * @async 69 | * @param {string} storeDir - The directory where the projects list is stored. 70 | * @param {ProjectDetails} project - The project details to add or update. 71 | * @returns {Promise} A promise that resolves to the updated list of project details. 72 | */ 73 | export async function addProjectToList(storeDir: string, project: ProjectDetails): Promise { 74 | 75 | const projects = await getStoredProjectsList(storeDir); 76 | 77 | // check if project path is already there and replace 78 | const existing = projects.findIndex(p => p.path === project.path); 79 | if (existing !== -1) { 80 | projects[existing] = project; 81 | } 82 | else { 83 | projects.push(project); 84 | } 85 | 86 | projects.sort((a, b) => (a.last_opened ?? new Date(0)).getTime() - (b.last_opened ?? new Date(0)).getTime()); 87 | 88 | return storeProjectsList(storeDir, projects); 89 | } -------------------------------------------------------------------------------- /src/ui/App.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotlauncher/launcher/7edbc83db18128c20b2e04d61c89d7eb1725878a/src/ui/App.css -------------------------------------------------------------------------------- /src/ui/assets/Nunito_Sans/NunitoSans-Italic-VariableFont_YTLC,opsz,wdth,wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotlauncher/launcher/7edbc83db18128c20b2e04d61c89d7eb1725878a/src/ui/assets/Nunito_Sans/NunitoSans-Italic-VariableFont_YTLC,opsz,wdth,wght.ttf -------------------------------------------------------------------------------- /src/ui/assets/Nunito_Sans/NunitoSans-VariableFont_YTLC,opsz,wdth,wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotlauncher/launcher/7edbc83db18128c20b2e04d61c89d7eb1725878a/src/ui/assets/Nunito_Sans/NunitoSans-VariableFont_YTLC,opsz,wdth,wght.ttf -------------------------------------------------------------------------------- /src/ui/assets/Nunito_Sans/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright 2016 The Nunito Sans Project Authors (https://github.com/Fonthausen/NunitoSans) 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | https://openfontlicense.org 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /src/ui/assets/Nunito_Sans/README.txt: -------------------------------------------------------------------------------- 1 | Nunito Sans Variable Font 2 | ========================= 3 | 4 | This download contains Nunito Sans as both variable fonts and static fonts. 5 | 6 | Nunito Sans is a variable font with these axes: 7 | YTLC 8 | opsz 9 | wdth 10 | wght 11 | 12 | This means all the styles are contained in these files: 13 | NunitoSans-VariableFont_YTLC,opsz,wdth,wght.ttf 14 | NunitoSans-Italic-VariableFont_YTLC,opsz,wdth,wght.ttf 15 | 16 | If your app fully supports variable fonts, you can now pick intermediate styles 17 | that aren’t available as static fonts. Not all apps support variable fonts, and 18 | in those cases you can use the static font files for Nunito Sans: 19 | 20 | Get started 21 | ----------- 22 | 23 | 1. Install the font files you want to use 24 | 25 | 2. Use your app's font picker to view the font family and all the 26 | available styles 27 | 28 | Learn more about variable fonts 29 | ------------------------------- 30 | 31 | https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts 32 | https://variablefonts.typenetwork.com 33 | https://medium.com/variable-fonts 34 | 35 | In desktop apps 36 | 37 | https://theblog.adobe.com/can-variable-fonts-illustrator-cc 38 | https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts 39 | 40 | Online 41 | 42 | https://developers.google.com/fonts/docs/getting_started 43 | https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide 44 | https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts 45 | 46 | Installing fonts 47 | 48 | MacOS: https://support.apple.com/en-us/HT201749 49 | Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux 50 | Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows 51 | 52 | Android Apps 53 | 54 | https://developers.google.com/fonts/docs/android 55 | https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts 56 | 57 | License 58 | ------- 59 | Please read the full license text (OFL.txt) to understand the permissions, 60 | restrictions and requirements for usage, redistribution, and modification. 61 | 62 | You can use them in your products & projects – print or digital, 63 | commercial or otherwise. 64 | 65 | This isn't legal advice, please consider consulting a lawyer and see the full 66 | license for all details. 67 | -------------------------------------------------------------------------------- /src/ui/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotlauncher/launcher/7edbc83db18128c20b2e04d61c89d7eb1725878a/src/ui/assets/icon.png -------------------------------------------------------------------------------- /src/ui/assets/icons/Discord-Symbol-Blurple.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ui/assets/icons/Discord-Symbol-White.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/ui/assets/icons/godot_icon_color.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 11 | 14 | 17 | 20 | 23 | 26 | -------------------------------------------------------------------------------- /src/ui/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godotlauncher/launcher/7edbc83db18128c20b2e04d61c89d7eb1725878a/src/ui/assets/logo.png -------------------------------------------------------------------------------- /src/ui/components/alert.component.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | interface AlertProps { 4 | icon?: React.ReactNode; 5 | title: string; 6 | message: string | ReactNode; 7 | onOk: () => void; 8 | } 9 | export const Alert: React.FC = ({ message, onOk, title, icon }) => { 10 | return ( 11 |
12 |
13 |

{icon}

{title}

14 |
15 | {typeof message === 'string' 16 | ? message.split('\n').map((line, index) =>

{line}

) 17 | : message 18 | } 19 |
20 |
21 | 22 |
23 |
24 |
25 | ); 26 | }; -------------------------------------------------------------------------------- /src/ui/components/closeButton.component.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { X } from 'lucide-react'; 3 | import React, { ComponentProps } from 'react'; 4 | 5 | type CloseButtonProps = ComponentProps<'button'>; 6 | 7 | export const CloseButton: React.FC = ({ key, onClick = () => { }, className = '' }) => { 8 | return ( 9 | 10 | ); 11 | }; -------------------------------------------------------------------------------- /src/ui/components/confirm.component.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | interface AlertProps { 4 | icon?: React.ReactNode; 5 | title: string; 6 | content: ReactNode; 7 | buttons?: ConfirmButtons[]; 8 | shouldClose: (callback?: () => void) => void; 9 | } 10 | 11 | export interface ConfirmButtons { 12 | isCancel?: boolean; 13 | typeClass: string; 14 | text: string; 15 | onClick?: () => boolean | void | Promise; 16 | } 17 | 18 | const onClickShouldClose = (callback?: () => boolean | void | Promise, shouldClose?: () => void) => { 19 | if (callback && callback()) { 20 | shouldClose?.(); 21 | } 22 | }; 23 | 24 | export const Confirm: React.FC = ({ content, buttons, title, icon, shouldClose }) => { 25 | return ( 26 |
27 |
28 |

{icon}

{title}

29 |
30 | {content} 31 |
32 |
33 | { 34 | buttons?.map((button, index) => ( 35 | button.isCancel 36 | ? 37 | : 38 | // 39 | // 40 | )) 41 | } 42 |
43 |
44 |
45 | ); 46 | }; -------------------------------------------------------------------------------- /src/ui/components/installReleaseTable.tsx: -------------------------------------------------------------------------------- 1 | import { HardDrive, HardDriveDownload } from 'lucide-react'; 2 | import { useRelease } from '../hooks/useRelease'; 3 | 4 | type InstallReleaseTableProps = { 5 | releases: ReleaseSummary[]; 6 | onInstall: (release: ReleaseSummary, mono: boolean) => void; 7 | }; 8 | 9 | export const InstallReleaseTable: React.FC = ({ releases, onInstall }) => { 10 | 11 | const { isInstalledRelease, isDownloadingRelease } = useRelease(); 12 | 13 | const installReleaseRequest = (release: ReleaseSummary, mono: boolean) => { 14 | onInstall(release, mono); 15 | }; 16 | 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | { 29 | releases.map((row, index) => ( 30 | 31 | 32 | 33 | 51 | 65 | 66 | )) 67 | } 68 | 69 | 70 |
VersionReleasedDownload
{row.version}{row.published_at?.split('T')[0]} 34 | { 35 | isInstalledRelease(row.version, false) 36 | ? (

37 | (GDScript) 38 |

) 39 | : isDownloadingRelease(row.version, false) 40 | ?

(GDScript) Installing...
41 | : (

42 | 48 |

) 49 | } 50 |
52 | 53 | { 54 | isInstalledRelease(row.version, true) 55 | ? (

56 | (.NET) 57 |

) 58 | : isDownloadingRelease(row.version, true) 59 | ?
(.NET) Installing...
60 | : (

61 | 62 |

) 63 | } 64 |
71 | ); 72 | }; -------------------------------------------------------------------------------- /src/ui/components/installedReleasesTable.tsx: -------------------------------------------------------------------------------- 1 | import { HardDrive } from 'lucide-react'; 2 | 3 | type InstalledReleaseTableProps = { 4 | releases: InstalledRelease[]; 5 | }; 6 | 7 | export const InstalledReleaseTable: React.FC = ({ releases }) => { 8 | 9 | 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | { 21 | releases.map((row, index) => ( 22 | 23 | 24 | 25 | 47 | 48 | )) 49 | } 50 | 51 | 52 |
VersionReleasedInstalled
{row.version}{row.published_at?.split('T')[0]} 26 | {!row.mono && ( 27 |

28 | {(row.install_path.length > 0) 29 | ? 30 | : <>

downloading... 31 | } 32 |

33 | )} 34 | 35 | {row.mono && 36 | ( 37 |

38 | 39 | {(row.install_path.length > 0) 40 | ?

(.NET)

41 | : <>
downloading (.NET)... 42 | } 43 | 44 |

45 | )} 46 |
53 | ); 54 | }; -------------------------------------------------------------------------------- /src/ui/components/settings/AutoStartSetting.component.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { TriangleAlert } from 'lucide-react'; 3 | import { usePreferences } from '../../hooks/usePreferences'; 4 | 5 | 6 | export const AutoStartSetting: React.FC = () => { 7 | 8 | const { preferences, setAutoStart } = usePreferences(); 9 | const { platform } = usePreferences(); 10 | 11 | return ( 12 |
13 |
14 |

Startup Preference

15 |

Choose what GD Launcher does when your computer starts

16 |
17 |
18 | {platform === 'linux' 19 | ? (Auto start currently is not supported on Linux) 20 | : (<> 21 | 25 | 29 | ) 30 | } 31 |
32 |
33 | ); 34 | }; -------------------------------------------------------------------------------- /src/ui/components/settings/EditorLocation.component.tsx: -------------------------------------------------------------------------------- 1 | import { Folder } from 'lucide-react'; 2 | import { useState } from 'react'; 3 | import { usePreferences } from '../../hooks/usePreferences'; 4 | 5 | export const EditorsLocation: React.FC = () => { 6 | const [dialogOpen, setDialogOpen] = useState(false); 7 | const { preferences, savePreferences } = usePreferences(); 8 | 9 | 10 | const selectInstallDir = async (currentPath: string) => { 11 | setDialogOpen(true); 12 | const result = await window.electron.openDirectoryDialog(currentPath, 'Select Install Directory'); 13 | if (!result.canceled) { 14 | if (preferences) { 15 | await savePreferences({ ...preferences, install_location: result.filePaths[0] }); 16 | } 17 | } 18 | setDialogOpen(false); 19 | }; 20 | 21 | return ( 22 | <> 23 | { 24 | dialogOpen && 25 |
26 |

Waiting for dialog...

27 |
28 | } 29 |
30 | 31 |
32 |

Editor Installs Location

33 |

Choose a location for Editor installs. Existing installs will not be affected.

34 |
35 | 47 |
48 | 49 | ); 50 | }; -------------------------------------------------------------------------------- /src/ui/components/settings/checkForUpdates.component.tsx: -------------------------------------------------------------------------------- 1 | import { ChangeEvent } from 'react'; 2 | import { usePreferences } from '../../hooks/usePreferences'; 3 | import { useApp } from '../../hooks/useApp'; 4 | 5 | export const CheckForUpdates: React.FC = () => { 6 | 7 | 8 | const { updateAvailable, installAndRelaunch, checkForAppUpdates } = useApp(); 9 | const { preferences, setAutoUpdates } = usePreferences(); 10 | 11 | const setAutoCheckUpdates = async (e: ChangeEvent) => { 12 | await setAutoUpdates(e.currentTarget.checked); 13 | }; 14 | 15 | 16 | return (
17 |
18 |

Updates

19 |

Configure how GD Launcher checks for updates

20 |
21 |
22 |
23 |
24 | 25 | Automatically check for updates 26 |
27 |
28 |
29 | 30 |
31 | {updateAvailable?.message} 32 |
33 | 34 |
35 | 36 | {(!updateAvailable || (updateAvailable && updateAvailable?.type === 'none')) && ( 37 | 38 | )} 39 | 40 | {updateAvailable && updateAvailable?.type === 'ready' && ( 41 |
42 | {updateAvailable?.version ? `Version ${updateAvailable?.version}` : 'A new version'} is available, restart Godot Launcher to install. 43 |
44 | )} 45 |
46 |
47 |
48 |
); 49 | }; -------------------------------------------------------------------------------- /src/ui/components/settings/gitToolSettings.component.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | export const GitToolSettings: React.FC = () => { 4 | 5 | const [tool, setTool] = useState(); 6 | 7 | const checkGit = async () => { 8 | const tools = await window.electron.getInstalledTools(); 9 | const git = tools.find(tool => tool.name === 'Git'); 10 | setTool(git); 11 | }; 12 | 13 | useEffect(() => { 14 | checkGit(); 15 | }, []); 16 | 17 | 18 | return ( 19 |
20 |
21 |

Git

22 |

Git is a distributed version control system for managing code changes and collaboration.

23 |
24 |
25 | 26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
Installed:{tool ? '✅' : '❌'}
Version:{tool?.version}
40 | 41 |
42 |
43 |
44 | ); 45 | }; -------------------------------------------------------------------------------- /src/ui/components/settings/projectLaunchAction.component.tsx: -------------------------------------------------------------------------------- 1 | import { ChangeEvent } from 'react'; 2 | import { usePreferences } from '../../hooks/usePreferences'; 3 | 4 | export const ProjectLaunchAction: React.FC = () => { 5 | const { preferences, savePreferences } = usePreferences(); 6 | 7 | const setProjectLaunchAction = async (e: ChangeEvent) => { 8 | if (preferences && e.target.value) { 9 | // validate the value 10 | if (['none', 'minimize', 'close_to_tray'].includes(e.target.value)) { 11 | await savePreferences({ ...preferences, post_launch_action: e.target.value as 'none' | 'minimize' | 'close_to_tray' }); 12 | } 13 | } 14 | }; 15 | 16 | return ( 17 |
18 |
19 |

Project Launch Action

20 |

Choose what GD Launcher does after launching a project

21 |
22 |
23 |
24 |
25 | 26 | Nothing 27 |
28 |
29 | 30 | Minimize 31 |
32 |
33 | 34 | Close to system tray 35 |
36 |
37 |
38 |
39 | ); 40 | }; -------------------------------------------------------------------------------- /src/ui/components/settings/projectsLocation.component.tsx: -------------------------------------------------------------------------------- 1 | import { Folder } from 'lucide-react'; 2 | import { useState } from 'react'; 3 | import { usePreferences } from '../../hooks/usePreferences'; 4 | 5 | export const ProjectsLocation: React.FC = () => { 6 | const [dialogOpen, setDialogOpen] = useState(false); 7 | const { preferences, savePreferences } = usePreferences(); 8 | 9 | 10 | const selectProjectDir = async (currentPath: string) => { 11 | setDialogOpen(true); 12 | const result = await window.electron.openDirectoryDialog(currentPath, 'Select Project Directory'); 13 | if (!result.canceled) { 14 | if (preferences) { 15 | await savePreferences({ ...preferences, projects_location: result.filePaths[0] }); 16 | } 17 | } 18 | setDialogOpen(false); 19 | }; 20 | 21 | return ( 22 | <> 23 | { 24 | dialogOpen && 25 |
26 |

Waiting for dialog...

27 |
28 | } 29 |
30 | 31 |
32 |

Project Location

33 |

Select a location for new projects. Existing projects will not be affected.

34 |
35 | 47 |
48 | 49 | ); 50 | }; -------------------------------------------------------------------------------- /src/ui/components/welcomeSteps/CustomizeBehaviorStep.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { ChangeEvent } from 'react'; 3 | 4 | import { usePreferences } from '../../hooks/usePreferences'; 5 | 6 | export const CustomizeBehaviorStep: React.FC = () => { 7 | 8 | const { preferences, updatePreferences, setAutoStart } = usePreferences(); 9 | 10 | const setAutoCheckUpdates = async (e: ChangeEvent) => { 11 | if (preferences) { 12 | await updatePreferences({ auto_check_updates: e.target.checked }); 13 | } 14 | }; 15 | 16 | const onAutoStartChanged = async (e: ChangeEvent) => { 17 | if (preferences) { 18 | await setAutoStart(e.target.checked, preferences.start_in_tray); 19 | } 20 | }; 21 | 22 | const setStartInTray = async (e: ChangeEvent) => { 23 | if (preferences) { 24 | await setAutoStart(preferences.auto_start, e.target.checked); 25 | } 26 | }; 27 | 28 | const setProjectLaunchAction = async (e: ChangeEvent) => { 29 | if (preferences && e.target.value) { 30 | // validate the value 31 | if (['none', 'minimize', 'close_to_tray'].includes(e.target.value)) { 32 | await updatePreferences({ post_launch_action: e.target.value as 'none' | 'minimize' | 'close_to_tray' }); 33 | } 34 | } 35 | }; 36 | 37 | return ( 38 |
39 |
40 |
41 |

After Launching a Project

42 |
43 |
44 | 45 | Nothing 46 |
47 |
48 | 49 | Minimize 50 |
51 |
52 | 53 | Close to system tray 54 |
55 |
56 |
57 |
58 | 59 |
60 | 61 | 62 |
63 |
64 | 65 | Automatically check for updates 66 |
67 |
68 | 69 |
70 |
71 |
72 | 76 | 80 |
81 |
82 |
83 | ); 84 | }; 85 | -------------------------------------------------------------------------------- /src/ui/components/welcomeSteps/SetLocationStep.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { EditorsLocation } from '../settings/EditorLocation.component'; 3 | import { ProjectsLocation } from '../settings/projectsLocation.component'; 4 | 5 | export const SetLocationStep: React.FC = () => { 6 | 7 | return ( 8 |
9 | 10 | 11 |
12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /src/ui/components/welcomeSteps/StartStep.tsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | export const StartStep: React.FC = () => { 5 | 6 | 7 | 8 | return ( 9 |
10 |

11 |

You're All Set! 🚀

12 |

Godot Launcher is ready to go! You can now manage your projects and explore all the features.

13 |

14 |
15 |

What's Next?

16 |
    17 |
  • Install and experiment with the latest prereleases in the Install tab.
  • 18 |
  • Quickly add or create projects in the Projects tab.
  • 19 |
  • Customize your experience just the way you like in the Settings tab.
  • 20 |
21 |
22 |
23 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /src/ui/components/welcomeSteps/WindowsStep.tsx: -------------------------------------------------------------------------------- 1 | import { ExternalLink } from 'lucide-react'; 2 | import { useAppNavigation } from '../../hooks/useAppNavigation'; 3 | 4 | 5 | 6 | export const WindowsStep: React.FC = () => { 7 | 8 | const { openExternalLink } = useAppNavigation(); 9 | 10 | return ( 11 |
12 |

Windows Note

13 |

14 | Starting from version 1.4.0, Godot Launcher creates{' '} 15 | symlinks to the editor for each project. 16 |

17 |
18 |

What changed?

19 |
    20 |
  • 21 | Creating symbolic links on Windows requires administrator privileges and elevated command execution. The launcher now elevates permissions only when creating symlinks. 22 |
  • 23 |
  • 24 | You will see a UAC prompt only if you are not an Administrator on your PC and if{' '} 25 | Developer Mode is not enabled. 26 |
  • 27 |
  • 28 | NOTE: If you are using .NET-based editors, you need to install the .NET SDK from{' '} 29 | 35 |
  • 36 |
37 |
38 |
39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /src/ui/components/welcomeSteps/currentSettingsStep.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { usePreferences } from '../../hooks/usePreferences'; 3 | 4 | type CurrentSettingsStepProps = { 5 | onSkip: () => void; 6 | }; 7 | 8 | export const CurrentSettingsStep: React.FC = ({ onSkip }) => { 9 | 10 | const [loading, setLoading] = useState(true); 11 | const { preferences } = usePreferences(); 12 | 13 | useEffect(() => { 14 | if (preferences) { 15 | setLoading(false); 16 | } 17 | }, [preferences]); 18 | 19 | const getPostLaunchText = (action: UserPreferences['post_launch_action']) => { 20 | switch (action) { 21 | case 'none': 22 | return 'Nothing'; 23 | case 'minimize': 24 | return 'Minimize'; 25 | case 'close_to_tray': 26 | return 'Close to system tray'; 27 | default: 28 | return 'Nothing'; 29 | } 30 | }; 31 | return ( 32 |
33 | {/* Default settings */} 34 | {loading &&

} 35 | {!loading && ( 36 | <> 37 |

Default Settings:

38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | { 67 | preferences?.auto_start && 68 | 69 | 70 | 71 | 72 | } 73 | 74 | 75 | 76 |
Projects Location:{preferences?.projects_location}
Godot Install Location:{preferences?.install_location}
Action After Launching a Project:{getPostLaunchText(preferences?.post_launch_action || 'close_to_tray')}
Auto Check for Updates:{preferences?.auto_check_updates ? 'Yes' : 'No'}
Auto Start When Computer Starts:{preferences?.auto_start ? 'Yes' : 'No'}
Auto Start Type:{preferences?.start_in_tray ? 'System Tray' : 'Normal Window'}
77 | 78 |
79 | 80 | 81 | ) 82 | } 83 | 84 | 85 | 86 |
87 | 88 | 89 | 90 | 91 | 92 | ); 93 | }; 94 | -------------------------------------------------------------------------------- /src/ui/components/welcomeSteps/linuxStep.tsx: -------------------------------------------------------------------------------- 1 | import { ExternalLink } from 'lucide-react'; 2 | import { useAppNavigation } from '../../hooks/useAppNavigation'; 3 | 4 | 5 | 6 | export const LinuxStep: React.FC = () => { 7 | const { openExternalLink } = useAppNavigation(); 8 | 9 | 10 | return ( 11 |
12 |

Linux Note

13 |

Godot Launcher uses symbolic links for each project

14 |
15 |

Why?

16 |
    17 |
  • 18 | Unlike Windows and MacOS version, Linux uses symbolic links to the editor to launch projects. 19 | This saves disk space and allows for per project settings. 20 |
  • 21 |
  • 22 | NOTE: If you are working with .NET Editors, you will need to install the .NET SDK from 23 | 24 |
  • 25 |
26 |
27 |
28 | 29 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /src/ui/components/welcomeSteps/macosStep.tsx: -------------------------------------------------------------------------------- 1 | import { ExternalLink } from 'lucide-react'; 2 | import { useAppNavigation } from '../../hooks/useAppNavigation'; 3 | 4 | 5 | 6 | export const MacOSStep: React.FC = () => { 7 | 8 | const { openExternalLink } = useAppNavigation(); 9 | 10 | 11 | return ( 12 |
13 |

macOS Note

14 |

The Godot Launcher creates a copy of the editor for each project.

15 |
16 |

Why?

17 |
    18 |
  • 19 | Creating symbolic links on macOS is not restricted by permissions, but opening the editor through a symlink has proven to be unreliable. To ensure a smooth experience, we provide a dedicated copy of the engine for each project instead. 20 | While Godot is only around 200MB—and this is generally not a problem—be aware that multiple projects can lead to increased disk usage. 21 |
  • 22 |
  • 23 | NOTE: If you are working with .NET Editors, you will need to install the .NET SDK from 24 | 25 |
  • 26 |
27 |
28 |
29 | 30 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /src/ui/components/welcomeSteps/welcomeStep.tsx: -------------------------------------------------------------------------------- 1 | import { ExternalLinkIcon } from 'lucide-react'; 2 | 3 | export const WelcomeStep: React.FC = () => { 4 | 5 | return ( 6 |
7 |

Thanks for using Godot Launcher!

8 |

Godot Launcher makes it easy to manage multiple versions of the Godot Engine and keeps editor settings separate for each project.

9 |

By using it, you're helping to support the project! If you ever have feedback or find a bug, let us know, we'd love to make it even better for you.

10 |

Enjoy and happy coding! 11 | 15 |

16 |
17 |
18 |
19 | 20 | 21 | 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/ui/constants.ts: -------------------------------------------------------------------------------- 1 | // LAUNCHER 2 | export const LAUNCHER_PAGE_URL = 'https://godotlauncher.org'; 3 | export const LAUNCHER_DOCS_URL = 'https://docs.godotlauncher.org'; 4 | export const LAUNCHER_CONTRIBUTE_URL = 'https://godotlauncher.org/contribute'; 5 | export const LAUNCHER_THIRD_PARTY_RAW_URL = 'https://raw.githubusercontent.com/godotlauncher/launcher/refs/heads/main/COPYRIGHT.txt'; 6 | 7 | // GItHub 8 | export const LAUNCHER_GITHUB_URL = 'https://guthub.com/godotlauncher/launcher'; 9 | export const LAUNCHER_GITHUB_ISSUES_URL = 'https://github.com/godotlauncher/launcher/issues'; 10 | export const LAUNCHER_GITHUB_PROPOSALS_URL = 'https://github.com/godotlauncher/launcher/issues'; 11 | 12 | 13 | // GODOT 14 | export const GODOT_PAGE_URL = 'https://godotengine.org'; 15 | export const GODOT_DOCS_URL = 'https://docs.godotengine.org'; 16 | 17 | // COMMUNITY 18 | export const COMMUNITY_PAGE_URL = 'https://godotlauncher.org/community'; 19 | export const COMMUNITY_DISCORD_URL = 'https://discord.gg/Ju9jkFJGvz'; 20 | 21 | -------------------------------------------------------------------------------- /src/ui/hooks/useAlerts.tsx: -------------------------------------------------------------------------------- 1 | import React, { PropsWithChildren, ReactNode, useContext, useState } from 'react'; 2 | import { Alert } from '../components/alert.component'; 3 | import { Confirm, ConfirmButtons } from '../components/confirm.component'; 4 | 5 | 6 | interface IAlert { 7 | icon?: React.ReactNode; 8 | title: string; 9 | message: ReactNode | string; 10 | } 11 | 12 | interface IConfirm { 13 | icon?: React.ReactNode; 14 | title: string; 15 | content: ReactNode; 16 | buttons?: ConfirmButtons[]; 17 | } 18 | 19 | 20 | type AlertContext = { 21 | clearAlerts: () => void; 22 | addAlert: (title: string, message: string | ReactNode, icon?: React.ReactNode) => void; 23 | closeAlert: () => void; 24 | addConfirm: (title: string, content: ReactNode, onOk: () => void, onCancel?: () => void, icon?: ReactNode) => void; 25 | addCustomConfirm: (title: string, content: ReactNode, buttons: ConfirmButtons[], icon?: ReactNode) => void; 26 | }; 27 | 28 | const AlertsContext = React.createContext({} as AlertContext); 29 | 30 | export const useAlerts = () => useContext(AlertsContext); 31 | 32 | 33 | export const AlertsProvider: React.FC = ({ children }) => { 34 | const [alerts, setAlerts] = useState([]); 35 | 36 | const [confirm, setConfirm] = useState(null); 37 | 38 | const clearAlerts = () => { 39 | setAlerts([]); 40 | }; 41 | 42 | const addAlert = (title: string, message: string | ReactNode, icon?: React.ReactNode) => { 43 | setAlerts([...alerts, { title, message, icon }]); 44 | }; 45 | 46 | const closeAlert = () => { 47 | setAlerts(alerts.slice(1)); 48 | }; 49 | 50 | const addCustomConfirm = (title: string, content: ReactNode, buttons: ConfirmButtons[], icon?: ReactNode) => { 51 | setConfirm({ title, content, buttons, icon }); 52 | }; 53 | 54 | const addConfirm = (title: string, content: ReactNode, onOk: () => void, onCancel?: () => void, icon?: ReactNode) => { 55 | setConfirm({ 56 | title, 57 | content, 58 | buttons: [{ typeClass: 'btn-primary', text: 'Ok', onClick: onOk }, { typeClass: 'btn-neutral', text: 'Cancel', onClick: onCancel }], 59 | icon, 60 | }); 61 | }; 62 | 63 | const closeConfirm = (callback?: () => void): void => { 64 | setConfirm(null); 65 | callback?.(); 66 | }; 67 | 68 | const showConfirm = () => { 69 | if (confirm) { 70 | return ; 71 | } 72 | 73 | return null; 74 | }; 75 | 76 | 77 | const showAlerts = () => { 78 | if (alerts.length > 0) { 79 | const error = alerts[0]; 80 | return ; 81 | } 82 | 83 | return null; 84 | }; 85 | 86 | return 93 | <> 94 | {showConfirm()} 95 | {showAlerts()} 96 | {children} 97 | 98 | ; 99 | }; -------------------------------------------------------------------------------- /src/ui/hooks/useApp.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, FC, PropsWithChildren, useContext, useEffect, useState } from 'react'; 2 | 3 | type AppContext = { 4 | appVersion: string | undefined; 5 | updateAvailable: AppUpdateMessage | undefined; 6 | installAndRelaunch: () => void; 7 | checkForAppUpdates: () => void; 8 | }; 9 | 10 | const appContext = createContext({} as AppContext); 11 | 12 | export const useApp = () => useContext(appContext); 13 | 14 | export const AppProvider: FC = ({ children }) => { 15 | 16 | const [updateAvailable, setUpdateAvailable] = useState(); 17 | const [appVersion, setAppVersion] = useState(); 18 | 19 | const installAndRelaunch = async () => { 20 | await window.electron.installUpdateAndRestart(); 21 | }; 22 | 23 | const checkForAppUpdates = async () => { 24 | await window.electron.checkForUpdates(); 25 | }; 26 | useEffect(() => { 27 | 28 | // get app version 29 | window.electron.getAppVersion().then(setAppVersion); 30 | 31 | const unsubscribeUpdates = window.electron.subscribeAppUpdates(setUpdateAvailable); 32 | return () => { 33 | unsubscribeUpdates(); 34 | }; 35 | }, []); 36 | 37 | return 38 | {children} 39 | ; 40 | }; -------------------------------------------------------------------------------- /src/ui/hooks/useAppNavigation.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, FC, PropsWithChildren, useContext, useState } from 'react'; 2 | 3 | export type View = 'projects' | 'installs' | 'settings' | 'help'; 4 | 5 | type AppNavigationContext = { 6 | currentView: View; 7 | setCurrentView: (view: View) => void; 8 | openExternalLink: (url: string) => void; 9 | 10 | }; 11 | 12 | 13 | const AppNavigationContext = createContext({} as AppNavigationContext); 14 | 15 | export const useAppNavigation = () => { 16 | const context = useContext(AppNavigationContext); 17 | if (!context) { 18 | throw new Error('useAppNavigation must be used within a AppNavigationProvider'); 19 | } 20 | return context; 21 | }; 22 | 23 | 24 | type AppNavigationProviderProps = PropsWithChildren; 25 | 26 | export const AppNavigationProvider: FC = ({ children }) => { 27 | const [currentView, setCurrentView] = useState('projects'); 28 | 29 | const openExternalLink = async (url: string) => { 30 | await window.electron.openExternal(url); 31 | }; 32 | 33 | return 34 | {children} 35 | ; 36 | }; 37 | -------------------------------------------------------------------------------- /src/ui/hooks/usePreferences.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, PropsWithChildren, useContext, useEffect, useState } from 'react'; 2 | 3 | interface AppPreferences { 4 | preferences: UserPreferences | null; 5 | savePreferences: (preferences: UserPreferences) => void; 6 | loadPreferences: () => Promise; 7 | updatePreferences: (preferences: Partial) => void; 8 | setAutoStart: (autoStart: boolean, hidden: boolean) => Promise; 9 | setAutoUpdates: (enabled: boolean) => Promise; 10 | platform: string; 11 | } 12 | 13 | const preferencesContext = createContext({} as AppPreferences); 14 | 15 | export const usePreferences = () => { 16 | const context = useContext(preferencesContext); 17 | if (!context) { 18 | throw new Error('usePreferences must be used within a PrefsProvider'); 19 | } 20 | return context; 21 | }; 22 | 23 | type AppPreferencesProviderProps = PropsWithChildren; 24 | 25 | export const PreferencesProvider: React.FC = ({ children }) => { 26 | const [preferences, setPreferences] = useState(null); 27 | const [platform, setPlatform] = useState(''); 28 | 29 | useEffect(() => { 30 | window.electron.getPlatform().then(setPlatform); 31 | loadPreferences(); 32 | }, []); 33 | 34 | const updatePreferences = async (newPrefs: Partial) => { 35 | const prefs = { ...preferences, ...newPrefs } as UserPreferences; 36 | savePreferences(prefs).then(setPreferences); 37 | }; 38 | 39 | 40 | const loadPreferences = async () => { 41 | const preferences = await window.electron.getUserPreferences(); 42 | setPreferences(preferences); 43 | return preferences; 44 | }; 45 | 46 | const savePreferences = async (preferences: UserPreferences) => { 47 | const newPreferences = await window.electron.setUserPreferences(preferences); 48 | setPreferences({ ...newPreferences }); 49 | return newPreferences; 50 | }; 51 | 52 | const setAutoStart = async (autoStart: boolean, hidden: boolean): Promise => { 53 | 54 | const result = await window.electron.setAutoStart(autoStart, hidden); 55 | await loadPreferences(); 56 | return result; 57 | 58 | }; 59 | 60 | const setAutoUpdates = async (enabled: boolean): Promise => { 61 | const result = await window.electron.setAutoCheckUpdates(enabled); 62 | await loadPreferences(); 63 | return result; 64 | }; 65 | 66 | return {children}; 76 | }; -------------------------------------------------------------------------------- /src/ui/hooks/useProjects.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, FC, PropsWithChildren, useContext, useEffect, useState } from 'react'; 2 | 3 | 4 | interface ProjectsContext { 5 | projects: ProjectDetails[]; 6 | loading: boolean; 7 | addProject: (projectPath: string) => Promise; 8 | setProjectEditor: (project: ProjectDetails, release: InstalledRelease) => Promise; 9 | openProjectFolder: (project: ProjectDetails) => Promise; 10 | showProjectMenu: (project: ProjectDetails) => Promise; 11 | openProjectEditorFolder: (project: ProjectDetails) => Promise; 12 | removeProject: (project: ProjectDetails) => Promise; 13 | launchProject: (project: ProjectDetails) => Promise; 14 | refreshProjects: () => Promise; 15 | checkProjectValid: (project: ProjectDetails) => Promise; 16 | createProject: (name: string, release: InstalledRelease, renderer: RendererType[5], withVSCode: boolean, withGit: boolean) => Promise; 17 | } 18 | 19 | export const ProjectsContext = createContext({} as ProjectsContext); 20 | 21 | export const useProjects = () => { 22 | const context = useContext(ProjectsContext); 23 | if (!context) { 24 | throw new Error('useProjects must be used within a ProjectsProvider'); 25 | } 26 | return context; 27 | }; 28 | 29 | type ProjectsProviderProps = PropsWithChildren; 30 | 31 | export const ProjectsProvider: FC = ({ children }) => { 32 | 33 | const [projects, setProjects] = useState([]); 34 | const [loading, setLoading] = useState(true); 35 | 36 | const getProjects = async () => { 37 | setLoading(true); 38 | const projects = await window.electron.getProjectsDetails(); 39 | setProjects(projects); 40 | setLoading(false); 41 | }; 42 | 43 | const createProject = async (projectName: string, release: InstalledRelease, renderer: RendererType[5], withVSCode: boolean, withGit: boolean) => { 44 | const result = await window.electron.createProject( 45 | projectName, 46 | release, 47 | renderer, 48 | withVSCode, 49 | withGit); 50 | 51 | if (result.success) { 52 | await refreshProjects(); 53 | } 54 | 55 | return result; 56 | }; 57 | 58 | const addProject = async (projectPath: string) => { 59 | const addResult = await window.electron.addProject(projectPath); 60 | if (addResult.success) { 61 | setProjects(addResult.projects!); 62 | } 63 | return addResult; 64 | }; 65 | 66 | const setProjectEditor = async (project: ProjectDetails, release: InstalledRelease) => { 67 | const result = await window.electron.setProjectEditor(project, release); 68 | if (result.success) { 69 | setProjects(result.projects!); 70 | } 71 | 72 | return result; 73 | }; 74 | 75 | const openProjectFolder = async (project: ProjectDetails) => { 76 | await window.electron.openShellFolder(project.path); 77 | }; 78 | 79 | const openProjectEditorFolder = async (project: ProjectDetails) => { 80 | await window.electron.openShellFolder(project.editor_settings_path); 81 | }; 82 | 83 | const removeProject = async (project: ProjectDetails) => { 84 | const result = await window.electron.removeProject(project); 85 | setProjects(result); 86 | }; 87 | 88 | const launchProject = async (project: ProjectDetails) => { 89 | const all = await window.electron.checkAllProjectsValid(); 90 | setProjects(all); 91 | 92 | const p = all.find(p => p.path === project.path); 93 | 94 | if (p && p?.valid) { 95 | await window.electron.launchProject(project); 96 | } 97 | 98 | return p?.valid ?? false; 99 | 100 | }; 101 | 102 | const refreshProjects = async () => { 103 | getProjects(); 104 | }; 105 | 106 | const checkProjectValid = (project: ProjectDetails) => { 107 | const result = window.electron.checkProjectValid(project); 108 | return result; 109 | }; 110 | 111 | const showProjectMenu = async (project: ProjectDetails) => { 112 | await window.electron.showProjectMenu(project); 113 | }; 114 | 115 | useEffect(() => { 116 | const off = window.electron.subscribeProjects(setProjects); 117 | getProjects(); 118 | 119 | return () => { 120 | off(); 121 | }; 122 | }, []); 123 | 124 | return ( 125 | 139 | {children} 140 | 141 | ); 142 | 143 | }; 144 | -------------------------------------------------------------------------------- /src/ui/hooks/useTheme.tsx: -------------------------------------------------------------------------------- 1 | import React, { PropsWithChildren } from 'react'; 2 | 3 | export type ThemeMode = 'dark' | 'light' | 'auto'; 4 | export type ThemeProviderContext = { 5 | systemTheme: ThemeMode; 6 | theme: ThemeMode | null; 7 | setTheme: (theme: ThemeMode) => void; 8 | }; 9 | 10 | const getStoredTheme = () => { 11 | const storedTheme = localStorage.getItem('theme') as ThemeMode | null; 12 | if (storedTheme === 'dark' || storedTheme === 'light') { 13 | return storedTheme; 14 | } 15 | return 'auto'; 16 | }; 17 | 18 | const setStoredTheme = (theme: ThemeMode) => { 19 | localStorage.setItem('theme', theme); 20 | }; 21 | 22 | const themeContext = React.createContext({} as ThemeProviderContext); 23 | 24 | /* eslint-disable-next-line react-refresh/only-export-components */ 25 | export const useTheme = () => React.useContext(themeContext); 26 | 27 | type ThemeProviderProps = PropsWithChildren; 28 | 29 | export const ThemeProvider: React.FC = ({ children }) => { 30 | const [theme, setTheme] = React.useState(getStoredTheme()); 31 | 32 | const updateDocumentTheme = (theme: ThemeMode) => { 33 | 34 | const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; 35 | 36 | const isDark = theme === 'dark' || (!('theme' in localStorage) && systemPrefersDark); 37 | 38 | if (theme !== 'auto') { 39 | document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light'); 40 | } 41 | else { 42 | document.documentElement.removeAttribute('data-theme'); 43 | } 44 | 45 | setStoredTheme(theme); 46 | 47 | }; 48 | 49 | const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; 50 | 51 | React.useEffect(() => { 52 | const storedTheme = localStorage.getItem('theme') as ThemeMode | null; 53 | 54 | if (storedTheme === 'dark' || storedTheme === 'light') { 55 | setTheme(storedTheme); 56 | } else { 57 | setTheme('auto'); 58 | } 59 | 60 | updateDocumentTheme(storedTheme || 'auto'); 61 | 62 | }, []); 63 | 64 | React.useEffect(() => { 65 | updateDocumentTheme(theme); 66 | localStorage.setItem('theme', theme); 67 | 68 | updateDocumentTheme(theme); 69 | 70 | }, [theme]); 71 | 72 | 73 | return ( 74 | 75 | {children} 76 | 77 | ); 78 | }; 79 | -------------------------------------------------------------------------------- /src/ui/index.css: -------------------------------------------------------------------------------- 1 | @import './styles/buttons.css'; 2 | 3 | @tailwind base; 4 | @tailwind components; 5 | @tailwind utilities; 6 | 7 | @custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *)); 8 | /* @custom-variant dark (&:where(.dark, .dark *)); */ 9 | 10 | @font-face { 11 | font-family: 'Nunito Sans'; 12 | src: url('/assets/Nunito_Sans/NunitoSans-VariableFont_YTLC,opsz,wdth,wght.ttf') format('truetype'); 13 | font-weight: 100 900; 14 | font-style: normal; 15 | } 16 | 17 | @font-face { 18 | font-family: 'Nunito Sans'; 19 | src: url('/assets/Nunito_Sans/NunitoSans-Italic-VariableFont_YTLC,opsz,wdth,wght.ttf') format('truetype'); 20 | font-weight: 100 900; 21 | font-style: italic; 22 | } 23 | 24 | html, 25 | body, 26 | #root { 27 | height: 100%; 28 | width: 100%; 29 | margin: 0; 30 | padding: 0; 31 | 32 | background-color: "#1d232a"; 33 | 34 | font-family: 'Nunito Sans', sans-serif; 35 | font-size: 16px; 36 | 37 | @apply select-none; 38 | /* @apply bg-slate-100 text-slate-900 dark:bg-slate-800 dark:text-slate-100; */ 39 | } 40 | 41 | /* *:focus { 42 | user-select: none; 43 | outline: none !important; 44 | 45 | } */ -------------------------------------------------------------------------------- /src/ui/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App.tsx'; 4 | import './index.css'; 5 | import { ThemeProvider } from './hooks/useTheme.tsx'; 6 | import { PreferencesProvider } from './hooks/usePreferences.tsx'; 7 | import { ReleaseProvider } from './hooks/useRelease.tsx'; 8 | import { AlertsProvider } from './hooks/useAlerts.tsx'; 9 | import { ProjectsProvider } from './hooks/useProjects.tsx'; 10 | import { AppNavigationProvider } from './hooks/useAppNavigation.tsx'; 11 | import { AppProvider } from './hooks/useApp.tsx'; 12 | 13 | ReactDOM.createRoot(document.getElementById('root')!).render( 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | , 31 | ); 32 | -------------------------------------------------------------------------------- /src/ui/styles/buttons.css: -------------------------------------------------------------------------------- 1 | .btn-primary { 2 | @apply text-slate-50 !important; 3 | } 4 | 5 | .btn-primary:disabled { 6 | @apply text-slate-50/50 bg-primary/50 !important; 7 | } 8 | 9 | /* .btn { 10 | @apply py-2 px-4 rounded font-sans font-bold text-base; 11 | @apply bg-slate-200 text-slate-900; 12 | 13 | 14 | @apply dark:bg-slate-800 dark:text-slate-100; 15 | 16 | @apply border-none; 17 | 18 | } 19 | 20 | .btn:hover { 21 | @apply bg-slate-300 text-slate-900; 22 | @apply dark:bg-slate-800/90 dark:text-slate-100; 23 | } 24 | 25 | .btn:active { 26 | @apply bg-slate-300/80 text-slate-900; 27 | @apply dark:bg-slate-800/80 dark:text-slate-100; 28 | } 29 | 30 | .btn:disabled { 31 | @apply bg-slate-200/50 text-slate-900/50 cursor-not-allowed; 32 | @apply dark:bg-slate-800/50 dark:text-slate-100/50; 33 | } 34 | 35 | 36 | .btn.btn-primary { 37 | @apply bg-blue-500 text-slate-100; 38 | 39 | @apply dark:bg-blue-700 dark:text-slate-100; 40 | } 41 | 42 | .btn.btn-primary:hover { 43 | @apply bg-blue-500/90; 44 | } 45 | 46 | .btn.btn-primary:active { 47 | @apply bg-blue-500/80; 48 | } 49 | 50 | .btn.btn-primary:disabled { 51 | @apply bg-blue-500/50 text-slate-50/50 cursor-not-allowed; 52 | } */ -------------------------------------------------------------------------------- /src/ui/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly VITE_APP_VERSION: string; 5 | } 6 | 7 | interface ImportMeta { 8 | readonly env: ImportMetaEnv; 9 | } 10 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | 3 | // ************* 4 | // * Note on Menu Icon Colors 5 | // * The light and dark colours are set in the png files. 6 | // * the filenames _dark and _light represent the current theme not the icon Colour. 7 | // * The icon colours directly set in fonts.google.com/icons and downloaded as pngs. 8 | // * The icons size is 24px 9 | // * Light Colour: #1F2937 10 | // * Dark Colour: #A6ADBB 11 | // ************* 12 | 13 | export default { 14 | content: [ 15 | './index.html', 16 | './src/**/*.{js,ts,jsx,tsx}', 17 | ], 18 | darkMode: ['selector', '[data-theme="dark"]'], 19 | theme: { 20 | extend: {}, 21 | }, 22 | plugins: [ 23 | require('@tailwindcss/typography'), 24 | require('daisyui') 25 | ], 26 | 27 | daisyui: { 28 | themes: [{ 29 | light: 30 | { 31 | ...require('daisyui/src/theming/themes')['light'], 32 | primary: '#3C77C2', 33 | secondary: '#EB9486', 34 | background: '#F9FAFB', 35 | }, 36 | dark: 37 | { 38 | ...require('daisyui/src/theming/themes')['dark'], 39 | primary: '#3C77C2', 40 | secondary: '#EB9486', 41 | background: '#1D232A', 42 | } 43 | }], 44 | 45 | }, 46 | }; 47 | 48 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": [ 6 | "ES2020", 7 | "DOM", 8 | "DOM.Iterable" 9 | ], 10 | "module": "ESNext", 11 | "skipLibCheck": true, 12 | /* Bundler mode */ 13 | "moduleResolution": "bundler", 14 | "allowImportingTsExtensions": true, 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true, 18 | "jsx": "react-jsx", 19 | /* Linting */ 20 | "strict": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "noFallthroughCasesInSwitch": true, 24 | "types": [ 25 | "./types" 26 | ] 27 | }, 28 | "include": [ 29 | "src" 30 | ], 31 | "exclude": [ 32 | "src/electron" 33 | ], 34 | "references": [ 35 | { 36 | "path": "./tsconfig.node.json" 37 | } 38 | ] 39 | } -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true, 9 | }, 10 | "include": [ 11 | "vite.config.ts" 12 | ] 13 | } -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | import { version } from './package.json'; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [react()], 9 | base: './', 10 | build: { 11 | outDir: 'dist-react' 12 | }, 13 | server: { 14 | port: 5123, 15 | strictPort: true 16 | }, 17 | define: { 18 | 'import.meta.env.VITE_APP_VERSION': JSON.stringify(version) 19 | } 20 | 21 | }); 22 | 23 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { defineConfig } from 'vite'; 4 | 5 | export default defineConfig({ 6 | test: { 7 | onConsoleLog(log, type) { 8 | // eslint-disable-next-line no-console 9 | console[type](log); 10 | }, 11 | }, 12 | }); 13 | --------------------------------------------------------------------------------