Merge pull request #504 from presenton/refactor/loading-screen

refactor: makes launch screen light theme
This commit is contained in:
Saurav Niraula 2026-04-09 18:52:58 +05:45 committed by GitHub
commit 5a6bfdb1be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 304 additions and 85 deletions

View file

@ -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();

Binary file not shown.

Binary file not shown.

View file

@ -1,32 +1,212 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Presenton</title>
<link rel="stylesheet" href="../assets/css/tailwind.css">
<style>
.loading-circle {
width: 40px;
height: 40px;
border: 4px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: white;
animation: spin 1s ease-in-out infinite;
@font-face {
font-family: "Unbounded";
src: url("../assets/fonts/Unbounded-Medium.ttf") format("truetype");
font-weight: 500;
font-style: normal;
}
@keyframes spin {
to {
transform: rotate(360deg);
@font-face {
font-family: "Syne";
src: url("../assets/fonts/Syne-Regular.ttf") format("truetype");
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: "Syne";
src: url("../assets/fonts/Syne-Medium.ttf") format("truetype");
font-weight: 500;
font-style: normal;
}
:root {
color-scheme: light;
--bg-top: #ffffff;
--bg-bottom: #f2f4ff;
--title: #2d3347;
--subtitle: #9298ae;
--hint: #7e86a0;
--track: rgba(255, 255, 255, 0.76);
--track-border: rgba(128, 120, 212, 0.12);
--fill-start: #735cf7;
--fill-mid: #6e67f7;
--fill-end: #6b8eff;
--progress: 0.24;
}
* {
box-sizing: border-box;
}
html,
body {
width: 100%;
height: 100%;
min-height: 100%;
margin: 0;
}
body {
display: grid;
place-items: center;
width: 100vw;
min-height: 100vh;
overflow: hidden;
font-family: "Syne", "Segoe UI", system-ui, sans-serif;
color: var(--title);
background:
radial-gradient(circle at 20% 18%, rgba(127, 104, 255, 0.16), transparent 28%),
radial-gradient(circle at 82% 84%, rgba(108, 144, 255, 0.12), transparent 30%),
linear-gradient(135deg, var(--bg-top) 0%, #f8f9ff 42%, var(--bg-bottom) 100%);
}
body::before {
content: "";
position: fixed;
inset: -20%;
pointer-events: none;
background:
radial-gradient(circle at center, rgba(255, 255, 255, 0.55), transparent 45%),
radial-gradient(circle at 30% 20%, rgba(145, 124, 255, 0.14), transparent 24%);
filter: blur(90px);
opacity: 0.9;
}
.launch-screen {
position: relative;
z-index: 1;
width: min(100%, 540px);
padding: 24px 28px 32px;
text-align: center;
}
.logo {
width: min(54vw, 300px);
margin: 0 auto 36px;
}
.title {
margin: 0;
font-size: clamp(1.3rem, 2.05vw, 1.72rem);
line-height: 1.12;
letter-spacing: -0.045em;
font-family: "Unbounded", "Syne", system-ui, sans-serif;
font-weight: 500;
white-space: nowrap;
}
.subtitle {
margin: 14px 0 0;
font-size: clamp(0.98rem, 1.8vw, 1.04rem);
line-height: 1.5;
color: var(--subtitle);
font-weight: 500;
font-family: "Syne", "Segoe UI", system-ui, sans-serif;
}
.progress-track {
position: relative;
width: min(100%, 360px);
height: 12px;
margin: 40px auto 0;
overflow: hidden;
border-radius: 999px;
background: var(--track);
box-shadow:
inset 0 0 0 1px var(--track-border),
0 14px 32px rgba(112, 105, 186, 0.12);
}
.progress-fill {
position: absolute;
inset: 0 auto 0 0;
width: 100%;
overflow: hidden;
border-radius: inherit;
background: linear-gradient(90deg, var(--fill-start) 0%, var(--fill-mid) 56%, var(--fill-end) 100%);
box-shadow: 0 6px 18px rgba(112, 97, 242, 0.28);
transform: scaleX(var(--progress));
transform-origin: left center;
transition: transform 380ms ease;
}
.progress-fill::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(90deg,
transparent 0%,
rgba(255, 255, 255, 0.12) 36%,
rgba(255, 255, 255, 0.58) 50%,
rgba(255, 255, 255, 0.12) 64%,
transparent 100%);
animation: shimmer 1.85s linear infinite;
}
.hint {
min-height: 1.5rem;
margin: 16px 0 0;
font-size: 0.88rem;
line-height: 1.5;
color: var(--hint);
letter-spacing: 0.01em;
font-family: "Syne", "Segoe UI", system-ui, sans-serif;
}
@keyframes shimmer {
0% {
transform: translateX(-125%);
}
100% {
transform: translateX(220%);
}
}
@media (max-width: 640px) {
.launch-screen {
width: min(100%, 440px);
padding: 16px 24px 24px;
}
.logo {
width: min(74vw, 260px);
margin-bottom: 30px;
}
.title {
font-size: clamp(1.35rem, 6.7vw, 1.8rem);
white-space: normal;
}
.progress-track {
width: min(100%, 320px);
margin-top: 34px;
}
}
</style>
</head>
<body class="bg-gray-900 text-white flex flex-col items-center justify-center h-screen">
<img src="../assets/images/presenton_logo.png" alt="Presenton Logo" class="h-20">
<p class="mt-16 text-lg">Just a moment...</p>
<div class="loading-circle mt-8"></div>
<body>
<main class="launch-screen" aria-live="polite">
<img src="../assets/images/presenton_logo.png" alt="Presenton" class="logo">
<h1 class="title">Launching Presenton...</h1>
<p class="subtitle" data-startup-subtitle>Please wait a moment</p>
<div class="progress-track" role="progressbar" aria-label="Launching Presenton" aria-valuemin="0"
aria-valuemax="100" aria-valuenow="24" data-startup-meter>
<div class="progress-fill" data-startup-progress></div>
</div>
<p class="hint" data-startup-hint>Preparing your workspace</p>
</main>
<script src="./script.js"></script>
</body>
</html>
</html>

View file

@ -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);
}
});
}
});
window.addEventListener("beforeunload", () => {
targetProgress = 1;
applyProgress(1);
});
});