Merge pull request #504 from presenton/refactor/loading-screen
refactor: makes launch screen light theme
This commit is contained in:
commit
5a6bfdb1be
7 changed files with 304 additions and 85 deletions
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
BIN
electron/resources/ui/assets/fonts/Syne-Medium.ttf
Normal file
BIN
electron/resources/ui/assets/fonts/Syne-Medium.ttf
Normal file
Binary file not shown.
BIN
electron/resources/ui/assets/fonts/Syne-Regular.ttf
Normal file
BIN
electron/resources/ui/assets/fonts/Syne-Regular.ttf
Normal file
Binary file not shown.
BIN
electron/resources/ui/assets/fonts/Unbounded-Medium.ttf
Normal file
BIN
electron/resources/ui/assets/fonts/Unbounded-Medium.ttf
Normal file
Binary file not shown.
BIN
electron/resources/ui/assets/fonts/Unbounded-SemiBold.ttf
Normal file
BIN
electron/resources/ui/assets/fonts/Unbounded-SemiBold.ttf
Normal file
Binary file not shown.
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue