forked from OctoWoW/OctoLauncher
OctoLauncher 1.3.1
Manifest-based CDN updater and mod manager for the OctoWoW 1.12.1 client: launcher-owned realmlist, torrent-backed content sync with bundled aria2c, antivirus and Defender exclusion handling, hardware-aware render distance, optional client tweaks and mods, and the in-launcher news feed.
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
import { join } from 'path';
|
||||
|
||||
import { app, shell, session, BrowserWindow, screen } from 'electron';
|
||||
import { electronApp, optimizer, is } from '@electron-toolkit/utils';
|
||||
import { createIPCHandler } from 'electron-trpc/main';
|
||||
import Logger from 'electron-log/main';
|
||||
|
||||
import icon from '~build/icon.png?asset';
|
||||
import { PreferencesSchema } from '~common/schemas';
|
||||
|
||||
import { appRouter } from './api/root';
|
||||
import Preferences from './modules/preferences';
|
||||
import Updater from './modules/updater';
|
||||
import Addons from './modules/addons';
|
||||
import Mods from './modules/mods';
|
||||
import { initSelfUpdater } from './modules/selfUpdater';
|
||||
import {
|
||||
detectHardware,
|
||||
recommendFarClip,
|
||||
HARDWARE_SCHEMA_VERSION
|
||||
} from './modules/hardware';
|
||||
|
||||
Logger.initialize();
|
||||
Logger.errorHandler.startCatching();
|
||||
Logger.transports.ipc.level = false;
|
||||
Logger.info('Launcher starting...');
|
||||
|
||||
app.disableHardwareAcceleration();
|
||||
|
||||
export let mainWindow: BrowserWindow | null = null;
|
||||
|
||||
const isOnScreen = (
|
||||
pos?: {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
} | null
|
||||
) => {
|
||||
if (!pos) return false;
|
||||
return screen.getAllDisplays().some(d => {
|
||||
const a = d.workArea;
|
||||
return (
|
||||
pos.x < a.x + a.width &&
|
||||
pos.x + pos.width > a.x &&
|
||||
pos.y < a.y + a.height &&
|
||||
pos.y + pos.height > a.y
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const createWindow = async () => {
|
||||
const saved =
|
||||
Preferences.data.rememberPosition &&
|
||||
isOnScreen(Preferences.data.windowPosition)
|
||||
? Preferences.data.windowPosition
|
||||
: undefined;
|
||||
const position = saved ?? { width: 1000, height: 700 };
|
||||
|
||||
mainWindow = new BrowserWindow({
|
||||
...position,
|
||||
minWidth: 1000,
|
||||
minHeight: 700,
|
||||
icon,
|
||||
frame: false,
|
||||
maximizable: false,
|
||||
fullscreenable: false,
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
contextIsolation: true,
|
||||
sandbox: false,
|
||||
devTools: true
|
||||
}
|
||||
});
|
||||
|
||||
mainWindow.webContents.on('render-process-gone', (_e, details) => {
|
||||
Logger.error('Renderer process gone:', details);
|
||||
});
|
||||
mainWindow.webContents.on('unresponsive', () => {
|
||||
Logger.error('Renderer unresponsive');
|
||||
});
|
||||
|
||||
mainWindow.webContents.on(
|
||||
'console-message',
|
||||
(_e, level, message, line, sourceId) => {
|
||||
const lvl = level === 3 ? 'error' : level === 2 ? 'warn' : 'info';
|
||||
Logger[lvl](`[renderer:${lvl}] ${message} (${sourceId}:${line})`);
|
||||
}
|
||||
);
|
||||
|
||||
mainWindow.webContents.on('before-input-event', (_e, input) => {
|
||||
if (input.type !== 'keyDown') return;
|
||||
if (input.key === 'F12') {
|
||||
mainWindow?.webContents.toggleDevTools();
|
||||
return;
|
||||
}
|
||||
if ((input.control || input.meta) && input.key.toLowerCase() === 'c')
|
||||
mainWindow?.webContents.copy();
|
||||
});
|
||||
|
||||
createIPCHandler({ router: appRouter, windows: [mainWindow] });
|
||||
|
||||
mainWindow.on('ready-to-show', () => {
|
||||
mainWindow?.show();
|
||||
});
|
||||
mainWindow.webContents.setWindowOpenHandler(details => {
|
||||
shell.openExternal(details.url);
|
||||
return { action: 'deny' };
|
||||
});
|
||||
mainWindow.on('close', () => {
|
||||
if (!mainWindow) return;
|
||||
const [x = 0, y = 0] = mainWindow.getPosition();
|
||||
const [width = 0, height = 0] = mainWindow.getSize();
|
||||
Preferences.data = { windowPosition: { x, y, width, height } };
|
||||
});
|
||||
|
||||
if (is.dev && process.env.ELECTRON_RENDERER_URL) {
|
||||
mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL);
|
||||
} else {
|
||||
mainWindow.loadFile(join(__dirname, '../renderer/index.html'));
|
||||
}
|
||||
};
|
||||
|
||||
const gotSingleInstanceLock = is.dev || app.requestSingleInstanceLock();
|
||||
|
||||
if (!gotSingleInstanceLock) {
|
||||
app.quit();
|
||||
} else {
|
||||
app.on('second-instance', () => {
|
||||
if (!mainWindow) return;
|
||||
if (mainWindow.isMinimized()) mainWindow.restore();
|
||||
if (!mainWindow.isVisible()) mainWindow.show();
|
||||
mainWindow.focus();
|
||||
});
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
// defaults on failure so createWindow() below still runs
|
||||
try {
|
||||
Preferences.data = await Preferences.load();
|
||||
} catch (e) {
|
||||
Logger.error('Preferences.load() failed; starting on defaults', e);
|
||||
Preferences.data = PreferencesSchema.parse({});
|
||||
}
|
||||
|
||||
Addons.verify();
|
||||
Updater.verify();
|
||||
Mods.verify();
|
||||
initSelfUpdater();
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
let hardware = Preferences.data.hardware;
|
||||
if (!hardware || hardware.schemaVersion < HARDWARE_SCHEMA_VERSION) {
|
||||
hardware = await detectHardware();
|
||||
Preferences.data = { hardware };
|
||||
}
|
||||
const rec = recommendFarClip(hardware ?? null);
|
||||
if (
|
||||
Preferences.data.farClipUserSet !== true &&
|
||||
Preferences.data.config.farClip !== rec
|
||||
)
|
||||
Preferences.data = {
|
||||
config: { ...Preferences.data.config, farClip: rec }
|
||||
};
|
||||
} catch (e) {
|
||||
Logger.error('Hardware detection / farClip recommendation failed', e);
|
||||
}
|
||||
})();
|
||||
|
||||
electronApp.setAppUserModelId('st.octowow.launcher');
|
||||
|
||||
if (app.isPackaged) {
|
||||
const serverOrigin = new URL(
|
||||
import.meta.env.MAIN_VITE_SERVER_URL || 'https://octowow.st'
|
||||
).origin;
|
||||
session.defaultSession.webRequest.onHeadersReceived((details, cb) => {
|
||||
cb({
|
||||
responseHeaders: {
|
||||
...details.responseHeaders,
|
||||
'Content-Security-Policy': [
|
||||
[
|
||||
"default-src 'self'",
|
||||
"script-src 'self'",
|
||||
"style-src 'self' 'unsafe-inline'",
|
||||
`img-src 'self' data: https://octowow.st https://forum.octowow.st ${serverOrigin}`,
|
||||
"font-src 'self' data:",
|
||||
"connect-src 'self'"
|
||||
].join('; ')
|
||||
]
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
app.on('browser-window-created', (_, window) => {
|
||||
optimizer.watchWindowShortcuts(window);
|
||||
});
|
||||
|
||||
await createWindow();
|
||||
});
|
||||
|
||||
let settingsFlushed = false;
|
||||
app.on('before-quit', event => {
|
||||
if (settingsFlushed) return;
|
||||
settingsFlushed = true;
|
||||
event.preventDefault();
|
||||
Promise.race([Preferences.save(), new Promise(r => setTimeout(r, 3000))])
|
||||
.catch(e => Logger.error('Failed to flush settings before quit', e))
|
||||
.finally(() => app.quit());
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
app.quit();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user