├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── help_wanted.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── build.yml │ └── ci.yml ├── .gitignore ├── .npmrc ├── .vscode ├── .debug.script.mjs ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── README.md ├── build └── installer.nsh ├── electron-builder.json5 ├── electron ├── electron-env.d.ts ├── main │ ├── class │ │ ├── appData.ts │ │ ├── appGlobals.ts │ │ ├── config.ts │ │ ├── files.ts │ │ ├── functions.ts │ │ ├── installedMod.ts │ │ ├── ipcHandler.ts │ │ ├── mod.ts │ │ ├── modSource.ts │ │ ├── modVersion.ts │ │ ├── modWorker.ts │ │ ├── onlineCheck.ts │ │ ├── regionInfo.ts │ │ └── updater.ts │ └── index.ts └── preload │ └── index.ts ├── index.html ├── package.json ├── public ├── modmanager.ico └── modmanager.png ├── src ├── App.vue ├── assets │ ├── account.png │ ├── add.png │ ├── bottom.png │ ├── cn.png │ ├── credits.png │ ├── cross.png │ ├── delete.png │ ├── deleteHover.png │ ├── discord.png │ ├── discordHover.png │ ├── download.png │ ├── downloadHover.png │ ├── edit.png │ ├── en.png │ ├── es.png │ ├── faq.png │ ├── favorite.png │ ├── favoriteFilled.png │ ├── fr.png │ ├── github.png │ ├── info.png │ ├── installer_large.bmp │ ├── installer_mini.bmp │ ├── jp.png │ ├── left.png │ ├── list.png │ ├── modmanager.ico │ ├── modmanager.png │ ├── modmanager_logo.png │ ├── mods.png │ ├── news.png │ ├── play.png │ ├── playHover.png │ ├── right.png │ ├── roadmap.png │ ├── servers.png │ ├── settings.png │ ├── shortcut.png │ ├── shortcutHover.png │ ├── title.png │ ├── top.png │ ├── update.png │ ├── updateHover.png │ ├── valid.png │ └── warn.png ├── components │ ├── AddLocal.vue │ ├── AppSettings.vue │ ├── ConfirmPopin.vue │ ├── CreditsPage.vue │ ├── LoadingPage.vue │ ├── MenuLeft.vue │ ├── ModCard.vue │ ├── ModsInstalled.vue │ ├── ModsStore.vue │ ├── ServersList.vue │ └── UpdatingPage.vue ├── import │ ├── router.ts │ └── store.ts ├── main.ts ├── tailwind.css └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.flat.txt └── vite.config.ts /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: 🐞 Bug report 4 | about: Create a report to help us improve 5 | title: "[Bug] the title of bug report" 6 | labels: bug 7 | assignees: '' 8 | 9 | --- 10 | 11 | #### Describe the bug 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/help_wanted.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🥺 Help wanted 3 | about: Confuse about the use of electron-vue-vite 4 | title: "[Help] the title of help wanted report" 5 | labels: help wanted 6 | assignees: '' 7 | 8 | --- 9 | 10 | #### Describe the problem you confuse 11 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Description 4 | 5 | 6 | 7 | ### What is the purpose of this pull request? 8 | 9 | - [ ] Bug fix 10 | - [ ] New Feature 11 | - [ ] Documentation update 12 | - [ ] Other 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "monthly" 12 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths-ignore: 7 | - "**.md" 8 | - "**.spec.js" 9 | - ".idea" 10 | - ".vscode" 11 | - ".dockerignore" 12 | - "Dockerfile" 13 | - ".gitignore" 14 | - ".github/**" 15 | - "!.github/workflows/build.yml" 16 | 17 | jobs: 18 | build: 19 | runs-on: ${{ matrix.os }} 20 | 21 | strategy: 22 | matrix: 23 | os: [macos-latest, ubuntu-latest, windows-latest] 24 | 25 | steps: 26 | - name: Checkout Code 27 | uses: actions/checkout@v3 28 | 29 | - name: Setup Node.js 30 | uses: actions/setup-node@v3 31 | with: 32 | node-version: 18 33 | 34 | - name: Install Dependencies 35 | run: npm install 36 | 37 | - name: Build Release Files 38 | run: npm run build 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | 42 | - name: Upload Artifact 43 | uses: actions/upload-artifact@v3 44 | with: 45 | name: release_on_${{ matrix. os }} 46 | path: release/ 47 | retention-days: 5 -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request_target: 5 | branches: 6 | - main 7 | 8 | permissions: 9 | pull-requests: write 10 | 11 | jobs: 12 | job1: 13 | name: Check Not Allowed File Changes 14 | runs-on: ubuntu-latest 15 | outputs: 16 | markdown_change: ${{ steps.filter_markdown.outputs.change }} 17 | markdown_files: ${{ steps.filter_markdown.outputs.change_files }} 18 | steps: 19 | 20 | - name: Check Not Allowed File Changes 21 | uses: dorny/paths-filter@v2 22 | id: filter_not_allowed 23 | with: 24 | list-files: json 25 | filters: | 26 | change: 27 | - 'package-lock.json' 28 | - 'yarn.lock' 29 | - 'pnpm-lock.yaml' 30 | 31 | # ref: https://github.com/github/docs/blob/main/.github/workflows/triage-unallowed-contributions.yml 32 | - name: Comment About Changes We Can't Accept 33 | if: ${{ steps.filter_not_allowed.outputs.change == 'true' }} 34 | uses: actions/github-script@v6 35 | with: 36 | script: | 37 | let workflowFailMessage = "It looks like you've modified some files that we can't accept as contributions." 38 | try { 39 | const badFilesArr = [ 40 | 'package-lock.json', 41 | 'yarn.lock', 42 | 'pnpm-lock.yaml', 43 | ] 44 | const badFiles = badFilesArr.join('\n- ') 45 | const reviewMessage = `👋 Hey there spelunker. It looks like you've modified some files that we can't accept as contributions. The complete list of files we can't accept are:\n- ${badFiles}\n\nYou'll need to revert all of the files you changed in that list using [GitHub Desktop](https://docs.github.com/en/free-pro-team@latest/desktop/contributing-and-collaborating-using-github-desktop/managing-commits/reverting-a-commit) or \`git checkout origin/main \`. Once you get those files reverted, we can continue with the review process. :octocat:\n\nMore discussion:\n- https://github.com/electron-vite/electron-vite-vue/issues/192` 46 | createdComment = await github.rest.issues.createComment({ 47 | owner: context.repo.owner, 48 | repo: context.repo.repo, 49 | issue_number: context.payload.number, 50 | body: reviewMessage, 51 | }) 52 | workflowFailMessage = `${workflowFailMessage} Please see ${createdComment.data.html_url} for details.` 53 | } catch(err) { 54 | console.log("Error creating comment.", err) 55 | } 56 | core.setFailed(workflowFailMessage) 57 | 58 | - name: Check Not Linted Markdown 59 | if: ${{ always() }} 60 | uses: dorny/paths-filter@v2 61 | id: filter_markdown 62 | with: 63 | list-files: shell 64 | filters: | 65 | change: 66 | - added|modified: '*.md' 67 | 68 | 69 | job2: 70 | name: Lint Markdown 71 | runs-on: ubuntu-latest 72 | needs: job1 73 | if: ${{ always() && needs.job1.outputs.markdown_change == 'true' }} 74 | steps: 75 | - name: Checkout Code 76 | uses: actions/checkout@v3 77 | with: 78 | ref: ${{ github.event.pull_request.head.sha }} 79 | 80 | - name: Lint markdown 81 | run: npx markdownlint-cli ${{ needs.job1.outputs.markdown_files }} --ignore node_modules -------------------------------------------------------------------------------- /.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 | node_modules 11 | dist 12 | dist-ssr 13 | dist-electron 14 | release 15 | *.local 16 | 17 | # Editor directories and files 18 | .vscode/.debug.env 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | 27 | # lockfile 28 | package-lock.json 29 | pnpm-lock.yaml 30 | yarn.lock 31 | 32 | src/compiled.css -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # For electron-builder 2 | # https://github.com/electron-userland/electron-builder/issues/6289#issuecomment-1042620422 3 | shamefully-hoist=true 4 | 5 | # For China 🇨🇳 developers 6 | # electron_mirror=https://npmmirror.com/mirrors/electron/ 7 | -------------------------------------------------------------------------------- /.vscode/.debug.script.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import path from 'node:path' 3 | import { fileURLToPath } from 'node:url' 4 | import { createRequire } from 'node:module' 5 | import { spawn } from 'node:child_process' 6 | 7 | const pkg = createRequire(import.meta.url)('../package.json') 8 | const __dirname = path.dirname(fileURLToPath(import.meta.url)) 9 | 10 | // write .debug.env 11 | const envContent = Object.entries(pkg.debug.env).map(([key, val]) => `${key}=${val}`) 12 | fs.writeFileSync(path.join(__dirname, '.debug.env'), envContent.join('\n')) 13 | 14 | // bootstrap 15 | spawn( 16 | // TODO: terminate `npm run dev` when Debug exits. 17 | process.platform === 'win32' ? 'npm.cmd' : 'npm', 18 | ['run', 'dev'], 19 | { 20 | stdio: 'inherit', 21 | env: Object.assign(process.env, { VSCODE_DEBUG: 'true' }), 22 | }, 23 | ) 24 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "Vue.volar", 4 | "Vue.vscode-typescript-vue-plugin" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "compounds": [ 7 | { 8 | "name": "Debug App", 9 | "preLaunchTask": "Before Debug", 10 | "configurations": [ 11 | "Debug Main Process", 12 | "Debug Renderer Process" 13 | ], 14 | "presentation": { 15 | "hidden": false, 16 | "group": "", 17 | "order": 1 18 | }, 19 | "stopAll": true 20 | } 21 | ], 22 | "configurations": [ 23 | { 24 | "name": "Debug Main Process", 25 | "type": "node", 26 | "request": "launch", 27 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron", 28 | "windows": { 29 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd" 30 | }, 31 | "runtimeArgs": [ 32 | "--remote-debugging-port=9229", 33 | "." 34 | ], 35 | "envFile": "${workspaceFolder}/.vscode/.debug.env", 36 | "console": "integratedTerminal" 37 | }, 38 | { 39 | "name": "Debug Renderer Process", 40 | "port": 9229, 41 | "request": "attach", 42 | "type": "chrome", 43 | "timeout": 60000, 44 | "skipFiles": [ 45 | "/**", 46 | "${workspaceRoot}/node_modules/**", 47 | "${workspaceRoot}/dist-electron/**", 48 | // Skip files in host(VITE_DEV_SERVER_URL) 49 | "http://127.0.0.1:3344/**" 50 | ] 51 | }, 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.tsc.autoDetect": "off", 4 | "json.schemas": [ 5 | { 6 | "fileMatch": [ 7 | "/*electron-builder.json5", 8 | "/*electron-builder.json" 9 | ], 10 | "url": "https://json.schemastore.org/electron-builder" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "Before Debug", 8 | "type": "shell", 9 | "command": "node .vscode/.debug.script.mjs", 10 | "isBackground": true, 11 | "problemMatcher": { 12 | "owner": "typescript", 13 | "fileLocation": "relative", 14 | "pattern": { 15 | // TODO: correct "regexp" 16 | "regexp": "^([a-zA-Z]\\:\/?([\\w\\-]\/?)+\\.\\w+):(\\d+):(\\d+): (ERROR|WARNING)\\: (.*)$", 17 | "file": 1, 18 | "line": 3, 19 | "column": 4, 20 | "code": 5, 21 | "message": 6 22 | }, 23 | "background": { 24 | "activeOnStart": true, 25 | "beginsPattern": "^.*VITE v.* ready in \\d* ms.*$", 26 | "endsPattern": "^.*\\[startup\\] Electron App.*$" 27 | } 28 | } 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | GitHub Downloads 3 | Curseforge Downloads 4 |

