├── .babelrc ├── .eslintrc.json ├── .gitattributes ├── .github └── ISSUE_TEMPLATE │ └── bug_report.md ├── .gitignore ├── BnetHelper.ps1 ├── CONTRIBUTING.md ├── LICENSE.md ├── LauncherAutoClose.ps1 ├── README.md ├── SteamGridDB Manager.VisualElementsManifest.xml ├── assets ├── VisualElements │ ├── VisualElements_150.png │ └── VisualElements_70.png └── icons │ ├── 192x192.png │ ├── 194x194.png │ ├── icon.ico │ ├── icon.png │ └── installerSidebar.bmp ├── main.js ├── package-lock.json ├── package.json ├── public └── index.html ├── src ├── css │ └── App.css ├── img │ ├── capsule_none.png │ ├── capsule_vertical_none.png │ ├── hero_none.png │ ├── logo_none.png │ └── uwp-noise.png └── js │ ├── App.jsx │ ├── Bethesda.js │ ├── Components │ ├── Games │ │ └── GameListItem.jsx │ ├── Import │ │ ├── ImportAllButton.jsx │ │ ├── ImportList.jsx │ │ └── ImportListItem.jsx │ ├── TopBlur.jsx │ ├── spinner.jsx │ └── toastHandler.jsx │ ├── Game.jsx │ ├── Import.jsx │ ├── Search.jsx │ ├── Steam.js │ ├── games.jsx │ ├── importers.js │ ├── importers │ ├── BattleNet.js │ ├── Epic.js │ ├── Gog.js │ ├── Oculus.js │ ├── Origin.js │ └── Uplay.js │ ├── index.jsx │ └── paths.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": [ 4 | ["@babel/plugin-proposal-decorators", { "decoratorsBeforeExport": false }], 5 | ["@babel/plugin-proposal-class-properties", { "loose": true }], 6 | "@babel/plugin-transform-runtime" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es6": true, 5 | "node": true, 6 | "browser": true 7 | }, 8 | "extends": ["airbnb"], 9 | "settings": { 10 | "import/core-modules": ["electron"] 11 | }, 12 | "globals": { 13 | "Atomics": "readonly", 14 | "SharedArrayBuffer": "readonly", 15 | "BigInt": true 16 | }, 17 | "rules": { 18 | "indent": ["error", 2], 19 | "react/jsx-indent": ["error", 2], 20 | "class-methods-use-this": ["off"], 21 | "no-underscore-dangle": ["off"], 22 | "no-param-reassign": ["off"], 23 | "react/forbid-prop-types": ["off"], 24 | "react/destructuring-assignment": ["off"], 25 | "jsx-a11y/click-events-have-key-events": ["off"], 26 | "jsx-a11y/no-static-element-interactions": ["off"], 27 | "linebreak-style": ["off"], 28 | "arrow-body-style": ["off"] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Operating System** 24 | - Windows 10, 1903 25 | 26 | **Installed Launchers** 27 | - Epic Games Launcher 28 | - Uplay 29 | - ... 30 | 31 | **Screenshots** 32 | If applicable, add screenshots to help explain your problem. 33 | 34 | **Log** 35 | You can find the log file by opening the "Run" dialog by pressing Win+R, pasting `%appdata%/steamgriddb-manager` into it then pressing "OK". Upload it here as an attachment. 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | public/bundle.js 2 | public/bundle.js.map 3 | public/img/* 4 | node_modules/ 5 | .idea/ 6 | dist/ -------------------------------------------------------------------------------- /BnetHelper.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [Parameter(Mandatory=$True)][string]$bnet, # Path to the Battle.net executable 3 | [Parameter(Mandatory=$True)][string]$launchid # Battle.net launch id to launch 4 | ) 5 | 6 | Write-Host 'Starting Battle.net' 7 | Start-Process $bnet 8 | 9 | # Wait to be sure log file gets created (just to be safe, usually gets created instantly) 10 | Start-Sleep -Seconds 3 11 | 12 | # Get latest log file 13 | $log = Get-ChildItem -Path "$env:LOCALAPPDATA\Battle.net\Logs" -Filter "battle.net-*.log" | Sort-Object LastAccessTime -Descending | Select-Object -First 1 14 | 15 | $bnetStarted = $False 16 | 17 | Write-Host 'Waiting for Battle.net to start completely' 18 | 19 | # Get current system date 20 | $currentDate = Get-Date 21 | Do { 22 | # Check log file until we find this string 23 | $launchedCompletely = Select-String -path $log -pattern 'GameController initialization complete' 24 | 25 | If (!($launchedCompletely)) { 26 | # Timeout after 1 minute 27 | If ($currentDate.AddMinutes(1) -lt (Get-Date)) 28 | { 29 | Write-Host 'Could not find successful launch' 30 | exit 31 | } 32 | Start-Sleep -Seconds 1 33 | } Else { 34 | Write-Host 'Bnet started!' 35 | $bnetStarted = $True 36 | } 37 | } Until ($bnetStarted) 38 | 39 | # Launch 40 | Write-Host "Starting game ($launchid)" 41 | Start-Process $bnet "--exec=`"launch $launchid`"" -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | Like any other open source projects, there are multiple ways to contribute to this project: 4 | 5 | * As a developer, depending on your skills and experience, 6 | * As a user who enjoys the project and wants to help. 7 | 8 | ##### Reporting Bugs 9 | 10 | If you found something broken or not working properly, feel free to create an issue in Github with as much information as possible, such as logs and how to reproduce the problem. Before opening the issue, make sure that: 11 | 12 | * You have read this documentation, 13 | * You are using the latest version of project, 14 | * You already searched other issues to see if your problem or request was already reported. 15 | 16 | ##### Improving the Documentation 17 | 18 | You can improve this documentation by forking its repository, updating the content and sending a pull request. 19 | 20 | 21 | #### We ❤️ Pull Requests 22 | 23 | A pull request does not need to be a fix for a bug or implementing something new. Software can always be improved, legacy code removed and tests are always welcome! 24 | 25 | Please do not be afraid of contributing code, make sure it follows these rules: 26 | 27 | * Your code compiles, does not break any of the existing code in the master branch and does not cause conflicts, 28 | * The code is readable and has comments, that aren’t superfluous or unnecessary, 29 | * An overview or context is provided as body of the Pull Request. It does not need to be too extensive. 30 | 31 | Extra points if your code comes with tests! 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2019 1 HP Games, LLC 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /LauncherAutoClose.ps1: -------------------------------------------------------------------------------- 1 | # This script is used to auto-close launchers that don't have an auto-close setting. 2 | 3 | 4 | param ( 5 | [Parameter(Mandatory=$True)][string]$launchcmd, # Command to launch game 6 | [Parameter(Mandatory=$True)][string]$launcher, # Launcher to kill 7 | [Parameter(Mandatory=$True)][string[]]$game, # Game(s) to watch 8 | 9 | [Parameter(Mandatory=$False)][bool]$bnet = $False, # Use Battle.net-specific launch method 10 | [Parameter(Mandatory=$False)][string]$bnetpath, # Battle.net executable 11 | [Parameter(Mandatory=$False)][string]$bnetlaunchid # Battle.net launch ID 12 | ) 13 | 14 | $scriptPath = Split-Path -parent $MyInvocation.MyCommand.Definition 15 | 16 | function Wait-ProcessChildren($id) { 17 | $child = Get-WmiObject win32_process | where {$_.ParentProcessId -In $id} 18 | if ($child) { 19 | Write-Host 'Child found' 20 | Wait-Process -Id $child.handle 21 | Wait-ProcessChildren $child.handle 22 | } 23 | } 24 | 25 | # Kill launcher 26 | Write-Host 'Killing launcher' 27 | Get-Process $launcher -ErrorAction SilentlyContinue | Stop-Process 28 | 29 | # Start Game 30 | If ($bnet) { 31 | & "$scriptPath\BnetHelper.ps1" -bnet $bnetpath -launchid $bnetlaunchid 32 | } Else { 33 | Start-Process $launchcmd 34 | } 35 | 36 | $gameStarted = $False 37 | 38 | Write-Host 'Waiting for game to start' 39 | 40 | # Get current system date 41 | $currentDate = Get-Date 42 | Do { 43 | $gameProcess = Get-Process $game -ErrorAction SilentlyContinue 44 | 45 | If (!($gameProcess)) { 46 | # Timeout after 30 minutes 47 | If ($currentDate.AddMinutes(30) -lt (Get-Date)) { 48 | Write-Host 'Game process could not be found' 49 | exit 50 | } 51 | Start-Sleep -Seconds 1 52 | } Else { 53 | Write-Host 'Game started!' 54 | $gameStarted = $true 55 | } 56 | } Until ($gameStarted) 57 | 58 | # Wait until game closes 59 | Wait-Process -InputObject $gameProcess 60 | 61 | # Wait until child processes close 62 | Wait-ProcessChildren $gameProcess.Id 63 | 64 | Write-Host 'Game closed' 65 | 66 | # Wait for cloud saves or whatever 67 | Start-Sleep -Seconds 5 68 | 69 | # Kill launcher 70 | Write-Host 'Killing launcher' 71 | Get-Process $launcher | Stop-Process 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [SteamGridDB Manager](https://www.steamgriddb.com/manager) 2 | SteamGridDB Manager automatically finds games from launchers on your system and imports them into your Steam library with a click of a button. 3 | 4 | See the website for download and more information: https://www.steamgriddb.com/manager 5 | 6 | # Supported Launchers 7 | SteamGridDB Manager supports importing from the following launchers: 8 | - Origin 9 | - Uplay 10 | - Epic Games Launcher 11 | - Blizzard Battle.net 12 | - GOG.com 13 | - *More coming soon!* 14 | 15 | # Building From Source 16 | 1. Install the dependencies with `npm install`. 17 | 2. Run one of the npm scripts: 18 | - `npm run run` Builds and starts the app. 19 | - `npm run watch` Builds and starts the app. Reloads the app when any file changes. 20 | - `npm run dist` Builds, then outputs an installer into the `dist` directory using electron-builder. 21 | 22 | # License 23 | [MIT](LICENSE.md) 24 | -------------------------------------------------------------------------------- /SteamGridDB Manager.VisualElementsManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | -------------------------------------------------------------------------------- /assets/VisualElements/VisualElements_150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SteamGridDB/steamgriddb-manager/93fa9f1fea99527d90eb0f270465479e4be71673/assets/VisualElements/VisualElements_150.png -------------------------------------------------------------------------------- /assets/VisualElements/VisualElements_70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SteamGridDB/steamgriddb-manager/93fa9f1fea99527d90eb0f270465479e4be71673/assets/VisualElements/VisualElements_70.png -------------------------------------------------------------------------------- /assets/icons/192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SteamGridDB/steamgriddb-manager/93fa9f1fea99527d90eb0f270465479e4be71673/assets/icons/192x192.png -------------------------------------------------------------------------------- /assets/icons/194x194.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SteamGridDB/steamgriddb-manager/93fa9f1fea99527d90eb0f270465479e4be71673/assets/icons/194x194.png -------------------------------------------------------------------------------- /assets/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SteamGridDB/steamgriddb-manager/93fa9f1fea99527d90eb0f270465479e4be71673/assets/icons/icon.ico -------------------------------------------------------------------------------- /assets/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SteamGridDB/steamgriddb-manager/93fa9f1fea99527d90eb0f270465479e4be71673/assets/icons/icon.png -------------------------------------------------------------------------------- /assets/icons/installerSidebar.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SteamGridDB/steamgriddb-manager/93fa9f1fea99527d90eb0f270465479e4be71673/assets/icons/installerSidebar.bmp -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | const { app, globalShortcut, BrowserWindow } = require('electron'); 2 | const { autoUpdater } = require('electron-updater'); 3 | const log = require('electron-log'); 4 | const chokidar = require('chokidar') 5 | 6 | const path = require('path'); 7 | const url = require('url'); 8 | 9 | autoUpdater.autoInstallOnAppQuit = true; 10 | 11 | log.catchErrors({ showDialog: true }); 12 | 13 | log.info(`Started SGDB Manager ${app.getVersion()}`); 14 | 15 | // Keep a global reference of the window object, if you don't, the window will 16 | // be closed automatically when the JavaScript object is garbage collected. 17 | let mainWindow; 18 | 19 | function createWindow() { 20 | autoUpdater.checkForUpdatesAndNotify(); 21 | mainWindow = new BrowserWindow({ 22 | width: 800, 23 | height: 600, 24 | frame: false, 25 | icon: path.join(__dirname, 'assets/icons/192x192.png'), 26 | transparent: false, 27 | webPreferences: { 28 | nodeIntegration: true, 29 | }, 30 | }); 31 | 32 | mainWindow.loadURL(url.format({ 33 | pathname: path.join(__dirname, 'public', 'index.html'), 34 | protocol: 'file:', 35 | slashes: true, 36 | })); 37 | 38 | // Set up live reload 39 | if (process.mainModule.filename.indexOf('app.asar') === -1) { 40 | chokidar.watch('public') 41 | .on('change', () => { 42 | mainWindow.webContents.executeJavaScript('window.location = window.location.origin + window.location.pathname'); 43 | }); 44 | } 45 | 46 | // Add a global shortcut for opening the dev tools 47 | globalShortcut.register('CommandOrControl+Shift+L', () => { 48 | mainWindow.webContents.openDevTools(); 49 | }); 50 | 51 | mainWindow.on('beforeunload', () => { 52 | globalShortcut.unregisterAll(); 53 | }); 54 | 55 | // Emitted when the window is closed. 56 | mainWindow.on('closed', () => { 57 | // Dereference the window object, usually you would store windows 58 | // in an array if your app supports multi windows, this is the time 59 | // when you should delete the corresponding element. 60 | mainWindow = null; 61 | }); 62 | } 63 | 64 | // This method will be called when Electron has finished 65 | // initialization and is ready to create browser windows. 66 | // Some APIs can only be used after this event occurs. 67 | app.on('ready', createWindow); 68 | 69 | // Quit when all windows are closed. 70 | app.on('window-all-closed', () => { 71 | // On OS X it is common for applications and their menu bar 72 | // to stay active until the user quits explicitly with Cmd + Q 73 | if (process.platform !== 'darwin') { 74 | app.quit(); 75 | } 76 | }); 77 | 78 | app.on('activate', () => { 79 | // On OS X it's common to re-create a window in the app when the 80 | // dock icon is clicked and there are no other windows open. 81 | if (mainWindow === null) { 82 | createWindow(); 83 | } 84 | }); 85 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "steamgriddb-manager", 3 | "version": "0.4.2", 4 | "description": "Easily find and download new grid images for your Steam games.", 5 | "main": "main.js", 6 | "author": "SteamGridDB.com", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/steamgriddb/steamgriddb-manager.git" 11 | }, 12 | "scripts": { 13 | "start": "electron .", 14 | "build": "webpack --mode production && electron-builder", 15 | "run": "webpack --mode development && electron .", 16 | "watch": "concurrently \"webpack --mode development --watch\" \"electron .\"", 17 | "pack": "electron-builder --dir", 18 | "publish": "webpack --mode production && electron-builder --publish always", 19 | "postinstall": "electron-builder install-app-deps" 20 | }, 21 | "build": { 22 | "appId": "com.steamgriddb.manager", 23 | "productName": "SteamGridDB Manager", 24 | "copyright": "SteamGridDB.com", 25 | "compression": "maximum", 26 | "directories": { 27 | "buildResources": "assets/icons" 28 | }, 29 | "extraResources": [ 30 | { 31 | "from": "assets/VisualElements", 32 | "to": "VisualElements" 33 | } 34 | ], 35 | "extraFiles": [ 36 | { 37 | "from": "LauncherAutoClose.ps1", 38 | "to": "." 39 | }, 40 | { 41 | "from": "BnetHelper.ps1", 42 | "to": "." 43 | }, 44 | "SteamGridDB Manager.VisualElementsManifest.xml" 45 | ], 46 | "win": { 47 | "target": [ 48 | "nsis", 49 | "7z" 50 | ] 51 | }, 52 | "nsis": { 53 | "oneClick": false, 54 | "allowToChangeInstallationDirectory": true 55 | }, 56 | "publish": { 57 | "provider": "github" 58 | } 59 | }, 60 | "devDependencies": { 61 | "@babel/cli": "^7.12.10", 62 | "@babel/core": "^7.12.10", 63 | "@babel/plugin-proposal-class-properties": "^7.12.1", 64 | "@babel/plugin-proposal-decorators": "^7.12.12", 65 | "@babel/plugin-transform-runtime": "^7.12.10", 66 | "@babel/preset-env": "^7.12.11", 67 | "@babel/preset-react": "^7.12.10", 68 | "babel-loader": "^8.2.2", 69 | "concurrently": "^5.3.0", 70 | "css-loader": "^5.0.1", 71 | "electron": "^6.1.12", 72 | "electron-builder": "^21.2.0", 73 | "electron-builder-squirrel-windows": "^21.2.0", 74 | "electron-packager": "^14.2.1", 75 | "eslint": "^7.17.0", 76 | "eslint-config-airbnb": "^18.2.1", 77 | "eslint-plugin-import": "^2.22.1", 78 | "eslint-plugin-jsx-a11y": "^6.4.1", 79 | "eslint-plugin-react": "^7.22.0", 80 | "eslint-plugin-react-hooks": "^4.2.0", 81 | "file-loader": "^4.3.0", 82 | "steam-id-convertor": "^1.0.1", 83 | "style-loader": "^1.3.0", 84 | "webpack": "^4.44.2", 85 | "webpack-cli": "^3.3.12" 86 | }, 87 | "dependencies": { 88 | "@node-steam/vdf": "^2.1.0", 89 | "blizzard-product-parser": "^1.0.1", 90 | "cheerio": "^1.0.0-rc.5", 91 | "chokidar": "^3.5.0", 92 | "crc": "^3.8.0", 93 | "electron-log": "^3.0.7", 94 | "electron-store": "^5.2.0", 95 | "electron-updater": "^4.3.5", 96 | "fuse.js": "^3.6.1", 97 | "iconv-lite": "^0.6.2", 98 | "js-yaml": "^4.0.0", 99 | "jsonminify": "^0.4.1", 100 | "lodash": "^4.17.20", 101 | "metrohash": "^2.6.0", 102 | "node-powershell": "^4.0.0", 103 | "promise-reflect": "^1.1.0", 104 | "promise-settle": "^0.3.0", 105 | "prop-types": "^15.7.2", 106 | "pubsub-js": "^1.9.2", 107 | "query-string": "^6.13.8", 108 | "react": "^16.14.0", 109 | "react-desktop": "^0.3.9", 110 | "react-dom": "^16.14.0", 111 | "react-lazyload": "^2.6.9", 112 | "react-motion": "^0.5.2", 113 | "react-router-dom": "^5.2.0", 114 | "react-transition-group": "1.2.1", 115 | "react-uwp": "^1.3.3", 116 | "steam-categories": "^1.1.3", 117 | "steam-shortcut-editor": "^3.1.1", 118 | "steamgriddb": "^1.3.1", 119 | "steamid": "^1.1.3", 120 | "winreg": "^1.2.4", 121 | "xml-js": "^1.6.11" 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | SteamGridDB Manager 9 | 10 | 11 | 12 |
13 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/css/App.css: -------------------------------------------------------------------------------- 1 | ::-webkit-scrollbar { 2 | width: 12px; 3 | } 4 | 5 | ::-webkit-scrollbar-track { 6 | -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); 7 | border-radius: 10px; 8 | } 9 | 10 | ::-webkit-scrollbar-thumb { 11 | border-radius: 10px; 12 | -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.5); 13 | } 14 | 15 | /* hack to offset scrollbar */ 16 | ::-webkit-scrollbar-button:start { 17 | background: transparent; 18 | } 19 | 20 | #import-container::-webkit-scrollbar-button:start { 21 | height: 30px; 22 | } 23 | 24 | #search-container::-webkit-scrollbar-button:start { 25 | height: 30px; 26 | } 27 | 28 | #grids-container::-webkit-scrollbar-button:start { 29 | height: 78px; 30 | } 31 | 32 | div[class^="fluent-background-"] { 33 | background-color: #1a1a1a; 34 | } 35 | 36 | /* Transition progress bar */ 37 | [class*="progress-bar-bar"] { 38 | transition: transform ease 300ms; 39 | } 40 | 41 | #import-container [class*="list-view-item-"]:hover button { 42 | opacity: 1; 43 | } 44 | 45 | /* react-uwp sidebar is broken */ 46 | [class*="split-view-command-icon-"] { 47 | font-size: 16px !important; 48 | line-height: 48px !important; 49 | } 50 | 51 | [class*="checkbox-iconParent"] { 52 | margin-left: auto; 53 | } 54 | 55 | /* react-uwp autosuggest is broken */ 56 | [class*="autosuggest-box-root"] { 57 | vertical-align: top !important; 58 | } 59 | 60 | /* react-uwp toasts have no font set and you can't set them via style attr */ 61 | [class*="toast-wrapper-"] { 62 | font-family: Segoe UI, Microsoft YaHei, Open Sans, sans-serif, Hiragino Sans GB, Arial, Lantinghei SC, STHeiti, WenQuanYi Micro Hei, SimSun; 63 | } 64 | 65 | .grid-fadein-appear { 66 | opacity: 0; 67 | } 68 | 69 | .grid-fadein-appear.grid-fadein-appear-active { 70 | opacity: 1; 71 | transition: opacity 1s ease-in; 72 | } 73 | 74 | .grid-wrapper { 75 | background-color: rgba(0, 0, 0, 0.4); 76 | } 77 | 78 | .grid-wrapper:hover { 79 | background-color: rgba(0, 0, 0, 0.8); 80 | } 81 | 82 | .grid-wrapper:hover .grid-overlay { 83 | opacity: 1; 84 | } 85 | 86 | .grid-overlay { 87 | opacity: 0; 88 | position: absolute; 89 | top: 0; 90 | left: 0; 91 | background: rgba(0,0,0,0.5); 92 | padding: 10px; 93 | z-index: 1; 94 | } 95 | 96 | .grid-overlay:hover { 97 | opacity: 1; 98 | } -------------------------------------------------------------------------------- /src/img/capsule_none.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SteamGridDB/steamgriddb-manager/93fa9f1fea99527d90eb0f270465479e4be71673/src/img/capsule_none.png -------------------------------------------------------------------------------- /src/img/capsule_vertical_none.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SteamGridDB/steamgriddb-manager/93fa9f1fea99527d90eb0f270465479e4be71673/src/img/capsule_vertical_none.png -------------------------------------------------------------------------------- /src/img/hero_none.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SteamGridDB/steamgriddb-manager/93fa9f1fea99527d90eb0f270465479e4be71673/src/img/hero_none.png -------------------------------------------------------------------------------- /src/img/logo_none.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SteamGridDB/steamgriddb-manager/93fa9f1fea99527d90eb0f270465479e4be71673/src/img/logo_none.png -------------------------------------------------------------------------------- /src/img/uwp-noise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SteamGridDB/steamgriddb-manager/93fa9f1fea99527d90eb0f270465479e4be71673/src/img/uwp-noise.png -------------------------------------------------------------------------------- /src/js/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TitleBar } from 'react-desktop/windows'; 3 | import { Theme as UWPThemeProvider, getTheme } from 'react-uwp/Theme'; 4 | import NavigationView from 'react-uwp/NavigationView'; 5 | import SplitViewCommand from 'react-uwp/SplitViewCommand'; 6 | import { IconButton } from 'react-uwp'; 7 | import PubSub from 'pubsub-js'; 8 | import { 9 | HashRouter as Router, 10 | Redirect, 11 | Link, 12 | Route, 13 | } from 'react-router-dom'; 14 | import ToastHandler from './Components/toastHandler'; 15 | 16 | import UWPNoise from '../img/uwp-noise.png'; 17 | import '../css/App.css'; 18 | import Games from './games'; 19 | import Game from './Game'; 20 | import Import from './Import'; 21 | import Search from './Search'; 22 | 23 | import Steam from './Steam'; 24 | 25 | // Using window.require so babel doesn't change the node require 26 | const electron = window.require('electron'); 27 | const { remote } = electron; 28 | 29 | // Log renderer errors 30 | const log = window.require('electron-log'); 31 | log.catchErrors({ showDialog: true }); 32 | 33 | window.Steam = Steam; 34 | 35 | class App extends React.Component { 36 | constructor(props) { 37 | super(props); 38 | 39 | this.state = { isMaximized: false, showBack: false }; 40 | this.toggleMaximize = this.toggleMaximize.bind(this); 41 | 42 | // Track windows snap calling maximize / unmaximize 43 | const window = remote.getCurrentWindow(); 44 | 45 | window.on('maximize', () => { 46 | this.setState({ isMaximized: true }); 47 | }); 48 | 49 | window.on('unmaximize', () => { 50 | this.setState({ isMaximized: false }); 51 | }); 52 | 53 | PubSub.subscribe('showBack', (message, args) => { 54 | this.setState({ showBack: args }); 55 | }); 56 | } 57 | 58 | toggleMaximize() { 59 | const { isMaximized } = this.state; 60 | const window = remote.getCurrentWindow(); 61 | this.setState({ isMaximized: !isMaximized }); 62 | if (!isMaximized) { 63 | window.maximize(); 64 | } else { 65 | window.unmaximize(); 66 | } 67 | } 68 | 69 | handleNavRedirect(path) { 70 | this.setState({ redirectTo: path }); 71 | } 72 | 73 | minimize() { 74 | remote.getCurrentWindow().minimize(); 75 | } 76 | 77 | close() { 78 | remote.getCurrentWindow().close(); 79 | } 80 | 81 | render() { 82 | const accentColor = remote.systemPreferences.getAccentColor(); 83 | const navWidth = 48; 84 | const { showBack, isMaximized, redirectTo } = this.state; 85 | 86 | const navigationTopNodes = [ 87 | this.handleNavRedirect('/')} />, 88 | this.handleNavRedirect('/import')} />, 89 | ]; 90 | 91 | let backBtn; 92 | let titleWidth = '100%'; 93 | if (showBack) { 94 | backBtn = ( 95 | { 98 | this.setState({ showBack: false }); 99 | }} 100 | > 101 | 114 | Back 115 | 116 | 117 | ); 118 | titleWidth = `calc(100% - ${navWidth}px)`; 119 | } 120 | 121 | return ( 122 | 129 | 130 |
131 | {backBtn} 132 | 151 | 171 |
180 | {redirectTo && } 181 | 182 | 183 | 184 | 185 | 186 | 187 |
188 |
189 |
190 |
191 | 192 | 193 |
194 | ); 195 | } 196 | } 197 | 198 | export default App; 199 | -------------------------------------------------------------------------------- /src/js/Bethesda.js: -------------------------------------------------------------------------------- 1 | const Registry = window.require('winreg'); 2 | const path = window.require('path'); 3 | const promiseReflect = window.require('promise-reflect'); 4 | 5 | class Bethesda { 6 | static isInstalled() { 7 | return new Promise((resolve, reject) => { 8 | const reg = new Registry({ 9 | hive: Registry.HKLM, 10 | arch: 'x86', 11 | key: '\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{3448917E-E4FE-4E30-9502-9FD52EABB6F5}_is1' 12 | }); 13 | 14 | reg.valueExists('', (err, exists) => { 15 | if (err) { 16 | reject(new Error('Could not check if Bethesda Launcher is installed.')); 17 | } 18 | 19 | resolve(exists); 20 | }); 21 | }); 22 | } 23 | 24 | static getBethesdaPath() { 25 | return new Promise((resolve, reject) => { 26 | const reg = new Registry({ 27 | hive: Registry.HKLM, 28 | arch: 'x86', 29 | key: '\\SOFTWARE\\Bethesda Softworks\\Bethesda.net' 30 | }); 31 | 32 | reg.values((err, items) => { 33 | if (err) { 34 | reject(err); 35 | } 36 | 37 | let bethesdaPath = false; 38 | 39 | items.forEach((item) => { 40 | if (item.name === 'installLocation') { 41 | bethesdaPath = item.value; 42 | } 43 | }); 44 | 45 | if (bethesdaPath) { 46 | resolve(bethesdaPath); 47 | } else { 48 | reject(new Error('Could not find Bethesda Launcher path.')); 49 | } 50 | }); 51 | }); 52 | } 53 | 54 | static _processRegKey(key, bethesdaPath) { 55 | return new Promise((resolve, reject) => { 56 | key.get('UninstallString', (err, UninstallString) => { 57 | if (UninstallString != null && UninstallString.value.match(/bethesdanet:\/\/uninstall\//)) { 58 | key.values((err, items) => { 59 | const game = { 60 | platform: 'bethesda' 61 | }; 62 | 63 | items.forEach((item) => { 64 | if (item.name === 'ProductID') { 65 | game.id = parseInt(item.value, 16); 66 | } 67 | 68 | if (item.name === 'DisplayName') { 69 | game.name = item.value; 70 | } 71 | }); 72 | game.path = `"${bethesdaPath}"`; 73 | game.exe = `"${path.join(bethesdaPath, 'BethesdaNetUpdater.exe')}"`; 74 | game.params = `bethesdanet://run/${game.id}`; 75 | 76 | resolve(game); 77 | }); 78 | } else { 79 | reject(key); 80 | } 81 | }); 82 | }); 83 | } 84 | 85 | static getGames() { 86 | return new Promise((resolve, reject) => { 87 | this.getBethesdaPath().then((bethesdaPath) => { 88 | // Loop everything and get ones with bethesdanet://uninstall/ in UninstallString 89 | // Look for another way cause this is horrible 90 | const reg = new Registry({ 91 | hive: Registry.HKLM, 92 | arch: 'x86', 93 | key: '\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall' 94 | }); 95 | 96 | reg.keys((err, keys) => { 97 | if (err) { 98 | reject(new Error('Could not get Bethesda Launcher games.')); 99 | } 100 | 101 | const promiseArr = keys.map((key) => this._processRegKey(key, bethesdaPath).then((res) => res)); 102 | Promise.all(promiseArr.map(promiseReflect)) 103 | .then((results) => results.filter((result) => result.status === 'resolved').map((result) => result.data)) 104 | .then((results) => resolve(results)); 105 | }); 106 | }).catch((err) => reject(err)); 107 | }); 108 | } 109 | } 110 | 111 | export default Bethesda; -------------------------------------------------------------------------------- /src/js/Components/Games/GameListItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import ListView from 'react-uwp/ListView'; 4 | 5 | class GameListItem extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | 9 | const { platform } = this.props; 10 | 11 | this.platform = platform; 12 | this.handleClick = this.handleClick.bind(this); 13 | } 14 | 15 | handleClick(index) { 16 | const { onItemClick } = this.props; 17 | onItemClick(this.platform, index); 18 | } 19 | 20 | render() { 21 | const { platform, platformName, listSource } = this.props; 22 | const { theme } = this.context; 23 | 24 | return ( 25 |
26 |
35 | {platformName} 36 |
37 | 43 |
44 | ); 45 | } 46 | } 47 | 48 | GameListItem.propTypes = { 49 | platform: PropTypes.string.isRequired, 50 | listSource: PropTypes.arrayOf(PropTypes.node).isRequired, 51 | platformName: PropTypes.string.isRequired, 52 | onItemClick: PropTypes.func, 53 | }; 54 | 55 | GameListItem.defaultProps = { 56 | onItemClick: () => {}, 57 | }; 58 | 59 | GameListItem.contextTypes = { theme: PropTypes.object }; 60 | 61 | export default GameListItem; 62 | -------------------------------------------------------------------------------- /src/js/Components/Import/ImportAllButton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Button from 'react-uwp/Button'; 3 | import PropTypes from 'prop-types'; 4 | 5 | class ImportAllButton extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.handleClick = this.handleClick.bind(this); 9 | } 10 | 11 | handleClick() { 12 | const { 13 | onButtonClick, 14 | games, 15 | platform, 16 | } = this.props; 17 | 18 | onButtonClick(games, platform); 19 | } 20 | 21 | render() { 22 | const { steamIsRunning } = this.props; 23 | 24 | return ( 25 | 32 | ); 33 | } 34 | } 35 | 36 | ImportAllButton.propTypes = { 37 | platform: PropTypes.object.isRequired, 38 | games: PropTypes.array.isRequired, 39 | onButtonClick: PropTypes.func, 40 | steamIsRunning: PropTypes.bool, 41 | }; 42 | 43 | ImportAllButton.defaultProps = { 44 | onButtonClick: () => {}, 45 | steamIsRunning: false, 46 | }; 47 | 48 | export default ImportAllButton; 49 | -------------------------------------------------------------------------------- /src/js/Components/Import/ImportList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ListView from 'react-uwp/ListView'; 3 | import PropTypes from 'prop-types'; 4 | import ImportListItem from './ImportListItem'; 5 | 6 | class ImportList extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | const { 11 | onImportClick, 12 | games, 13 | grids, 14 | platform, 15 | } = this.props; 16 | 17 | this.onImportClick = onImportClick; 18 | this.games = games; 19 | this.grids = grids; 20 | this.platform = platform; 21 | } 22 | 23 | render() { 24 | const listStyle = { 25 | background: 'none', 26 | border: 0, 27 | width: '100%', 28 | marginBottom: 10, 29 | clear: 'both', 30 | }; 31 | 32 | const { steamIsRunning } = this.props; 33 | 34 | const importList = this.games.map((game, i) => { 35 | let { progress } = game; 36 | let thumb; 37 | 38 | if (game.progress === undefined) { 39 | progress = 0; 40 | } 41 | 42 | if (this.grids[i]) { 43 | thumb = this.grids[i].thumb; 44 | } 45 | 46 | return { 47 | itemNode: ( 48 | 57 | ), 58 | }; 59 | }); 60 | 61 | return ( 62 | <> 63 | 64 | 65 | ); 66 | } 67 | } 68 | 69 | ImportList.propTypes = { 70 | games: PropTypes.array.isRequired, 71 | grids: PropTypes.oneOfType([ 72 | PropTypes.array, 73 | PropTypes.bool, 74 | ]).isRequired, 75 | platform: PropTypes.object.isRequired, 76 | onImportClick: PropTypes.func, 77 | steamIsRunning: PropTypes.bool, 78 | }; 79 | 80 | ImportList.defaultProps = { 81 | onImportClick: () => {}, 82 | steamIsRunning: false, 83 | }; 84 | export default ImportList; 85 | -------------------------------------------------------------------------------- /src/js/Components/Import/ImportListItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Image from 'react-uwp/Image'; 3 | import Button from 'react-uwp/Button'; 4 | import ProgressBar from 'react-uwp/ProgressBar'; 5 | import PropTypes from 'prop-types'; 6 | 7 | class ImportListItem extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | const { 12 | game, 13 | platform, 14 | } = this.props; 15 | 16 | this.game = game; 17 | this.platform = platform; 18 | this.handleClick = this.handleClick.bind(this); 19 | } 20 | 21 | handleClick() { 22 | const { onImportClick } = this.props; 23 | onImportClick(this.game, this.platform); 24 | } 25 | 26 | render() { 27 | const { progress, thumb, steamIsRunning } = this.props; 28 | 29 | let progressBar = <>; 30 | if (progress && progress !== 1) { 31 | progressBar = ; 32 | } 33 | 34 | return ( 35 |
43 | 49 | {this.game.name} 50 | 57 | {progressBar} 58 |
59 | ); 60 | } 61 | } 62 | 63 | ImportListItem.propTypes = { 64 | platform: PropTypes.object.isRequired, 65 | game: PropTypes.object.isRequired, 66 | progress: PropTypes.number, 67 | thumb: PropTypes.oneOfType([ 68 | PropTypes.string, 69 | PropTypes.bool, 70 | ]), 71 | onImportClick: PropTypes.func, 72 | steamIsRunning: PropTypes.bool, 73 | }; 74 | 75 | ImportListItem.defaultProps = { 76 | progress: 0, 77 | thumb: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkqAcAAIUAgUW0RjgAAAAASUVORK5CYII=', 78 | onImportClick: () => {}, 79 | steamIsRunning: false, 80 | }; 81 | 82 | export default ImportListItem; 83 | -------------------------------------------------------------------------------- /src/js/Components/TopBlur.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import UWPNoise from '../../img/uwp-noise.png'; 4 | 5 | const TopBlur = ({ additionalHeight }) => ( 6 |
18 | ); 19 | 20 | TopBlur.propTypes = { 21 | additionalHeight: PropTypes.number, 22 | }; 23 | 24 | TopBlur.defaultProps = { 25 | additionalHeight: 0, 26 | }; 27 | 28 | export default TopBlur; 29 | -------------------------------------------------------------------------------- /src/js/Components/spinner.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { ProgressCircle } from 'react-desktop/windows'; 4 | 5 | const Spinner = (props, context) => { 6 | const { theme } = context; 7 | const { text, size, style } = props; 8 | 9 | return ( 10 |
21 | 22 |

{text}

23 |
24 | ); 25 | }; 26 | 27 | Spinner.propTypes = { 28 | text: PropTypes.string, 29 | size: PropTypes.number, 30 | style: PropTypes.object, 31 | }; 32 | 33 | Spinner.defaultProps = { 34 | text: '', 35 | size: 100, 36 | style: {}, 37 | }; 38 | 39 | Spinner.contextTypes = { theme: PropTypes.object }; 40 | export default Spinner; 41 | -------------------------------------------------------------------------------- /src/js/Components/toastHandler.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PubSub from 'pubsub-js'; 3 | import Toast from 'react-uwp/Toast'; 4 | import Icon from 'react-uwp/Icon'; 5 | 6 | class ToastHandler extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.close = this.close; 10 | this.state = { 11 | toasts: [], 12 | }; 13 | } 14 | 15 | componentDidMount() { 16 | PubSub.subscribe('toast', (message, args) => { 17 | const { toasts } = this.state; 18 | const toast = { toast: args, show: true }; 19 | this.close(toast, 3000); 20 | this.setState({ 21 | toasts: toasts.concat(toast), 22 | }); 23 | }); 24 | } 25 | 26 | close(toast, closeDelay) { 27 | const self = this; 28 | setTimeout(() => { 29 | const toasts = self.state.toasts.slice(0); 30 | toasts[toasts.indexOf(toast)].show = false; 31 | self.setState({ toasts }); 32 | }, closeDelay); 33 | } 34 | 35 | render() { 36 | const { toasts } = this.state; 37 | return ( 38 | <> 39 | {toasts.slice(0).map((x, i) => ( 40 | {x.toast.logoNode}} 44 | title={x.toast.title} 45 | showCloseIcon 46 | > 47 | {x.toast.contents} 48 | 49 | ))} 50 | 51 | ); 52 | } 53 | } 54 | 55 | export default ToastHandler; 56 | -------------------------------------------------------------------------------- /src/js/Game.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Redirect } from 'react-router-dom'; 4 | import Image from 'react-uwp/Image'; 5 | import Button from 'react-uwp/Button'; 6 | import PubSub from 'pubsub-js'; 7 | import TopBlur from './Components/TopBlur'; 8 | import Steam from './Steam'; 9 | import heroPlaceholder from '../img/hero_none.png'; 10 | import capsuleVerticalPlaceholder from '../img/capsule_vertical_none.png'; 11 | import capsulePlaceholder from '../img/capsule_none.png'; 12 | import logoPlaceholder from '../img/logo_none.png'; 13 | 14 | const { join } = window.require('path'); 15 | const fs = window.require('fs'); 16 | 17 | class Game extends React.Component { 18 | constructor(props) { 19 | super(props); 20 | this.toSearch = this.toSearch.bind(this); 21 | 22 | const { location } = this.props; 23 | 24 | this.state = { 25 | game: location.state, 26 | toSearch: false, 27 | grid: null, 28 | poster: null, 29 | hero: null, 30 | logo: null, 31 | }; 32 | 33 | PubSub.publish('showBack', true); 34 | } 35 | 36 | componentDidMount() { 37 | const { game } = this.state; 38 | const self = this; 39 | 40 | Steam.getSteamPath().then((steamPath) => { 41 | Steam.getLoggedInUser().then((user) => { 42 | const userdataGridPath = join(steamPath, 'userdata', String(user), 'config', 'grid'); 43 | 44 | let grid = Steam.getCustomImage('horizontalGrid', userdataGridPath, game.appid); 45 | let poster = Steam.getCustomImage('verticalGrid', userdataGridPath, game.appid); 46 | let hero = Steam.getCustomImage('hero', userdataGridPath, game.appid); 47 | let logo = Steam.getCustomImage('logo', userdataGridPath, game.appid); 48 | 49 | // Find defaults from the cache if it doesn't exist 50 | const librarycachePath = join(steamPath, 'appcache', 'librarycache'); 51 | 52 | if (!grid && fs.existsSync(join(librarycachePath, `${game.appid}_header.jpg`))) { 53 | grid = join(librarycachePath, `${game.appid}_header.jpg`); 54 | } 55 | 56 | if (!poster && fs.existsSync(join(librarycachePath, `${game.appid}_library_600x900.jpg`))) { 57 | poster = join(librarycachePath, `${game.appid}_library_600x900.jpg`); 58 | } 59 | 60 | if (!hero && fs.existsSync(join(librarycachePath, `${game.appid}_library_hero.jpg`))) { 61 | hero = join(librarycachePath, `${game.appid}_library_hero.jpg`); 62 | } 63 | 64 | if (!logo && fs.existsSync(join(librarycachePath, `${game.appid}_logo.png`))) { 65 | logo = join(librarycachePath, `${game.appid}_logo.png`); 66 | } 67 | 68 | self.setState({ 69 | grid, 70 | poster, 71 | hero, 72 | logo, 73 | }); 74 | }); 75 | }); 76 | } 77 | 78 | toSearch(assetType) { 79 | const { location } = this.props; 80 | this.setState({ toSearch: }); 81 | } 82 | 83 | addNoCache(imageURI) { 84 | if (!imageURI) { 85 | return false; 86 | } 87 | 88 | return `${imageURI}?${(new Date().getTime())}`; 89 | } 90 | 91 | render() { 92 | const { 93 | toSearch, 94 | game, 95 | grid, 96 | hero, 97 | poster, 98 | logo, 99 | } = this.state; 100 | 101 | if (toSearch) { 102 | return toSearch; 103 | } 104 | 105 | const { theme } = this.context; 106 | const titleStyle = { 107 | ...theme.typographyStyles.subTitle, 108 | padding: '20px 0px 10px 0', 109 | width: '100%', 110 | }; 111 | const buttonStyle = { 112 | padding: 0, 113 | }; 114 | 115 | return ( 116 | <> 117 | 118 |
128 |

{game.name}

129 |
Hero
130 | 139 | 140 |
141 |
142 |
Vertical Capsule
143 | 152 |
153 |
159 |
Horizontal Capsule
160 | 169 |
170 |
171 |
172 |
Logo
173 | 182 |
183 |
184 | 185 | ); 186 | } 187 | } 188 | 189 | Game.propTypes = { 190 | location: PropTypes.object.isRequired, 191 | }; 192 | Game.contextTypes = { theme: PropTypes.object }; 193 | export default Game; 194 | -------------------------------------------------------------------------------- /src/js/Import.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import PubSub from 'pubsub-js'; 4 | import { Icon } from 'react-uwp'; 5 | import { isEqual } from 'lodash'; 6 | import ImportList from './Components/Import/ImportList'; 7 | import ImportAllButton from './Components/Import/ImportAllButton'; 8 | import Spinner from './Components/spinner'; 9 | import Steam from './Steam'; 10 | import platformModules from './importers'; 11 | 12 | const Store = window.require('electron-store'); 13 | const SGDB = window.require('steamgriddb'); 14 | const { metrohash64 } = window.require('metrohash'); 15 | const log = window.require('electron-log'); 16 | 17 | class Import extends React.Component { 18 | constructor(props) { 19 | super(props); 20 | 21 | this.addGame = this.addGame.bind(this); 22 | this.addGames = this.addGames.bind(this); 23 | this.checkIfSteamIsRunning = this.checkIfSteamIsRunning.bind(this); 24 | this.getInstalledPlatforms = this.getInstalledPlatforms.bind(this); 25 | 26 | this.store = new Store(); 27 | 28 | this.checkSteamInterval = null; 29 | 30 | this.platforms = Object.keys(platformModules).map((key) => ({ 31 | id: platformModules[key].id, 32 | name: platformModules[key].name, 33 | class: platformModules[key].default, 34 | games: [], 35 | grids: [], 36 | posters: [], 37 | heroes: [], 38 | logos: [], 39 | installed: false, 40 | error: false, 41 | })); 42 | 43 | this.SGDB = new SGDB('b971a6f5f280490ab62c0ee7d0fd1d16'); 44 | this.lastNonSteamGames = null; 45 | 46 | this.state = { 47 | isLoaded: false, 48 | loadingText: '', 49 | installedPlatforms: [], 50 | steamIsRunning: null, 51 | }; 52 | } 53 | 54 | async componentDidMount() { 55 | log.info('Opened Import Page'); 56 | 57 | await this.checkIfSteamIsRunning(); 58 | this.checkSteamInterval = setInterval(this.checkIfSteamIsRunning, 2000); 59 | 60 | this.getInstalledPlatforms(); 61 | } 62 | 63 | componentWillUnmount() { 64 | clearInterval(this.checkSteamInterval); 65 | } 66 | 67 | async getInstalledPlatforms() { 68 | const nonSteamGames = await Steam.getNonSteamGames(); 69 | 70 | if (!isEqual(nonSteamGames, this.lastNonSteamGames)) { 71 | log.info('Getting installed games for import list'); 72 | 73 | this.setState({ 74 | isLoaded: false, 75 | }); 76 | 77 | this.lastNonSteamGames = nonSteamGames; 78 | 79 | Promise.all(this.platforms.map((platform) => platform.class.isInstalled())) 80 | .then((values) => { 81 | // Set .installed 82 | this.platforms.forEach((platform, index) => { 83 | platform.installed = values[index]; 84 | }); 85 | 86 | const installedPlatforms = this.platforms.filter((platform) => (platform.installed)); 87 | 88 | // Do .getGames() in sequential order 89 | const getGames = installedPlatforms 90 | .reduce((promise, platform) => promise.then(() => { 91 | this.setState({ loadingText: `Grabbing games from ${platform.name}...` }); 92 | 93 | return platform.class.getGames() 94 | .then((games) => { 95 | // Filter out any games that are already imported 96 | if (nonSteamGames && nonSteamGames[platform.id]) { 97 | games = games.filter((game) => { 98 | return !nonSteamGames[platform.id].find((nonSteamGame) => { 99 | return nonSteamGame.gameId === game.id; 100 | }); 101 | }); 102 | } 103 | 104 | // nonSteamGames[platform.id].gameId 105 | // Populate games array 106 | platform.games = games; 107 | }); 108 | }) 109 | .catch((err) => { 110 | platform.error = true; 111 | log.info(`Import: ${platform.id} rejected ${err}`); 112 | }), Promise.resolve()); 113 | 114 | getGames.then(() => { 115 | this.setState({ loadingText: 'Getting images...' }); 116 | 117 | const gridsPromises = []; 118 | installedPlatforms.forEach((platform) => { 119 | if (platform.games.length) { 120 | // Get grids for each platform 121 | const ids = platform.games.map((x) => encodeURIComponent(x.id)); 122 | 123 | const getGrids = this.SGDB.getGrids({ 124 | type: platform.id, 125 | id: ids.join(','), 126 | dimensions: ['460x215', '920x430'], 127 | }) 128 | .then((res) => { 129 | platform.grids = this._formatResponse(ids, res); 130 | }) 131 | .catch((e) => { 132 | log.error('Erorr getting grids from SGDB'); 133 | console.error(e); 134 | // @todo We need a way to log which game caused the error 135 | // @todo Fallback to text search 136 | // @todo show an error toast 137 | }); 138 | 139 | gridsPromises.push(getGrids); 140 | } 141 | }); 142 | 143 | // Update state after we got the grids 144 | Promise.all(gridsPromises) 145 | .then(() => { 146 | this.setState({ 147 | isLoaded: true, 148 | installedPlatforms, 149 | }); 150 | }); 151 | }) 152 | .catch((err) => { 153 | log.info(`Import: ${err}`); 154 | }); 155 | }); 156 | } 157 | } 158 | 159 | /* 160 | * @todo We might want to put this at the App level, and publish changes via PubSub or props, 161 | * so different pages can display their own message if Steam is running. 162 | */ 163 | async checkIfSteamIsRunning() { 164 | const steamIsRunning = await Steam.checkIfSteamIsRunning(); 165 | 166 | if (steamIsRunning !== this.state.steamIsRunning) { 167 | log.info(`Steam is ${steamIsRunning ? 'open' : 'closed'}`); 168 | 169 | this.setState({ 170 | steamIsRunning, 171 | }); 172 | 173 | // Update non-Steam games in case changes were made while Steam was open 174 | if (!steamIsRunning) { 175 | setTimeout(() => { 176 | this.getInstalledPlatforms(); 177 | }, 0); 178 | } 179 | } 180 | } 181 | 182 | saveImportedGames(games) { 183 | const gamesStorage = this.store.get('games', {}); 184 | 185 | games.forEach((game) => { 186 | const key = game.exe + ( 187 | typeof game.params !== 'undefined' 188 | ? game.params 189 | : '' 190 | ); 191 | 192 | const configId = metrohash64(key); 193 | gamesStorage[configId] = game; 194 | }); 195 | 196 | this.store.set('games', gamesStorage); 197 | } 198 | 199 | // @todo this is horrible but can't be arsed right now 200 | _formatResponse(ids, res) { 201 | let formatted = false; 202 | 203 | // if only single id then return first grid 204 | if (ids.length === 1) { 205 | if (res.length > 0) { 206 | formatted = [res[0]]; 207 | } 208 | } else { 209 | // if multiple ids treat each object as a request 210 | formatted = res.map((x) => { 211 | if (x.success) { 212 | if (x.data[0]) { 213 | return x.data[0]; 214 | } 215 | } 216 | return false; 217 | }); 218 | } 219 | return formatted; 220 | } 221 | 222 | addGames(games, platform) { 223 | this.saveImportedGames(games); 224 | 225 | const shortcuts = games.map((game) => ({ 226 | name: game.name, 227 | exe: game.exe, 228 | startIn: game.startIn, 229 | params: game.params, 230 | tags: [platform.name], 231 | icon: game.icon, 232 | })); 233 | 234 | Steam.addShortcuts(shortcuts).then(() => { 235 | Steam.addCategory(games, platform.name).then(() => { 236 | PubSub.publish('toast', { 237 | logoNode: 'ImportAll', 238 | title: 'Successfully Imported!', 239 | contents: ( 240 |

241 | {games.length} 242 | { ' ' } 243 | games imported from 244 | { ' ' } 245 | {platform.name} 246 |

247 | ), 248 | }); 249 | }).then(() => { 250 | // Download images 251 | PubSub.publish('toast', { 252 | logoNode: 'Download', 253 | title: 'Downloading Images...', 254 | contents: (

Downloading images for imported games...

), 255 | }); 256 | 257 | const ids = games.map((x) => encodeURIComponent(x.id)); 258 | let posters = []; 259 | let heroes = []; 260 | let logos = []; 261 | 262 | // Get posters 263 | const getPosters = this.SGDB.getGrids({ type: platform.id, id: ids.join(','), dimensions: ['600x900'] }).then((res) => { 264 | posters = this._formatResponse(ids, res); 265 | }).catch((e) => { 266 | log.error('Error getting posters'); 267 | console.error(e); 268 | // @todo show an error toast 269 | }); 270 | 271 | // Get heroes 272 | const getHeroes = this.SGDB.getHeroes({ type: platform.id, id: ids.join(',') }).then((res) => { 273 | heroes = this._formatResponse(ids, res); 274 | }).catch((e) => { 275 | log.error('Error getting heroes'); 276 | console.error(e); 277 | // @todo show an error toast 278 | }); 279 | 280 | // Get heroes 281 | const getLogos = this.SGDB.getLogos({ type: platform.id, id: ids.join(',') }).then((res) => { 282 | logos = this._formatResponse(ids, res); 283 | }).catch((e) => { 284 | log.error('Error getting logos'); 285 | console.error(e); 286 | // @todo show an error toast 287 | }); 288 | 289 | Promise.all([getPosters, getHeroes, getLogos]).then(() => { 290 | const downloadPromises = []; 291 | 292 | games.forEach((game, i) => { 293 | const appId = Steam.generateNewAppId(game.exe, game.name); 294 | 295 | // Take (legacy) grids from when we got them for the ImportList 296 | const savedGrid = platform.grids[platform.games.indexOf(games[i])]; 297 | 298 | if (platform.grids[i] && savedGrid) { 299 | const appIdOld = Steam.generateAppId(game.exe, game.name); 300 | 301 | downloadPromises.push(Steam.addAsset('horizontalGrid', appId, savedGrid.url)); 302 | 303 | // Old app id is for Big Picture Mode 304 | downloadPromises.push(Steam.addAsset('horizontalGrid', appIdOld, savedGrid.url)); 305 | } 306 | 307 | // Download posters 308 | if (posters[i]) { 309 | downloadPromises.push(Steam.addAsset('verticalGrid', appId, posters[i].url)); 310 | } 311 | 312 | // Download heroes 313 | if (heroes[i]) { 314 | downloadPromises.push(Steam.addAsset('hero', appId, heroes[i].url)); 315 | } 316 | 317 | // Download logos 318 | if (heroes[i]) { 319 | downloadPromises.push(Steam.addAsset('logo', appId, logos[i].url)); 320 | } 321 | }); 322 | 323 | Promise.all(downloadPromises).then(() => { 324 | PubSub.publish('toast', { 325 | logoNode: 'Download', 326 | title: 'Downloads Complete', 327 | contents: (

All Images Downloaded!

), 328 | }); 329 | }); 330 | }); 331 | }).catch((err) => { 332 | log.error('Cannot import while Steam is running'); 333 | 334 | if (err.type === 'OpenError') { 335 | PubSub.publish('toast', { 336 | logoNode: 'Error', 337 | title: 'Error Importing', 338 | contents: ( 339 |

340 | Cannot import while Steam is running. 341 |
342 | Close Steam and try again. 343 |

344 | ), 345 | }); 346 | } 347 | }); 348 | }); 349 | } 350 | 351 | addGame(game, platform) { 352 | return this.addGames([game], platform); 353 | } 354 | 355 | render() { 356 | const { 357 | isLoaded, loadingText, installedPlatforms, steamIsRunning, 358 | } = this.state; 359 | const { theme } = this.context; 360 | 361 | if (!isLoaded) { 362 | return (); 363 | } 364 | 365 | return ( 366 | <> 367 |
377 | {steamIsRunning 378 | && ( 379 |
386 | 391 | IncidentTriangle 392 | 393 | SteamGridDB Manager can not import games while Steam is running. Please close Steam. 394 |
395 | )} 396 | { 397 | installedPlatforms.map((platform) => { 398 | if (!platform.error && platform.games.length) { 399 | return ( 400 |
401 |
{platform.name}
402 | 409 | 416 |
417 | ); 418 | } 419 | 420 | return <>; 421 | }) 422 | } 423 |
424 | 425 | ); 426 | } 427 | } 428 | 429 | Import.contextTypes = { theme: PropTypes.object }; 430 | export default Import; 431 | -------------------------------------------------------------------------------- /src/js/Search.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Redirect } from 'react-router-dom'; 4 | import Image from 'react-uwp/Image'; 5 | import Button from 'react-uwp/Button'; 6 | import PubSub from 'pubsub-js'; 7 | import TopBlur from './Components/TopBlur'; 8 | import Spinner from './Components/spinner'; 9 | import Steam from './Steam'; 10 | 11 | const SGDB = window.require('steamgriddb'); 12 | 13 | class Search extends React.Component { 14 | constructor(props) { 15 | super(props); 16 | 17 | this.onClick = this.onClick.bind(this); 18 | this.SGDB = new SGDB('b971a6f5f280490ab62c0ee7d0fd1d16'); 19 | 20 | const { location } = this.props; 21 | 22 | this.state = { 23 | game: location.state, 24 | items: [], 25 | toGame: false, 26 | isLoaded: false, 27 | }; 28 | 29 | PubSub.publish('showBack', true); 30 | } 31 | 32 | componentDidMount() { 33 | const { game } = this.state; 34 | 35 | let type = 'steam'; 36 | if (game.platform) { 37 | type = game.platform; 38 | } 39 | 40 | let id; 41 | if (game.platform) { 42 | id = game.gameId; 43 | } else { 44 | id = game.appid; 45 | } 46 | 47 | if (game.platform === 'other') { 48 | type = 'game'; 49 | this.SGDB.searchGame(game.name) 50 | .then((gameResp) => { 51 | id = gameResp[0].id; 52 | this.queryApi(type, id); 53 | }); 54 | } else { 55 | this.queryApi(type, id); 56 | } 57 | } 58 | 59 | onClick(item, itemIndex) { 60 | const { game, items } = this.state; 61 | const { location } = this.props; 62 | 63 | const clonedItems = [...items]; 64 | clonedItems[itemIndex].downloading = true; 65 | 66 | this.setState({ 67 | items: clonedItems, 68 | }); 69 | 70 | Steam.addAsset(location.state.assetType, game.appid, item.url).then(() => { 71 | clonedItems[itemIndex].downloading = false; 72 | this.setState({ 73 | items: clonedItems, 74 | }); 75 | this.setState({ toGame: }); 76 | }); 77 | } 78 | 79 | queryApi(type, id) { 80 | const { location } = this.props; 81 | 82 | switch (location.state.assetType) { 83 | case 'horizontalGrid': 84 | this.SGDB.getGrids({ type, id }).then((res) => { 85 | this.setState({ 86 | isLoaded: true, 87 | items: res, 88 | }); 89 | }); 90 | break; 91 | case 'verticalGrid': 92 | this.SGDB.getGrids({ type, id, dimensions: ['600x900'] }).then((res) => { 93 | this.setState({ 94 | isLoaded: true, 95 | items: res, 96 | }); 97 | }); 98 | break; 99 | case 'hero': 100 | this.SGDB.getHeroes({ type, id }).then((res) => { 101 | this.setState({ 102 | isLoaded: true, 103 | items: res, 104 | }); 105 | }); 106 | break; 107 | case 'logo': 108 | this.SGDB.getLogos({ type, id }).then((res) => { 109 | this.setState({ 110 | isLoaded: true, 111 | items: res, 112 | }); 113 | }); 114 | break; 115 | default: 116 | break; 117 | } 118 | } 119 | 120 | render() { 121 | const { isLoaded, toGame, items } = this.state; 122 | const { theme } = this.context; 123 | 124 | if (!isLoaded) { 125 | return ; 126 | } 127 | 128 | if (toGame) { 129 | return toGame; 130 | } 131 | 132 | return ( 133 | <> 134 | 135 |
145 | {items.map((item, i) => ( 146 | 176 | ))} 177 |
178 | 179 | ); 180 | } 181 | } 182 | 183 | Search.propTypes = { 184 | location: PropTypes.object.isRequired, 185 | }; 186 | Search.contextTypes = { theme: PropTypes.object }; 187 | export default Search; 188 | -------------------------------------------------------------------------------- /src/js/Steam.js: -------------------------------------------------------------------------------- 1 | import SteamID from 'steamid'; 2 | import { crc32 } from 'crc'; 3 | import React from 'react'; 4 | 5 | const Registry = window.require('winreg'); 6 | const Store = window.require('electron-store'); 7 | const fs = window.require('fs'); 8 | const { join, extname } = window.require('path'); 9 | const VDF = window.require('@node-steam/vdf'); 10 | const shortcut = window.require('steam-shortcut-editor'); 11 | const https = window.require('https'); 12 | const Stream = window.require('stream').Transform; 13 | const { metrohash64 } = window.require('metrohash'); 14 | const log = window.require('electron-log'); 15 | const Categories = window.require('steam-categories'); 16 | const glob = window.require('glob'); 17 | 18 | class Steam { 19 | constructor() { 20 | this.steamPath = null; 21 | this.loggedInUser = null; 22 | this.currentUserGridPath = null; 23 | } 24 | 25 | static getSteamPath() { 26 | return new Promise((resolve, reject) => { 27 | if (this.steamPath) { 28 | return resolve(this.steamPath); 29 | } 30 | 31 | const key = new Registry({ 32 | hive: Registry.HKCU, 33 | key: '\\Software\\Valve\\Steam', 34 | }); 35 | 36 | key.values((err, items) => { 37 | let steamPath = false; 38 | 39 | items.forEach((item) => { 40 | if (item.name === 'SteamPath') { 41 | steamPath = item.value; 42 | } 43 | }); 44 | 45 | if (steamPath) { 46 | this.steamPath = steamPath; 47 | log.info(`Got Steam path: ${steamPath}`); 48 | return resolve(steamPath); 49 | } 50 | 51 | return reject(new Error('Could not find Steam path.')); 52 | }); 53 | 54 | return false; 55 | }); 56 | } 57 | 58 | static getCurrentUserGridPath() { 59 | return new Promise((resolve) => { 60 | if (this.currentUserGridPath) { 61 | return resolve(this.currentUserGridPath); 62 | } 63 | this.getSteamPath().then((steamPath) => { 64 | this.getLoggedInUser().then((user) => { 65 | const gridPath = join(steamPath, 'userdata', String(user), 'config', 'grid'); 66 | if (!fs.existsSync(gridPath)) { 67 | fs.mkdirSync(gridPath); 68 | } 69 | this.currentUserGridPath = gridPath; 70 | resolve(gridPath); 71 | }); 72 | }); 73 | return false; 74 | }); 75 | } 76 | 77 | static getSteamGames() { 78 | return new Promise((resolve) => { 79 | this.getSteamPath().then((steamPath) => { 80 | const parsedLibFolders = VDF.parse(fs.readFileSync(join(steamPath, 'steamapps', 'libraryfolders.vdf'), 'utf-8')); 81 | const games = []; 82 | 83 | // Load extra library paths from libraryfolders.vdf 84 | const extraLibraries = Object.entries(parsedLibFolders.LibraryFolders || parsedLibFolders.libraryfolders || {}) 85 | .filter(([key]) => !Number.isNaN(parseInt(key, 10))) 86 | .filter(([_, library]) => typeof library === 'string' || library.mounted !== 0) 87 | .map(([_, library]) => typeof library === 'string' ? library : library.path); 88 | 89 | // Add Steam install dir and extra libraries 90 | const libraries = [steamPath, ...extraLibraries] 91 | 92 | log.info(`Found ${libraries.length} Steam libraries`); 93 | 94 | libraries.forEach((library) => { 95 | const appsPath = join(library, 'steamapps'); 96 | const files = fs.readdirSync(appsPath); 97 | files.forEach((file) => { 98 | const ext = file.split('.').pop(); 99 | 100 | if (ext === 'acf') { 101 | const filePath = join(appsPath, file); 102 | const data = fs.readFileSync(filePath, 'utf-8'); 103 | try { 104 | const gameData = VDF.parse(data); 105 | if (gameData.AppState.appid === 228980) { 106 | return; 107 | } 108 | 109 | games.push({ 110 | appid: gameData.AppState.appid, 111 | name: gameData.AppState.name, 112 | type: 'game', 113 | }); 114 | } catch (err) { 115 | log.warn(`Error while parsing ${file}: ${err}`); 116 | } 117 | } 118 | }); 119 | }); 120 | log.info(`Fetched ${games.length} Steam games`); 121 | 122 | resolve(games); 123 | }); 124 | }); 125 | } 126 | 127 | static getNonSteamGames() { 128 | return new Promise((resolve) => { 129 | this.getSteamPath().then((steamPath) => { 130 | this.getLoggedInUser().then((user) => { 131 | const store = new Store(); 132 | const userdataPath = join(steamPath, 'userdata', String(user)); 133 | const shortcutPath = join(userdataPath, 'config', 'shortcuts.vdf'); 134 | const processed = []; 135 | shortcut.parseFile(shortcutPath, (err, items) => { 136 | const games = {}; 137 | 138 | if (!items) { 139 | return resolve([]); 140 | } 141 | 142 | items.shortcuts.forEach((item) => { 143 | const appName = item.appname || item.AppName || item.appName; 144 | const exe = item.exe || item.Exe; 145 | const configId = metrohash64(exe + item.LaunchOptions); 146 | const appid = (item.appid) ? 147 | (item.appid >>> 0) : //bitwise unsigned 32 bit ID of manually added non-steam game 148 | this.generateNewAppId(exe, appName); 149 | 150 | 151 | if (store.has(`games.${configId}`)) { 152 | const storedGame = store.get(`games.${configId}`); 153 | if (typeof games[storedGame.platform] === 'undefined') { 154 | games[storedGame.platform] = []; 155 | } 156 | 157 | if (!processed.includes(configId)) { 158 | games[storedGame.platform].push({ 159 | gameId: storedGame.id, 160 | name: appName, 161 | platform: storedGame.platform, 162 | type: 'shortcut', 163 | appid, 164 | }); 165 | processed.push(configId); 166 | } 167 | } else { 168 | if (!games.other) { 169 | games.other = []; 170 | } 171 | 172 | games.other.push({ 173 | gameId: null, 174 | name: appName, 175 | platform: 'other', 176 | type: 'shortcut', 177 | appid, 178 | }); 179 | } 180 | }); 181 | return resolve(games); 182 | }); 183 | }); 184 | }); 185 | }); 186 | } 187 | 188 | /* eslint-disable no-bitwise, no-mixed-operators */ 189 | static generateAppId(exe, name) { 190 | const key = exe + name; 191 | const top = BigInt(crc32(key)) | BigInt(0x80000000); 192 | return String((BigInt(top) << BigInt(32) | BigInt(0x02000000))); 193 | } 194 | 195 | // Appid for new library. 196 | // Thanks to https://gist.github.com/stormyninja/6295d5e6c1c9c19ab0ce46d546e6d0b1 & https://gitlab.com/avalonparton/grid-beautification 197 | static generateNewAppId(exe, name) { 198 | const key = exe + name; 199 | const top = BigInt(crc32(key)) | BigInt(0x80000000); 200 | const shift = (BigInt(top) << BigInt(32) | BigInt(0x02000000)) >> BigInt(32); 201 | return parseInt(shift, 10); 202 | } 203 | /* eslint-enable no-bitwise, no-mixed-operators */ 204 | 205 | static getLoggedInUser() { 206 | return new Promise((resolve) => { 207 | if (this.loggedInUser) { 208 | return resolve(this.loggedInUser); 209 | } 210 | 211 | this.getSteamPath().then((steamPath) => { 212 | const loginusersPath = join(steamPath, 'config', 'loginusers.vdf'); 213 | const data = fs.readFileSync(loginusersPath, 'utf-8'); 214 | const loginusersData = VDF.parse(data); 215 | 216 | Object.keys(loginusersData.users).forEach((user) => { 217 | if (loginusersData.users[user].MostRecent || loginusersData.users[user].mostrecent) { 218 | const { accountid } = (new SteamID(user)); 219 | this.loggedInUser = accountid; 220 | log.info(`Got Steam user: ${accountid}`); 221 | resolve(accountid); 222 | return true; 223 | } 224 | return false; 225 | }); 226 | }); 227 | 228 | return false; 229 | }); 230 | } 231 | 232 | static getDefaultGridImage(appid) { 233 | return `https://steamcdn-a.akamaihd.net/steam/apps/${appid}/header.jpg`; 234 | } 235 | 236 | static getCustomImage(type, userdataGridPath, appid) { 237 | const fileTypes = ['png', 'jpg', 'jpeg', 'tga']; 238 | 239 | let basePath; 240 | switch (type) { 241 | case 'horizontalGrid': 242 | basePath = join(userdataGridPath, `${String(appid)}`); 243 | break; 244 | case 'verticalGrid': 245 | basePath = join(userdataGridPath, `${String(appid)}p`); 246 | break; 247 | case 'hero': 248 | basePath = join(userdataGridPath, `${String(appid)}_hero`); 249 | break; 250 | case 'logo': 251 | basePath = join(userdataGridPath, `${String(appid)}_logo`); 252 | break; 253 | default: 254 | basePath = join(userdataGridPath, `${String(appid)}`); 255 | } 256 | 257 | let image = false; 258 | fileTypes.some((ext) => { 259 | const path = `${basePath}.${ext}`; 260 | 261 | if (fs.existsSync(path)) { 262 | image = path; 263 | return true; 264 | } 265 | return false; 266 | }); 267 | 268 | return image; 269 | } 270 | 271 | static getShortcutFile() { 272 | return new Promise((resolve) => { 273 | this.getSteamPath().then((steamPath) => { 274 | this.getLoggedInUser().then((user) => { 275 | const userdataPath = join(steamPath, 'userdata', String(user)); 276 | const shortcutPath = join(userdataPath, 'config', 'shortcuts.vdf'); 277 | resolve(shortcutPath); 278 | }); 279 | }); 280 | }); 281 | } 282 | 283 | static addAsset(type, appId, url) { 284 | return new Promise((resolve, reject) => { 285 | this.getCurrentUserGridPath().then((userGridPath) => { 286 | const imageUrl = url; 287 | const imageExt = extname(imageUrl); 288 | 289 | let dest; 290 | 291 | switch (type) { 292 | case 'horizontalGrid': 293 | dest = join(userGridPath, `${appId}${imageExt}`); 294 | break; 295 | case 'verticalGrid': 296 | dest = join(userGridPath, `${appId}p${imageExt}`); 297 | break; 298 | case 'hero': 299 | dest = join(userGridPath, `${appId}_hero${imageExt}`); 300 | break; 301 | case 'logo': 302 | dest = join(userGridPath, `${appId}_logo${imageExt}`); 303 | break; 304 | default: 305 | reject(); 306 | } 307 | 308 | let cur = 0; 309 | const data = new Stream(); 310 | let progress = 0; 311 | let lastProgress = 0; 312 | https.get(url, (response) => { 313 | const len = parseInt(response.headers['content-length'], 10); 314 | 315 | response.on('data', (chunk) => { 316 | cur += chunk.length; 317 | data.push(chunk); 318 | progress = Math.round((cur / len) * 10) / 10; 319 | if (progress !== lastProgress) { 320 | lastProgress = progress; 321 | } 322 | }); 323 | 324 | response.on('end', () => { 325 | // Delete old image(s) 326 | glob(`${dest.replace(imageExt, '')}.*`, (er, files) => { 327 | files.forEach((file) => { 328 | fs.unlinkSync(file); 329 | }); 330 | 331 | fs.writeFileSync(dest, data.read()); 332 | resolve(dest); 333 | }); 334 | }); 335 | }).on('error', (err) => { 336 | fs.unlink(dest); 337 | reject(err); 338 | }); 339 | }); 340 | }); 341 | } 342 | 343 | static addShortcuts(shortcuts) { 344 | return new Promise((resolve) => { 345 | this.getShortcutFile().then((shortcutPath) => { 346 | shortcut.parseFile(shortcutPath, (err, items) => { 347 | const newShorcuts = { 348 | shortcuts: [], 349 | }; 350 | 351 | let apps = []; 352 | if (typeof items !== 'undefined') { 353 | apps = items.shortcuts; 354 | } 355 | 356 | shortcuts.forEach((value) => { 357 | // Don't add dupes 358 | apps.some((app) => { 359 | const appid = this.generateAppId(app.exe, app.appname); 360 | if (this.generateAppId(value.exe, value.name) === appid) { 361 | return true; 362 | } 363 | return false; 364 | }); 365 | 366 | apps.push({ 367 | appname: value.name, 368 | exe: value.exe, 369 | StartDir: value.startIn, 370 | LaunchOptions: value.params, 371 | icon: (typeof value.icon !== 'undefined' ? value.icon : ''), 372 | IsHidden: false, 373 | ShortcutPath: '', 374 | AllowDesktopConfig: true, 375 | OpenVR: false, 376 | tags: (typeof value.tags !== 'undefined' ? value.tags : []), 377 | }); 378 | }); 379 | 380 | newShorcuts.shortcuts = apps; 381 | 382 | shortcut.writeFile(shortcutPath, newShorcuts, () => resolve()); 383 | }); 384 | }); 385 | }); 386 | } 387 | 388 | static getLevelDBPath() { 389 | return join(process.env.localappdata, 'Steam', 'htmlcache', 'Local Storage', 'leveldb'); 390 | } 391 | 392 | static async checkIfSteamIsRunning() { 393 | return new Promise((resolve) => { 394 | const levelDBPath = this.getLevelDBPath(); 395 | 396 | this.getLoggedInUser().then((user) => { 397 | const cats = new Categories(levelDBPath, String(user)); 398 | 399 | /* 400 | * Without checking Windows processes directly, this is the most reliable way 401 | * to check if Steam is running. When Steam is running, there is a lock on 402 | * this file, so if we can't read it, that means Steam must be running. 403 | */ 404 | cats.read() 405 | .then(() => { 406 | resolve(false); 407 | }) 408 | .catch(() => { 409 | resolve(true); 410 | }) 411 | .finally(() => { 412 | cats.close(); 413 | }); 414 | }); 415 | }); 416 | } 417 | 418 | static addCategory(games, categoryId) { 419 | return new Promise((resolve, reject) => { 420 | const levelDBPath = this.getLevelDBPath(); 421 | 422 | this.getLoggedInUser().then((user) => { 423 | const cats = new Categories(levelDBPath, String(user)); 424 | 425 | cats.read().then(() => { 426 | this.getCurrentUserGridPath().then((userGridPath) => { 427 | const localConfigPath = join(userGridPath, '../', 'localconfig.vdf'); 428 | const localConfig = VDF.parse(fs.readFileSync(localConfigPath, 'utf-8')); 429 | 430 | let collections = {}; 431 | if (localConfig.UserLocalConfigStore.WebStorage['user-collections']) { 432 | collections = JSON.parse(localConfig.UserLocalConfigStore.WebStorage['user-collections'].replace(/\\/g, '')); 433 | } 434 | 435 | games.forEach((app) => { 436 | const platformName = categoryId; 437 | const appId = this.generateNewAppId(app.exe, app.name); 438 | 439 | // Create new category if it doesn't exist 440 | const catKey = `sgdb-${platformName}`; // just use the name as the id 441 | const platformCat = cats.get(catKey); 442 | if (platformCat.is_deleted || !platformCat) { 443 | cats.add(catKey, { 444 | name: platformName, 445 | added: [], 446 | }); 447 | } 448 | 449 | // Create entry in localconfig.vdf 450 | if (!collections[catKey]) { 451 | collections[catKey] = { 452 | id: catKey, 453 | added: [], 454 | removed: [], 455 | }; 456 | } 457 | 458 | // Add appids to localconfig.vdf 459 | if (collections[catKey].added.indexOf(appId) === -1) { 460 | // Only add if it doesn't exist already 461 | collections[catKey].added.push(appId); 462 | } 463 | }); 464 | 465 | cats.save().then(() => { 466 | localConfig.UserLocalConfigStore.WebStorage['user-collections'] = JSON.stringify(collections).replace(/"/g, '\\"'); // I hate Steam 467 | 468 | const newVDF = VDF.stringify(localConfig); 469 | 470 | try { 471 | fs.writeFileSync(localConfigPath, newVDF); 472 | } catch (e) { 473 | log.error('Error writing categories file'); 474 | console.error(e); 475 | } 476 | 477 | cats.close(); 478 | return resolve(); 479 | }); 480 | }); 481 | }).catch((err) => { 482 | reject(err); 483 | }); 484 | }); 485 | }); 486 | } 487 | } 488 | 489 | export default Steam; 490 | -------------------------------------------------------------------------------- /src/js/games.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Redirect } from 'react-router-dom'; 4 | import AutoSuggestBox from 'react-uwp/AutoSuggestBox'; 5 | import AppBarButton from 'react-uwp/AppBarButton'; 6 | import AppBarSeparator from 'react-uwp/AppBarSeparator'; 7 | import Separator from 'react-uwp/Separator'; 8 | import Fuse from 'fuse.js'; 9 | import PubSub from 'pubsub-js'; 10 | import { debounce } from 'lodash'; 11 | import { forceCheck } from 'react-lazyload'; 12 | import Spinner from './Components/spinner'; 13 | import Steam from './Steam'; 14 | import TopBlur from './Components/TopBlur'; 15 | import GameListItem from './Components/Games/GameListItem'; 16 | import platformModules from './importers'; 17 | 18 | const log = window.require('electron-log'); 19 | 20 | class Games extends React.Component { 21 | constructor(props) { 22 | super(props); 23 | this.toGame = this.toGame.bind(this); 24 | this.refreshGames = this.refreshGames.bind(this); 25 | this.filterGames = this.filterGames.bind(this); 26 | this.searchInput = debounce((searchTerm) => { 27 | this.filterGames(searchTerm); 28 | }, 300); 29 | 30 | this.zoom = 1; 31 | 32 | // Fetched games are stored here and shouldn't be changed unless a fetch is triggered again 33 | this.fetchedGames = {}; 34 | this.platformNames = { 35 | steam: 'Steam', 36 | other: 'Other Games', 37 | }; 38 | 39 | Object.keys(platformModules).forEach((module) => { 40 | this.platformNames[platformModules[module].id] = platformModules[module].name; 41 | }); 42 | 43 | this.state = { 44 | isLoaded: false, 45 | toGame: false, 46 | hasSteam: true, 47 | items: {}, 48 | }; 49 | } 50 | 51 | componentDidMount() { 52 | const { items } = this.state; 53 | PubSub.publish('showBack', false); 54 | 55 | if (Object.entries(items).length <= 0) { 56 | Steam.getSteamPath().then(() => { 57 | this.fetchGames(); 58 | }).catch(() => { 59 | log.warn('Steam is not installed'); 60 | this.setState({ hasSteam: false }); 61 | }); 62 | } 63 | } 64 | 65 | fetchGames() { 66 | const steamGamesPromise = Steam.getSteamGames(); 67 | const nonSteamGamesPromise = Steam.getNonSteamGames(); 68 | Promise.all([steamGamesPromise, nonSteamGamesPromise]).then((values) => { 69 | const items = { steam: values[0], ...values[1] }; 70 | // Sort games alphabetically 71 | Object.keys(items).forEach((platform) => { 72 | items[platform] = items[platform].sort((a, b) => { 73 | if (a.name > b.name) { 74 | return 1; 75 | } 76 | 77 | return ((b.name > a.name) ? -1 : 0); 78 | }); 79 | }); 80 | 81 | this.fetchedGames = items; 82 | this.setState({ 83 | isLoaded: true, 84 | items, 85 | }); 86 | }); 87 | } 88 | 89 | toGame(platform, index) { 90 | const { items } = this.state; 91 | const data = items[platform][index]; 92 | this.setState({ 93 | toGame: , 94 | }); 95 | } 96 | 97 | refreshGames() { 98 | this.setState({ isLoaded: false }); 99 | this.fetchGames(); 100 | } 101 | 102 | filterGames(searchTerm) { 103 | const items = { ...this.fetchedGames }; 104 | if (searchTerm.trim() === '') { 105 | this.setState({ items }); 106 | return; 107 | } 108 | 109 | Object.keys(items).forEach((platform) => { 110 | const fuse = new Fuse(items[platform], { 111 | shouldSort: true, 112 | threshold: 0.6, 113 | location: 0, 114 | distance: 100, 115 | maxPatternLength: 32, 116 | minMatchCharLength: 1, 117 | keys: [ 118 | 'name', 119 | ], 120 | }); 121 | items[platform] = fuse.search(searchTerm); 122 | }); 123 | this.setState({ items }); 124 | 125 | forceCheck(); // Recheck lazyload 126 | } 127 | 128 | render() { 129 | const { 130 | isLoaded, 131 | hasSteam, 132 | items, 133 | toGame, 134 | } = this.state; 135 | const { theme } = this.context; 136 | 137 | if (!hasSteam) { 138 | return ( 139 |
140 | Steam installation not found. 141 |
142 | ); 143 | } 144 | 145 | if (!isLoaded) { 146 | return ; 147 | } 148 | 149 | if (toGame) { 150 | return toGame; 151 | } 152 | 153 | return ( 154 |
155 | 156 |
167 | 168 | 169 | 174 |
175 |
176 | {Object.keys(items).map((platform) => ( 177 |

{item.name}

), 183 | , 184 | ]} 185 | onItemClick={this.toGame} 186 | /> 187 | ))} 188 |
189 |
190 | ); 191 | } 192 | } 193 | 194 | Games.contextTypes = { theme: PropTypes.object }; 195 | export default Games; 196 | -------------------------------------------------------------------------------- /src/js/importers.js: -------------------------------------------------------------------------------- 1 | const importers = {}; 2 | 3 | const importAll = (r) => r.keys().forEach((key) => { 4 | importers[key] = r(key); 5 | }); 6 | const context = require.context('./importers/', false, /\.js$/); 7 | importAll(context); 8 | 9 | export default importers; 10 | 11 | function getOfficial() { 12 | const officialList = []; 13 | Object.keys(importers).forEach((module) => { 14 | if (importers[module].official) { 15 | officialList.push(importers[module].id); 16 | } 17 | }); 18 | return officialList; 19 | } 20 | 21 | // Array of imprter ids 22 | export const officialList = getOfficial(); 23 | -------------------------------------------------------------------------------- /src/js/importers/BattleNet.js: -------------------------------------------------------------------------------- 1 | import decoder from 'blizzard-product-parser/src/js/database'; // Workaround for badly configured lib 2 | import { PowerShell, LauncherAutoClose } from '../paths'; 3 | 4 | const Registry = window.require('winreg'); 5 | const fs = window.require('fs'); 6 | const path = window.require('path'); 7 | const log = window.require('electron-log'); 8 | 9 | const BNET_GAMES = { 10 | d3: { 11 | name: 'Diablo III', 12 | launchId: 'D3', 13 | exes: ['Diablo III', 'Diablo III64'], 14 | icon: 'Diablo III Launcher.exe', 15 | }, 16 | dst2: { 17 | name: 'Destiny 2', 18 | launchId: 'DST2', 19 | exes: ['destiny2'], 20 | icon: 'Destiny 2 Launcher.exe', 21 | }, 22 | hero: { 23 | name: 'Heroes of the Storm', 24 | launchId: 'Hero', 25 | exes: ['HeroesSwitcher', 'HeroesSwitcher_x64'], 26 | icon: 'Heroes of the Storm.exe', 27 | }, 28 | odin: { 29 | name: 'Call of Duty: Modern Warfare', 30 | launchId: 'ODIN', 31 | exes: ['codmw2019', 'ModernWarfare'], 32 | icon: 'Modern Warfare Launcher.exe', 33 | }, 34 | pro: { 35 | name: 'Overwatch', 36 | launchId: 'Pro', 37 | exes: ['Overwatch'], 38 | icon: 'Overwatch Launcher.exe', 39 | }, 40 | s1: { 41 | name: 'Starcraft Remastered', 42 | launchId: 'S1', 43 | exes: ['StarCraft'], 44 | icon: 'StarCraft Launcher.exe', 45 | }, 46 | s2: { 47 | name: 'Starcraft 2', 48 | launchId: 'S2', 49 | exes: ['SC2Switcher_x64', 'SC2Switcher'], 50 | icon: 'StarCraft II.exe', 51 | }, 52 | viper: { 53 | name: 'Call of Duty: Black Ops 4', 54 | launchId: 'VIPR', 55 | exes: ['BlackOps4', 'BlackOps4_boot'], 56 | icon: 'Black Ops 4 Launcher.exe', 57 | }, 58 | w3: { 59 | name: 'Warcraft 3: Reforged', 60 | launchId: 'W3', 61 | exes: ['Warcraft III'], 62 | icon: 'Warcraft III.exe', 63 | }, 64 | hsb: { 65 | name: 'Hearthstone', 66 | launchId: 'WTCG', 67 | exes: ['Hearthstone'], 68 | icon: 'Hearthstone.exe', 69 | }, 70 | wlby: { 71 | name: 'Crash Bandicoot 4', 72 | launchId: 'WLBY', 73 | exes: ['CrashBandicoot4'], 74 | icon: 'CrashBandicoot4.exe', 75 | }, 76 | wow: { 77 | name: 'World of Warcraft', 78 | launchId: 'WoW', 79 | exes: ['Wow'], 80 | icon: 'World of Warcraft Launcher.exe', 81 | }, 82 | zeus: { 83 | name: 'Call of Duty: Black Ops Cold War', 84 | launchId: 'ZEUS', 85 | exes: ['BlackOpsColdWar'], 86 | icon: 'Black Ops Cold War Launcher.exe', 87 | }, 88 | }; 89 | 90 | class BattleNet { 91 | static isInstalled() { 92 | return new Promise((resolve, reject) => { 93 | const reg = new Registry({ 94 | hive: Registry.HKLM, 95 | arch: 'x86', 96 | key: '\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Battle.net', 97 | }); 98 | 99 | reg.valueExists('', (err, exists) => { 100 | if (err) { 101 | reject(new Error('Could not check if the Battle.net is installed.')); 102 | } 103 | 104 | resolve(exists); 105 | }); 106 | }); 107 | } 108 | 109 | static getBattlenetPath() { 110 | return new Promise((resolve, reject) => { 111 | const reg = new Registry({ 112 | hive: Registry.HKLM, 113 | arch: 'x86', 114 | key: '\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Battle.net', 115 | }); 116 | 117 | reg.values((err, items) => { 118 | let bnetPath = false; 119 | items.forEach((item) => { 120 | if (item.name === 'InstallLocation') { 121 | bnetPath = item.value; 122 | } 123 | }); 124 | 125 | if (bnetPath) { 126 | resolve(bnetPath); 127 | } else { 128 | reject(new Error('Could not Battle.net path.')); 129 | } 130 | }); 131 | }); 132 | } 133 | 134 | static getGames() { 135 | return new Promise((resolve, reject) => { 136 | log.info('Import: Started bnet'); 137 | this.getBattlenetPath().then((bnetPath) => { 138 | const games = []; 139 | const bnetExe = path.join(bnetPath, 'Battle.net.exe'); 140 | 141 | try { 142 | const decoded = decoder.decode(fs.readFileSync('C:\\ProgramData\\Battle.net\\Agent\\product.db')); 143 | const installed = decoded.productInstall.filter((product) => !(product.uid === 'battle.net' || product.uid === 'agent')); // Filter out non-games 144 | 145 | installed.forEach((product) => { 146 | const gameId = product.uid; 147 | const productCode = product.productCode.toLowerCase(); 148 | if (BNET_GAMES[productCode]) { 149 | const { launchId, name, exes } = BNET_GAMES[productCode]; 150 | const icon = path.join(product.settings.installPath, BNET_GAMES[productCode].icon); 151 | games.push({ 152 | id: gameId, 153 | name, 154 | exe: `"${PowerShell}"`, 155 | icon: `"${icon}"`, 156 | startIn: `"${bnetPath}"`, 157 | params: `-windowstyle hidden -NoProfile -ExecutionPolicy Bypass -Command "& \\"${LauncherAutoClose}\\" -launcher \\"battle.net\\" -game \\"${exes.join('\\",\\"')}\\" -launchcmd \\"dummy\\" -bnet $True -bnetpath \\"${bnetExe}\\" -bnetlaunchid \\"${launchId}\\""`, 158 | platform: 'bnet', 159 | }); 160 | } 161 | }); 162 | log.info('Import: Completed bnet'); 163 | resolve(games); 164 | } catch (err) { 165 | reject(err); 166 | } 167 | }).catch((err) => reject(err)); 168 | }); 169 | } 170 | } 171 | 172 | export default BattleNet; 173 | export const name = 'Blizzard Battle.net'; 174 | export const id = 'bnet'; 175 | export const official = true; 176 | -------------------------------------------------------------------------------- /src/js/importers/Epic.js: -------------------------------------------------------------------------------- 1 | import { PowerShell, LauncherAutoClose } from '../paths'; 2 | 3 | const Registry = window.require('winreg'); 4 | const fs = window.require('fs'); 5 | const path = window.require('path'); 6 | const jsonminify = window.require('jsonminify'); 7 | const { arch } = window.require('os'); 8 | const log = window.require('electron-log'); 9 | 10 | class Epic { 11 | static isInstalled() { 12 | return new Promise((resolve, reject) => { 13 | const reg = new Registry({ 14 | hive: Registry.HKLM, 15 | arch: 'x86', 16 | key: '\\SOFTWARE\\EpicGames\\Unreal Engine', 17 | }); 18 | 19 | reg.get('INSTALLDIR', (err, installDir) => { 20 | if (err) { 21 | if (err.code === 1) { 22 | return resolve(false); 23 | } 24 | reject(new Error('Could not check if Epic Games Launcher is installed.')); 25 | } 26 | 27 | const exeExists = fs.existsSync(path.join(installDir.value, 'Launcher', 'Engine', 'Binaries', 'Win32', 'EpicGamesLauncher.exe')); 28 | return resolve(exeExists); 29 | }); 30 | }); 31 | } 32 | 33 | static getEpicPath() { 34 | return new Promise((resolve, reject) => { 35 | const reg = new Registry({ 36 | hive: Registry.HKLM, 37 | arch: 'x86', 38 | key: '\\SOFTWARE\\EpicGames\\Unreal Engine', 39 | }); 40 | 41 | reg.get('INSTALLDIR', (err, installDir) => { 42 | if (err) { 43 | reject(new Error('Could not find Epic Games Launcher path.')); 44 | } 45 | 46 | resolve(installDir.value); 47 | }); 48 | }); 49 | } 50 | 51 | static getGames() { 52 | return new Promise((resolve, reject) => { 53 | log.info('Import: Started egs'); 54 | this.getEpicPath().then((epicPath) => { 55 | const games = []; 56 | let binFolder; 57 | if (arch() === 'ia32') { 58 | binFolder = 'Win32'; 59 | } else if (arch() === 'x64') { 60 | binFolder = 'Win64'; 61 | } 62 | const binaryPath = path.join(epicPath, 'Launcher', 'Portal', 'Binaries', binFolder); 63 | const manifestsDir = 'C:\\ProgramData\\Epic\\EpicGamesLauncher\\Data\\Manifests'; 64 | 65 | if (!fs.existsSync(manifestsDir)) { 66 | return resolve([]); 67 | } 68 | 69 | fs.readdirSync(manifestsDir).forEach((file) => { 70 | if (path.extname(file) === '.item') { 71 | const launcherDataStr = fs.readFileSync(path.join(manifestsDir, file)).toString(); 72 | const parsed = JSON.parse(jsonminify(launcherDataStr)); 73 | games.push({ 74 | id: parsed.AppName, 75 | name: parsed.DisplayName, 76 | exe: `"${PowerShell}"`, 77 | icon: `"${path.join(parsed.InstallLocation, parsed.LaunchExecutable)}"`, 78 | startIn: `"${binaryPath}"`, 79 | params: `-windowstyle hidden -NoProfile -ExecutionPolicy Bypass -Command "& \\"${LauncherAutoClose}\\"" -launcher \\"EpicGamesLauncher\\" -game \\"${path.parse(parsed.LaunchExecutable).name}\\" -launchcmd \\"com.epicgames.launcher://apps/${parsed.AppName}?action=launch&silent=true\\""`, 80 | platform: 'egs', 81 | }); 82 | } 83 | }); 84 | log.info('Import: Completed egs'); 85 | return resolve(games); 86 | }).catch((err) => reject(err)); 87 | }); 88 | } 89 | } 90 | 91 | export default Epic; 92 | export const name = 'Epic Games Launcher'; 93 | export const id = 'egs'; 94 | export const official = true; 95 | -------------------------------------------------------------------------------- /src/js/importers/Gog.js: -------------------------------------------------------------------------------- 1 | const Registry = window.require('winreg'); 2 | const fs = window.require('fs'); 3 | const promiseReflect = window.require('promise-reflect'); 4 | const log = window.require('electron-log'); 5 | 6 | class Gog { 7 | static isInstalled() { 8 | return new Promise((resolve, reject) => { 9 | const reg = new Registry({ 10 | hive: Registry.HKLM, 11 | arch: 'x86', 12 | key: '\\SOFTWARE\\GOG.com\\GalaxyClient\\paths', 13 | }); 14 | 15 | reg.get('client', (err, installDir) => { 16 | if (err) { 17 | if (err.code === 1) { 18 | return resolve(false); 19 | } 20 | reject(new Error('Could not check if GOG Galaxy is installed.')); 21 | } 22 | const dirExists = fs.existsSync(installDir.value); 23 | return resolve(dirExists); 24 | }); 25 | }); 26 | } 27 | 28 | static getGogPath() { 29 | return new Promise((resolve, reject) => { 30 | const reg = new Registry({ 31 | hive: Registry.HKLM, 32 | arch: 'x86', 33 | key: '\\SOFTWARE\\GOG.com\\GalaxyClient\\paths', 34 | }); 35 | 36 | reg.get('client', (err, installDir) => { 37 | if (err) { 38 | reject(new Error('Could not find GOG Galaxy path.')); 39 | } 40 | 41 | resolve(installDir.value); 42 | }); 43 | }); 44 | } 45 | 46 | static _processRegKey(key) { 47 | return new Promise((resolve, reject) => { 48 | key.get('dependsOn', (err, dependsOn) => { 49 | if (dependsOn == null) { 50 | key.values((errItems, items) => { 51 | const game = { 52 | platform: 'gog', 53 | }; 54 | 55 | items.forEach((item) => { 56 | if (item.name === 'gameID' || item.name === 'GAMEID') { 57 | game.id = item.value; 58 | } 59 | 60 | if (item.name === 'gameName' || item.name === 'GAMENAME') { 61 | game.name = item.value; 62 | } 63 | 64 | if (item.name === 'exe' || item.name === 'EXE') { 65 | game.exe = `"${item.value}"`; 66 | } 67 | 68 | if (item.name === 'launchParam' || item.name === 'LAUNCHPARAM') { 69 | game.params = item.value; 70 | } 71 | 72 | if (item.name === 'path' || item.name === 'PATH') { 73 | game.startIn = `"${item.value}"`; 74 | } 75 | }); 76 | resolve(game); 77 | }); 78 | } else { 79 | reject(key); 80 | } 81 | }); 82 | }); 83 | } 84 | 85 | static getGames() { 86 | return new Promise((resolve, reject) => { 87 | log.info('Import: Started gog'); 88 | this.getGogPath().then(() => { 89 | const reg = new Registry({ 90 | hive: Registry.HKLM, 91 | arch: 'x86', 92 | key: '\\SOFTWARE\\GOG.com\\Games', 93 | }); 94 | 95 | reg.keys((err, keys) => { 96 | if (err) { 97 | reject(new Error('Could not get GOG games.')); 98 | } 99 | 100 | if (keys) { 101 | const promiseArr = keys.map((key) => this._processRegKey(key).then((res) => res)); 102 | Promise.all(promiseArr.map(promiseReflect)) 103 | .then((results) => results.filter((result) => result.status === 'resolved').map((result) => result.data)) 104 | .then((results) => { 105 | log.info('Import: Completed gog'); 106 | resolve(results); 107 | }); 108 | } else { 109 | return resolve([]); 110 | } 111 | return false; 112 | }); 113 | }).catch((err) => reject(err)); 114 | }); 115 | } 116 | } 117 | 118 | export default Gog; 119 | export const name = 'GOG.com'; 120 | export const id = 'gog'; 121 | export const official = true; 122 | -------------------------------------------------------------------------------- /src/js/importers/Oculus.js: -------------------------------------------------------------------------------- 1 | const Registry = window.require('winreg'); 2 | const fs = window.require('fs'); 3 | const log = window.require('electron-log'); 4 | const cheerio = window.require('cheerio'); 5 | const request = window.require('request'); 6 | const Shell = require('node-powershell'); 7 | 8 | class Oculus { 9 | static isInstalled() { 10 | return new Promise((resolve, reject) => { 11 | const reg = new Registry({ 12 | hive: Registry.HKCU, 13 | arch: 'x86', 14 | key: '\\Software\\Oculus VR, LLC\\Oculus\\Libraries', 15 | }); 16 | 17 | reg.valueExists('', (err, exists) => { 18 | if (err) { 19 | reject(new Error('Could not check if Oculus is installed.')); 20 | } 21 | resolve(exists); 22 | }); 23 | }); 24 | } 25 | 26 | // Gets the configured Oculus library path 27 | // TODO: Support multiple configured library paths 28 | static getOculusLibraryPath() { 29 | return new Promise((resolve, reject) => { 30 | const reg = new Registry({ 31 | hive: Registry.HKCU, 32 | arch: 'x86', 33 | key: '\\Software\\Oculus VR, LLC\\Oculus\\Libraries', 34 | }); 35 | 36 | // Get all subkeys (one subkey is one Library folder) 37 | reg.keys((err, keys) => { 38 | if (err) { 39 | reject(err); 40 | } 41 | 42 | keys.forEach(key => { 43 | // Get the Path for the Library 44 | key.values((err, items) => { 45 | if (err) { 46 | reject(err); 47 | } 48 | 49 | let oculusLibraryPath = false; 50 | 51 | items.forEach((item) => { 52 | if (item.name === 'Path') { 53 | oculusLibraryPath = item.value; 54 | } 55 | }); 56 | 57 | if (oculusLibraryPath) { 58 | resolve(oculusLibraryPath); 59 | } else { 60 | reject(new Error('Could not find Oculus Library path.')); 61 | } 62 | }); 63 | }); 64 | }); 65 | }); 66 | } 67 | 68 | static getFilesFromPath(path, extension) { 69 | return new Promise((resolve, reject) => { 70 | let dir = fs.readdirSync( path ); 71 | resolve(dir.filter( elm => elm.match(new RegExp(`.*\.(${extension})`, 'ig')))); 72 | }); 73 | } 74 | 75 | // Converts a GUID Volume path into a lettered path 76 | // i.e. "\\?\Volume{56d4b0e2-0000-0000-0000-00a861000000}\" 77 | // ---> "F:\" 78 | static getVolumeLetteredPath(volumeGUIDPath) { 79 | return new Promise((resolve, reject) => { 80 | const command = "GWMI -namespace root\\cimv2 -class win32_volume | FL -property DriveLetter, DeviceID"; 81 | const ps = new Shell({ 82 | executionPolicy: 'Bypass', 83 | noProfile: true 84 | }); 85 | ps.addCommand(command); 86 | ps.invoke().then(output => { 87 | // Ugly way to parse Drive Letters and GUIDs from the console output 88 | let pairs = output.split("\r\n\r\n").filter(p => p.includes("\r\n")); 89 | pairs.forEach(p => { 90 | let letterRow = p.split("\r\n")[0]; 91 | let guidRow = p.split("\r\n")[1]; 92 | let letter = letterRow.split(" : ")[1]; 93 | let guid = guidRow.split(" : ")[1]; 94 | //log.info(letter + " = " + guid); 95 | if (volumeGUIDPath.includes(guid)) { 96 | resolve(volumeGUIDPath.replace(guid, letter + "\\")); 97 | } 98 | }) 99 | reject(new Error('No letter found for GUID path: ' + volumeGUIDPath)); 100 | }); 101 | }) 102 | } 103 | 104 | // Gets the game title from Oculus website 105 | static getGameTitle(appId) { 106 | return new Promise((resolve, reject) => { 107 | const url = "https://www.oculus.com/experiences/rift/" + appId + "/"; 108 | request.get(url, (error, response, data) => { 109 | const $ = cheerio.load(data); 110 | let jsonStr = $("head > script[type='application/ld+json']").html(); 111 | let json = JSON.parse(jsonStr); 112 | //log.info(json); 113 | resolve(json.name); 114 | }); 115 | }); 116 | } 117 | 118 | static getGames() { 119 | return new Promise((resolve, reject) => { 120 | log.info('Import: Started oculus'); 121 | 122 | this.getOculusLibraryPath().then(oculusLibraryPath => { 123 | //log.info('Got Oculus Library path: ' + oculusLibraryPath); 124 | 125 | this.getVolumeLetteredPath(oculusLibraryPath).then(volumeLetteredPath => { 126 | const manifestDir = volumeLetteredPath + "\\Manifests"; 127 | const softwareDir = volumeLetteredPath + "\\Software"; 128 | const games = []; 129 | const addGamesPromises = []; 130 | 131 | this.getFilesFromPath(manifestDir, '.json.mini').then(filePaths => { 132 | filePaths.forEach(fp => { 133 | let manifest = JSON.parse(fs.readFileSync(manifestDir + "\\" + fp)); 134 | const exePath = softwareDir + "\\" + manifest.canonicalName + "\\" + manifest.launchFile; 135 | const addGame = this.getGameTitle(manifest.appId).then(name => { 136 | games.push({ 137 | id: manifest.appId, 138 | name: name, 139 | exe: exePath, 140 | icon: exePath, 141 | params: "", 142 | platform: 'oculus', 143 | }); 144 | }); 145 | addGamesPromises.push(addGame); 146 | }); 147 | 148 | Promise.all(addGamesPromises).then(() => { 149 | log.info('Import: Completed oculus'); 150 | return resolve(games); 151 | }).catch((err) => reject(err)); 152 | }).catch((err) => reject(err)); 153 | }).catch((err) => reject(err)); 154 | }); 155 | }); 156 | } 157 | } 158 | 159 | export default Oculus; 160 | export const name = 'Oculus'; 161 | export const id = 'oculus'; 162 | export const official = false; 163 | -------------------------------------------------------------------------------- /src/js/importers/Origin.js: -------------------------------------------------------------------------------- 1 | import { PowerShell, LauncherAutoClose } from '../paths'; 2 | 3 | const Registry = window.require('winreg'); 4 | const fs = window.require('fs'); 5 | const path = window.require('path'); 6 | const querystring = window.require('querystring'); 7 | const { xml2js } = window.require('xml-js'); 8 | const iconv = window.require('iconv-lite'); 9 | const log = window.require('electron-log'); 10 | 11 | class Origin { 12 | static isInstalled() { 13 | return new Promise((resolve, reject) => { 14 | const reg = new Registry({ 15 | hive: Registry.HKLM, 16 | arch: 'x86', 17 | key: '\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Origin', 18 | }); 19 | 20 | reg.valueExists('', (err, exists) => { 21 | if (err) { 22 | reject(new Error('Could not check if Origin is installed.')); 23 | } 24 | 25 | resolve(exists); 26 | }); 27 | }); 28 | } 29 | 30 | static getOriginPath() { 31 | return new Promise((resolve, reject) => { 32 | const reg = new Registry({ 33 | hive: Registry.HKLM, 34 | arch: 'x86', 35 | key: '\\SOFTWARE\\Origin', 36 | }); 37 | 38 | reg.values((err, items) => { 39 | if (err) { 40 | reject(new Error('Could not find Origin path.')); 41 | } 42 | 43 | let originPath = false; 44 | 45 | items.forEach((item) => { 46 | if (item.name === 'ClientPath') { 47 | originPath = item.value; 48 | } 49 | }); 50 | 51 | if (originPath) { 52 | resolve(originPath); 53 | } else { 54 | reject(new Error('Could not find Origin path.')); 55 | } 56 | }); 57 | }); 58 | } 59 | 60 | static _parseRuntime(runtime) { 61 | const exeDefs = []; 62 | if (runtime.launcher) { 63 | if (runtime.launcher.filePath) { 64 | // Only one exe 65 | exeDefs.push(runtime.launcher.filePath._text); 66 | } else if (runtime.launcher[0] && runtime.launcher[0].filePath) { 67 | // Multiple exes 68 | runtime.launcher.forEach((exe) => { 69 | if (exe.filePath) { 70 | exeDefs.push(exe.filePath._text); 71 | } 72 | }); 73 | } else { 74 | return false; 75 | } 76 | } else { 77 | return false; 78 | } 79 | 80 | // remove everything in [] cause we only need the exe name 81 | return exeDefs.map((exe) => (exe.replace(/\[.+\]/g, ''))); 82 | } 83 | 84 | static getGames() { 85 | return new Promise((resolve, reject) => { 86 | log.info('Import: Started origin'); 87 | this.getOriginPath().then((originPath) => { 88 | const originDataPath = 'C:\\ProgramData\\Origin'; 89 | const games = []; 90 | 91 | if (fs.existsSync(path.join(originDataPath, 'LocalContent'))) { 92 | fs.readdirSync(path.join(originDataPath, 'LocalContent')).forEach((folder) => { 93 | const manifestFolder = path.join(originDataPath, 'LocalContent', folder); 94 | if (fs.lstatSync(manifestFolder).isDirectory()) { 95 | fs.readdirSync(manifestFolder).some((file) => { 96 | // Get first file with .mfst extension 97 | if (path.extname(file) === '.mfst') { 98 | // .mfst file is just a text file with a query string 99 | const manifestFile = path.join(originDataPath, 'LocalContent', folder, file); 100 | const manifestStr = fs.readFileSync(manifestFile).toString(); 101 | const manifestStrParsed = querystring.parse(manifestStr); 102 | // Check if has a "dipinstallpath" param 103 | if (manifestStrParsed.dipinstallpath) { 104 | const installerDataPath = path.join(manifestStrParsed.dipinstallpath, '__Installer', 'installerdata.xml'); 105 | // If __Installer/installerdata.xml file exists in the install dir 106 | if (fs.existsSync(installerDataPath)) { 107 | // Parse installerdata.xml file 108 | let xml; let executables; let 109 | name; 110 | try { 111 | const installerDataFile = fs.readFileSync(installerDataPath); 112 | try { 113 | xml = xml2js(iconv.decode(installerDataFile, 'utf8'), { compact: true }); 114 | } catch (err) { 115 | xml = xml2js(iconv.decode(installerDataFile, 'utf16'), { compact: true }); 116 | } 117 | } catch (err) { 118 | return reject(new Error(`Could not parse installerdata.xml for ${path.basename(folder)}`)); 119 | } 120 | 121 | if (xml.DiPManifest) { 122 | if (xml.DiPManifest.runtime) { 123 | executables = this._parseRuntime(xml.DiPManifest.runtime); 124 | } 125 | if (xml.DiPManifest.gameTitles.gameTitle) { 126 | if (xml.DiPManifest.gameTitles.gameTitle._text) { 127 | name = xml.DiPManifest.gameTitles.gameTitle._text; 128 | } else if (xml.DiPManifest.gameTitles.gameTitle[0]) { 129 | name = xml.DiPManifest.gameTitles.gameTitle[0]._text; 130 | } 131 | } 132 | } else if (xml.game) { 133 | if (xml.game.runtime) { 134 | executables = this._parseRuntime(xml.game.runtime); 135 | } 136 | if (xml.game.metadata.localeInfo) { 137 | if (xml.game.metadata.localeInfo.title) { 138 | name = xml.game.metadata.localeInfo.title._text; 139 | } else if (xml.game.metadata.localeInfo[0]) { 140 | name = xml.game.metadata.localeInfo[0].title._text; 141 | } 142 | } 143 | } 144 | 145 | if (!name) { 146 | return true; 147 | } 148 | 149 | if (executables) { 150 | const watchedExes = executables.map((x) => path.parse(path.basename(x)).name); 151 | games.push({ 152 | id: manifestStrParsed.id, 153 | name, 154 | exe: `"${PowerShell}"`, 155 | icon: `"${path.join(manifestStrParsed.dipinstallpath, executables[0])}"`, 156 | startIn: `"${path.dirname(originPath)}"`, 157 | params: `-windowstyle hidden -NoProfile -ExecutionPolicy Bypass -Command "& \\"${LauncherAutoClose}\\"" -launcher \\"Origin\\" -game \\"${watchedExes.join('\\",\\"')}\\" -launchcmd \\"origin://launchgamejump/${manifestStrParsed.id}\\""`, 158 | platform: 'origin', 159 | }); 160 | } 161 | return true; 162 | } 163 | } 164 | } 165 | }); 166 | } 167 | }); 168 | log.info('Import: Completed origin'); 169 | resolve(games); 170 | } else { 171 | reject(new Error('Could not find Origin content folder.')); 172 | } 173 | }); 174 | }); 175 | } 176 | } 177 | 178 | export default Origin; 179 | export const name = 'Origin'; 180 | export const id = 'origin'; 181 | export const official = true; 182 | -------------------------------------------------------------------------------- /src/js/importers/Uplay.js: -------------------------------------------------------------------------------- 1 | import { PowerShell, LauncherAutoClose } from '../paths'; 2 | 3 | const Registry = window.require('winreg'); 4 | const yaml = window.require('js-yaml'); 5 | const fs = window.require('fs'); 6 | const path = window.require('path'); 7 | const log = window.require('electron-log'); 8 | 9 | class Uplay { 10 | static isInstalled() { 11 | return new Promise((resolve, reject) => { 12 | const reg = new Registry({ 13 | hive: Registry.HKLM, 14 | arch: 'x86', 15 | key: '\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Uplay', 16 | }); 17 | 18 | reg.valueExists('', (err, exists) => { 19 | if (err) { 20 | reject(new Error('Could not check if Uplay is installed.')); 21 | } 22 | resolve(exists); 23 | }); 24 | }); 25 | } 26 | 27 | static getUplayPath() { 28 | return new Promise((resolve, reject) => { 29 | const reg = new Registry({ 30 | hive: Registry.HKLM, 31 | arch: 'x86', 32 | key: '\\SOFTWARE\\Ubisoft\\Launcher', 33 | }); 34 | 35 | reg.values((err, items) => { 36 | if (err) { 37 | reject(err); 38 | } 39 | 40 | let uplayPath = false; 41 | 42 | items.forEach((item) => { 43 | if (item.name === 'InstallDir') { 44 | uplayPath = item.value; 45 | } 46 | }); 47 | 48 | if (uplayPath) { 49 | resolve(uplayPath); 50 | } else { 51 | reject(new Error('Could not find Uplay path.')); 52 | } 53 | }); 54 | }); 55 | } 56 | 57 | static parseConfig(config) { 58 | return new Promise((resolve, reject) => { 59 | const configFile = fs.readFileSync(config, 'hex'); 60 | 61 | const finalOutput = []; 62 | let game = ['root:']; 63 | let launcherId = null; 64 | let end = false; 65 | this._generateHexArr(configFile).forEach((hexStr) => { 66 | const line = Buffer.from(hexStr, 'hex').toString('utf8').replace(/\n/g, ''); 67 | const foundId = hexStr.match(/08([0-9a-f]+)10[0-9a-f]+1a/); 68 | if (foundId) { 69 | if (game.length === 1) { 70 | const hexChars = foundId[1].match(/.{1,2}/g); 71 | const ints = hexChars.map((x) => parseInt(x, 16)); 72 | launcherId = this._convertLaunchId(ints); 73 | return; 74 | } if (game.length > 1) { 75 | try { 76 | const gameParsed = yaml.load(game.join('\n'), {'json': true }); 77 | 78 | if (launcherId) { 79 | gameParsed.root.launcher_id = launcherId; 80 | } 81 | finalOutput.push(gameParsed); 82 | } catch (e) { 83 | reject(new Error('Could not parse YAML')); 84 | } 85 | 86 | const hexChars = foundId[1].match(/.{1,2}/g); 87 | const ints = hexChars.map((x) => parseInt(x, 16)); 88 | launcherId = this._convertLaunchId(ints); 89 | game = ['root:']; 90 | return; 91 | } 92 | } 93 | 94 | if (line.indexOf('localizations:') === 0) { 95 | end = true; 96 | return; 97 | } 98 | 99 | // Already manually saved "root:" 100 | if (line.trim().includes('root:') && !line.trim().includes('_')) { 101 | end = false; 102 | return; 103 | } 104 | 105 | if (!end) { 106 | // Save lines if starts with spaces 107 | if (line.substr(0, 2) === ' ' && !line.includes('sort_string:')) { 108 | game.push(line); 109 | } 110 | } 111 | }); 112 | resolve(finalOutput); 113 | }); 114 | } 115 | 116 | static _generateHexArr(str) { 117 | const lines = []; 118 | const split = str.match(/.{1,2}/g); 119 | let line = ''; 120 | for (let i = 0; i < split.length; i++) { 121 | line += split[i]; 122 | if (split[i] === '0a' && split[i - 2] !== '08') { 123 | lines.push(line); 124 | line = ''; 125 | } 126 | } 127 | return lines; 128 | } 129 | 130 | static _processRegKey(key) { 131 | return new Promise((resolve) => { 132 | const id = path.basename(key.key); 133 | key.get('InstallDir', (err, installDir) => { 134 | resolve({ 135 | id, 136 | installDir: installDir.value, 137 | }); 138 | }); 139 | }); 140 | } 141 | 142 | static _getRegInstalled() { 143 | return new Promise((resolve, reject) => { 144 | const reg = new Registry({ 145 | hive: Registry.HKLM, 146 | arch: 'x86', 147 | key: '\\SOFTWARE\\Ubisoft\\Launcher\\Installs', 148 | }); 149 | reg.keys((err, keys) => { 150 | if (err) { 151 | reject(err); 152 | } 153 | 154 | if (keys) { 155 | const promiseArr = keys.map((key) => this._processRegKey(key).then((res) => res)); 156 | Promise.all(promiseArr).then((resultsArray) => { 157 | const out = {}; 158 | resultsArray.forEach((item) => { 159 | out[String(item.id)] = item.installDir; 160 | }); 161 | return resolve(out); 162 | }); 163 | } else { 164 | return resolve({}); 165 | } 166 | return false; 167 | }); 168 | }); 169 | } 170 | 171 | static _convertLaunchId(hexArr) { 172 | let launchId = 0; 173 | let multiplier = 1; 174 | for (let i = 0; i < hexArr.length; i++, multiplier *= 256) { 175 | if (hexArr[i] === 16) { 176 | break; 177 | } 178 | launchId += (hexArr[i] * multiplier); 179 | } 180 | 181 | if (launchId > 256 * 256) { 182 | launchId -= (128 * 256 * Math.ceil(launchId / (256 * 256))); 183 | launchId -= (128 * Math.ceil(launchId / 256)); 184 | } else if (launchId > 256) { 185 | launchId -= (128 * Math.ceil(launchId / 256)); 186 | } 187 | return launchId; 188 | } 189 | 190 | static resolveConfigPath(value) { 191 | const hives = { 192 | HKEY_LOCAL_MACHINE: 'HKLM', 193 | HKEY_CURRENT_USER: 'HKCU', 194 | HKEY_CLASSES_ROOT: 'HKCR', 195 | HKEY_USERS: 'HKU', 196 | HKEY_CURRENT_CONFIG: 'HKCC', 197 | }; 198 | let output = ''; 199 | return new Promise((resolve) => { 200 | // Value is stored in registry 201 | if (value.register) { 202 | const key = value.register.split('\\'); 203 | const hive = hives[key.shift()]; 204 | const valueName = key.pop(); 205 | const reg = new Registry({ 206 | hive, 207 | arch: 'x86', 208 | key: `\\${key.join('\\')}`, 209 | }); 210 | reg.values((err, items) => { 211 | if (err) { 212 | return resolve(false); 213 | } 214 | items.forEach((item) => { 215 | if (item.name.toLowerCase() === valueName.toLowerCase()) { 216 | output += item.value; 217 | if (value.append) { 218 | output += value.append; 219 | } 220 | return resolve(output); 221 | } 222 | return false; 223 | }); 224 | return false; 225 | }); 226 | } else if (value.relative) { 227 | return resolve(value.relative); 228 | } 229 | }); 230 | } 231 | 232 | static getGameExes(executables, workingDirFallback = false) { 233 | return new Promise((resolve) => { 234 | const promises = []; 235 | executables.forEach((exe) => { 236 | const promise = new Promise((resolveExe) => { 237 | let append = ''; 238 | if (exe.working_directory.append) { 239 | append = exe.working_directory.append; 240 | } 241 | this.resolveConfigPath(exe.path).then((exePath) => { 242 | if (!exePath) { 243 | resolveExe(false); 244 | } 245 | 246 | // Get working directory 247 | this.resolveConfigPath(exe.working_directory).then((workingDir) => { 248 | if (workingDir) { 249 | // check if exe is actually there 250 | if (fs.existsSync(path.join(workingDir, append, exePath))) { 251 | resolveExe(path.join(workingDir, append, exePath)); 252 | } else { 253 | resolveExe(false); 254 | } 255 | } else if (workingDirFallback && fs.existsSync(path.join(workingDirFallback, exePath))) { 256 | resolveExe(path.join(workingDirFallback, exePath)); 257 | } else if (workingDirFallback && fs.existsSync(path.join(workingDirFallback, append, exePath))) { 258 | resolveExe(path.join(workingDirFallback, append, exePath)); 259 | } else { 260 | resolveExe(false); 261 | } 262 | }); 263 | }); 264 | }); 265 | promises.push(promise); 266 | }); 267 | Promise.all(promises).then((results) => resolve(results)); 268 | }); 269 | } 270 | 271 | static getGames() { 272 | return new Promise((resolve, reject) => { 273 | log.info('Import: Started uplay'); 274 | 275 | this.getUplayPath().then((uplayPath) => { 276 | this.parseConfig(path.join(uplayPath, 'cache', 'configuration', 'configurations')).then((configItems) => { 277 | this._getRegInstalled().then((installedGames) => { 278 | // Only need launch IDs 279 | const installedGamesIds = Object.keys(installedGames); 280 | 281 | const games = []; 282 | const addGamesPromises = []; 283 | const invalidNames = ['NAME', 'GAMENAME', 'l1']; 284 | configItems.forEach((game) => { 285 | if (game.root.start_game) { // DLC's and other non-games dont have this key 286 | // Skip adding games launched via Steam. 287 | if (game.root.start_game.steam) { 288 | return; 289 | } 290 | 291 | let gameName = game.root.name; 292 | let gameId; 293 | 294 | // Get name from another key if has weird name assigned 295 | if (invalidNames.includes(game.root.name)) { 296 | if (typeof game.root.installer !== 'undefined') { 297 | gameName = game.root.installer.game_identifier; 298 | } 299 | 300 | // Override installer name if this value 301 | if (typeof game.root.default !== 'undefined' && typeof game.root.default[game.root.name] !== 'undefined') { 302 | gameName = game.root.default[game.root.name]; 303 | } 304 | } 305 | 306 | console.log(gameName); 307 | 308 | if (game.root.space_id) { 309 | gameId = game.root.space_id; 310 | } else { 311 | // No space_id means legacy game. Use launch id as ID. 312 | gameId = game.root.launcher_id; 313 | } 314 | 315 | // Only add if launcher id is found in registry and has executables 316 | if (installedGamesIds.includes(String(game.root.launcher_id)) && (game.root.start_game.offline || game.root.start_game.online)) { 317 | const addGame = this.getGameExes((game.root.start_game.offline || game.root.start_game.online).executables, installedGames[game.root.launcher_id]) 318 | .then((executables) => { 319 | if (executables.every((x) => x !== false)) { 320 | const watchedExes = executables.map((x) => path.parse(path.basename(x)).name); 321 | games.push({ 322 | id: gameId, 323 | name: gameName, 324 | exe: `"${PowerShell}"`, 325 | icon: `"${executables[0]}"`, 326 | startIn: `"${uplayPath}"`, 327 | params: `-windowstyle hidden -NoProfile -ExecutionPolicy Bypass -Command "& \\"${LauncherAutoClose}\\" -launcher \\"upc\\" -game \\"${watchedExes.join('\\",\\"')}\\" -launchcmd \\"uplay://launch/${game.root.launcher_id}\\""`, 328 | platform: 'uplay', 329 | }); 330 | } else { 331 | log.info(`Import: uplay - Could not resolve executable for ${gameName}`); 332 | } 333 | }); 334 | addGamesPromises.push(addGame); 335 | } 336 | } 337 | }); 338 | Promise.all(addGamesPromises).then(() => { 339 | log.info('Import: Completed uplay'); 340 | return resolve(games); 341 | }); 342 | }).catch((err) => reject(err)); 343 | }).catch((err) => reject(err)); 344 | }).catch((err) => reject(err)); 345 | }); 346 | } 347 | } 348 | 349 | export default Uplay; 350 | export const name = 'Uplay'; 351 | export const id = 'uplay'; 352 | export const official = true; 353 | -------------------------------------------------------------------------------- /src/js/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | ReactDOM.render(, document.getElementById('root')); 6 | -------------------------------------------------------------------------------- /src/js/paths.js: -------------------------------------------------------------------------------- 1 | const fs = window.require('fs'); 2 | const path = window.require('path'); 3 | 4 | let launcherWatcher = path.resolve(path.dirname(process.resourcesPath), '../../../', 'LauncherAutoClose.ps1'); 5 | if (!fs.existsSync(launcherWatcher)) { 6 | launcherWatcher = path.join(path.dirname(process.resourcesPath), 'LauncherAutoClose.ps1'); 7 | } 8 | 9 | export const PowerShell = path.join(process.env.windir, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe'); 10 | 11 | export const LauncherAutoClose = launcherWatcher; 12 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './src/js/index.jsx', 5 | mode: 'development', 6 | devtool: 'source-map', 7 | target: 'node', 8 | module: { 9 | rules: [ 10 | { 11 | test: /\.(js|jsx)$/, 12 | exclude: /(node_modules|bower_components)/, 13 | loader: 'babel-loader', 14 | options: { presets: ['@babel/env'] }, 15 | }, 16 | { 17 | test: /\.css$/, 18 | use: ['style-loader', 'css-loader'], 19 | }, 20 | { 21 | test: /\.(png|svg|jpg|gif)$/, 22 | use: { 23 | loader: 'file-loader', 24 | options: { 25 | outputPath: 'img', 26 | publicPath: './img', 27 | }, 28 | }, 29 | }, 30 | ], 31 | }, 32 | resolve: { extensions: ['*', '.js', '.jsx'] }, 33 | output: { 34 | path: path.resolve(__dirname, 'public/'), 35 | publicPath: '/public/', 36 | filename: 'bundle.js', 37 | }, 38 | }; 39 | --------------------------------------------------------------------------------