diff --git a/electron/app/main.ts b/electron/app/main.ts
index 15d85348..f91f517a 100644
--- a/electron/app/main.ts
+++ b/electron/app/main.ts
@@ -65,7 +65,8 @@ const createWindow = () => {
win = new BrowserWindow({
width: 1280,
height: 720,
- show: false, // Shown after LibreOffice check so "Skip" doesn't quit the app
+ show: false, // Reveal once the launch screen has painted to avoid a blank flash.
+ backgroundColor: "#f3f5ff",
icon: path.join(baseDir, "resources/ui/assets/images/presenton_short_filled.png"),
webPreferences: {
webSecurity: false,
@@ -82,6 +83,14 @@ const createWindow = () => {
}
return { action: "allow" };
});
+
+ win.once("ready-to-show", () => {
+ if (!win || win.isDestroyed()) {
+ return;
+ }
+ win.show();
+ win.focus();
+ });
};
async function startServers(fastApiPort: number, nextjsPort: number) {
@@ -233,7 +242,7 @@ app.whenReady().then(async () => {
startupStatus.puppeteer = chromeOk ? "installed" : "missing";
startupStatus.imagemagick = imageMagickOk ? "installed" : "missing";
- // Show and focus main window
+ // Ensure the launch screen stays visible and focused during the server boot.
win?.show();
win?.focus();
diff --git a/electron/resources/ui/assets/fonts/Syne-Medium.ttf b/electron/resources/ui/assets/fonts/Syne-Medium.ttf
new file mode 100644
index 00000000..793e967f
Binary files /dev/null and b/electron/resources/ui/assets/fonts/Syne-Medium.ttf differ
diff --git a/electron/resources/ui/assets/fonts/Syne-Regular.ttf b/electron/resources/ui/assets/fonts/Syne-Regular.ttf
new file mode 100644
index 00000000..3ef725cd
Binary files /dev/null and b/electron/resources/ui/assets/fonts/Syne-Regular.ttf differ
diff --git a/electron/resources/ui/assets/fonts/Unbounded-Medium.ttf b/electron/resources/ui/assets/fonts/Unbounded-Medium.ttf
new file mode 100644
index 00000000..aa093453
Binary files /dev/null and b/electron/resources/ui/assets/fonts/Unbounded-Medium.ttf differ
diff --git a/electron/resources/ui/assets/fonts/Unbounded-SemiBold.ttf b/electron/resources/ui/assets/fonts/Unbounded-SemiBold.ttf
new file mode 100644
index 00000000..683397a3
Binary files /dev/null and b/electron/resources/ui/assets/fonts/Unbounded-SemiBold.ttf differ
diff --git a/electron/resources/ui/homepage/index.html b/electron/resources/ui/homepage/index.html
index 495af613..7922ce57 100644
--- a/electron/resources/ui/homepage/index.html
+++ b/electron/resources/ui/homepage/index.html
@@ -1,32 +1,212 @@
-
+
+
+
Presenton
-
-
-
- Just a moment...
-
+
+
+
+ Launching Presenton...
+ Please wait a moment
+
+ Preparing your workspace
+
+
-
\ No newline at end of file
+
diff --git a/electron/resources/ui/homepage/script.js b/electron/resources/ui/homepage/script.js
index a047db7e..c0ed87e1 100644
--- a/electron/resources/ui/homepage/script.js
+++ b/electron/resources/ui/homepage/script.js
@@ -1,91 +1,121 @@
window.addEventListener("DOMContentLoaded", () => {
- const statusMap = {
- checking: "Checking...",
- installed: "Installed",
- missing: "Missing",
- installing: "Installing...",
- downloading: "Downloading...",
- downloaded: "Downloaded",
- skipped: "Skipped",
- failed: "Failed",
- };
- const labelMap = {
- libreoffice: "LibreOffice",
- puppeteer: "Chromium",
- imagemagick: "ImageMagick",
- };
+ const subtitleEl = document.querySelector("[data-startup-subtitle]");
+ const hintEl = document.querySelector("[data-startup-hint]");
+ const progressEl = document.querySelector("[data-startup-progress]");
+ const meterEl = document.querySelector("[data-startup-meter]");
- const dependenciesEl = document.getElementById("status-dependencies");
- const dependenciesIcon = document.getElementById("icon-dependencies");
- const dependenciesTooltip = document.getElementById("dependencies-tooltip");
- const libreofficeTooltip = document.getElementById("tooltip-libreoffice");
- const puppeteerTooltip = document.getElementById("tooltip-puppeteer");
- const libreofficeLabel = document.getElementById("tooltip-label-libreoffice");
- const puppeteerLabel = document.getElementById("tooltip-label-puppeteer");
const currentStatus = {
libreoffice: "checking",
puppeteer: "checking",
imagemagick: "checking",
};
- function setStatus(name, status) {
- if (currentStatus[name] !== undefined) {
- currentStatus[name] = status;
+ const completeStates = new Set(["installed", "downloaded", "skipped"]);
+ const errorStates = new Set(["missing", "failed"]);
+
+ let visualProgress = 0.24;
+ let targetProgress = 0.58;
+
+ function applyProgress(value) {
+ const clampedValue = Math.max(0.18, Math.min(value, 1));
+ if (progressEl) {
+ progressEl.style.setProperty("--progress", String(clampedValue));
+ progressEl.style.transform = `scaleX(${clampedValue})`;
}
- if (dependenciesEl) dependenciesEl.textContent = "Dependencies";
-
- const statuses = Object.values(currentStatus);
- const hasError = statuses.some((s) => s === "missing" || s === "failed");
- const isBusy = statuses.some((s) => s === "checking" || s === "installing" || s === "downloading");
- const isDone = statuses.every((s) => s === "installed" || s === "downloaded" || s === "skipped");
-
- let iconClass = "loading";
- let iconText = "";
- if (hasError) {
- iconClass = "error";
- iconText = "×";
- } else if (isDone && !isBusy) {
- iconClass = "ok";
- iconText = "✓";
- } else {
- iconClass = "loading";
- iconText = "";
+ if (meterEl) {
+ meterEl.setAttribute("aria-valuenow", String(Math.round(clampedValue * 100)));
}
-
- if (dependenciesIcon) {
- dependenciesIcon.className = `status-icon ${iconClass}`;
- }
-
- const libreofficeStatus = currentStatus.libreoffice;
- const puppeteerStatus = currentStatus.puppeteer;
- const libreofficeText = statusMap[libreofficeStatus] || libreofficeStatus;
- const puppeteerText = statusMap[puppeteerStatus] || puppeteerStatus;
-
- const toDotClass = (value) => {
- if (value === "missing" || value === "failed") return "error";
- if (value === "installed" || value === "downloaded" || value === "skipped") return "ok";
- return "loading";
- };
-
- if (libreofficeTooltip) libreofficeTooltip.textContent = libreofficeText;
- if (puppeteerTooltip) puppeteerTooltip.textContent = puppeteerText;
- if (libreofficeLabel) libreofficeLabel.className = `status-tooltip-label ${toDotClass(libreofficeStatus)}`;
- if (puppeteerLabel) puppeteerLabel.className = `status-tooltip-label ${toDotClass(puppeteerStatus)}`;
- if (dependenciesTooltip) dependenciesTooltip.setAttribute("aria-live", "polite");
}
+ function updateStateCopy() {
+ const statuses = Object.values(currentStatus);
+ const hasError = statuses.some((status) => errorStates.has(status));
+ const isInstalling = statuses.some(
+ (status) => status === "installing" || status === "downloading"
+ );
+ const isChecking = statuses.some((status) => status === "checking");
+ const isReady =
+ statuses.length > 0 &&
+ statuses.every((status) => completeStates.has(status));
+
+ if (hasError) {
+ targetProgress = Math.max(targetProgress, 0.54);
+ if (subtitleEl) subtitleEl.textContent = "Please wait a moment";
+ if (hintEl) hintEl.textContent = "Setup required before launch";
+ return;
+ }
+
+ if (isInstalling) {
+ targetProgress = Math.max(targetProgress, 0.72);
+ if (subtitleEl) subtitleEl.textContent = "Please wait a moment";
+ if (hintEl) hintEl.textContent = "Installing required components";
+ return;
+ }
+
+ if (isChecking) {
+ targetProgress = Math.max(targetProgress, 0.58);
+ if (subtitleEl) subtitleEl.textContent = "Please wait a moment";
+ if (hintEl) hintEl.textContent = "Checking required components";
+ return;
+ }
+
+ if (isReady) {
+ targetProgress = 0.88;
+ if (subtitleEl) subtitleEl.textContent = "Please wait a moment";
+ if (hintEl) hintEl.textContent = "Opening your workspace";
+ return;
+ }
+
+ targetProgress = Math.max(targetProgress, 0.68);
+ if (subtitleEl) subtitleEl.textContent = "Please wait a moment";
+ if (hintEl) hintEl.textContent = "Preparing your workspace";
+ }
+
+ function setStatus(name, status) {
+ if (!(name in currentStatus)) {
+ return;
+ }
+
+ currentStatus[name] = status;
+ updateStateCopy();
+ }
+
+ function animateProgress() {
+ visualProgress += (targetProgress - visualProgress) * 0.08;
+ applyProgress(visualProgress);
+ window.requestAnimationFrame(animateProgress);
+ }
+
+ updateStateCopy();
+ applyProgress(visualProgress);
+ animateProgress();
+
+ window.setInterval(() => {
+ if (targetProgress < 0.82) {
+ targetProgress = Math.min(0.82, targetProgress + 0.03);
+ }
+ }, 1200);
+
if (window.electron?.onStartupStatus) {
window.electron.onStartupStatus((payload) => {
if (!payload) return;
setStatus(payload.name, payload.status);
});
}
+
if (window.electron?.getStartupStatus) {
window.electron.getStartupStatus().then((statusMap) => {
if (!statusMap) return;
if (statusMap.libreoffice) setStatus("libreoffice", statusMap.libreoffice);
if (statusMap.puppeteer) setStatus("puppeteer", statusMap.puppeteer);
- if (statusMap.imagemagick) setStatus("imagemagick", statusMap.imagemagick);
+ if (statusMap.imagemagick) {
+ setStatus("imagemagick", statusMap.imagemagick);
+ }
});
}
-});
\ No newline at end of file
+
+ window.addEventListener("beforeunload", () => {
+ targetProgress = 1;
+ applyProgress(1);
+ });
+});