├── .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 |
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 |
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 |
--------------------------------------------------------------------------------