1011 lines
26 KiB
JavaScript
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();
|
|
}
|
|
});
|