5 | 6 | # Mod Manager is no longer maintained! 7 | 8 | # Mod Manager 9 | Among Us Mod Manager is an open-source launcher for the popular game Among Us. It allows you to switch between many established mods with only a few clicks! 10 | 11 | Join the discord: https://goodloss.fr/discord \ 12 | Official website: https://goodloss.fr 13 | 14 | ![ModManager_7](https://amodsus.com/attachments/modmanager7-png.27601/) 15 | 16 | ## How to install? 17 | 18 | 1. Download the latest version available [here](https://goodloss.fr/latest). 19 | 20 | 3. Launch the installer and choose a location for the software. (You should not select your Among Us directory!) 21 | 22 | 4. Everything is set up! :) 23 | 24 | ## How to use it? 25 | 26 | - Launch Mod Manager from windows (use the shortcut or the search bar). 27 | 28 | - A window will open. 29 | 30 | - Choose a mod and download it by using the download button on the left side. 31 | 32 | - Once done, start a mod by clicking the launch button, which replaced the download button. 33 | 34 | - You are ready to play! :) 35 | 36 | ## Making a mod compatible with Mod Manager 37 | 38 | Publish a release in a public GitHub repository for your mod. In this release, you can add either (or both): 39 | 40 | - The .dll file of the mod 41 | 42 | - A .zip with of the Among Us modded directory 43 | 44 | Then, join the [discord](https://goodloss.fr/discord) and send your repository link. It will be a pleasure adding your mod. :) 45 | 46 | ## Credits & Resources 47 | 48 | [BepInEx](https://github.com/NuclearPowered/BepInEx) - The main dependency of every mod\ 49 | [Reactor](https://github.com/NuclearPowered/Reactor) - The most used modding API for Among Us 50 | 51 | Thanks to every mod creator. Go check their respective GitHub repositories directly in Mod Manager! 52 | 53 | If your mod is on Mod Manager and you do not want me to include it, just send me a message in a GitHub issue or in discord DM. I will remove them without asking any question. 54 | 55 | ## License 56 | 57 | This software is distributed under the GNU GPLv3 License. BepInEx is distributed under LGPL-3.0 License. 58 | -------------------------------------------------------------------------------- /build/installer.nsh: -------------------------------------------------------------------------------- 1 | !macro customInstall 2 | WriteRegStr HKCR "modmanager" "" "Mod Manager 7" 3 | WriteRegStr HKCR "modmanager" "URL Protocol" "" 4 | WriteRegStr HKCR "modmanager\\DefaultIcon" "" "$INSTDIR\\Mod Manager 7.exe,1" 5 | WriteRegStr HKCR "modmanager\\shell" "" "" 6 | WriteRegStr HKCR "modmanager\\shell\\open" "" "" 7 | WriteRegStr HKCR "modmanager\\shell\\open\\command" "" '"$INSTDIR\\Mod Manager 7.exe" "%1"' 8 | !macroend 9 | 10 | !macro customUnInstall 11 | DeleteRegKey HKCR "modmanager7" 12 | !macroend -------------------------------------------------------------------------------- /electron-builder.json5: -------------------------------------------------------------------------------- 1 | // @see https://www.electron.build/configuration/configuration 2 | { 3 | "$schema": "https://raw.githubusercontent.com/electron-userland/electron-builder/master/packages/app-builder-lib/scheme.json", 4 | "appId": "modmanager7", 5 | "asar": true, 6 | "productName": "Mod Manager 7", 7 | "directories": { 8 | "output": "release/${version}" 9 | }, 10 | "files": [ 11 | "dist", 12 | "dist-electron" 13 | ], 14 | "mac": { 15 | "target": [ 16 | "dmg" 17 | ], 18 | "artifactName": "${productName}-Mac-Installer.${ext}" 19 | }, 20 | "win": { 21 | "target": [ 22 | { 23 | "target": "nsis", 24 | "arch": [ 25 | "x64" 26 | ] 27 | } 28 | ], 29 | "artifactName": "${productName}-Windows-Installer.${ext}", 30 | "icon": "public/modmanager.png" 31 | }, 32 | "nsis": { 33 | "oneClick": false, 34 | "perMachine": false, 35 | "installerIcon": "public/modmanager.ico", 36 | "uninstallerIcon": "public/modmanager.ico", 37 | "allowToChangeInstallationDirectory": true, 38 | "deleteAppDataOnUninstall": false, 39 | "include": "build/installer.nsh" 40 | }, 41 | "linux": { 42 | "target": [ 43 | "AppImage" 44 | ], 45 | "artifactName": "${productName}-Linux-Installer.${ext}" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /electron/electron-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare namespace NodeJS { 4 | interface ProcessEnv { 5 | VSCODE_DEBUG?: 'true' 6 | /** 7 | * The built directory structure 8 | * 9 | * ```tree 10 | * ├─┬ dist-electron 11 | * │ ├─┬ main 12 | * │ │ └── index.js > Electron-Main 13 | * │ └─┬ preload 14 | * │ └── index.mjs > Preload-Scripts 15 | * ├─┬ dist 16 | * │ └── index.html > Electron-Renderer 17 | * ``` 18 | */ 19 | APP_ROOT: string 20 | /** /dist/ or /public/ */ 21 | VITE_PUBLIC: string 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /electron/main/class/appData.ts: -------------------------------------------------------------------------------- 1 | import Config from "./config"; 2 | import path from 'path'; 3 | import Files from "./files"; 4 | import fs from 'fs'; 5 | import { GL_API_URL, MM_CONFIG_PATH, getAppData } from "./appGlobals"; 6 | // @ts-ignore 7 | import { app, BrowserWindow } from 'electron'; 8 | import {ModSource} from "./modSource"; 9 | import {Mod} from "./mod"; 10 | import {ModVersion} from "./modVersion"; 11 | import {logError, logToServ} from "./functions"; 12 | 13 | // const RegionInfo = require("./regionInfo"); 14 | // const { version } = require("os"); 15 | 16 | 17 | class AppData { 18 | private isLoaded: boolean; 19 | private isUpdating: boolean; 20 | private config: Config; 21 | private githubToken: string; 22 | private modSources: ModSource[]; 23 | private startedMod: boolean; 24 | private subFolders = ['game', 'clients', 'mods', 'temp', 'data']; 25 | 26 | constructor() { 27 | this.isLoaded = false; 28 | this.isUpdating = false; 29 | this.startedMod = false; 30 | } 31 | 32 | async loadLocalConfig(): Promise { 33 | console.log("Appdata load..."); 34 | this.config = new Config(app.getVersion()); 35 | this.githubToken = await Files.downloadString(`${GL_API_URL}/github/token`); 36 | await Files.loadOrCreate(MM_CONFIG_PATH, this.config); 37 | } 38 | 39 | async load(): Promise { 40 | await this.config.loadAmongUsPath(); 41 | Files.createDirectoryIfNotExist(this.config.dataPath); 42 | // Delete Mod Manager 5 folder if exist 43 | let ModManager5Path = path.join(process.env.APPDATA, 'ModManager'); 44 | Files.deleteDirectoryIfExist(ModManager5Path); 45 | this.subFolders.forEach(folder => Files.createDirectoryIfNotExist(path.join(this.config.dataPath, folder))); 46 | await this.updateAppData(); 47 | this.config.version = app.getVersion(); 48 | this.isLoaded = true; 49 | console.log("Appdata loaded"); 50 | } 51 | 52 | async updateAppData(): Promise { 53 | this.modSources = []; 54 | let downloadPromises = this.config.sources.map(source => this.downloadSource(source)); 55 | try { 56 | await Promise.all(downloadPromises); 57 | } catch (error: any) { 58 | logError("Error when downloading sources (load)", error.message); 59 | } 60 | } 61 | 62 | async resetApp(): Promise { 63 | Files.deleteDirectoryIfExist(this.config.dataPath); 64 | this.config = new Config(app.getVersion()); 65 | this.updateConfig(); 66 | app.quit() 67 | process.exit(0) 68 | } 69 | 70 | async changeDataFolder(newFolder: string): Promise { 71 | if (Files.existsFolder(newFolder)) { 72 | this.subFolders.forEach(folder => Files.moveDirectory(path.join(this.config.dataPath, folder), path.join(newFolder, folder))); 73 | this.config.dataPath = newFolder; 74 | this.updateConfig(); 75 | return true; 76 | } 77 | return false; 78 | } 79 | 80 | async downloadSource(sourceUrl: string): Promise { 81 | let sourceData = await Files.downloadString(sourceUrl); 82 | let newSource: ModSource = JSON.parse(sourceData); 83 | this.modSources.push(newSource); 84 | 85 | let downloadPromises = this.modSources.flatMap(source => 86 | source.mods 87 | .filter(mod => mod.type !== "allInOne" && mod.githubLink) 88 | .map(mod => this.downloadRelease(mod)) 89 | ); 90 | 91 | try { 92 | await Promise.all(downloadPromises); 93 | } catch (error: any) { 94 | logError("Erreur during downloading source", sourceUrl, error.message); 95 | } 96 | 97 | console.log("Mods loaded: ", this.modSources.flatMap(source => source.mods.map(mod => mod.name)).length); 98 | 99 | // Cleanup mods that have no releases 100 | this.modSources.forEach(source => { 101 | const nonConserves = source.mods.filter(mod => !(mod.type === 'allInOne' || !mod.githubLink || mod.versions.some(version => version.release))); 102 | console.log("Mods without releases", nonConserves.map(mod => mod.name)); 103 | source.mods = source.mods.filter(mod => mod.type === 'allInOne' || !mod.githubLink || mod.versions.some(version => version.release)); 104 | }); 105 | } 106 | 107 | async downloadRelease(mod: Mod): Promise { 108 | try { 109 | mod.releases = await Files.getGithubReleases(mod.author, mod.github, this.githubToken); 110 | if (!mod.releases) return; 111 | 112 | mod.versions.forEach(version => { 113 | if (version.version === 'latest') { 114 | version.release = mod.releases[0] === null ? null : mod.releases[0]; 115 | } else { 116 | version.release = mod.releases.find(release => release.tag_name === version.version); 117 | } 118 | // console.log(mod.name, version.version); LOG 119 | // if (version.release) { 120 | // console.log(mod.name, version.version, version.release.tag_name); 121 | // } else { 122 | // console.log(mod.name, version.version, "release missing"); 123 | // } 124 | }); 125 | } catch (error: any) { 126 | logError("Error when downloading release", mod.name, error.message); 127 | } 128 | } 129 | 130 | updateConfig(newConfig: string | null = null): void { 131 | let configData: string; 132 | if (newConfig) { 133 | Object.assign(this.config, JSON.parse(newConfig)); 134 | configData = JSON.stringify(this.config, null, 2); 135 | } else { 136 | configData = JSON.stringify(this.config, null, 2); 137 | } 138 | fs.writeFileSync(MM_CONFIG_PATH, configData); 139 | } 140 | 141 | getModFromIdAndVersion(modId: string, modVersion: string | null = null): [Mod | null, ModVersion | null] { 142 | for (const modSource of this.modSources) { 143 | const mod = modSource.mods.find(mod => mod.sid === modId); 144 | if (mod) { 145 | const version = mod.versions.find(v => v.version === modVersion); 146 | if (version) { 147 | return [mod, version]; 148 | } 149 | } 150 | } 151 | return [null, null]; 152 | } 153 | 154 | isInstalledModFromIdAndVersion(modId: string, modVersion: string | null = null): boolean { 155 | return this.config.installedMods.some(m => m.modId === modId && m.version === modVersion); 156 | } 157 | 158 | getInstalledModVersions(modId: string): { modId: string, version: string }[] { 159 | return this.config.installedMods.filter(m => m.modId === modId); 160 | } 161 | 162 | getModVersions(modId: string): ModVersion[] | null { 163 | for (const modSource of this.modSources) { 164 | const mod = modSource.mods.find(mod => mod.sid === modId); 165 | if (mod) { 166 | return mod.versions; 167 | } 168 | } 169 | return null; 170 | } 171 | 172 | getMod(modId: string): Mod | null { 173 | for (const modSource of this.modSources) { 174 | const mod = modSource.mods.find(mod => mod.sid === modId); 175 | if (mod) { 176 | return mod; 177 | } 178 | } 179 | return null; 180 | } 181 | 182 | hasInstalledVanilla(gameVersion: string): boolean { 183 | return this.config.installedVanilla.includes(gameVersion); 184 | } 185 | } 186 | 187 | export default AppData; -------------------------------------------------------------------------------- /electron/main/class/appGlobals.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import {fileURLToPath} from "node:url"; 3 | import {VITE_DEV_SERVER_URL} from "../index"; 4 | 5 | let mainWindow = null; 6 | let tray = null; 7 | let appData = null; 8 | let autoLaunch = null; 9 | let currentDownloads = []; 10 | let args = []; 11 | let translations = {}; 12 | let MM_ICON_PATH = null; 13 | 14 | // Convertir l'URL de fichier en chemin de fichier 15 | const __filename = fileURLToPath(import.meta.url); 16 | const __dirname = path.dirname(__filename); 17 | 18 | export const GL_WEBSITE_URL = "https://goodloss.fr"; 19 | export const GL_FILES_URL = "https://goodloss.fr/files"; 20 | export const GL_API_URL = "https://goodloss.fr/api"; 21 | export const MM_CONFIG_PATH = path.join(process.env.APPDATA, 'ModManager7', 'config7.json'); 22 | export const MM_LOG_PATH = path.join(process.env.APPDATA, 'ModManager7', 'log.txt'); 23 | export const MM_INSTALLER_PATH = path.join(process.env.APPDATA, 'ModManager7', 'installer.exe'); 24 | export const AMONGUS_REGIONINFO_PATH = path.join(process.env.APPDATA, '..', 'LocalLow', 'Innersloth', 'Among Us', 'regionInfo.json'); 25 | export const AMONGUS_SETTINGS_PATH = path.join(process.env.APPDATA, '..', 'LocalLow', 'Innersloth', 'Among Us', 'settings.amogus'); 26 | export const AMONGUS_OLD_SETTINGS_PATH = path.join(process.env.APPDATA, '..', 'LocalLow', 'Innersloth', 'Among Us', 'settings.amogus.old'); 27 | export const AMONGUS_NEW_SETTINGS_PATH = path.join(process.env.APPDATA, '..', 'LocalLow', 'Innersloth', 'Among Us', 'settings.amogus.new'); 28 | export const AMONGUS_DOWNLOAD_LINK = GL_WEBSITE_URL + "/amonguspage"; 29 | export const UPDATE_APP_DATA_INTERVAL = process.env.VITE_DEV_SERVER_URL ? 60000 : 600000; 30 | 31 | export const setMainWindow = (win) => { 32 | mainWindow = win; 33 | } 34 | 35 | export const getMainWindow = () => { 36 | return mainWindow; 37 | } 38 | 39 | export const setAppData = (ad) => { 40 | appData = ad; 41 | } 42 | 43 | export const getAppData = () => { 44 | return appData; 45 | } 46 | 47 | export const setTray = (t) => { 48 | tray = t; 49 | } 50 | 51 | export const getTray = () => { 52 | return tray; 53 | } 54 | 55 | export const setAutoLaunch = (al) => { 56 | autoLaunch = al; 57 | } 58 | 59 | export const getAutoLaunch = () => { 60 | return autoLaunch; 61 | } 62 | 63 | export const setArgs = (arg) => { 64 | args = arg; 65 | } 66 | 67 | export const getArgs = () => { 68 | return args; 69 | } 70 | 71 | export const setMMIconPath = (path) => { 72 | MM_ICON_PATH = path; 73 | } 74 | 75 | export const getMMIconPath = () => { 76 | return MM_ICON_PATH; 77 | } 78 | 79 | // Only get because you can only add or splice 80 | export const getCurrentDownloads = () => { 81 | return currentDownloads; 82 | } 83 | 84 | export const isDownloadInProgress = (type, mod, version) => { 85 | return currentDownloads.some(([existingType, existingMod, existingVersion]) => 86 | type === existingType && (mod === null || existingMod.sid === mod.sid) && (version === null || existingVersion.version === version.version)); 87 | } 88 | 89 | export const removeFinishedDownload = (type, mod, version) => { 90 | const index = currentDownloads.findIndex(([existingType, existingMod, existingVersion]) => 91 | existingType === type && (mod === null || existingMod.sid === mod.sid) && (version === null || existingVersion.version === version.version)); 92 | if (index !== -1) { 93 | currentDownloads.splice(index, 1); 94 | } 95 | } 96 | 97 | export const trans = (text: string, ...values: any[]) => { 98 | if (!getAppData() || !getAppData().config || !getAppData().config.lg) return text; 99 | const lg = getAppData().config.lg; 100 | let translatedValue = translations[lg] && translations[lg][text] ? translations[lg][text] : text; 101 | 102 | let index = 0; 103 | return translatedValue.replace(/\$/g, () => { 104 | return values[index++] || '$'; 105 | }); 106 | } 107 | 108 | export const setTranslations = (newTrans) => { 109 | translations = newTrans; 110 | } 111 | 112 | export const getDataPaths = () => { 113 | return { 114 | 'config': path.join(getAppData().config.dataPath, 'game', 'BepInEx', 'config'), 115 | 'TheOtherHats': path.join(getAppData().config.dataPath, 'game', 'TheOtherHats') 116 | }; 117 | } -------------------------------------------------------------------------------- /electron/main/class/config.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import Winreg from 'winreg'; 3 | import path from 'path'; 4 | import {GL_API_URL, getAppData, trans, AMONGUS_DOWNLOAD_LINK} from './appGlobals'; 5 | import Files from './files'; 6 | import { InstalledMod } from './installedMod'; 7 | import {ModVersion} from "./modVersion"; 8 | import {Mod} from "./mod"; 9 | import {logError} from "./functions"; 10 | import {app, dialog, Notification, shell} from "electron"; 11 | 12 | const regKey = new Winreg({ 13 | hive: Winreg.HKLM, // Hive du registre 14 | key: '\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Steam App 945360' // Chemin de la clé 15 | }); 16 | 17 | class Config { 18 | version: string; 19 | sources: string[]; 20 | installedMods: InstalledMod[]; 21 | installedVanilla: string[]; 22 | amongUsPath: string; 23 | dataPath: string; 24 | lg: string; 25 | supportId: string; 26 | favoriteMods: InstalledMod[]; 27 | minimizeToTray: boolean; 28 | launchOnStartup: boolean; 29 | theme: string; 30 | 31 | constructor( 32 | version = "", 33 | sources = [`${GL_API_URL}/mm`], 34 | installedMods: InstalledMod[] = [], 35 | installedVanilla: string[] = [], 36 | amongUsPath = "", 37 | dataPath = "", 38 | lg = "en", 39 | supportId = "", 40 | favoriteMods: InstalledMod[] = [], 41 | minimizeToTray = false, 42 | launchOnStartup = true, 43 | theme = "dark" 44 | ) { 45 | this.version = version; 46 | this.sources = sources; 47 | this.installedMods = installedMods; 48 | this.installedVanilla = installedVanilla; 49 | this.amongUsPath = amongUsPath; 50 | this.dataPath = dataPath || path.join(process.env.APPDATA || '', 'ModManager7', 'ModManager7Data'); 51 | this.lg = lg; 52 | this.favoriteMods = favoriteMods; 53 | this.supportId = supportId || this.generateRandomTenDigitNumber(); 54 | this.minimizeToTray = minimizeToTray; 55 | this.launchOnStartup = launchOnStartup; 56 | this.theme = theme; 57 | } 58 | 59 | async loadAmongUsPath(): Promise { 60 | try { 61 | this.amongUsPath = await this.getSteamLocation(); 62 | } catch (err) { 63 | let notification = new Notification({ title: trans('Among Us not found'), body: trans('Please uninstall and reinstall it to solve the issue.\nOnly Steam version is supported.\nMod Manager will close!') }); 64 | notification.show(); 65 | shell.openExternal(AMONGUS_DOWNLOAD_LINK); 66 | app.quit() 67 | process.exit(0) 68 | } 69 | } 70 | 71 | async getSteamLocation(): Promise { 72 | return new Promise((resolve, reject) => { 73 | regKey.get('InstallLocation', (err, item) => { 74 | if (err) { 75 | logError("Erreur lors de la lecture de la clé de registre:", err); 76 | reject(err); 77 | } else if (item) { 78 | resolve(item.value); 79 | } else { 80 | reject(new Error("Location not found")); 81 | } 82 | }); 83 | }); 84 | } 85 | 86 | generateRandomTenDigitNumber(): string { 87 | let randomNum = Math.random(); 88 | let tenDigitNum = Math.floor(randomNum * 1e10); 89 | return String(tenDigitNum).padStart(10, '0'); 90 | } 91 | 92 | loadData(data: Partial): void { 93 | Object.assign(this, data); 94 | } 95 | 96 | addInstalledVanilla(gameVersion: string): void { 97 | if (!this.installedVanilla.includes(gameVersion)) { 98 | this.installedVanilla.push(gameVersion); 99 | } 100 | } 101 | 102 | addInstalledMod(mod: Mod, version: ModVersion | null): void { 103 | let versionString = version === null ? null : version.version; 104 | let releasedVersion = version === null || version.release === null ? null : version.release.tag_name; 105 | if (!this.installedMods.some(m => m.modId === mod.sid && m.version === versionString && m.releaseVersion === releasedVersion)) { 106 | this.installedMods.push({ modId: mod.sid, version: versionString, releaseVersion: releasedVersion }); 107 | } 108 | } 109 | 110 | removeInstalledMod(mod: { sid: string }, version: { version: string } | null): void { 111 | const index = this.installedMods.findIndex(installedMod => 112 | installedMod.modId === mod.sid && (version === null || installedMod.version === version.version)); 113 | if (index !== -1) { 114 | this.installedMods.splice(index, 1); 115 | } 116 | } 117 | 118 | addFavoriteMod(mod: Mod, version: ModVersion): void { 119 | if (!this.favoriteMods.some(m => m.modId === mod.sid && (version === null || m.version === version.version))) { 120 | this.favoriteMods.push({ modId: mod.sid, version: version === null ? null : version.version, releaseVersion: null }); 121 | } 122 | } 123 | 124 | removeFavoriteMod(mod: { sid: string }, version: { version: string } | null): void { 125 | const index = this.favoriteMods.findIndex(favoriteMod => 126 | favoriteMod.modId === mod.sid && (version === null || favoriteMod.version === version.version)); 127 | if (index !== -1) { 128 | this.favoriteMods.splice(index, 1); 129 | } 130 | } 131 | } 132 | 133 | export default Config; -------------------------------------------------------------------------------- /electron/main/class/files.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import https from 'https'; 4 | import {logError} from "./functions"; 5 | 6 | class Files { 7 | static loadOrCreate(dir, objet) { 8 | if (!fs.existsSync(dir)) { 9 | const json = JSON.stringify(objet, null, 2); 10 | 11 | const dossier = path.dirname(dir); 12 | if (!fs.existsSync(dossier)) { 13 | fs.mkdirSync(dossier, { recursive: true }); 14 | } 15 | 16 | fs.writeFileSync(dir, json, 'utf8'); 17 | } else { 18 | const data = fs.readFileSync(dir, 'utf8'); 19 | const objetJson = JSON.parse(data); 20 | objet.loadData(objetJson); 21 | } 22 | } 23 | 24 | static createDirectoryIfNotExist(dir) { 25 | fs.mkdirSync(dir, { recursive: true }); 26 | } 27 | 28 | static existsFolder(dir) { 29 | return fs.existsSync(dir); 30 | } 31 | 32 | static deleteDirectoryIfExist(dir) { 33 | if (!fs.existsSync(dir)) return; 34 | fs.rmSync(dir, { recursive: true }); 35 | } 36 | 37 | static moveDirectory(dirSource, dirDest) { 38 | if (!fs.existsSync(dirSource)) return; 39 | fs.renameSync(dirSource, dirDest); 40 | } 41 | 42 | static copyFile(source, target) { 43 | if (!fs.existsSync(source)) return; 44 | fs.copyFileSync(source, target); 45 | } 46 | 47 | static deleteFile(file) { 48 | if (!fs.existsSync(file)) return; 49 | fs.rmSync(file); 50 | } 51 | 52 | static getAllFiles(dir) { 53 | return this.getAllFilesWorker(dir, dir); 54 | } 55 | 56 | static getAllFilesWorker(dir, rootDir) { 57 | let returnedFiles = []; 58 | let files = fs.readdirSync(dir); 59 | for (const file of files) { 60 | let fullFilePath = path.join(dir, file); 61 | let fileStats = fs.statSync(fullFilePath); 62 | if (!fileStats.isDirectory()) { 63 | let relativePath = path.relative(rootDir, fullFilePath); 64 | returnedFiles.push(relativePath); 65 | } else { 66 | const result = this.getAllFilesWorker(fullFilePath, rootDir); 67 | for (const f of result) { 68 | returnedFiles.push(f); 69 | } 70 | } 71 | } 72 | return returnedFiles; 73 | } 74 | 75 | static getAllDifferentFiles(dir, files) { 76 | return this.getAllDifferentFilesWorker(dir, dir, files); 77 | } 78 | 79 | static getAllDifferentFilesWorker(dir, rootDir, differentFiles) { 80 | let returnedFiles = []; 81 | let files = fs.readdirSync(dir); 82 | for (const file of files) { 83 | let fullFilePath = path.join(dir, file); 84 | let fileStats = fs.statSync(fullFilePath); 85 | if (!fileStats.isDirectory()) { 86 | let relativePath = path.relative(rootDir, fullFilePath); 87 | if (!differentFiles.includes(relativePath)) 88 | returnedFiles.push(relativePath); 89 | } else { 90 | const result = this.getAllFilesWorker(fullFilePath, rootDir); 91 | for (const f of result) { 92 | returnedFiles.push(f); 93 | } 94 | } 95 | } 96 | return returnedFiles; 97 | } 98 | 99 | static moveFilesIntoDirectory(rootDir, files, targetDir) { 100 | files.forEach(file => { 101 | const oldPath = path.join(rootDir, file); 102 | const newPath = path.join(targetDir, file); 103 | 104 | fs.rename(oldPath, newPath, function(err) { 105 | if (err) throw err; 106 | }); 107 | }); 108 | } 109 | 110 | static copyDirectoryContent(sourceDir, targetDir) { 111 | const files = fs.readdirSync(sourceDir); 112 | files.forEach(file => { 113 | const oldPath = path.join(sourceDir, file); 114 | const newPath = path.join(targetDir, file); 115 | if (!fs.existsSync(newPath)) 116 | fs.copyFileSync(oldPath, newPath); 117 | }); 118 | } 119 | 120 | static getBepInExInsideDir(nodePath) { 121 | const fullPath = path.resolve(nodePath, 'BepInEx'); 122 | if (fs.existsSync(fullPath)) { 123 | return nodePath; 124 | } 125 | 126 | const dirs = fs.readdirSync(nodePath, { withFileTypes: true }) 127 | .filter(dirent => dirent.isDirectory()) 128 | .map(dirent => path.resolve(nodePath, dirent.name)); 129 | 130 | for (let dir of dirs) { 131 | const result = this.getBepInExInsideDir(dir); 132 | if (result !== null) { 133 | return result; 134 | } 135 | } 136 | 137 | return null; 138 | } 139 | 140 | static async downloadString(url) { 141 | return new Promise((resolve, reject) => { 142 | const req = https.get(url, { rejectUnauthorized: false }, (res) => { 143 | let data = ''; 144 | 145 | res.on('data', (chunk) => { 146 | data += chunk; 147 | }); 148 | 149 | res.on('end', () => { 150 | resolve(data); 151 | }); 152 | }); 153 | 154 | req.on('error', (error: any) => { 155 | logError(error); 156 | reject(error); 157 | }); 158 | 159 | req.end(); 160 | }); 161 | } 162 | 163 | static async getGithubReleases(author, repo, token) { 164 | return new Promise((resolve, reject) => { 165 | var options = { 166 | host: 'goodloss.fr', 167 | path: `/api/mm/releases/${author}/${repo}`, 168 | method: 'GET' 169 | }; 170 | 171 | const req = https.request(options, (res) => { 172 | let data = ''; 173 | 174 | res.on('data', (chunk) => { 175 | data += chunk; 176 | }); 177 | 178 | res.on('end', () => { 179 | if (res.statusCode >= 200 && res.statusCode < 300) { 180 | try { 181 | const parsedData = JSON.parse(data); 182 | resolve(parsedData); 183 | console.log(`Loaded releases for mod: ${author}/${repo}`); 184 | } catch (e) { 185 | console.log(`Error parsing response: ${e}`); 186 | reject(`Error parsing response: ${e}`); 187 | } 188 | } else if (res.statusCode >= 300 && res.statusCode < 400) { 189 | console.log(`Redirection status code received: ${res.statusCode}`); 190 | reject(`Redirection status code received: ${res.statusCode}`); 191 | } else { 192 | console.log(`Request failed with status code ${res.statusCode}`); 193 | reject(`Request failed with status code ${res.statusCode}`); 194 | } 195 | }); 196 | }); 197 | 198 | req.on('error', (e) => { 199 | console.log("Request error: "+e); 200 | reject(`Request error: ${e}`); 201 | }); 202 | 203 | req.end(); 204 | }); 205 | } 206 | } 207 | 208 | export default Files; -------------------------------------------------------------------------------- /electron/main/class/functions.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import {app, BrowserWindow, Menu, shell} from "electron"; 3 | // @ts-ignore 4 | import AutoLaunch from "auto-launch"; 5 | import ModWorker from "./modWorker"; 6 | import { 7 | getAppData, 8 | getArgs, 9 | getAutoLaunch, 10 | getCurrentDownloads, 11 | getMainWindow, getMMIconPath, 12 | getTray, GL_API_URL, isDownloadInProgress, MM_LOG_PATH, removeFinishedDownload, 13 | setAutoLaunch, trans 14 | } from "./appGlobals"; 15 | import path from "path"; 16 | import fs from "fs"; 17 | // @ts-ignore 18 | import axios from "axios"; 19 | import {isOnline} from "./onlineCheck"; 20 | import {ModVersion} from "./modVersion"; 21 | 22 | export const handleArgs = () => { 23 | let args = getArgs(); 24 | if (args.length > 0) { 25 | console.log("Handle args: ", args); 26 | switch (args[0]) { 27 | case "startvanilla": 28 | { 29 | getMainWindow().webContents.send('handleArgs', 'startVanilla'); 30 | } 31 | case "startmod": 32 | { 33 | const [mod, version] = getAppData().getModFromIdAndVersion(args[1], args[2]); 34 | if (mod !== null && version !== null) { 35 | getMainWindow().webContents.send('handleArgs', 'startmod', [JSON.stringify(mod), JSON.stringify(version)]); 36 | } else { 37 | let mod = getAppData().getMod(args[1]); 38 | let modVersions = getAppData().getModVersions(args[1]); 39 | if (mod && modVersions) { 40 | getMainWindow().webContents.send('handleArgs', 'startmod', [JSON.stringify(mod), JSON.stringify(modVersions[0])]); 41 | } 42 | } 43 | } 44 | break; 45 | // case "startlocalmod": 46 | // console.log("start local mod" + args[1]); 47 | // break; 48 | // case "addsource": 49 | // break; 50 | default: 51 | console.log('No arg') 52 | break; 53 | } 54 | } 55 | } 56 | 57 | export const downloadMod = async (event, mod, version) => { 58 | console.log("Downloading mod on server..."); 59 | let downloadLines = []; 60 | if (mod.type !== "allInOne") { 61 | let oldVersion = null; 62 | let installedVersions = getAppData().getInstalledModVersions(mod.sid); 63 | let versions = getAppData().getModVersions(mod.sid); 64 | for (const installedVersion of installedVersions) { 65 | if (versions.some(v => v.version === installedVersion.version && v.release.tag_name !== installedVersion.releaseVersion)) { 66 | oldVersion = installedVersion; 67 | } 68 | } 69 | 70 | if (oldVersion !== null) { 71 | if (!isDownloadInProgress("update", mod, version)) { 72 | let generatedVersion = {'version': oldVersion.version, 'release': {'tag_name': oldVersion.releaseVersion}}; 73 | downloadLines.push(["update", mod, version, generatedVersion]); 74 | } 75 | } else if (!getAppData().isInstalledModFromIdAndVersion(mod.sid, version.version)) { 76 | if (!isDownloadInProgress("mod", mod, version)) { 77 | downloadLines.push(["mod", mod, version]); 78 | } 79 | } 80 | 81 | for (const dep of version.modDependencies) { 82 | let [depMod, depVersion] = getAppData().getModFromIdAndVersion(dep.modDependency, dep.modVersion); 83 | if (depMod && depVersion && !isDownloadInProgress("mod", depMod, depVersion) && !getAppData().isInstalledModFromIdAndVersion(depMod.sid, depVersion.version)) { 84 | downloadLines.push(["mod", depMod, depVersion]); 85 | } 86 | } 87 | 88 | if (!isDownloadInProgress("vanilla", version.gameVersion, null) && !getAppData().hasInstalledVanilla(version.gameVersion)) { 89 | downloadLines.push(["vanilla", null, version]); 90 | } 91 | } else { 92 | if (!isDownloadInProgress("mod", mod, version) && !getAppData().isInstalledModFromIdAndVersion(mod.sid, null)) { 93 | downloadLines.push(["allInOne", mod, version]); 94 | } 95 | } 96 | 97 | if (downloadLines.length === 0) { 98 | return; 99 | } 100 | 101 | let promises = []; 102 | for (const dl of downloadLines) { 103 | getCurrentDownloads().push(dl); 104 | switch (dl[0]) { 105 | case "vanilla": 106 | promises.push(ModWorker.downloadClient(event, dl[2])); 107 | break; 108 | case "mod": 109 | promises.push(ModWorker.downloadMod(event, dl[1], dl[2])); 110 | break; 111 | case "allInOne": 112 | if (dl[1].sid === "BetterCrewlink") { 113 | promises.push(ModWorker.downloadBcl(event, dl[1])); 114 | } else if (dl[1].sid === "Challenger") { 115 | promises.push(ModWorker.downloadChall(event, dl[1])); 116 | } 117 | break; 118 | case "update": 119 | promises.push(ModWorker.uninstallMod(event, dl[1], dl[3])); 120 | await new Promise((resolve) => setTimeout(resolve, 100)); 121 | promises.push(ModWorker.downloadMod(event, dl[1], dl[2])); 122 | break; 123 | } 124 | //promises.push(dl[3]); 125 | } 126 | 127 | await Promise.all(promises); 128 | 129 | for (const dl of downloadLines) { 130 | removeFinishedDownload(dl[0], dl[1], dl[2]); 131 | if (dl[0] === "mod") { 132 | getAppData().config.addInstalledMod(dl[1], dl[2]); 133 | } else if (dl[0] === "vanilla") { 134 | getAppData().config.addInstalledVanilla(dl[2].gameVersion); 135 | } else if (dl[0] === "update") { 136 | getAppData().config.removeInstalledMod(dl[1], dl[3]); 137 | getAppData().config.addInstalledMod(dl[1], dl[2]); 138 | } else if (dl[0] === "allInOne") { 139 | getAppData().config.addInstalledMod(dl[1], null); 140 | } 141 | } 142 | 143 | getAppData().updateConfig(); 144 | event.reply('updateConfig', JSON.stringify(getAppData().config)); 145 | 146 | updateTray(); 147 | 148 | console.log("Mod downloaded on server"); 149 | } 150 | 151 | export const uninstallMod = async (event, mod, version) => { 152 | console.log("Uninstalling mod on server..."); 153 | if (!getAppData().isInstalledModFromIdAndVersion(mod.sid, version === null ? null : version.version)) return; 154 | 155 | if (mod.sid === "BetterCrewlink") { 156 | await ModWorker.uninstallBcl(event, mod); 157 | } else if (mod.sid === "Challenger") { 158 | await ModWorker.uninstallChall(event, mod); 159 | } else { 160 | await ModWorker.uninstallMod(event, mod, version); 161 | } 162 | 163 | getAppData().config.removeInstalledMod(mod, version); 164 | getAppData().updateConfig(); 165 | 166 | updateTray(); 167 | 168 | event.reply('updateConfig', JSON.stringify(getAppData().config)); 169 | 170 | console.log("Mod uninstalled on server"); 171 | } 172 | 173 | export const startMod = async (event, mod, version) => { 174 | console.log("Starting mod on server..."); 175 | const result: any = await downloadMod(event, mod, version); 176 | if (result === false) { 177 | return; 178 | } 179 | 180 | if (mod.sid === "BetterCrewlink") { 181 | await ModWorker.startBcl(event, mod); 182 | } else if (mod.sid === "Challenger") { 183 | await ModWorker.startChall(event, mod); 184 | } else { 185 | await ModWorker.startMod(event, mod, version); 186 | } 187 | 188 | console.log("Mod started on server"); 189 | } 190 | 191 | export const createShortcut = (mod, version) => { 192 | let appPath = app.getPath('exe'); 193 | let desktopPath = path.join(app.getPath('home'), 'Desktop'); 194 | let shortcutPath = path.join(desktopPath, mod.name + (version !== null ? (" " + version.version) : "") + '.lnk'); 195 | 196 | let success = shell.writeShortcutLink(shortcutPath, 'create', { 197 | target: appPath+' startmod '+ (version !== null ? (" " + version.version) : ""), 198 | cwd: path.dirname(appPath), 199 | icon: appPath, 200 | iconIndex: 0, 201 | appUserModelId: 'modmanager7', 202 | description: 'Mod Manager' 203 | }); 204 | 205 | if (success) { 206 | console.log('Shortcut created successfully'); 207 | } else { 208 | console.log('Failed to create shortcut'); 209 | } 210 | } 211 | 212 | export const updateTray = () => { 213 | let modsLines = [ 214 | { 215 | label: 'Mod Manager', 216 | click: function () { 217 | getMainWindow().show(); 218 | } 219 | }, 220 | { type: 'separator' }, 221 | { 222 | label: trans('Library'), 223 | click: function () { 224 | getMainWindow().webContents.send('navigate', '/library'); 225 | getMainWindow().show(); 226 | } 227 | }, 228 | { 229 | label: trans('Store'), 230 | click: function () { 231 | getMainWindow().webContents.send('navigate', '/store'); 232 | getMainWindow().show(); 233 | } 234 | }, 235 | { 236 | label: trans('Settings'), 237 | click: function () { 238 | getMainWindow().webContents.send('navigate', '/settings'); 239 | getMainWindow().show(); 240 | } 241 | }, 242 | ]; 243 | 244 | if (getAppData() || getAppData().isLoaded) { 245 | modsLines.push({ type: 'separator' }); 246 | if (getAppData().startedMod === false) { 247 | modsLines.push({ 248 | label: "Start Vanilla", 249 | click: function () { 250 | getMainWindow().webContents.send('handleArgs', 'startVanilla'); 251 | } 252 | }); 253 | } else { 254 | modsLines.push({ 255 | label: getAppData().startedMod[0] === 'Vanilla' ? 'Stop vanilla' : trans("Stop $", getAppData().startedMod[0].name+" "+getAppData().startedMod[1].release.tag_name), 256 | click: function () { 257 | getMainWindow().webContents.send('handleArgs', 'stopCurrentMod'); 258 | } 259 | }); 260 | } 261 | modsLines.push({ type: 'separator' }); 262 | getAppData().config.installedMods.forEach(im => { 263 | let [mod, version] = getAppData().getModFromIdAndVersion(im.modId, im.version); 264 | if (mod && version && mod.type !== "dependency") { 265 | modsLines.push({ 266 | label: mod.name + " " + im.releaseVersion, 267 | click: function () { 268 | getMainWindow().webContents.send('handleArgs', 'startmod', [JSON.stringify(mod), JSON.stringify(version)]); 269 | } 270 | }) 271 | } 272 | }); 273 | } 274 | 275 | modsLines.push({ type: 'separator' }); 276 | modsLines.push({ 277 | label: trans('Exit'), 278 | click: function () { 279 | app.quit() 280 | process.exit(0) 281 | } 282 | }); 283 | 284 | const contextMenu = Menu.buildFromTemplate(modsLines); 285 | 286 | getTray().setToolTip('Mod Manager'); 287 | getTray().setContextMenu(contextMenu); 288 | 289 | getTray().on('double-click', () => { 290 | if (!getAppData() || !getAppData().isLoaded) return; 291 | getMainWindow().isVisible() ? getMainWindow().hide() : getMainWindow().show(); 292 | }); 293 | } 294 | 295 | export const enableAutoLaunch = () => { 296 | getAutoLaunch().isEnabled().then((isEnabled) => { 297 | if (!isEnabled) getAutoLaunch().enable(); 298 | }).catch((err) => { 299 | logError(err); 300 | }); 301 | } 302 | 303 | export const disableAutoLaunch = () => { 304 | getAutoLaunch().isEnabled().then((isEnabled) => { 305 | if (isEnabled) getAutoLaunch().disable(); 306 | }).catch((err) => { 307 | logError(err); 308 | }); 309 | } 310 | 311 | export const updateLaunchOnStart = () => { 312 | setAutoLaunch(new AutoLaunch({ 313 | name: 'ModManager', 314 | icon: getMMIconPath(), 315 | path: app.getPath('exe'), 316 | })); 317 | 318 | if (getAppData().config.launchOnStartup) { 319 | enableAutoLaunch() 320 | } else { 321 | disableAutoLaunch() 322 | } 323 | } 324 | 325 | export const getFormattedLogMessage = (message: string) => { 326 | let supportId = getAppData()?.config?.supportId ?? ''; 327 | return `[ModManager7][${supportId}] ${message}`; 328 | } 329 | 330 | export const logError = (firstError: string, ...errors: any[]) => { 331 | errors = errors.map((error) => { 332 | if (typeof error === 'string') { 333 | return error; 334 | } else { 335 | return JSON.stringify(error); 336 | } 337 | }); 338 | let concatenatedErrors = errors.join(', '); 339 | concatenatedErrors = firstError + " " + concatenatedErrors; 340 | logToServ(concatenatedErrors); 341 | console.error(concatenatedErrors); 342 | fs.appendFile(MM_LOG_PATH, "[" + new Date().toISOString() + "] " + concatenatedErrors + "\n", (err) => { 343 | if (err) { 344 | logError('Failed to write to log file:', err); 345 | } 346 | }); 347 | } 348 | 349 | export const logToServ = (message: string) => { 350 | if (!isOnline()) return; 351 | message = getFormattedLogMessage(message); 352 | axios.post(GL_API_URL+"/log", { 353 | text: message 354 | }, { 355 | headers: { 356 | 'Content-Type': 'application/x-www-form-urlencoded' 357 | } 358 | }).then(r => { 359 | // console.log(r.status); 360 | }); 361 | } -------------------------------------------------------------------------------- /electron/main/class/installedMod.ts: -------------------------------------------------------------------------------- 1 | export class InstalledMod { 2 | modId: string; 3 | version: string | null; 4 | releaseVersion: string | null; 5 | 6 | constructor(modId: string, version: string | null, releaseVersion: string | null) { 7 | this.modId = modId; 8 | this.version = version; 9 | this.releaseVersion = releaseVersion; 10 | } 11 | } -------------------------------------------------------------------------------- /electron/main/class/ipcHandler.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import {dialog, ipcMain, shell} from "electron"; 3 | import {getAppData, getCurrentDownloads, getMainWindow, setTranslations, trans} from "./appGlobals"; 4 | import { 5 | createShortcut, 6 | downloadMod, 7 | handleArgs, 8 | logToServ, 9 | startMod, 10 | uninstallMod, 11 | updateLaunchOnStart, 12 | updateTray 13 | } from "./functions"; 14 | import modWorker from "./modWorker"; 15 | import {initializeUpdater} from "./updater"; 16 | 17 | const setupIPCMainHandlers = () => { 18 | 19 | ipcMain.on('openExternal', async (event, url) => { 20 | shell.openExternal(url) 21 | }); 22 | 23 | ipcMain.on('loadDataServer', async (event) => { 24 | console.log("Loading data server..."); 25 | if (!getAppData() || !getAppData().isLoaded) { 26 | await getAppData().loadLocalConfig(); 27 | getMainWindow().webContents.send('navigate', '/'); 28 | getMainWindow().show(); 29 | await initializeUpdater(); 30 | if (getAppData().isUpdating) return; 31 | event.reply('loadLanguage', getAppData().config.lg); 32 | } 33 | }); 34 | 35 | ipcMain.on('loadDataServer2', async (event, translations) => { 36 | setTranslations((JSON.parse(translations))); 37 | if (getAppData().config.minimizeToTray === false) { 38 | getMainWindow().show(); 39 | getMainWindow().focus(); 40 | } 41 | 42 | await getAppData().load(); 43 | updateTray(); 44 | updateLaunchOnStart(); 45 | handleArgs(); 46 | 47 | let sentData = JSON.stringify(getAppData()); 48 | console.log("Data server loaded"); 49 | event.reply('loadDataClient', sentData); 50 | console.log("Mod Manager started"); 51 | }); 52 | 53 | ipcMain.on('updateConfigServer', async (event, newConfig) => { 54 | console.log("Save config on server..."); 55 | getAppData().updateConfig(newConfig); 56 | updateLaunchOnStart(); 57 | console.log("Config saved on server"); 58 | let sentData = JSON.stringify(getAppData()); 59 | event.reply('loadDataClient', sentData); 60 | }); 61 | 62 | ipcMain.on('updateTray', async () => { 63 | updateTray(); 64 | }); 65 | 66 | ipcMain.on('downloadMod', async (event, modStr, versionStr) => { 67 | 68 | let mod = JSON.parse(modStr); 69 | let version = JSON.parse(versionStr); 70 | 71 | await downloadMod(event, mod, version); 72 | 73 | }); 74 | 75 | ipcMain.on('uninstallMod', async (event, modStr, versionStr) => { 76 | let mod = JSON.parse(modStr); 77 | let version = JSON.parse(versionStr); 78 | 79 | await uninstallMod(event, mod, version); 80 | }); 81 | 82 | ipcMain.on('startMod', async (event, modStr, versionStr) => { 83 | let mod = JSON.parse(modStr); 84 | let version = JSON.parse(versionStr); 85 | 86 | await startMod(event, mod, version); 87 | }); 88 | 89 | ipcMain.on('startVanilla', async (event) => { 90 | await modWorker.startVanilla(event); 91 | }); 92 | 93 | ipcMain.on('addFavoriteMod', async (event, modStr, versionStr) => { 94 | let mod = JSON.parse(modStr); 95 | let version = JSON.parse(versionStr); 96 | 97 | getAppData().config.addFavoriteMod(mod, version); 98 | getAppData().updateConfig(); 99 | event.reply('updateConfig', JSON.stringify(getAppData().config)); 100 | }); 101 | 102 | 103 | ipcMain.on('removeFavoriteMod', async (event, modStr, versionStr) => { 104 | let mod = JSON.parse(modStr); 105 | let version = JSON.parse(versionStr); 106 | 107 | getAppData().config.removeFavoriteMod(mod, version); 108 | getAppData().updateConfig(); 109 | event.reply('updateConfig', JSON.stringify(getAppData().config)); 110 | }); 111 | 112 | ipcMain.on('addShortcut', async (event, modStr, versionStr) => { 113 | let mod = JSON.parse(modStr); 114 | let version = JSON.parse(versionStr); 115 | createShortcut(mod, version); 116 | let downloadId = Date.now().toString(); 117 | let visibleVersion = mod.name+(version !== null ? (" " + version.version) : ""); 118 | event.sender.send('createPopin', "

