fbad749f0c
Build check / build (push) Has been cancelled
Squashed sync from upstream. Highlights: - Updater no longer reports already-applied deletes as a pending update on every launch (guard the del branch on the target still existing) - Derive the packaged CSP image origin from the configured server URL - Forum Announcements panel + News tab; hardware-aware farClip recommendation; parchment UI; localization and tweak updates - Addon source refresh; schema and mod-state fixes Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
198 lines
5.0 KiB
TypeScript
198 lines
5.0 KiB
TypeScript
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 { 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.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 () => {
|
|
Preferences.data = await Preferences.load();
|
|
|
|
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();
|
|
});
|
|
|
|
app.on('window-all-closed', () => {
|
|
app.quit();
|
|
});
|
|
}
|