Files
GDL-Continuation/public/electron.js
T
2025-07-31 00:53:24 +02:00

1011 lines
26 KiB
JavaScript

const {
app,
BrowserWindow,
ipcMain,
Tray,
Menu,
dialog,
shell,
screen,
globalShortcut,
nativeImage
} = require('electron');
const path = require('path');
const { spawn, exec } = require('child_process');
const { autoUpdater } = require('electron-updater');
const log = require('electron-log');
const fss = require('fs');
const os = require('os');
const { promisify } = require('util');
const { createHash } = require('crypto');
const {
default: { fromBase64: toBase64URL }
} = require('base64url');
const { URL } = require('url');
const UserAgent = require('user-agents');
const nsfw = require('./nsfw');
const napi = require('./native/napi.node');
// console.log(napi.fibonacci(10));
const fs = fss.promises;
let mainWindow;
let tray;
let watcher;
const discordRPC = require('./discordRPC');
const gotTheLock = app.requestSingleInstanceLock();
const isDev = process.env.NODE_ENV === 'development';
// Prevent multiple instances
if (gotTheLock) {
app.on('second-instance', (e, argv) => {
if (process.platform === 'win32') {
const args = process.argv.slice(1);
const args1 = argv.slice(1);
log.log([...args, ...args1]);
if (mainWindow) {
mainWindow.webContents.send('custom-protocol-event', [
...args,
...args1
]);
}
}
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
}
});
} else {
app.quit();
}
if (!app.isDefaultProtocolClient('gdlauncher')) {
app.setAsDefaultProtocolClient('gdlauncher');
}
// This gets rid of this: https://github.com/electron/electron/issues/13186
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = true;
// app.commandLine.appendSwitch("disable-web-security");
app.commandLine.appendSwitch('disable-gpu-vsync=gpu');
app.commandLine.appendSwitch('disable-features', 'OutOfBlinkCors');
const edit = [
...(process.platform === 'darwin'
? [
{
label: 'GDLauncher',
submenu: [
{
label: 'About GDLauncher',
role: 'about'
},
{ type: 'separator' },
{
label: 'Services',
role: 'services',
submenu: []
},
{ type: 'separator' },
{
label: 'Hide GDLauncher',
accelerator: 'Command+H',
role: 'hide'
},
{
label: 'Hide Others',
accelerator: 'Command+Alt+H',
role: 'hideOthers'
},
{
label: 'Show All',
role: 'unhide'
},
{ type: 'separator' },
{
label: 'Quit GDLauncher',
accelerator: 'Command+Q',
click: () => {
app.quit();
}
}
]
}
]
: []),
{
label: 'Edit',
submenu: [
{
label: 'Cut',
accelerator: 'CmdOrCtrl+X',
selector: 'cut:'
},
{
label: 'Copy',
accelerator: 'CmdOrCtrl+C',
selector: 'copy:'
},
{
label: 'Paste',
accelerator: 'CmdOrCtrl+V',
selector: 'paste:'
},
{
label: 'Select All',
accelerator: 'CmdOrCtrl+A',
selector: 'selectAll:'
},
{ type: 'separator' },
{ label: 'Undo', accelerator: 'CmdOrCtrl+Z', selector: 'undo:' },
{ label: 'Redo', accelerator: 'Shift+CmdOrCtrl+Z', selector: 'redo:' }
]
}
];
let navPlatform = null;
switch (os.platform()) {
case 'win32':
navPlatform = 'Win32';
break;
case 'darwin':
navPlatform = 'MacIntel';
break;
case 'linux':
default:
navPlatform = 'Linux x86_64';
}
const userAgent = new UserAgent({
platform: navPlatform,
deviceCategory: 'desktop'
}).toString();
// app.allowRendererProcessReuse = true;
Menu.setApplicationMenu(Menu.buildFromTemplate(edit));
const baseUserPath = path.join(app.getPath('appData'), 'gdlauncher_next');
if (!fss.existsSync(baseUserPath)) {
fss.mkdirSync(baseUserPath);
}
app.setPath('userData', baseUserPath);
let allowUnstableReleases = false;
const releaseChannelExists = fss.existsSync(
path.join(app.getPath('userData'), 'rChannel')
);
if (releaseChannelExists) {
const releaseChannelConfig = fss.readFileSync(
path.join(app.getPath('userData'), 'rChannel')
);
const releaseId = parseInt(releaseChannelConfig.toString(), 10);
if (releaseId === 1) {
allowUnstableReleases = true;
}
} else if (!releaseChannelExists && app.getVersion().includes('beta')) {
fss.writeFileSync(path.join(app.getPath('userData'), 'rChannel'), '1');
allowUnstableReleases = true;
}
if (
process.env.REACT_APP_RELEASE_TYPE === 'portable' &&
process.platform !== 'linux'
) {
app.setPath('userData', path.join(path.dirname(app.getPath('exe')), 'data'));
} else {
const overrideExists = fss.existsSync(
path.join(app.getPath('userData'), 'override.data')
);
if (overrideExists) {
const override = fss.readFileSync(
path.join(app.getPath('userData'), 'override.data')
);
app.setPath('userData', override.toString());
}
}
// Read config and eventually use new path
try {
const configPath = path.join(app.getPath('userData'), 'config.json');
const configFile = fss.readFileSync(configPath);
const config = JSON.parse(configFile);
const requiredArgs = [
'-Dfml.ignorePatchDiscrepancies=true',
'-Dfml.ignoreInvalidMinecraftCertificates=true'
];
const javaArgs = config.settings.java.args;
if (config.settings.java.args) {
const cleanedJavaArgs = javaArgs
.split(' ')
.filter(arg => !requiredArgs.includes(arg))
.join(' ');
if (cleanedJavaArgs.split(' ').length !== javaArgs.split(' ').length) {
config.settings.java.args = cleanedJavaArgs;
fss.writeFileSync(configPath, JSON.stringify(config));
}
}
} catch {
// Do nothing
}
log.log(process.env.REACT_APP_RELEASE_TYPE, app.getVersion());
const get7zPath = async () => {
let baseDir = path.dirname(app.getPath('exe'));
if (isDev) {
baseDir = path.resolve(baseDir, '../../');
if (process.platform === 'win32') {
baseDir = path.join(baseDir, '7zip-bin/win/x64');
} else if (process.platform === 'linux') {
baseDir = path.join(baseDir, '7zip-bin/linux', process.arch);
} else if (process.platform === 'darwin') {
baseDir = path.resolve(baseDir, '../../../', '7zip-bin/mac/x64');
}
}
if (process.platform === 'linux') {
return path.join(baseDir, '7za');
}
if (process.platform === 'darwin') {
return path.resolve(baseDir, isDev ? '' : '../', '7za');
}
return path.join(baseDir, '7za.exe');
};
async function patchSevenZip() {
try {
if (process.platform === 'linux' || process.platform === 'darwin') {
const sevenZipPath = await get7zPath();
await promisify(exec)(`chmod +x "${sevenZipPath}"`);
await promisify(exec)(`chmod 755 "${sevenZipPath}"`);
}
} catch (e) {
log.error(e);
}
}
patchSevenZip();
function createWindow() {
mainWindow = new BrowserWindow({
width: 1100,
height: 700,
minWidth: 1100,
minHeight: 700,
show: true,
frame: false,
backgroundColor: '#1B2533',
webPreferences: {
experimentalFeatures: true,
nodeIntegration: true,
contextIsolation: false,
enableRemoteModule: true,
sandbox: false,
// Disable in dev since I think hot reload is messing with it
webSecurity: !isDev
}
});
if (isDev) {
globalShortcut.register('CommandOrControl+R', () => {
mainWindow.reload();
});
globalShortcut.register('F5', () => {
mainWindow.reload();
});
}
mainWindow.webContents.session.webRequest.onHeadersReceived(
{
urls: ['http://*/*', 'https://*/*']
},
(details, callback) => {
// eslint-disable-next-line
delete details.responseHeaders['Access-Control-Allow-Origin'];
// eslint-disable-next-line
delete details.responseHeaders['access-control-allow-origin'];
if (details.url.includes('www.google-analytics.com')) {
// eslint-disable-next-line
details.responseHeaders['Access-Control-Allow-Origin'] = [
'http://localhost:3000'
];
} else {
// eslint-disable-next-line
details.responseHeaders['Access-Control-Allow-Origin'] = ['*'];
}
callback({
cancel: false,
responseHeaders: details.responseHeaders
});
}
);
mainWindow.webContents.session.webRequest.onBeforeSendHeaders(
(details, callback) => {
// Use a header to skip sending Origin on request.
const {
'X-Skip-Origin': xSkipOrigin,
Origin: _origin,
...requestHeaders
} = details.requestHeaders;
if (xSkipOrigin !== 'skip') {
requestHeaders.Origin = 'https://gdevs.io';
}
callback({ cancel: false, requestHeaders });
}
);
const RESOURCE_DIR = isDev ? __dirname : path.join(__dirname, '../build');
const iconPath = path.join(RESOURCE_DIR, 'logo_32x32.png');
const nimage = nativeImage.createFromPath(iconPath);
tray = new Tray(nimage);
const trayMenuTemplate = [
{
label: 'GDLauncher',
enabled: false
},
{
label: 'Show Dev Tools',
click: () => mainWindow.webContents.openDevTools()
}
];
const trayMenu = Menu.buildFromTemplate(trayMenuTemplate);
tray.setContextMenu(trayMenu);
tray.setToolTip('GDLauncher');
tray.on('double-click', () => mainWindow.show());
mainWindow.loadURL(
isDev
? 'http://localhost:3000'
: `file://${path.join(__dirname, '../build/index.html')}`,
{
userAgent: 'GDLauncher'
}
);
if (isDev) {
mainWindow.webContents.openDevTools();
}
mainWindow.on('maximize', () => {
mainWindow.webContents.send('window-maximized');
});
mainWindow.on('unmaximize', () => {
mainWindow.webContents.send('window-minimized');
});
const handleRedirect = (e, url) => {
if (url !== mainWindow.webContents.getURL()) {
e.preventDefault();
shell.openExternal(url);
}
};
mainWindow.webContents.on('will-navigate', handleRedirect);
mainWindow.webContents.on('new-window', handleRedirect);
}
app.on('ready', createWindow);
app.on('window-all-closed', async () => {
if (watcher) {
await watcher.stop();
watcher = null;
}
app.quit();
});
app.on('before-quit', async () => {
if (watcher) {
await watcher.stop();
watcher = null;
}
if (mainWindow) {
mainWindow.removeAllListeners('close');
}
mainWindow = null;
});
app.on('activate', () => {
if (mainWindow === null) {
createWindow();
}
});
ipcMain.handle(
'msLoginOAuth',
(_event, clientId, codeVerifier, redirectUrl) =>
new Promise((resolve, reject) => {
const codeChallenge = toBase64URL(
createHash('sha256').update(codeVerifier).digest('base64')
);
const msAuthorizeUrl = new URL(
'https://login.live.com/oauth20_authorize.srf'
);
msAuthorizeUrl.searchParams.set('client_id', clientId);
msAuthorizeUrl.searchParams.set('redirect_uri', redirectUrl);
msAuthorizeUrl.searchParams.set('code_challenge', codeChallenge);
msAuthorizeUrl.searchParams.set('code_challenge_method', 'S256');
msAuthorizeUrl.searchParams.set('response_type', 'code');
msAuthorizeUrl.searchParams.set(
'scope',
'offline_access xboxlive.signin xboxlive.offline_access'
);
msAuthorizeUrl.searchParams.set(
'cobrandid',
'8058f65d-ce06-4c30-9559-473c9275a65d'
);
const handleRedirect = (url, authWindow) => {
const rdUrl = new URL(url);
const orUrl = new URL(redirectUrl);
if (
rdUrl.origin === orUrl.origin &&
rdUrl.pathname === orUrl.pathname
) {
const redirectCode = rdUrl.searchParams.get('code');
const redirectError = rdUrl.searchParams.get('error');
authWindow.destroy(); // Will not trigger 'close'
if (redirectCode) {
return resolve(redirectCode);
}
return reject(redirectError);
}
};
const oAuthWindow = new BrowserWindow({
title: 'Sign in to your Microsoft account',
show: false,
parent: mainWindow,
autoHideMenuBar: true,
webPreferences: {
nodeIntegration: false
}
});
oAuthWindow.webContents.session.clearStorageData();
// Remove Origin
oAuthWindow.webContents.session.webRequest.onBeforeSendHeaders(
(details, callback) => {
const {
requestHeaders: { Origin, ...requestHeaders }
} = details;
callback({ cancel: false, requestHeaders });
}
);
oAuthWindow.on('close', () =>
reject(new Error('User closed login window'))
);
oAuthWindow.webContents.on('will-navigate', (_e, url) =>
handleRedirect(url, oAuthWindow)
);
oAuthWindow.webContents.on('will-redirect', (_e, url) =>
handleRedirect(url, oAuthWindow)
);
oAuthWindow.show();
return oAuthWindow
.loadURL(msAuthorizeUrl.toString())
.catch(error => reject(error));
})
);
ipcMain.handle('update-progress-bar', (event, p) => {
mainWindow.setProgressBar(p / 100);
});
ipcMain.handle('hide-window', () => {
if (mainWindow) {
mainWindow.hide();
}
});
ipcMain.handle('min-max-window', () => {
if (mainWindow.isMaximized()) {
mainWindow.unmaximize();
} else {
mainWindow.maximize();
}
});
ipcMain.handle('minimize-window', () => {
mainWindow.minimize();
});
ipcMain.handle('show-window', () => {
if (mainWindow) {
mainWindow.show();
mainWindow.focus();
}
});
ipcMain.handle('quit-app', async () => {
if (watcher) {
log.log('Stopping listener');
await watcher.stop();
watcher = null;
}
mainWindow.close();
mainWindow = null;
});
ipcMain.handle('isAppImage', () => {
return process.env.APPIMAGE;
});
ipcMain.handle('getAppdataPath', () => {
return app.getPath('appData');
});
// Returns path to app.asar
ipcMain.handle('getAppPath', () => {
return app.getAppPath();
});
ipcMain.handle('getUserData', () => {
return app.getPath('userData');
});
ipcMain.handle('getSentryDsn', () => {
return process.env.SENTRY_DSN;
});
ipcMain.handle('getExecutablePath', () => {
return path.dirname(app.getPath('exe'));
});
ipcMain.handle('getAppVersion', () => {
return app.getVersion();
});
ipcMain.handle('getIsWindowMaximized', () => {
return !mainWindow.maximizable;
});
ipcMain.handle('openFolder', (e, folderPath) => {
shell.openPath(folderPath);
});
ipcMain.handle('open-devtools', () => {
mainWindow.webContents.openDevTools({ mode: 'undocked' });
});
ipcMain.handle('openFolderDialog', (e, defaultPath) => {
return dialog.showOpenDialog({
properties: ['openDirectory'],
defaultPath: path.dirname(defaultPath)
});
});
ipcMain.handle('openFileDialog', (e, filters) => {
return dialog.showOpenDialog({
properties: ['openFile'],
filters
});
});
ipcMain.handle('appRestart', async () => {
log.log('Restarting app');
if (watcher) {
log.log('Stopping listener');
await watcher.stop();
watcher = null;
}
app.relaunch();
mainWindow.close();
});
ipcMain.handle('getAllDisplaysBounds', () => {
return screen.getAllDisplays().map(v => v.bounds);
});
ipcMain.handle('init-discord-rpc', () => {
discordRPC.initRPC();
});
ipcMain.handle('update-discord-rpc', (event, p) => {
discordRPC.update(p);
});
ipcMain.handle('reset-discord-rpc', () => {
discordRPC.reset();
});
ipcMain.handle('shutdown-discord-rpc', () => {
discordRPC.shutdownRPC();
});
const removeOriginHeader = (details, callback) => {
// Use a header to skip sending Origin on request.
const {
'X-Skip-Origin': xSkipOrigin,
Origin: _origin,
...requestHeaders
} = details.requestHeaders;
delete requestHeaders.Origin;
callback({ cancel: false, requestHeaders });
};
ipcMain.handle('download-optedout-mod', async (e, { url, filePath }) => {
let win = new BrowserWindow();
await win.webContents.session.clearCache();
// await win.webContents.session.clearStorageData();
win.webContents.session.webRequest.onBeforeSendHeaders(removeOriginHeader);
const mainWindowListener = () => {
if (win) {
win.removeAllListeners();
win.close();
win = null;
}
};
win.once('close', () => {
win = null;
});
let cleanupFn = null;
win.once('closed', () => {
mainWindow.webContents.send('opted-out-window-closed-unexpected');
if (cleanupFn) cleanupFn(`Window has been closed unexpectedly`);
mainWindow.removeListener('closed', mainWindowListener);
});
mainWindow.on('closed', mainWindowListener);
if (!win) return;
try {
// eslint-disable-next-line no-loop-func
await new Promise((resolve, reject) => {
win.webContents.session.webRequest.onCompleted(
{ urls: [url] },
details => {
if (details.statusCode === 404) {
mainWindow.webContents.send('opted-out-download-mod-status', {
error: new Error('Error 404, Mod page not found')
});
reject();
}
}
);
win.loadURL(url, { userAgent });
cleanupFn = async err => {
reject(new Error(err));
// eslint-disable-next-line promise/param-names
await new Promise(r => setTimeout(r, 50));
mainWindowListener();
};
const timer = setTimeout(
() => cleanupFn(`Download for ${url} timed out`),
60 * 1000 * 10
);
win.webContents.session.once('will-download', (_, item) => {
item.setSavePath(filePath);
item.once('updated', () => {
clearTimeout(timer);
});
item.once('done', (event, state) => {
if (state === 'completed') {
resolve();
} else {
reject(new Error(`Download for ${url} failed`));
}
});
});
});
if (mainWindow?.webContents) {
// send success event to front end
mainWindow.webContents.send('opted-out-download-mod-status', {
error: false
});
}
} catch (err) {
if (mainWindow?.webContents && win) {
// send error event to front end
mainWindow.webContents.send('opted-out-download-mod-status', {
error: err
});
}
}
if (win) {
win.removeAllListeners();
win.close();
win.once('closed', () => {
win = null;
});
}
});
ipcMain.handle('download-optedout-mods', async (e, { mods, instancePath }) => {
let win = new BrowserWindow();
await win.webContents.session.clearCache();
// await win.webContents.session.clearStorageData();
win.webContents.session.webRequest.onBeforeSendHeaders(removeOriginHeader);
const mainWindowListener = () => {
if (win) {
win.removeAllListeners();
win.close();
win = null;
}
};
win.once('close', () => {
win = null;
});
let cleanupFn = null;
win.once('closed', () => {
mainWindow.webContents.send('opted-out-window-closed-unexpected');
if (cleanupFn) cleanupFn(`Window has been closed unexpectedly`);
mainWindow.removeListener('closed', mainWindowListener);
});
mainWindow.on('closed', mainWindowListener);
for (const mod of mods) {
if (!win) return;
const { modManifest, addon } = mod;
try {
// eslint-disable-next-line no-loop-func
await new Promise((resolve, reject) => {
const urlDownloadPage = `${addon.links.websiteUrl}/download/${modManifest.id}`;
win.webContents.session.webRequest.onCompleted(
{ urls: [urlDownloadPage] },
details => {
if (details.statusCode === 404) {
resolve();
mainWindow.webContents.send('opted-out-download-mod-status', {
modId: modManifest.id,
error: false,
warning: true
});
} else if (details.statusCode > 400) {
/**
* Check for Cloudflare blocking automated downloads.
*
* Sometimes, Cloudflare prevents the internal browser from navigating to the
* Curseforge mod download page and starting the download. The HTTP status code
* it returns is (generally) either 403 or 503. The code below retrieves the
* HTML of the page returned to the browser and checks for the title and some
* content on the page to determine if the returned page is Cloudflare.
* Unfortunately using the `webContents.getTitle()` returns an empty string.
*/
details.webContents
.executeJavaScript(
`
function getHTML () {
return new Promise((resolve, reject) => { resolve(document.documentElement.innerHTML); });
}
getHTML();
`
)
.then(content => {
const isCloudflare =
content.includes('Just a moment...') &&
content.includes(
'needs to review the security of your connection before proceeding.'
);
if (isCloudflare) {
resolve();
mainWindow.webContents.send(
'opted-out-download-mod-status',
{
modId: modManifest.id,
error: false,
warning: true,
cloudflareBlock: true
}
);
}
return null;
})
.catch(() => {
// no-op
});
}
}
);
win.loadURL(urlDownloadPage, { userAgent });
cleanupFn = async err => {
reject(new Error(err));
// eslint-disable-next-line promise/param-names
await new Promise(r => setTimeout(r, 50));
mainWindowListener();
};
const timer = setTimeout(
() => cleanupFn(`Download for ${modManifest.fileName} timed out`),
60 * 1000 * 10
);
const isResourcePack = addon.classId === 12;
const modDestFile = path.join(
instancePath,
isResourcePack ? 'resourcepacks' : 'mods'
);
win.webContents.session.once('will-download', (_, item) => {
item.setSavePath(path.join(modDestFile, modManifest.fileName));
item.once('updated', () => {
clearTimeout(timer);
});
item.once('done', (event, state) => {
if (state === 'completed') {
resolve();
} else {
reject(new Error(`Download for ${modManifest.fileName} failed`));
}
});
});
});
if (mainWindow?.webContents) {
// send success event to front end
mainWindow.webContents.send('opted-out-download-mod-status', {
modId: modManifest.id,
error: false
});
}
} catch (err) {
if (mainWindow?.webContents && win) {
// send error event to front end
mainWindow.webContents.send('opted-out-download-mod-status', {
modId: modManifest.id,
error: err
});
}
}
}
if (win) {
win.removeAllListeners();
win.close();
win.once('closed', () => {
win = null;
});
}
});
ipcMain.handle('start-listener', async (e, dirPath) => {
try {
log.log('Trying to start listener');
if (watcher) {
await watcher.stop();
watcher = null;
}
watcher = await nsfw(dirPath, events => {
log.log(`Detected ${events.length} events from listener`);
mainWindow.webContents.send('listener-events', events);
});
log.log('Started listener');
return watcher.start();
} catch (err) {
log.error(err);
return Promise.reject(err);
}
});
ipcMain.handle('stop-listener', async () => {
if (watcher) {
log.log('Stopping listener');
await watcher.stop();
watcher = null;
}
});
ipcMain.handle('calculateMurmur2FromPath', async (e, filePath) => {
const res = await napi.computePathMurmur(filePath);
return res.toString();
});
// AutoUpdater
if (process.env.REACT_APP_RELEASE_TYPE === 'setup') {
autoUpdater.autoDownload = false;
autoUpdater.allowDowngrade =
!allowUnstableReleases && app.getVersion().includes('beta');
autoUpdater.allowPrerelease = allowUnstableReleases;
autoUpdater.setFeedURL({
owner: 'gorilla-devs',
repo: 'GDLauncher',
provider: 'github'
});
autoUpdater.on('update-available', () => {
autoUpdater.downloadUpdate();
});
autoUpdater.on('update-downloaded', () => {
mainWindow.webContents.send('updateAvailable');
});
ipcMain.handle('checkForUpdates', () => {
autoUpdater.checkForUpdates();
});
}
ipcMain.handle('installUpdateAndQuitOrRestart', async (e, quitAfterInstall) => {
const tempFolder = path.join(
path.dirname(app.getPath('exe')),
'data',
'temp'
);
if (process.env.REACT_APP_RELEASE_TYPE === 'setup') {
autoUpdater.quitAndInstall(true, !quitAfterInstall);
} else {
const updaterVbs = 'updater.vbs';
const updaterBat = 'updateLauncher.bat';
await fs.writeFile(
path.join(tempFolder, updaterBat),
`ping 127.0.0.1 -n 1 > nul & robocopy "${path.join(
tempFolder,
'update'
)}" "." /MOV /E${
quitAfterInstall ? '' : ` & start "" "${app.getPath('exe')}"`
}
DEL "${path.join(tempFolder, updaterVbs)}"
DEL "%~f0"
`
);
await fs.writeFile(
path.join(tempFolder, updaterVbs),
`Set WshShell = CreateObject("WScript.Shell")
WshShell.Run chr(34) & "${path.join(
tempFolder,
updaterBat
)}" & Chr(34), 0
Set WshShell = Nothing
`
);
const updateSpawn = spawn(path.join(tempFolder, updaterVbs), {
cwd: path.dirname(app.getPath('exe')),
detached: true,
shell: true,
stdio: [
'ignore' /* stdin */,
'ignore' /* stdout */,
'ignore' /* stderr */
]
});
updateSpawn.unref();
mainWindow.close();
}
});