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 - - - Presenton Logo -

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); + }); +});