117 lines
5.3 KiB
JavaScript
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 };
|