Files
Git-Manager-Gui/src/features/splash.js
T
2026-10-22 09:13:21 +00:00

117 lines
5.3 KiB
JavaScript

// Startanimation des Hauptfensters (abschaltbar unter Einstellungen → Darstellung).
//
// Ablauf: Ein randloses, durchsichtiges Fenster liegt genau dort, wo das Programm erscheinen wird,
// und zeigt das sich drehende Logo samt Schriftzug. Ist die Oberfläche geladen, wird das noch
// unsichtbare Hauptfenster abfotografiert und dieses Bild im Startfenster von der Mitte nach außen
// aufgezogen. Am Ende übernimmt das echte Fenster an exakt derselben Stelle, so wirkt es, als baue
// sich das Programm auf.
//
// Das Startfenster wird VOR dem Hauptfenster angelegt (begin), damit das Logo so früh wie möglich
// erscheint. Danach lädt das Hauptfenster, und play() übernimmt den Rest.
const { BrowserWindow } = require('electron');
const MIN_SPIN_MS = 1600; // so lange dreht sich das Logo mindestens
const MAX_WAIT_MS = 12000; // spätestens dann wird das Hauptfenster gezeigt
const MAX_GROW_MS = 2500; // der Aufbau darf nie länger dauern, sonst bliebe das Fenster unsichtbar
const MAX_PAINT_WAIT_MS = 1500;
const IGNORE_MOUSE_DELAY_MS = 600; // Mausereignisse erst nach dem ersten Zeichnen ignorieren, sonst wird das Fenster weiß
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
// Wie lange noch gewartet werden muss, damit das Logo mindestens minMs lang zu sehen war.
function remainingMs(shownAt, now, minMs) {
if (!shownAt) return 0;
return Math.max(0, minMs - (now - shownAt));
}
// Position und Größe des Hauptfensters, zentriert im Arbeitsbereich. Wird für Start- und Hauptfenster
// gleich berechnet, damit beide exakt übereinander liegen.
function centeredBounds(workArea, width, height) {
const w = Math.min(width, workArea.width);
const h = Math.min(height, workArea.height);
return {
x: Math.round(workArea.x + (workArea.width - w) / 2),
y: Math.round(workArea.y + (workArea.height - h) / 2),
width: w,
height: h
};
}
// Legt das Startfenster an und zeigt es, sobald es geladen ist.
// Liefert { window, shownAt }: shownAt ist ein Promise auf den Zeitpunkt, an dem das Logo sichtbar wurde.
function begin({ htmlPath, bounds }) {
const window = new BrowserWindow({
x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height,
frame: false, transparent: true, resizable: false, movable: false,
minimizable: false, maximizable: false, fullscreenable: false,
skipTaskbar: true, hasShadow: false, focusable: false, show: false,
backgroundColor: '#00000000', title: 'Git Manager',
webPreferences: { contextIsolation: true, nodeIntegration: false, sandbox: true, spellcheck: false }
});
const shownAt = window.loadFile(htmlPath).then(() => {
if (window.isDestroyed()) return 0;
window.showInactive();
// Das Startfenster soll keine Mausklicks schlucken. WICHTIG: Der Aufruf muss NACH dem Zeigen und etwas
// verzögert erfolgen. Davor aufgerufen wird das durchsichtige Fenster unter Windows weiß (getestet).
setTimeout(() => { if (!window.isDestroyed()) window.setIgnoreMouseEvents(true); }, IGNORE_MOUSE_DELAY_MS);
return Date.now();
}).catch(() => 0);
return { window, shownAt };
}
// Wartet, bis die Oberfläche gezeichnet ist (Liste da oder nach höchstens MAX_PAINT_WAIT_MS),
// sonst wüchse ein halbleeres Fenster auf.
async function waitForPaintedUi(webContents) {
const check = (expression) => webContents.executeJavaScript(expression, true).catch(() => true);
const until = Date.now() + MAX_PAINT_WAIT_MS;
while (Date.now() < until) {
if (await check("document.readyState === 'complete' && (document.querySelectorAll('#explorerGrid > *').length > 0)")) break;
await wait(100);
}
await wait(200);
}
/**
* Spielt die Startanimation und zeigt am Ende das Hauptfenster.
* @param {BrowserWindow} mainWin Hauptfenster, noch nicht angezeigt, an denselben Bounds wie das Startfenster
* @param {Promise<void>} uiReady wird erfüllt, sobald die Oberfläche geladen ist
* @param {{window: BrowserWindow, shownAt: Promise<number>}} startHandle Ergebnis von begin()
*/
async function play(mainWin, uiReady, startHandle) {
const start = startHandle.window;
const showMain = () => {
if (mainWin.isDestroyed()) return;
mainWin.setOpacity(1);
mainWin.setIgnoreMouseEvents(false);
if (!mainWin.isVisible()) mainWin.show();
mainWin.focus();
};
try {
const shownAt = await startHandle.shownAt;
await Promise.race([uiReady, wait(MAX_WAIT_MS)]);
await wait(remainingMs(shownAt, Date.now(), MIN_SPIN_MS));
if (mainWin.isDestroyed()) return;
// Unsichtbar zeigen, damit Chromium die Oberfläche zeichnet, dann abfotografieren
mainWin.setOpacity(0);
mainWin.setIgnoreMouseEvents(true); // unsichtbar: Klicks gehen nicht versehentlich hinein
mainWin.show();
await waitForPaintedUi(mainWin.webContents);
if (mainWin.isDestroyed()) return;
const image = await mainWin.webContents.capturePage();
if (!image.isEmpty() && !start.isDestroyed()) {
await Promise.race([start.webContents.executeJavaScript(`aufbauen(${JSON.stringify(image.toDataURL())})`), wait(MAX_GROW_MS)]);
}
} catch (_) {
// Die Animation ist nur Zierde: im Fehlerfall einfach das Hauptfenster zeigen
} finally {
showMain();
setTimeout(() => { if (!start.isDestroyed()) start.destroy(); }, 80);
}
}
module.exports = { begin, play, remainingMs, centeredBounds, MIN_SPIN_MS };