"+trans('Shortcut created for $', visibleVersion)+"

", downloadId, "bg-green-700"); 119 | event.sender.send('removePopin', downloadId); 120 | }); 121 | 122 | ipcMain.on('rateMod', async (event, modStr, versionStr, rating) => { 123 | let mod = JSON.parse(modStr); 124 | let version = JSON.parse(versionStr); 125 | 126 | logToServ("Rate of "+rating+"/5 for mod "+mod.sid+" "+version.version); 127 | }); 128 | 129 | ipcMain.on('stopCurrentMod', async () => { 130 | await modWorker.stopChild(); 131 | }); 132 | 133 | ipcMain.on('resetApp', async () => { 134 | await getAppData().resetApp(); 135 | }); 136 | 137 | ipcMain.handle('openFolderDialog', async (event, newPath = null) => { 138 | let worked = false; 139 | if (getCurrentDownloads().length === 0) { 140 | if (newPath === null) { 141 | const result = await dialog.showOpenDialog(getMainWindow(), { 142 | properties: ['openDirectory'] 143 | }); 144 | worked = !result.canceled; 145 | newPath = result.filePaths[0]; 146 | } else { 147 | worked = true; 148 | } 149 | 150 | let changeResult = await getAppData().changeDataFolder(newPath); 151 | if (worked) { 152 | let downloadId = Date.now().toString(); 153 | if (changeResult) { 154 | event.sender.send('updateConfig', JSON.stringify(getAppData().config)); 155 | event.sender.send('createPopin', "
"+trans('Path successfully updated!')+"
", downloadId, "bg-green-700"); 156 | } else { 157 | event.sender.send('createPopin', "
"+trans("Path doesn't exist!")+"
", downloadId, "bg-red-700"); 158 | } 159 | event.sender.send('removePopin', downloadId); 160 | } 161 | } 162 | return getAppData().config.dataPath; 163 | }); 164 | 165 | ipcMain.on('createPopin', async (event, text, downloadId, classes) => { 166 | event.sender.send('createPopin', text, downloadId, classes); 167 | }); 168 | 169 | ipcMain.on('removePopin', async (event, downloadId) => { 170 | event.sender.send('removePopin', downloadId); 171 | }); 172 | 173 | // ipcMain.on('updateRegionInfoServer', async (event, newRegionInfo) => { 174 | // console.log("Save regionInfo on server..."); 175 | // appData.updateConfig(newRegionInfo); 176 | // console.log("regionInfo saved on server"); 177 | // let sentData = JSON.stringify(appData); 178 | // event.reply('loadDataClient', sentData); 179 | // }); 180 | }; 181 | 182 | export default setupIPCMainHandlers; -------------------------------------------------------------------------------- /electron/main/class/mod.ts: -------------------------------------------------------------------------------- 1 | import {ModVersion} from "./modVersion"; 2 | 3 | export class Mod { 4 | sid: string; 5 | name: string; 6 | author: string; 7 | github: string; 8 | githubLink: string; 9 | releases: any[]; 10 | versions: ModVersion[]; 11 | type: string; 12 | needPattern?: string; 13 | ignorePattern?: string; 14 | 15 | constructor(modId: string, name: string, author: string, github: string, githubLink: string, type: string, needPattern?: string, ignorePattern?: string) { 16 | this.sid = modId; 17 | this.name = name; 18 | this.author = author; 19 | this.github = github; 20 | this.githubLink = githubLink; 21 | this.releases = []; 22 | this.versions = []; 23 | this.type = type; 24 | this.needPattern = needPattern; 25 | this.ignorePattern = ignorePattern; 26 | } 27 | } -------------------------------------------------------------------------------- /electron/main/class/modSource.ts: -------------------------------------------------------------------------------- 1 | import {Mod} from "./mod"; 2 | 3 | export class ModSource { 4 | mods: Mod[]; 5 | } -------------------------------------------------------------------------------- /electron/main/class/modVersion.ts: -------------------------------------------------------------------------------- 1 | export class ModVersion { 2 | version: string; 3 | release?: any; 4 | 5 | constructor(version: string) { 6 | this.version = version; 7 | } 8 | } -------------------------------------------------------------------------------- /electron/main/class/modWorker.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import axios from 'axios'; 3 | import * as fs from 'fs'; 4 | import * as path from 'path'; 5 | import Files from "./files"; 6 | import { spawn, exec } from 'child_process'; 7 | import * as os from 'os'; 8 | // @ts-ignore 9 | import Winreg from "winreg"; 10 | import { 11 | AMONGUS_NEW_SETTINGS_PATH, AMONGUS_OLD_SETTINGS_PATH, AMONGUS_SETTINGS_PATH, 12 | getAppData, getDataPaths, getMainWindow, 13 | GL_FILES_URL, 14 | GL_WEBSITE_URL, trans 15 | } from "./appGlobals"; 16 | import {Mod} from "./mod"; 17 | import {ModVersion} from "./modVersion"; 18 | import {logError, updateTray} from "./functions"; 19 | import AdmZip from 'adm-zip'; 20 | 21 | let child: any = null; 22 | 23 | class ModWorker { 24 | 25 | static async downloadClient(event: any, version: any): Promise { 26 | const gameVersion = version.gameVersion; 27 | let finished = false; 28 | const downloadId = Date.now().toString(); 29 | const url = `${GL_FILES_URL}/client/${gameVersion}.zip`; 30 | const tempPath = path.join(getAppData().config.dataPath, 'temp', `client-${gameVersion}.zip`); 31 | const clientPath = path.join(getAppData().config.dataPath, 'clients', gameVersion); 32 | 33 | const response = await axios({ 34 | method: 'get', 35 | url: url, 36 | responseType: 'stream' 37 | }); 38 | 39 | const totalLength = response.headers['content-length']; 40 | let progress = 0; 41 | let lastProgress = 0; 42 | let lastTime = Date.now(); 43 | 44 | response.data.on('data', (chunk: any) => { 45 | progress += chunk.length; 46 | const currentTime = Date.now(); 47 | const elapsedTime = currentTime - lastTime; 48 | const bytesDownloaded = progress - lastProgress; 49 | 50 | const percentCompleted = Math.round((progress / totalLength) * 100); 51 | 52 | const speed = elapsedTime > 0 ? (bytesDownloaded / (elapsedTime / 1000)) : 0; 53 | 54 | if (currentTime - lastTime > 100) { 55 | const downloadText = `

`+trans('Downloading client $', gameVersion)+`

`+this.formatPopinProgress(percentCompleted, speed, progress, totalLength)+`
`; 56 | 57 | if (!finished) { 58 | if (percentCompleted === 100) { 59 | finished = true; 60 | } else { 61 | event.sender.send('createPopin', downloadText, downloadId, "bg-blue-700"); 62 | } 63 | } 64 | lastTime = currentTime; 65 | lastProgress = progress; 66 | } 67 | }); 68 | 69 | const writer = fs.createWriteStream(tempPath); 70 | response.data.pipe(writer); 71 | 72 | return new Promise((resolve, reject) => { 73 | writer.on('finish', () => { 74 | const downloadText = `

`+trans('Extracting client $...', gameVersion)+`

`; 75 | const downloadTextEnd = `

`+trans('Client $ installed!', gameVersion)+`

`; 76 | this.extractZipFile(tempPath, clientPath, event, downloadText, downloadTextEnd, downloadId, "bg-green-700") 77 | .then(() => { 78 | resolve(true); 79 | }) 80 | .catch((error) => { 81 | reject(error); 82 | }); 83 | }); 84 | writer.on('error', reject); 85 | }); 86 | } 87 | 88 | static async downloadMod(event: any, mod: any, version: any): Promise { 89 | let finished = false; 90 | const downloadId = Date.now().toString(); 91 | let tempPath = path.join(getAppData().config.dataPath, 'temp', `mod-${mod.sid}-${version.version}.zip`); 92 | const tempWorker = path.join(getAppData().config.dataPath, 'temp', 'modWorker'); 93 | let modPath = path.join(getAppData().config.dataPath, 'mods', `${mod.sid}-${version.version}`); 94 | 95 | let installType: 'zip' | 'dll' | null = null; 96 | let filename: string | null = null; 97 | let fileUrl: string | null = null; 98 | 99 | version.release['assets'].forEach((asset: any) => { 100 | if (asset['name'].endsWith('.zip') && (version.needPattern === null || asset['name'].includes(version.needPattern)) && (version.ignorePattern === null || !asset['name'].includes(version.ignorePattern))) { 101 | installType = 'zip'; 102 | filename = asset['name']; 103 | fileUrl = asset['browser_download_url']; 104 | } 105 | }); 106 | 107 | if (installType === null) { 108 | version.release['assets'].forEach((asset: any) => { 109 | if (asset['name'].endsWith('.dll')) { 110 | installType = 'dll'; 111 | filename = asset['name']; 112 | fileUrl = asset['browser_download_url']; 113 | modPath = path.join(getAppData().config.dataPath, 'mods', `${mod.sid}-${version.version}`, 'BepInEx', 'plugins'); 114 | tempPath = path.join(getAppData().config.dataPath, 'temp', filename); 115 | } 116 | }); 117 | } 118 | 119 | const response = await axios({ 120 | method: 'get', 121 | url: fileUrl!, 122 | responseType: 'stream' 123 | }); 124 | 125 | const totalLength = response.headers['content-length']; 126 | let progress = 0; 127 | let lastProgress = 0; 128 | let lastTime = Date.now(); 129 | 130 | response.data.on('data', (chunk: any) => { 131 | progress += chunk.length; 132 | const currentTime = Date.now(); 133 | const elapsedTime = currentTime - lastTime; 134 | const bytesDownloaded = progress - lastProgress; 135 | 136 | const percentCompleted = Math.round((progress / totalLength) * 100); 137 | 138 | const speed = elapsedTime > 0 ? (bytesDownloaded / (elapsedTime / 1000)) : 0; 139 | 140 | if (currentTime - lastTime > 100) { 141 | const downloadText = `

`+trans('Downloading $', mod.name)+`

`+this.formatPopinProgress(percentCompleted, speed, progress, totalLength)+`
`; 142 | 143 | if (!finished) { 144 | if (percentCompleted === 100) { 145 | finished = true; 146 | } else { 147 | event.sender.send('createPopin', downloadText, downloadId, "bg-blue-700"); 148 | } 149 | } 150 | 151 | lastProgress = progress; 152 | lastTime = currentTime; 153 | } 154 | 155 | }); 156 | 157 | const writer = fs.createWriteStream(tempPath); 158 | response.data.pipe(writer); 159 | 160 | return new Promise((resolve, reject) => { 161 | writer.on('finish', () => { 162 | const downloadText = `

`+trans('Extracting $...', mod.name)+`

`; 163 | const downloadTextEnd = `

`+trans('$ installed!', mod.name)+`

`; 164 | if (installType === 'zip') { 165 | Files.deleteDirectoryIfExist(tempWorker); 166 | this.extractZipFile(tempPath, tempWorker, event, downloadText, downloadTextEnd, downloadId, "bg-green-700") 167 | .then(() => { 168 | const rootPath = Files.getBepInExInsideDir(tempWorker); 169 | Files.moveDirectory(rootPath, modPath); 170 | resolve(true); 171 | }) 172 | .catch((error) => { 173 | reject(error); 174 | }); 175 | } else if (installType === 'dll') { 176 | Files.createDirectoryIfNotExist(modPath); 177 | fs.cpSync(tempPath, path.join(modPath, filename!)); 178 | event.sender.send('updatePopin', downloadTextEnd, downloadId, "bg-green-700"); 179 | event.sender.send('removePopin', downloadId); 180 | resolve(true); 181 | } 182 | 183 | }); 184 | writer.on('error', reject); 185 | }); 186 | } 187 | 188 | static async extractZipFile(zipFilePath: string, outputFolderPath: string, event: any, downloadText: string, downloadTextEnd: string, downloadId: string, classes: string): Promise { 189 | console.log('Extracting...'); 190 | event.sender.send('createPopin', downloadText, downloadId, classes); 191 | Files.createDirectoryIfNotExist(outputFolderPath); 192 | const zip = new AdmZip(zipFilePath); 193 | await zip.extractAllToAsync(outputFolderPath, true); 194 | console.log('Extraction complete.'); 195 | event.sender.send('updatePopin', downloadTextEnd, downloadId, classes); 196 | event.sender.send('removePopin', downloadId); 197 | return true; 198 | } 199 | 200 | static async uninstallMod(event: any, mod: any, version: any): Promise { 201 | const downloadId = Date.now().toString(); 202 | event.sender.send('createPopin', `

`+trans('Uninstalling $...', mod.name)+`

`, downloadId, "bg-blue-700"); 203 | const modPath = path.join(getAppData().config.dataPath, 'mods', `${mod.sid}-${version.version}`); 204 | Files.deleteDirectoryIfExist(modPath); 205 | event.sender.send('updatePopin', `

`+trans('$ uninstalled!', mod.name)+`

`, downloadId, "bg-red-700"); 206 | event.sender.send('removePopin', downloadId); 207 | } 208 | 209 | static async startMod(event: any, mod: any, version: any): Promise { 210 | const isRunning = await this.isProcessRunning('Among Us') || getAppData().startedMod !== false; 211 | if (isRunning) return; 212 | 213 | let downloadId = Date.now().toString(); 214 | event.sender.send('createPopin', `

`+trans('Starting $...', mod.name)+`

`, downloadId, "bg-blue-700"); 215 | const gamePath = path.join(getAppData().config.dataPath, 'game'); 216 | const clientPath = path.join(getAppData().config.dataPath, 'clients', version.gameVersion); 217 | const modPath = path.join(getAppData().config.dataPath, 'mods', `${mod.sid}-${version.version}`); 218 | Files.deleteDirectoryIfExist(gamePath); 219 | Files.createDirectoryIfNotExist(gamePath); 220 | const promises = [ 221 | fs.promises.cp(clientPath, gamePath, { recursive: true }), 222 | fs.promises.cp(modPath, gamePath, { recursive: true }) 223 | ]; 224 | for (const dep of version.modDependencies) { 225 | const [depMod, depVersion] = getAppData().getModFromIdAndVersion(dep.modDependency, dep.modVersion); 226 | if (depMod && depVersion) { 227 | const depPath = path.join(getAppData().config.dataPath, 'mods', `${depMod.sid}-${depVersion.version}`); 228 | promises.push(fs.promises.cp(depPath, gamePath, { recursive: true })); 229 | } 230 | } 231 | 232 | await Promise.all(promises); 233 | 234 | const amongUsPath = path.join(gamePath, 'Among Us.exe'); 235 | this.loadGameSettings(version.version); 236 | this.loadData(mod, version); 237 | this.addSteamAppIdIfNotExist(gamePath); 238 | child = spawn(amongUsPath, {}); 239 | 240 | if (child.pid) { 241 | getAppData().startedMod = [mod, version]; 242 | event.sender.send('updateStartedMod', [mod, version]); 243 | updateTray(); 244 | event.sender.send('updatePopin', `

`+trans('$ started!', mod.name)+`

`, downloadId, "bg-green-700"); 245 | event.sender.send('removePopin', downloadId); 246 | } 247 | 248 | child.on('close', () => { 249 | this.saveData(mod, version); 250 | this.saveGameSettings(version.version); 251 | getAppData().startedMod = false; 252 | event.sender.send('updateStartedMod', false); 253 | updateTray(); 254 | downloadId = Date.now().toString(); 255 | getMainWindow().show(); 256 | getMainWindow().focus(); 257 | event.sender.send('createFeedback', downloadId, JSON.stringify(mod), JSON.stringify(version)); 258 | 259 | console.log("Mod stopped"); 260 | }); 261 | } 262 | 263 | static loadData(m: Mod, v: ModVersion): void { 264 | for (const [key, targetPath] of Object.entries(getDataPaths())) { 265 | let sourcePath = path.join(getAppData().config.dataPath, 'data', `${m.sid}-${v.version}`, key); 266 | if (Files.existsFolder(sourcePath)) { 267 | Files.copyDirectoryContent(sourcePath, targetPath); 268 | } 269 | } 270 | } 271 | 272 | static saveData(m: Mod, v: ModVersion): void { 273 | for (const [key, sourcePath] of Object.entries(getDataPaths())) { 274 | let targetPath = path.join(getAppData().config.dataPath, 'data', `${m.sid}-${v.version}`, key); 275 | if (Files.existsFolder(sourcePath)) { 276 | Files.deleteDirectoryIfExist(targetPath); 277 | Files.createDirectoryIfNotExist(targetPath); 278 | Files.copyDirectoryContent(sourcePath, targetPath); 279 | } 280 | } 281 | } 282 | 283 | // Backward compatibility system for version 2024.3.5 and older 284 | static loadGameSettings(version: string): void { 285 | const versionParts = version.split('.'); 286 | const versionInt = parseInt(versionParts[0], 10); 287 | 288 | if (versionInt <= 2023 || version === '2024.3.5') { 289 | if (Files.existsFolder(AMONGUS_OLD_SETTINGS_PATH)) { 290 | Files.copyFile(AMONGUS_OLD_SETTINGS_PATH, AMONGUS_SETTINGS_PATH); 291 | } else { 292 | Files.deleteFile(AMONGUS_SETTINGS_PATH); 293 | } 294 | } else { 295 | if (Files.existsFolder(AMONGUS_NEW_SETTINGS_PATH)) { 296 | Files.copyFile(AMONGUS_NEW_SETTINGS_PATH, AMONGUS_SETTINGS_PATH); 297 | } else { 298 | Files.deleteFile(AMONGUS_SETTINGS_PATH); 299 | } 300 | } 301 | } 302 | 303 | static saveGameSettings(version: string): void { 304 | const versionParts = version.split('.'); 305 | const versionInt = parseInt(versionParts[0], 10); 306 | 307 | if (versionInt <= 2023 || version === '2024.3.5') { 308 | Files.copyFile(AMONGUS_SETTINGS_PATH, AMONGUS_OLD_SETTINGS_PATH); 309 | } else { 310 | Files.copyFile(AMONGUS_SETTINGS_PATH, AMONGUS_NEW_SETTINGS_PATH); 311 | } 312 | } 313 | 314 | static addSteamAppIdIfNotExist(amongUsPath: string) { 315 | let steamAppIdFilePath = path.join(amongUsPath, 'steam_appid.txt'); 316 | if (!Files.existsFolder(steamAppIdFilePath)) { 317 | fs.writeFileSync(steamAppIdFilePath, '945360'); 318 | } 319 | } 320 | 321 | static async startVanilla(event: any): Promise { 322 | const isRunning = await this.isProcessRunning('Among Us') || getAppData().startedMod !== false; 323 | if (isRunning) return; 324 | 325 | const downloadId = Date.now().toString(); 326 | event.sender.send('createPopin', `

`+trans('Starting vanilla...')+`

`, downloadId, "bg-blue-700"); 327 | const amongUsPath = path.join(getAppData().config.amongUsPath, 'Among Us.exe'); 328 | 329 | this.addSteamAppIdIfNotExist(getAppData().config.amongUsPath); 330 | child = spawn(amongUsPath, {}); 331 | 332 | if (child.pid) { 333 | getAppData().startedMod = ["Vanilla", null]; 334 | event.sender.send('updateStartedMod', ["Vanilla", null]); 335 | updateTray(); 336 | event.sender.send('updatePopin', `

`+trans('Vanilla started!')+`

`, downloadId, "bg-green-700"); 337 | event.sender.send('removePopin', downloadId); 338 | } 339 | 340 | child.on('close', () => { 341 | getAppData().startedMod = false; 342 | event.sender.send('updateStartedMod', false); 343 | updateTray(); 344 | getMainWindow().show(); 345 | getMainWindow().focus(); 346 | 347 | console.log("Vanilla stopped"); 348 | }); 349 | } 350 | 351 | static async downloadBcl(event: any, mod: any): Promise { 352 | try { 353 | let finished = false; 354 | const downloadId = Date.now().toString(); 355 | const tempPath = path.join(getAppData().config.dataPath, 'temp', 'Better-CrewLink-Setup.exe'); 356 | Files.deleteDirectoryIfExist(tempPath); 357 | 358 | const response = await axios({ 359 | method: 'get', 360 | url: `${GL_WEBSITE_URL}/bcl`, 361 | responseType: 'stream' 362 | }); 363 | 364 | const totalLength = response.headers['content-length']; 365 | let progress = 0; 366 | let lastProgress = 0; 367 | let lastTime = Date.now(); 368 | 369 | response.data.on('data', (chunk: any) => { 370 | progress += chunk.length; 371 | const currentTime = Date.now(); 372 | const elapsedTime = currentTime - lastTime; 373 | const bytesDownloaded = progress - lastProgress; 374 | 375 | const percentCompleted = Math.round((progress / totalLength) * 100); 376 | 377 | const speed = elapsedTime > 0 ? (bytesDownloaded / (elapsedTime / 1000)) : 0; 378 | 379 | if (currentTime - lastTime > 100) { 380 | const downloadText = `

`+trans('Downloading $', mod.name)+`

`+this.formatPopinProgress(percentCompleted, speed, progress, totalLength)+`
`; 381 | 382 | if (!finished) { 383 | if (percentCompleted === 100) { 384 | finished = true; 385 | } else { 386 | event.sender.send('createPopin', downloadText, downloadId, "bg-blue-700"); 387 | } 388 | } 389 | 390 | lastProgress = progress; 391 | lastTime = currentTime; 392 | } 393 | 394 | }); 395 | 396 | const writer = fs.createWriteStream(tempPath); 397 | response.data.pipe(writer); 398 | 399 | return new Promise((resolve, reject) => { 400 | writer.on('finish', () => { 401 | exec(tempPath, (error, stdout, stderr) => { 402 | if (error) { 403 | logError(`Erreur d'exécution : ${error}`); 404 | console.log(`Code de sortie : ${error.code}`); 405 | return; 406 | } 407 | if (stderr) { 408 | logError(`Erreur : ${stderr}`); 409 | } else { 410 | event.sender.send('updatePopin', `

`+trans('$ installed!', mod.name)+`

`, downloadId, "bg-green-700"); 411 | event.sender.send('removePopin', downloadId); 412 | resolve(true); 413 | } 414 | }); 415 | }); 416 | writer.on('error', reject); 417 | }); 418 | } catch (error) { 419 | logError('Error downloading the mod:', error.message); 420 | return false; 421 | } 422 | } 423 | 424 | static async startBcl(event: any, mod: any): Promise { 425 | const downloadId = Date.now().toString(); 426 | event.sender.send('createPopin', `

`+trans('Starting $...', mod.name)+`

`, downloadId, "bg-blue-700"); 427 | const regKey = new Winreg({ 428 | hive: Winreg.HKCU, 429 | key: '\\SOFTWARE\\03ceac78-9166-585d-b33a-90982f435933' 430 | }); 431 | regKey.get('InstallLocation', (err: any, item: any) => { 432 | if (err) { 433 | logError("Erreur lors de la lecture de la clé de registre:", err); 434 | } else if (item) { 435 | child = spawn(path.join(item.value, "Better-CrewLink.exe"), {}); 436 | 437 | if (child.pid) { 438 | getAppData().startedMod = [mod, null]; 439 | event.sender.send('updateStartedMod', [mod, null]); 440 | updateTray(); 441 | event.sender.send('updatePopin', `

`+trans('$ started!', mod.name)+`

`, downloadId, "bg-green-700"); 442 | event.sender.send('removePopin', downloadId); 443 | } 444 | 445 | child.on('close', () => { 446 | getAppData().startedMod = false; 447 | event.sender.send('updateStartedMod', false); 448 | updateTray(); 449 | }); 450 | } else { 451 | console.log(null); 452 | } 453 | }); 454 | } 455 | 456 | static async uninstallBcl(event: any, mod: any): Promise { 457 | const downloadId = Date.now().toString(); 458 | event.sender.send('createPopin', `

`+trans('Uninstalling $...', mod.name)+`

`, downloadId, "bg-blue-700"); 459 | 460 | const regKey = new Winreg({ 461 | hive: Winreg.HKCU, 462 | key: '\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\03ceac78-9166-585d-b33a-90982f435933' 463 | }); 464 | 465 | regKey.get('QuietUninstallString', (err: any, item: any) => { 466 | if (err) { 467 | console.log('Erreur lors de la lecture de la clé du registre:', err); 468 | } else { 469 | exec(`cmd /c ${item.value}`, { windowsHide: true }, async (error, stdout, stderr) => { 470 | if (error) { 471 | logError(`Erreur d'exécution : ${error}`); 472 | console.log(`Code de sortie : ${error.code}`); 473 | return; 474 | } 475 | if (stderr) { 476 | logError(`Erreur : ${stderr}`); 477 | } else { 478 | event.sender.send('updatePopin', `

`+trans('$ uninstalled!', mod.name)+`

`, downloadId, "bg-red-700"); 479 | event.sender.send('removePopin', downloadId); 480 | } 481 | }); 482 | } 483 | }); 484 | 485 | event.sender.send('updatePopin', `

`+trans('$ uninstalled!', mod.name)+`

`, downloadId, "bg-red-700"); 486 | event.sender.send('removePopin', downloadId); 487 | } 488 | 489 | static async downloadChall(event: any, mod: any): Promise { 490 | try { 491 | const downloadId = Date.now().toString(); 492 | event.sender.send('createPopin', `

`+trans('Installing $...', mod.name)+`

`, downloadId, "bg-blue-700"); 493 | exec(`start steam://run/2160150`, (error, stdout, stderr) => { 494 | if (error) { 495 | logError(`Erreur d'exécution : ${error}`); 496 | console.log(`Code de sortie : ${error.code}`); 497 | return; 498 | } 499 | if (stderr) { 500 | logError(`Erreur : ${stderr}`); 501 | } else { 502 | event.sender.send('updatePopin', `

`+trans('$ installed!', mod.name)+`

`, downloadId, "bg-green-700"); 503 | event.sender.send('removePopin', downloadId); 504 | return; 505 | } 506 | }); 507 | } catch (error) { 508 | logError('Error downloading the mod:', error.message); 509 | return false; 510 | } 511 | } 512 | 513 | static async startChall(event: any, mod: any): Promise { 514 | const downloadId = Date.now().toString(); 515 | event.sender.send('createPopin', `

`+trans('Starting $...', mod.name)+`

`, downloadId, "bg-blue-700"); 516 | 517 | child = spawn('start steam://rungameid/2160150', { shell: true }); 518 | 519 | child.on('error', (error: any) => { 520 | logError(`Error: ${error.message}`); 521 | }); 522 | 523 | if (child.pid) { 524 | getAppData().startedMod = [mod, null]; 525 | event.sender.send('updateStartedMod', [mod, null]); 526 | updateTray(); 527 | event.sender.send('updatePopin', `

`+trans('$ started!', mod.name)+`

`, downloadId, "bg-green-700"); 528 | event.sender.send('removePopin', downloadId); 529 | } 530 | 531 | child.on('close', () => { 532 | getAppData().startedMod = false; 533 | event.sender.send('updateStartedMod', false); 534 | updateTray(); 535 | }); 536 | } 537 | 538 | static async uninstallChall(event: any, mod: any): Promise { 539 | const downloadId = Date.now().toString(); 540 | event.sender.send('createPopin', `

`+trans('Uninstalling $...', mod.name)+`

`, downloadId, "bg-blue-700"); 541 | 542 | const regKey = new Winreg({ 543 | hive: Winreg.HKLM, 544 | key: '\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Steam App 2160150' 545 | }); 546 | 547 | regKey.get('UninstallString', (err: any, item: any) => { 548 | if (err) { 549 | console.log('Erreur lors de la lecture de la clé du registre:', err); 550 | } else { 551 | exec(`cmd /c ${item.value}`, { windowsHide: true }, async (error, stdout, stderr) => { 552 | if (error) { 553 | logError(`Erreur d'exécution : ${error}`); 554 | console.log(`Code de sortie : ${error.code}`); 555 | return; 556 | } 557 | if (stderr) { 558 | logError(`Erreur : ${stderr}`); 559 | } else { 560 | event.sender.send('updatePopin', `

`+trans('$ uninstalled!', mod.name)+`

`, downloadId, "bg-red-700"); 561 | event.sender.send('removePopin', downloadId); 562 | } 563 | }); 564 | } 565 | }); 566 | 567 | event.sender.send('updatePopin', `

`+trans('$ uninstalled!', mod.name)+`

`, downloadId, "bg-red-700"); 568 | event.sender.send('removePopin', downloadId); 569 | } 570 | 571 | static formatByteSize(bytes: number): string { 572 | const sizes = ["B", "KB", "MB", "GB", "TB"]; 573 | if (bytes === 0) return '0 B'; 574 | const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)).toString(), 10); 575 | if (i === 0) return `${bytes} ${sizes[i]}`; 576 | return `${(bytes / (1024 ** i)).toFixed(2)} ${sizes[i]}`; 577 | } 578 | 579 | static formatPopinProgress(percentCompleted: any, speed: any, progress: any, totalLength: any): string { 580 | return `

`+trans('Progress: $%', percentCompleted)+`

581 |

`+trans('Speed: $/s', this.formatByteSize(speed))+`

582 |

${this.formatByteSize(progress)} / ${this.formatByteSize(totalLength)}

`; 583 | } 584 | 585 | static isProcessRunning(processName: string): Promise { 586 | return new Promise((resolve, reject) => { 587 | const platform = os.platform(); 588 | let command: string; 589 | 590 | if (platform === "win32") { 591 | command = `tasklist`; 592 | } else if (platform === "darwin" || platform === "linux") { 593 | command = `ps aux`; 594 | } else { 595 | return reject(new Error(`Plateforme non supportée : ${platform}`)); 596 | } 597 | 598 | exec(command, (err, stdout, stderr) => { 599 | if (err) { 600 | return reject(err); 601 | } 602 | if (stderr) { 603 | return reject(new Error(stderr)); 604 | } 605 | 606 | const isRunning = stdout.toLowerCase().includes(processName.toLowerCase()); 607 | resolve(isRunning); 608 | }); 609 | }); 610 | } 611 | 612 | static async stopChild(): Promise { 613 | if (child) { 614 | child.kill(); 615 | child = null; 616 | } 617 | } 618 | } 619 | 620 | export default ModWorker; 621 | -------------------------------------------------------------------------------- /electron/main/class/onlineCheck.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from "dns"; 2 | import {app, Notification} from "electron"; 3 | import {getAppData, getMainWindow, GL_API_URL, trans, UPDATE_APP_DATA_INTERVAL} from "./appGlobals"; 4 | import https from "https"; 5 | export let isConnected = false; 6 | let firstConnection = true; 7 | 8 | 9 | function liveCheck() { 10 | https.get(GL_API_URL+'/mm/status', (res) => { 11 | if (res.statusCode === 200) { 12 | if (isConnected || firstConnection) { 13 | } else { 14 | handleReconnection(); 15 | } 16 | isConnected = true; 17 | firstConnection = false; 18 | } else { 19 | if (isConnected) 20 | handleDisconnection(); 21 | } 22 | }).on('error', (err) => { 23 | console.error(err); 24 | if (isConnected) 25 | handleDisconnection(); 26 | }); 27 | } 28 | 29 | function handleDisconnection() { 30 | if (isConnected) { 31 | console.log('Disconnected'); 32 | if (!getAppData() || !getAppData().isLoaded) { 33 | handleDisconnectionAndQuit(); 34 | return; 35 | } 36 | getMainWindow().webContents.send('connection', false); 37 | // let notification = new Notification({title: trans('Connection lost'), body: trans('Mod Manager will try to reconnect during the next 30 seconds.\n If it fails, it will close.')}); 38 | // notification.show(); 39 | } 40 | isConnected = false; 41 | } 42 | 43 | function handleDisconnectionAndQuit() { 44 | console.log('Disconnected and quit'); 45 | let notification = new Notification({title: trans('Connection lost'), body: trans('Mod Manager will close.')}); 46 | notification.show(); 47 | app.quit(); 48 | process.exit(0); 49 | } 50 | 51 | function handleReconnection() { 52 | if (isConnected) return; 53 | console.log('Reconnected'); 54 | getMainWindow().webContents.send('connection', true); 55 | // let notification = new Notification({title: trans('Connection back'), body: trans('Mod Manager has reconnected.')}); 56 | // notification.show(); 57 | } 58 | 59 | async function updateAppData() { 60 | if (!isConnected) return; 61 | await getAppData().updateAppData(); 62 | getMainWindow().webContents.send('updateAppData', getAppData()); 63 | } 64 | 65 | export function initializeOnlineCheck() { 66 | liveCheck(); 67 | setInterval(function() { 68 | liveCheck(); 69 | }, 5000); 70 | setInterval(function() { 71 | console.log('Update app data'); 72 | updateAppData(); 73 | }, UPDATE_APP_DATA_INTERVAL); 74 | } 75 | 76 | export function isOnline() { 77 | liveCheck(); 78 | return isConnected; 79 | } -------------------------------------------------------------------------------- /electron/main/class/regionInfo.ts: -------------------------------------------------------------------------------- 1 | class RegionInfo { 2 | constructor( ) { 3 | const jsonData = ` 4 | { 5 | "CurrentRegionIdx": 1, 6 | "Regions": [ 7 | { 8 | "$type": "DnsRegionInfo, Assembly-CSharp", 9 | "Fqdn": "", 10 | "DefaultIp": "", 11 | "port": 22023, 12 | "name": "North America", 13 | "TranslateName": 289 14 | }, 15 | { 16 | "$type": "DnsRegionInfo, Assembly-CSharp", 17 | "Fqdn": "", 18 | "DefaultIp": "", 19 | "port": 22023, 20 | "name": "Europe", 21 | "TranslateName": 290 22 | }, 23 | { 24 | "$type": "DnsRegionInfo, Assembly-CSharp", 25 | "Fqdn": "", 26 | "DefaultIp": "", 27 | "port": 22023, 28 | "name": "Asia", 29 | "TranslateName": 291 30 | } 31 | ] 32 | }`; 33 | const data = JSON.parse(jsonData); 34 | Object.assign(this, data); 35 | } 36 | 37 | loadData(data) { 38 | Object.assign(this, data); 39 | } 40 | } 41 | 42 | export default RegionInfo; -------------------------------------------------------------------------------- /electron/main/class/updater.ts: -------------------------------------------------------------------------------- 1 | import {app, Notification} from "electron"; 2 | import https from "https"; 3 | import {getAppData, getMainWindow, MM_INSTALLER_PATH, trans} from "./appGlobals"; 4 | import {logError, logToServ, updateTray} from "./functions"; 5 | import axios from "axios"; 6 | import fs from "fs"; 7 | import path from "path"; 8 | import {spawn} from "child_process"; 9 | 10 | function compareDates(date1, date2) { 11 | function convertToDate(dateString) { 12 | const [year, month, day] = dateString.split('.').map(Number); 13 | return new Date(year, month - 1, day); 14 | } 15 | 16 | const d1 = convertToDate(date1); 17 | const d2 = convertToDate(date2); 18 | 19 | if (d1 < d2) { 20 | return 1; // D1 < D2 21 | } else if (d1 > d2) { 22 | return -1; // D1 > D2 23 | } else { 24 | return 0; // D1 = D2 25 | } 26 | } 27 | 28 | async function processUpdate(installerAsset: any) { 29 | getAppData().isUpdating = true; 30 | console.log('Processing update'); 31 | let installerUrl = installerAsset.browser_download_url; 32 | 33 | getMainWindow().webContents.send('navigate', '/updating'); 34 | let notification = new Notification({title: trans('Mod Manager update available'), body: trans('The update will be downloaded in the background and installed immediately afterward.\nYou cannot use Mod Manager during this process!')}); 35 | notification.show(); 36 | 37 | const response = await axios({ 38 | method: 'get', 39 | url: installerUrl, 40 | responseType: 'stream' 41 | }); 42 | 43 | const totalLength = response.headers['content-length']; 44 | let progress = 0; 45 | let lastProgress = 0; 46 | let lastTime = Date.now(); 47 | 48 | response.data.on('data', (chunk: any) => { 49 | progress += chunk.length; 50 | const currentTime = Date.now(); 51 | const elapsedTime = currentTime - lastTime; 52 | const bytesDownloaded = progress - lastProgress; 53 | 54 | const percentCompleted = Math.round((progress / totalLength) * 100); 55 | 56 | const speed = elapsedTime > 0 ? (bytesDownloaded / (elapsedTime / 1000)) : 0; 57 | 58 | if (currentTime - lastTime > 100) { 59 | lastTime = currentTime; 60 | lastProgress = progress; 61 | } 62 | }); 63 | 64 | const writer = fs.createWriteStream(MM_INSTALLER_PATH); 65 | response.data.pipe(writer); 66 | 67 | return new Promise((resolve, reject) => { 68 | writer.on('finish', () => { 69 | console.log('updater downloaded'); 70 | 71 | setTimeout(() => { 72 | let child = spawn(MM_INSTALLER_PATH, { 73 | detached: true, 74 | stdio: 'ignore' 75 | }); 76 | 77 | if (child.pid) { 78 | console.log('Process started with PID:', child.pid); 79 | app.quit(); 80 | process.exit(0); 81 | } 82 | 83 | child.on('close', () => { 84 | app.quit(); 85 | process.exit(0); 86 | }); 87 | resolve(true); 88 | }, 100); // 100 ms delay 89 | }); 90 | writer.on('error', reject); 91 | }); 92 | 93 | } 94 | 95 | async function updateCheck() { 96 | var options = { 97 | host: 'goodloss.fr', 98 | path: `/api/mm/releases/MatuxGG/ModManager`, 99 | method: 'GET', 100 | }; 101 | 102 | 103 | return new Promise((resolve, reject) => { 104 | const req = https.request(options, (res) => { 105 | let data = ''; 106 | 107 | res.on('data', (chunk) => { 108 | data += chunk; 109 | }); 110 | 111 | res.on('end', async () => { 112 | if (res.statusCode === 200 || res.statusCode === 301) { 113 | try { 114 | let releases = JSON.parse(data); 115 | if (!releases) { 116 | logError('No releases for updater'); 117 | return reject('No releases for updater'); 118 | } 119 | let latestRelease = releases[0]; 120 | if (!latestRelease) { 121 | logError('No latest release for updater'); 122 | return reject('No latest release for updater'); 123 | } 124 | let latestVersion = latestRelease.tag_name; 125 | let currentVersion = app.getVersion(); 126 | let compareResult = compareDates(currentVersion, latestVersion); 127 | if (compareResult > 0) { 128 | const platform = process.platform; 129 | let installerName = 'Mod.Manager.7-Windows-Installer.exe'; 130 | if (platform === 'darwin') { 131 | installerName = 'Mod.Manager.7-Mac-Installer.dmg'; 132 | } else if (platform === 'linux') { 133 | installerName = 'Mod.Manager.7-Linux-Installer.AppImage'; 134 | } 135 | let installerAsset = latestRelease.assets.find(asset => asset.name === installerName); 136 | if (!installerAsset) { 137 | logError('No installer asset for updater'); 138 | return reject('No installer asset for updater'); 139 | } 140 | // Need update 141 | await processUpdate(installerAsset); 142 | resolve('Update required'); 143 | } else { 144 | resolve('No update required'); 145 | } 146 | } catch (e) { 147 | logError(`Error parsing response: ${e}`); 148 | reject(`Error parsing response: ${e}`); 149 | } 150 | } else { 151 | logError(`Request failed with status code ${res.statusCode}`); 152 | reject(`Request failed with status code ${res.statusCode}`); 153 | } 154 | }); 155 | }); 156 | 157 | req.on('error', (e) => { 158 | logError(`Request error: ${e}`); 159 | reject(`Request error: ${e}`); 160 | }); 161 | 162 | req.end(); 163 | }); 164 | } 165 | 166 | export async function initializeUpdater() { 167 | const result = await updateCheck(); 168 | setInterval(function() { 169 | updateCheck(); 170 | }, 60000); 171 | } -------------------------------------------------------------------------------- /electron/main/index.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import {app, BrowserWindow, shell, ipcMain, Menu, Tray, screen} from 'electron' 3 | import { createRequire } from 'node:module' 4 | import { fileURLToPath } from 'node:url' 5 | import path from 'node:path' 6 | import os from 'node:os' 7 | import { 8 | getMainWindow, getMMIconPath, 9 | setAppData, 10 | setArgs, 11 | setMainWindow, setMMIconPath, 12 | setTray, trans 13 | } from "./class/appGlobals"; 14 | import AppData from "./class/appData"; 15 | import {handleArgs, logError} from "./class/functions"; 16 | import setupIPCMainHandlers from "./class/ipcHandler"; 17 | import {initializeOnlineCheck} from "./class/onlineCheck"; 18 | 19 | const require = createRequire(import.meta.url) 20 | const __dirname = path.dirname(fileURLToPath(import.meta.url)) 21 | 22 | // The built directory structure 23 | // 24 | // ├─┬ dist-electron 25 | // │ ├─┬ main 26 | // │ │ └── index.js > Electron-Main 27 | // │ └─┬ preload 28 | // │ └── index.mjs > Preload-Scripts 29 | // ├─┬ dist 30 | // │ └── index.html > Electron-Renderer 31 | // 32 | process.env.APP_ROOT = path.join(__dirname, '../..') 33 | 34 | export const MAIN_DIST = path.join(process.env.APP_ROOT, 'dist-electron') 35 | export const RENDERER_DIST = path.join(process.env.APP_ROOT, 'dist') 36 | export const VITE_DEV_SERVER_URL = process.env.VITE_DEV_SERVER_URL 37 | 38 | process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL 39 | ? path.join(process.env.APP_ROOT, 'public') 40 | : RENDERER_DIST 41 | 42 | setMMIconPath(path.join(process.env.VITE_PUBLIC, 'modmanager.ico')); 43 | 44 | setAppData(new AppData()); 45 | 46 | // Disable GPU Acceleration for Windows 7 47 | if (os.release().startsWith('6.1')) app.disableHardwareAcceleration() 48 | 49 | // Set application name for Windows 10+ notifications 50 | if (process.platform === 'win32') app.setAppUserModelId(app.getName()) 51 | 52 | if (!app.requestSingleInstanceLock()) { 53 | app.quit() 54 | process.exit(0) 55 | } 56 | 57 | const preload = path.join(__dirname, '../preload/index.mjs') 58 | const indexHtml = path.join(RENDERER_DIST, 'index.html') 59 | 60 | async function createWindow() { 61 | 62 | let { width, height } = screen.getPrimaryDisplay().workAreaSize; 63 | 64 | 65 | width = Math.round(width * 0.9); 66 | height = Math.round(height * 0.9); 67 | 68 | setMainWindow(new BrowserWindow({ 69 | title: 'Mod Manager', 70 | icon: path.join(process.env.VITE_PUBLIC, 'modmanager.ico'), 71 | show: false, 72 | autoHideMenuBar: true, 73 | width: width, 74 | height: height, 75 | webPreferences: { 76 | preload, 77 | // Warning: Enable nodeIntegration and disable contextIsolation is not secure in production 78 | // nodeIntegration: true, 79 | 80 | // Consider using contextBridge.exposeInMainWorld 81 | // Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation 82 | // contextIsolation: false, 83 | }, 84 | })); 85 | 86 | if (VITE_DEV_SERVER_URL) { // #298 87 | getMainWindow().loadURL(VITE_DEV_SERVER_URL) 88 | // Open devTool if the app is not packaged 89 | getMainWindow().webContents.openDevTools() 90 | } else { 91 | getMainWindow().loadFile(indexHtml) 92 | } 93 | 94 | Menu.setApplicationMenu(null); 95 | 96 | getMainWindow().on('close', function (event) { 97 | event.preventDefault(); 98 | getMainWindow().hide(); 99 | return false; 100 | }); 101 | 102 | // Test actively push message to the Electron-Renderer 103 | getMainWindow().webContents.on('did-finish-load', () => { 104 | getMainWindow().webContents.send('navigate', '/'); 105 | // win?.webContents.send('main-process-message', new Date().toLocaleString()) 106 | }) 107 | 108 | // Make all links open with the browser, not with the application 109 | getMainWindow().webContents.setWindowOpenHandler(({ url }) => { 110 | if (url.startsWith('https:')) shell.openExternal(url) 111 | return { action: 'deny' } 112 | }) 113 | 114 | // win.webContents.on('will-navigate', (event, url) => { }) #344 115 | } 116 | 117 | 118 | app.on('ready', async () => { 119 | initializeOnlineCheck(); 120 | setArgs(process.argv.slice(2)); 121 | let t = new Tray(getMMIconPath()); 122 | setTray(t); 123 | await createWindow(); 124 | setupIPCMainHandlers(); 125 | }) 126 | 127 | // app.whenReady().then(createWindow) 128 | 129 | app.on('window-all-closed', () => { 130 | setMainWindow(null); 131 | if (process.platform !== 'darwin') app.quit() 132 | }) 133 | 134 | app.on('second-instance', (event, commandLine) => { 135 | if (getMainWindow()) { 136 | // Focus on the main window if the user tried to open another 137 | if (getMainWindow().isMinimized()) getMainWindow().restore() 138 | getMainWindow().focus() 139 | } 140 | 141 | setArgs(commandLine.slice(5)); 142 | 143 | handleArgs(); 144 | }) 145 | 146 | app.on('activate', () => { 147 | const allWindows = BrowserWindow.getAllWindows() 148 | if (allWindows.length) { 149 | allWindows[0].focus() 150 | } else { 151 | createWindow() 152 | } 153 | }) 154 | 155 | // New window example arg: new windows url 156 | ipcMain.handle('open-win', (_, arg) => { 157 | const childWindow = new BrowserWindow({ 158 | webPreferences: { 159 | preload, 160 | nodeIntegration: true, 161 | contextIsolation: false, 162 | }, 163 | }) 164 | 165 | if (VITE_DEV_SERVER_URL) { 166 | childWindow.loadURL(`${VITE_DEV_SERVER_URL}#${arg}`) 167 | } else { 168 | childWindow.loadFile(indexHtml, { hash: arg }) 169 | } 170 | }) 171 | 172 | process.on('uncaughtException', (error) => { 173 | logError(error.stack || error.toString()); 174 | }); 175 | 176 | process.on('exit', (code) => { 177 | logError(`Process was stopped with code ${code}`); 178 | }); 179 | 180 | process.on('unhandledRejection', (reason, promise) => { 181 | if (reason instanceof Error) { 182 | logError(`Unhandled Rejection at: ${promise} reason: ${reason.message}`); 183 | logError(`Stack trace: ${reason.stack}`); 184 | } else { 185 | logError(`Unhandled Rejection at: ${promise} reason: ${reason}`); 186 | } 187 | }); -------------------------------------------------------------------------------- /electron/preload/index.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { ipcRenderer, contextBridge } from 'electron' 3 | 4 | // --------- Expose some API to the Renderer process --------- 5 | contextBridge.exposeInMainWorld('electronAPI', { 6 | on(...args: Parameters) { 7 | const [channel, listener] = args 8 | return ipcRenderer.on(channel, (event, ...args) => listener(event, ...args)) 9 | }, 10 | off(...args: Parameters) { 11 | const [channel, ...omit] = args 12 | return ipcRenderer.off(channel, ...omit) 13 | }, 14 | send(...args: Parameters) { 15 | const [channel, ...omit] = args 16 | return ipcRenderer.send(channel, ...omit) 17 | }, 18 | invoke(...args: Parameters) { 19 | const [channel, ...omit] = args 20 | return ipcRenderer.invoke(channel, ...omit) 21 | }, 22 | openExternal: (url) => { 23 | ipcRenderer.send("openExternal", url); 24 | }, 25 | openFolderDialog: (options) => ipcRenderer.invoke('openFolderDialog', options), 26 | sendData: (channel, ...data) => ipcRenderer.send(channel, ...data), 27 | receiveData: (channel, func) => { 28 | ipcRenderer.on(channel, (event, ...args) => func(...args)); 29 | }, 30 | }) 31 | 32 | // --------- Preload scripts loading --------- 33 | // function domReady(condition: DocumentReadyState[] = ['complete', 'interactive']) { 34 | // return new Promise((resolve) => { 35 | // if (condition.includes(document.readyState)) { 36 | // resolve(true) 37 | // } else { 38 | // document.addEventListener('readystatechange', () => { 39 | // if (condition.includes(document.readyState)) { 40 | // resolve(true) 41 | // } 42 | // }) 43 | // } 44 | // }) 45 | // } 46 | // 47 | // const safeDOM = { 48 | // append(parent: HTMLElement, child: HTMLElement) { 49 | // if (!Array.from(parent.children).find(e => e === child)) { 50 | // return parent.appendChild(child) 51 | // } 52 | // }, 53 | // remove(parent: HTMLElement, child: HTMLElement) { 54 | // if (Array.from(parent.children).find(e => e === child)) { 55 | // return parent.removeChild(child) 56 | // } 57 | // }, 58 | // } 59 | 60 | /** 61 | * https://tobiasahlin.com/spinkit 62 | * https://connoratherton.com/loaders 63 | * https://projects.lukehaas.me/css-loaders 64 | * https://matejkustec.github.io/SpinThatShit 65 | */ 66 | // function useLoading() { 67 | // const className = `loaders-css__square-spin` 68 | // const styleContent = ` 69 | // @keyframes square-spin { 70 | // 25% { transform: perspective(100px) rotateX(180deg) rotateY(0); } 71 | // 50% { transform: perspective(100px) rotateX(180deg) rotateY(180deg); } 72 | // 75% { transform: perspective(100px) rotateX(0) rotateY(180deg); } 73 | // 100% { transform: perspective(100px) rotateX(0) rotateY(0); } 74 | // } 75 | // .${className} > div { 76 | // animation-fill-mode: both; 77 | // width: 50px; 78 | // height: 50px; 79 | // background: #fff; 80 | // animation: square-spin 3s 0s cubic-bezier(0.09, 0.57, 0.49, 0.9) infinite; 81 | // } 82 | // .app-loading-wrap { 83 | // position: fixed; 84 | // top: 0; 85 | // left: 0; 86 | // width: 100vw; 87 | // height: 100vh; 88 | // display: flex; 89 | // align-items: center; 90 | // justify-content: center; 91 | // background: #282c34; 92 | // z-index: 9; 93 | // } 94 | // ` 95 | // const oStyle = document.createElement('style') 96 | // const oDiv = document.createElement('div') 97 | // 98 | // oStyle.id = 'app-loading-style' 99 | // oStyle.innerHTML = styleContent 100 | // oDiv.className = 'app-loading-wrap' 101 | // oDiv.innerHTML = `

` 102 | // 103 | // return { 104 | // appendLoading() { 105 | // safeDOM.append(document.head, oStyle) 106 | // safeDOM.append(document.body, oDiv) 107 | // }, 108 | // removeLoading() { 109 | // safeDOM.remove(document.head, oStyle) 110 | // safeDOM.remove(document.body, oDiv) 111 | // }, 112 | // } 113 | // } 114 | 115 | // ---------------------------------------------------------------------- 116 | 117 | // const { appendLoading, removeLoading } = useLoading() 118 | // domReady().then(appendLoading) 119 | // 120 | // window.onmessage = (ev) => { 121 | // ev.data.payload === 'removeLoading' && removeLoading() 122 | // } 123 | // 124 | // setTimeout(removeLoading, 4999) 125 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Mod Manager 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "modmanager7", 3 | "version": "7.0.4", 4 | "main": "dist-electron/main/index.js", 5 | "description": "Mod Manager 7 by Good Loss", 6 | "author": "MatuxGG", 7 | "license": "GPL-3.0-or-later", 8 | "icon": "public/modmanager.ico", 9 | "private": true, 10 | "keywords": [ 11 | "electron", 12 | "rollup", 13 | "vite", 14 | "vue3", 15 | "vue" 16 | ], 17 | "debug": { 18 | "env": { 19 | "VITE_DEV_SERVER_URL": "http://127.0.0.1:3344/" 20 | } 21 | }, 22 | "type": "module", 23 | "scripts": { 24 | "dev": "vite", 25 | "build": "vue-tsc --noEmit && vite build && electron-builder", 26 | "preview": "vite preview", 27 | "watch": "npx tailwindcss -i ./src/tailwind.css -o ./src/compiled.css --watch" 28 | }, 29 | "dependencies": { 30 | "adm-zip": "^0.5.15", 31 | "auto-launch": "^5.0.6", 32 | "axios": "^1.6.2", 33 | "electron-log": "^4.0.0", 34 | "jquery": "^3.7.1", 35 | "vue-i18n": "^9.11.0", 36 | "vue-router": "^4.2.5", 37 | "vuex": "^4.0.2", 38 | "winreg": "^1.2.5" 39 | }, 40 | "devDependencies": { 41 | "@vitejs/plugin-vue": "^5.0.4", 42 | "electron": "^29.1.1", 43 | "electron-builder": "^24.13.3", 44 | "tailwindcss": "^3.3.6", 45 | "typescript": "^5.4.2", 46 | "vite": "^5.1.5", 47 | "vite-plugin-electron": "^0.28.4", 48 | "vite-plugin-electron-renderer": "^0.14.5", 49 | "vue": "^3.4.21", 50 | "vue-tsc": "^2.0.6" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /public/modmanager.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/public/modmanager.ico -------------------------------------------------------------------------------- /public/modmanager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/public/modmanager.png -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 320 | 321 | 336 | -------------------------------------------------------------------------------- /src/assets/account.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/account.png -------------------------------------------------------------------------------- /src/assets/add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/add.png -------------------------------------------------------------------------------- /src/assets/bottom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/bottom.png -------------------------------------------------------------------------------- /src/assets/cn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/cn.png -------------------------------------------------------------------------------- /src/assets/credits.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/credits.png -------------------------------------------------------------------------------- /src/assets/cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/cross.png -------------------------------------------------------------------------------- /src/assets/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/delete.png -------------------------------------------------------------------------------- /src/assets/deleteHover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/deleteHover.png -------------------------------------------------------------------------------- /src/assets/discord.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/discord.png -------------------------------------------------------------------------------- /src/assets/discordHover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/discordHover.png -------------------------------------------------------------------------------- /src/assets/download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/download.png -------------------------------------------------------------------------------- /src/assets/downloadHover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/downloadHover.png -------------------------------------------------------------------------------- /src/assets/edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/edit.png -------------------------------------------------------------------------------- /src/assets/en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/en.png -------------------------------------------------------------------------------- /src/assets/es.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/es.png -------------------------------------------------------------------------------- /src/assets/faq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/faq.png -------------------------------------------------------------------------------- /src/assets/favorite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/favorite.png -------------------------------------------------------------------------------- /src/assets/favoriteFilled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/favoriteFilled.png -------------------------------------------------------------------------------- /src/assets/fr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/fr.png -------------------------------------------------------------------------------- /src/assets/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/github.png -------------------------------------------------------------------------------- /src/assets/info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/info.png -------------------------------------------------------------------------------- /src/assets/installer_large.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/installer_large.bmp -------------------------------------------------------------------------------- /src/assets/installer_mini.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/installer_mini.bmp -------------------------------------------------------------------------------- /src/assets/jp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/jp.png -------------------------------------------------------------------------------- /src/assets/left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/left.png -------------------------------------------------------------------------------- /src/assets/list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/list.png -------------------------------------------------------------------------------- /src/assets/modmanager.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/modmanager.ico -------------------------------------------------------------------------------- /src/assets/modmanager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/modmanager.png -------------------------------------------------------------------------------- /src/assets/modmanager_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/modmanager_logo.png -------------------------------------------------------------------------------- /src/assets/mods.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/mods.png -------------------------------------------------------------------------------- /src/assets/news.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/news.png -------------------------------------------------------------------------------- /src/assets/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/play.png -------------------------------------------------------------------------------- /src/assets/playHover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/playHover.png -------------------------------------------------------------------------------- /src/assets/right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/right.png -------------------------------------------------------------------------------- /src/assets/roadmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/roadmap.png -------------------------------------------------------------------------------- /src/assets/servers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/servers.png -------------------------------------------------------------------------------- /src/assets/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/settings.png -------------------------------------------------------------------------------- /src/assets/shortcut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/shortcut.png -------------------------------------------------------------------------------- /src/assets/shortcutHover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/shortcutHover.png -------------------------------------------------------------------------------- /src/assets/title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/title.png -------------------------------------------------------------------------------- /src/assets/top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/top.png -------------------------------------------------------------------------------- /src/assets/update.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/update.png -------------------------------------------------------------------------------- /src/assets/updateHover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/updateHover.png -------------------------------------------------------------------------------- /src/assets/valid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/valid.png -------------------------------------------------------------------------------- /src/assets/warn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatuxGG/ModManager/cb87ef1e4c69394d12d03efeeed7af454828db4f/src/assets/warn.png -------------------------------------------------------------------------------- /src/components/AddLocal.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 16 | -------------------------------------------------------------------------------- /src/components/AppSettings.vue: -------------------------------------------------------------------------------- 1 | 72 | 73 | 144 | -------------------------------------------------------------------------------- /src/components/ConfirmPopin.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 36 | -------------------------------------------------------------------------------- /src/components/CreditsPage.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 17 | -------------------------------------------------------------------------------- /src/components/LoadingPage.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 51 | 52 | 54 | -------------------------------------------------------------------------------- /src/components/MenuLeft.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /src/components/ModCard.vue: -------------------------------------------------------------------------------- 1 | 119 | 120 | -------------------------------------------------------------------------------- /src/components/ModsInstalled.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 102 | -------------------------------------------------------------------------------- /src/components/ModsStore.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 83 | -------------------------------------------------------------------------------- /src/components/ServersList.vue: -------------------------------------------------------------------------------- 1 | 38 | -------------------------------------------------------------------------------- /src/components/UpdatingPage.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 51 | 52 | 54 | -------------------------------------------------------------------------------- /src/import/router.ts: -------------------------------------------------------------------------------- 1 | import {createRouter, createWebHistory, Router} from 'vue-router'; 2 | import ModsStore from '../components/ModsStore.vue'; 3 | import ModsInstalled from '../components/ModsInstalled.vue'; 4 | import AppSettings from '../components/AppSettings.vue'; 5 | import AddLocal from '../components/AddLocal.vue'; 6 | import CreditsPage from '../components/CreditsPage.vue'; 7 | import LoadingPage from '../components/LoadingPage.vue'; 8 | import UpdatingPage from '../components/UpdatingPage.vue'; 9 | // import ServersList from './components/ServersList.vue'; 10 | 11 | const router: Router = createRouter({ 12 | history: createWebHistory(), 13 | routes: [ 14 | { path: '/', component: LoadingPage }, 15 | { path: '/updating', component: UpdatingPage }, 16 | { path: '/library', component: ModsInstalled }, 17 | { path: '/store', component: ModsStore }, 18 | { path: '/settings', component: AppSettings }, 19 | { path: '/addlocal', component: AddLocal }, 20 | { path: '/credits', component: CreditsPage }, 21 | // { path: '/servers', component: ServersList }, 22 | ], 23 | }); 24 | 25 | export default router; -------------------------------------------------------------------------------- /src/import/store.ts: -------------------------------------------------------------------------------- 1 | import { createStore, Store } from 'vuex'; 2 | 3 | interface Mod { 4 | sid: string; 5 | type: string; 6 | name?: string; 7 | author?: string; 8 | category?: { 9 | sid: string; 10 | name: string; 11 | weight: number; 12 | }; 13 | versions: Array<{ 14 | version: string; 15 | gameVersion: string; 16 | }>; 17 | needPattern?: string; 18 | ignorePattern?: string; 19 | } 20 | 21 | interface AppState { 22 | appData: { 23 | config: { 24 | favoriteMods: InstalledMod[]; 25 | installedMods: InstalledMod[]; 26 | }; 27 | modSources: Array<{ 28 | mods: Mod[]; 29 | }>; 30 | startedMod?: any; 31 | } | null; 32 | } 33 | 34 | interface ModVersion { 35 | version: string; 36 | release?: any; 37 | } 38 | 39 | interface InstalledMod { 40 | modId: string; 41 | version: string; 42 | releaseVersion: string; 43 | } 44 | 45 | export const store = createStore({ 46 | state(): AppState { 47 | return { 48 | appData: null 49 | }; 50 | }, 51 | mutations: { 52 | setAppData(state: AppState, data: any) { 53 | state.appData = data; 54 | }, 55 | setConfig(state: AppState, config: any) { 56 | if (state.appData) { 57 | state.appData.config = config; 58 | } 59 | } 60 | }, 61 | actions: { 62 | loadAppData(context) { 63 | return new Promise((resolve) => { 64 | // @ts-ignore 65 | window.electronAPI.sendData('loadDataServer'); 66 | // @ts-ignore 67 | window.electronAPI.receiveData('loadDataClient', (data: string) => { 68 | context.commit('setAppData', JSON.parse(data)); 69 | resolve(context.rootState); 70 | }); 71 | // @ts-ignore 72 | window.electronAPI.receiveData('updateConfig', (config: string) => { 73 | context.commit('setConfig', JSON.parse(config)); 74 | }); 75 | }); 76 | } 77 | }, 78 | getters: { 79 | gameVersionOptions: (state: AppState) => { 80 | const versions = new Set(); 81 | if (state.appData && state.appData.modSources) { 82 | state.appData.modSources.forEach(source => { 83 | source.mods.forEach(mod => { 84 | if (mod.type !== "dependency") { 85 | mod.versions.forEach(version => { 86 | versions.add(version.gameVersion); 87 | }); 88 | } 89 | }); 90 | }); 91 | } 92 | return Array.from(versions).sort((a, b) => { 93 | let dateA = new Date(a.split('.').join('-')); 94 | let dateB = new Date(b.split('.').join('-')); 95 | return dateB.getTime() - dateA.getTime(); 96 | }); 97 | }, 98 | gameVersionInstalledOptions: (state: AppState, getters: any) => { 99 | const versions = new Set(); 100 | if (state.appData && state.appData.modSources) { 101 | state.appData.modSources.forEach(source => { 102 | source.mods.forEach(mod => { 103 | if (mod.type !== "dependency") { 104 | mod.versions.forEach(version => { 105 | if (getters.isInstalledMod(mod.sid, version)) { 106 | versions.add(version.gameVersion); 107 | } 108 | }); 109 | } 110 | }); 111 | }); 112 | } 113 | return Array.from(versions).sort((a, b) => { 114 | let dateA = new Date(a.split('.').join('-')); 115 | let dateB = new Date(b.split('.').join('-')); 116 | return dateB.getTime() - dateA.getTime(); 117 | }); 118 | }, 119 | categoriesOptions: (state: AppState) => { 120 | const uniqueCategories: { [key: string]: { sid: string; name: string; weight: number } } = {}; 121 | if (state.appData && state.appData.modSources) { 122 | state.appData.modSources.forEach(source => { 123 | source.mods.forEach(mod => { 124 | if (mod.type !== "dependency") { 125 | const cat = mod.category; 126 | if (cat && !uniqueCategories[cat.sid]) { 127 | uniqueCategories[cat.sid] = cat; 128 | } 129 | } 130 | }); 131 | }); 132 | } 133 | return Object.values(uniqueCategories).sort((a, b) => a.weight - b.weight); 134 | }, 135 | categoriesInstalledOptions: (state: AppState, getters: any) => { 136 | const uniqueCategories: { [key: string]: { sid: string; name: string; weight: number } } = {}; 137 | const favoriteCat = { 138 | sid: 'Favorites', 139 | name: 'Favorites', 140 | weight: 0, 141 | }; 142 | if (getters.getFavoriteCount() > 0) { 143 | uniqueCategories["Favorites"] = favoriteCat; 144 | } 145 | if (state.appData && state.appData.modSources) { 146 | state.appData.modSources.forEach(source => { 147 | source.mods.forEach(mod => { 148 | if (mod.type !== "dependency") { 149 | const cat = mod.category; 150 | if (cat && !uniqueCategories[cat.sid] && getters.isInstalledMod(mod.sid, null)) { 151 | uniqueCategories[cat.sid] = cat; 152 | } 153 | } 154 | }); 155 | }); 156 | } 157 | return Object.values(uniqueCategories).sort((a, b) => a.weight - b.weight); 158 | }, 159 | filteredMods: (state: AppState, getters: any) => (filterCategory: string, filterGameVersion: string, searchOption: string) => { 160 | if (!state.appData) return []; 161 | return state.appData.modSources.flatMap(source => 162 | source.mods.filter(mod => { 163 | let hasCategory = false; 164 | let hasVersion = false; 165 | let matchSearch = false; 166 | if (mod.type === "dependency") return false; 167 | if (mod.name && mod.name.toLowerCase().includes(searchOption.toLowerCase())) matchSearch = true; 168 | if (mod.author && mod.author.toLowerCase().includes(searchOption.toLowerCase())) matchSearch = true; 169 | mod.versions.forEach(version => { 170 | if (version.version && version.version.toLowerCase().includes(searchOption.toLowerCase())) matchSearch = true; 171 | if (version.gameVersion && version.gameVersion.toLowerCase().includes(searchOption.toLowerCase())) matchSearch = true; 172 | }); 173 | 174 | if (filterCategory && filterCategory === "Favorites") { 175 | hasVersion = true; 176 | hasCategory = false; 177 | if (mod.type === "allInOne" && getters.isFavoriteMod(mod.sid, null)) { 178 | hasCategory = true; 179 | } else { 180 | mod.versions.forEach(version => { 181 | if (getters.isFavoriteMod(mod.sid, version)) { 182 | hasCategory = true; 183 | } 184 | }); 185 | } 186 | } else if (mod.type === "allInOne") { 187 | hasVersion = true; 188 | if (mod.category && filterCategory && mod.category.sid.toLowerCase().includes(filterCategory.toLowerCase())) { 189 | hasCategory = true; 190 | } 191 | } else { 192 | hasVersion = !filterGameVersion; 193 | if (filterGameVersion) { 194 | mod.versions.forEach(version => { 195 | if (version.gameVersion && version.gameVersion.toLowerCase().includes(filterGameVersion.toLowerCase())) { 196 | hasVersion = true; 197 | } 198 | }); 199 | } 200 | 201 | hasCategory = !filterCategory; 202 | if (filterCategory) { 203 | if (mod.category && mod.category.sid.toLowerCase().includes(filterCategory.toLowerCase())) { 204 | hasCategory = true; 205 | } 206 | } 207 | } 208 | 209 | return hasVersion && hasCategory && matchSearch; 210 | }) 211 | ); 212 | }, 213 | isInstalledMod: (state: AppState) => (modId: string, version: ModVersion | null) => { 214 | return state.appData?.config.installedMods.some(im => im.modId === modId && (version === null || im.version === version.version)); 215 | }, 216 | isFavoriteMod: (state: AppState) => (modId: string, version: ModVersion | null) => { 217 | return state.appData?.config.favoriteMods.some(fm => fm.modId === modId && (version === null || fm.version === version.version)); 218 | }, 219 | getFavoriteCount: (state: AppState, getters: any) => () => { 220 | // let count = 0; 221 | // state.appData?.config.installedMods.forEach(im => { 222 | // let versions = state.appData?.modSources.flatMap(source => source.mods.find(m => m.sid === im.modId)?.versions); 223 | // let favoriteVersions = versions?.filter(version => getters.isFavoriteMod(im.modId, version)); 224 | // count += favoriteVersions?.length || 0; 225 | // }); 226 | // return count; 227 | return state.appData?.config.favoriteMods.length; 228 | }, 229 | canBeUpdated: (state: AppState) => (modId: string, version: ModVersion | null) => { 230 | let installedMod: InstalledMod | undefined = state.appData?.config.installedMods.find(im => im.modId === modId && (version === null || im.version === version.version)); 231 | if (installedMod === undefined) return false; 232 | if (version === null) return false; 233 | return installedMod.releaseVersion !== version.release.tag_name; 234 | }, 235 | startedMod: (state: AppState) => () => { 236 | return state.appData?.startedMod; 237 | } 238 | } 239 | }); 240 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import router from './import/router'; 4 | import {store} from './import/store'; 5 | import { createI18n } from 'vue-i18n'; 6 | import axios from 'axios'; 7 | 8 | import './compiled.css' 9 | 10 | let translations: any = []; 11 | 12 | const i18n = createI18n({ 13 | legacy: false, 14 | locale: 'en', 15 | fallbackLocale: 'en', 16 | messages: {}, 17 | }); 18 | 19 | const loadLocaleMessages = async (locale: string) => { 20 | const retryInterval = 1000; // 1 seconde 21 | const maxRetryTime = 10000; // 10 secondes 22 | let startTime = Date.now(); 23 | 24 | const fetchTranslations: any = async () => { 25 | try { 26 | const response = await axios.get(`https://goodloss.fr/api/trans/${locale}`); 27 | const messages = response.data.reduce((acc: any, item: any) => { 28 | acc[item.original] = item.translation; 29 | return acc; 30 | }, {}); 31 | i18n.global.setLocaleMessage(locale, messages); 32 | translations[locale] = messages; 33 | console.log(`Translations loaded successfully for locale ${locale}`); 34 | return true; 35 | } catch (error: any) { 36 | const elapsedTime = Date.now() - startTime; 37 | if (elapsedTime < maxRetryTime) { 38 | console.warn(`Failed to load translations for locale ${locale}, retrying...`); 39 | await new Promise(res => setTimeout(res, retryInterval)); 40 | return fetchTranslations(); 41 | } else { 42 | console.error(`Failed to load translations for locale ${locale} after multiple attempts:`, error.message); 43 | return false; 44 | } 45 | } 46 | }; 47 | 48 | return fetchTranslations(); 49 | }; 50 | 51 | const loadAllTranslations = async () => { 52 | const retryInterval = 1000; // 1 seconde 53 | const maxRetryTime = 10000; // 10 secondes 54 | let startTime = Date.now(); 55 | 56 | const fetchTranslations: any = async () => { 57 | try { 58 | const response = await axios.get('https://goodloss.fr/api/trans'); 59 | const languages = response.data; 60 | 61 | const loadTranslationsPromises = languages.map((lang: any) => loadLocaleMessages(lang.code.toLowerCase())); 62 | 63 | await Promise.all(loadTranslationsPromises); 64 | console.log('All translations loaded successfully'); 65 | return languages; 66 | } catch (error: any) { 67 | const elapsedTime = Date.now() - startTime; 68 | if (elapsedTime < maxRetryTime) { 69 | console.warn('Failed to load all translations, retrying...'); 70 | await new Promise(res => setTimeout(res, retryInterval)); 71 | return fetchTranslations(); 72 | } else { 73 | console.error('Failed to load all translations after multiple attempts:', error.message); 74 | return []; 75 | } 76 | } 77 | }; 78 | 79 | return fetchTranslations(); 80 | }; 81 | 82 | const setupApp = async () => { 83 | const languages = await loadAllTranslations(); 84 | 85 | const app = createApp(App); 86 | app.config.globalProperties.$languages = languages; 87 | app.config.globalProperties.$translations = translations; 88 | 89 | app.use(router) 90 | .use(store) 91 | .use(i18n) 92 | .mount('#app'); 93 | }; 94 | 95 | setupApp(); -------------------------------------------------------------------------------- /src/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .menu-left-shadow { 6 | box-shadow: -5px 0 0 0 orange; 7 | text-shadow: 1px 0 0 currentColor; 8 | } 9 | 10 | .selectbox { 11 | @apply bg-gray-300 dark:bg-gray-700 p-2 rounded text-sm; 12 | } 13 | 14 | .button { 15 | @apply bg-gray-300 dark:bg-gray-700 p-2 rounded text-sm; 16 | } 17 | 18 | .searchbox { 19 | @apply bg-gray-300 dark:bg-gray-700 p-2 rounded text-sm; 20 | } 21 | 22 | .input { 23 | @apply bg-gray-300 dark:bg-gray-700 p-2 rounded text-sm; 24 | } 25 | 26 | .image-title { 27 | @apply brightness-0 dark:brightness-100 w-12 h-12; 28 | } 29 | 30 | .image-icon { 31 | @apply brightness-0 dark:brightness-100 h-5 w-5; 32 | } 33 | 34 | .title { 35 | @apply text-xl font-bold tracking-widest; 36 | } 37 | 38 | .title2 { 39 | @apply text-sm font-bold tracking-wider underline underline-offset-8 py-4 cursor-default; 40 | } 41 | 42 | .link { 43 | @apply text-blue-400 hover:underline; 44 | } 45 | 46 | .downloader-line { 47 | @apply p-2 border-white border rounded cursor-pointer; 48 | } 49 | 50 | .online { 51 | @apply bg-green-500 dark:bg-green-700; 52 | } 53 | 54 | .offline { 55 | @apply bg-red-500 dark:bg-red-800; 56 | } -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import type { DefineComponent } from 'vue' 5 | const component: DefineComponent<{}, {}, any> 6 | export default component 7 | } 8 | 9 | interface Window { 10 | // expose in the `electron/preload/index.ts` 11 | ipcRenderer: import('electron').IpcRenderer 12 | } 13 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: 'class', 4 | content: [ 5 | "./src/**/*.{vue,js,ts}", 6 | "./electron/**/*.{vue,js,ts}" 7 | ], 8 | theme: { 9 | extend: { 10 | minWidth: { 11 | '52': '13rem', 12 | '64': '256px', 13 | } 14 | } 15 | }, 16 | plugins: [], 17 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "resolveJsonModule": true, 10 | "isolatedModules": true, 11 | "esModuleInterop": true, 12 | "lib": ["ESNext", "DOM"], 13 | "skipLibCheck": true, 14 | "noEmit": true 15 | }, 16 | "include": ["src"], 17 | "references": [ 18 | { "path": "./tsconfig.node.json" } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "resolveJsonModule": true, 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts", "package.json", "electron"] 10 | } 11 | -------------------------------------------------------------------------------- /vite.config.flat.txt: -------------------------------------------------------------------------------- 1 | import { rmSync } from 'node:fs' 2 | import { defineConfig } from 'vite' 3 | import vue from '@vitejs/plugin-vue' 4 | import electron from 'vite-plugin-electron' 5 | import renderer from 'vite-plugin-electron-renderer' 6 | import pkg from './package.json' 7 | 8 | // https://vitejs.dev/config/ 9 | export default defineConfig(({ command }) => { 10 | rmSync('dist-electron', { recursive: true, force: true }) 11 | 12 | const isServe = command === 'serve' 13 | const isBuild = command === 'build' 14 | const sourcemap = isServe || !!process.env.VSCODE_DEBUG 15 | 16 | return { 17 | plugins: [ 18 | vue(), 19 | electron([ 20 | { 21 | // Main process entry file of the Electron App. 22 | entry: 'electron/main/index.ts', 23 | onstart({ startup }) { 24 | if (process.env.VSCODE_DEBUG) { 25 | console.log(/* For `.vscode/.debug.script.mjs` */'[startup] Electron App') 26 | } else { 27 | startup() 28 | } 29 | }, 30 | vite: { 31 | build: { 32 | sourcemap, 33 | minify: isBuild, 34 | outDir: 'dist-electron/main', 35 | rollupOptions: { 36 | // Some third-party Node.js libraries may not be built correctly by Vite, especially `C/C++` addons, 37 | // we can use `external` to exclude them to ensure they work correctly. 38 | // Others need to put them in `dependencies` to ensure they are collected into `app.asar` after the app is built. 39 | // Of course, this is not absolute, just this way is relatively simple. :) 40 | external: Object.keys('dependencies' in pkg ? pkg.dependencies : {}), 41 | }, 42 | }, 43 | }, 44 | }, 45 | { 46 | entry: 'electron/preload/index.ts', 47 | onstart({ reload }) { 48 | // Notify the Renderer process to reload the page when the Preload scripts build is complete, 49 | // instead of restarting the entire Electron App. 50 | reload() 51 | }, 52 | vite: { 53 | build: { 54 | sourcemap: sourcemap ? 'inline' : undefined, // #332 55 | minify: isBuild, 56 | outDir: 'dist-electron/preload', 57 | rollupOptions: { 58 | external: Object.keys('dependencies' in pkg ? pkg.dependencies : {}), 59 | }, 60 | }, 61 | }, 62 | } 63 | ]), 64 | // Use Node.js API in the Renderer process 65 | renderer(), 66 | ], 67 | server: process.env.VSCODE_DEBUG && (() => { 68 | const url = new URL(pkg.debug.env.VITE_DEV_SERVER_URL) 69 | return { 70 | host: url.hostname, 71 | port: +url.port, 72 | } 73 | })(), 74 | clearScreen: false, 75 | } 76 | }) 77 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | // @ts-ignore 3 | import { defineConfig } from 'vite' 4 | // @ts-ignore 5 | import vue from '@vitejs/plugin-vue' 6 | // @ts-ignore 7 | import electron from 'vite-plugin-electron/simple' 8 | import pkg from './package.json' 9 | 10 | // https://vitejs.dev/config/ 11 | export default defineConfig(({ command }) => { 12 | fs.rmSync('dist-electron', { recursive: true, force: true }) 13 | 14 | const isServe = command === 'serve' 15 | const isBuild = command === 'build' 16 | const sourcemap = isServe || !!process.env.VSCODE_DEBUG 17 | 18 | return { 19 | plugins: [ 20 | vue(), 21 | electron({ 22 | main: { 23 | // Shortcut of `build.lib.entry` 24 | entry: 'electron/main/index.ts', 25 | onstart({ startup }) { 26 | if (process.env.VSCODE_DEBUG) { 27 | console.log(/* For `.vscode/.debug.script.mjs` */'[startup] Electron App') 28 | } else { 29 | startup() 30 | } 31 | }, 32 | vite: { 33 | build: { 34 | sourcemap, 35 | minify: isBuild, 36 | outDir: 'dist-electron/main', 37 | rollupOptions: { 38 | // Some third-party Node.js libraries may not be built correctly by Vite, especially `C/C++` addons, 39 | // we can use `external` to exclude them to ensure they work correctly. 40 | // Others need to put them in `dependencies` to ensure they are collected into `app.asar` after the app is built. 41 | // Of course, this is not absolute, just this way is relatively simple. :) 42 | external: Object.keys('dependencies' in pkg ? pkg.dependencies : {}), 43 | }, 44 | }, 45 | }, 46 | }, 47 | preload: { 48 | // Shortcut of `build.rollupOptions.input`. 49 | // Preload scripts may contain Web assets, so use the `build.rollupOptions.input` instead `build.lib.entry`. 50 | input: 'electron/preload/index.ts', 51 | vite: { 52 | build: { 53 | sourcemap: sourcemap ? 'inline' : undefined, // #332 54 | minify: isBuild, 55 | outDir: 'dist-electron/preload', 56 | rollupOptions: { 57 | external: Object.keys('dependencies' in pkg ? pkg.dependencies : {}), 58 | }, 59 | }, 60 | }, 61 | }, 62 | // Ployfill the Electron and Node.js API for Renderer process. 63 | // If you want use Node.js in Renderer process, the `nodeIntegration` needs to be enabled in the Main process. 64 | // See 👉 https://github.com/electron-vite/vite-plugin-electron-renderer 65 | renderer: {}, 66 | }), 67 | ], 68 | server: process.env.VSCODE_DEBUG && (() => { 69 | const url = new URL(pkg.debug.env.VITE_DEV_SERVER_URL) 70 | return { 71 | host: url.hostname, 72 | port: +url.port, 73 | } 74 | })(), 75 | clearScreen: false, 76 | } 77 | }) 78 | --------------------------------------------------------------------------------