presenton/electron/resources/ui/setup-installer/index.html

449 lines
22 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Presenton Setup required</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0d0d14;
--surface: #16162a;
--surface2: #1e1e38;
--border: rgba(145, 52, 234, 0.25);
--grad-a: #9034EA;
--grad-b: #5146E5;
--text: #f0eeff;
--text-muted: #9b96c4;
--text-dim: #5e5a88;
--track: #1e1e38;
--log-bg: #0a0a12;
--log-border: rgba(145,52,234,0.15);
--radius: 14px;
--radius-sm: 8px;
}
html, body {
width: 100%; height: 100%;
background: var(--bg);
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
font-size: 14px;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
overflow: hidden;
user-select: none;
}
.shell { display: flex; flex-direction: column; height: 100vh; padding: 24px 32px 0; }
.logo-wrap { text-align: center; margin-bottom: 12px; flex-shrink: 0; }
.logo-wrap img { height: 36px; display: inline-block; }
.logo-fallback { font-size: 20px; font-weight: 700; background: linear-gradient(135deg, var(--grad-a), var(--grad-b)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
.step-badge {
font-size: 11px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase;
color: var(--text-dim); margin-bottom: 16px; text-align: center;
}
.card {
width: 100%; flex-shrink: 0; background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius); padding: 28px 28px 24px; display: flex; flex-direction: column;
align-items: center; gap: 16px; position: relative; overflow: hidden;
}
.card::before {
content: ''; position: absolute; inset: 0; border-radius: var(--radius);
background: radial-gradient(ellipse 60% 40% at 50% -10%, rgba(145,52,234,0.18) 0%, transparent 70%);
pointer-events: none;
}
.state { display: none; flex-direction: column; align-items: center; gap: 14px; width: 100%; }
.state.active { display: flex; }
.icon-wrap {
width: 52px; height: 52px; border-radius: 50%; display: flex; align-items: center; justify-content: center;
font-size: 24px;
}
.icon-wrap.purple { background: linear-gradient(135deg, rgba(145,52,234,0.2), rgba(81,70,229,0.2)); border: 1px solid rgba(145,52,234,0.35); }
.icon-wrap.green { background: rgba(34,197,94,0.12); border: 1px solid rgba(34,197,94,0.3); }
.icon-wrap.red { background: rgba(239,68,68,0.12); border: 1px solid rgba(239,68,68,0.3); }
.heading { font-size: 16px; font-weight: 600; color: var(--text); text-align: center; }
.sub { font-size: 13px; color: var(--text-muted); text-align: center; max-width: 340px; line-height: 1.6; }
.sub strong { color: var(--text); font-weight: 500; }
.btn-row { display: flex; gap: 12px; width: 100%; justify-content: center; margin-top: 2px; }
button {
cursor: pointer; border: none; border-radius: var(--radius-sm); font-family: inherit;
font-size: 13px; font-weight: 500; padding: 8px 20px; transition: opacity 0.15s, transform 0.1s; outline: none;
}
button:active { transform: scale(0.97); }
button:disabled { opacity: 0.45; cursor: not-allowed; transform: none; }
.btn-primary { background: linear-gradient(135deg, var(--grad-a), var(--grad-b)); color: #fff; min-width: 150px; box-shadow: 0 4px 20px rgba(145,52,234,0.35); }
.btn-primary:hover:not(:disabled) { opacity: 0.9; }
.btn-ghost { background: transparent; color: var(--text-muted); border: 1px solid rgba(155,150,196,0.25); }
.btn-ghost:hover:not(:disabled) { color: var(--text); border-color: rgba(155,150,196,0.5); }
.progress-wrap { width: 100%; display: flex; flex-direction: column; gap: 7px; }
.progress-meta { display: flex; justify-content: space-between; font-size: 12px; color: var(--text-muted); }
.progress-track { width: 100%; height: 6px; background: var(--track); border-radius: 99px; overflow: hidden; }
.progress-fill { height: 100%; border-radius: 99px; background: linear-gradient(90deg, var(--grad-a), var(--grad-b)); width: 0%; transition: width 0.3s ease; }
.progress-fill.indeterminate { width: 40% !important; animation: shimmer 1.4s ease-in-out infinite; }
@keyframes shimmer { 0% { transform: translateX(-120%); } 100% { transform: translateX(310%); } }
.spinner {
width: 28px; height: 28px; border: 3px solid rgba(145,52,234,0.2);
border-top-color: var(--grad-a); border-radius: 50%; animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.phase-label { font-size: 12px; color: var(--text-dim); text-align: center; }
.dots { position: absolute; bottom: -20px; right: -20px; width: 100px; height: 100px; background-image: radial-gradient(circle, rgba(145,52,234,0.18) 1px, transparent 1px); background-size: 14px 14px; pointer-events: none; opacity: 0.6; }
.log-section { width: 100%; flex: 1; display: flex; flex-direction: column; min-height: 0; margin-top: 10px; padding-bottom: 12px; }
.log-toggle { display: flex; align-items: center; gap: 6px; cursor: pointer; padding: 4px 0; color: var(--text-dim); font-size: 12px; font-weight: 500; background: none; border: none; font-family: inherit; text-align: left; width: 100%; }
.log-toggle:hover { color: var(--text-muted); }
.log-toggle-chevron { display: inline-block; width: 14px; height: 14px; position: relative; flex-shrink: 0; }
.log-toggle-chevron::before, .log-toggle-chevron::after { content: ''; position: absolute; top: 50%; left: 50%; width: 6px; height: 1.5px; background: currentColor; border-radius: 1px; transition: transform 0.2s; }
.log-toggle-chevron::before { transform: translate(-50%, -50%) rotate(-40deg) translateX(-2px); }
.log-toggle-chevron::after { transform: translate(-50%, -50%) rotate(40deg) translateX(2px); }
.log-toggle.open .log-toggle-chevron::before { transform: translate(-50%, -50%) rotate(40deg) translateX(-2px); }
.log-toggle.open .log-toggle-chevron::after { transform: translate(-50%, -50%) rotate(-40deg) translateX(2px); }
.log-count { margin-left: auto; font-size: 11px; color: var(--text-dim); font-variant-numeric: tabular-nums; }
.log-panel { display: none; flex-direction: column; flex: 1; min-height: 0; margin-top: 6px; background: var(--log-bg); border: 1px solid var(--log-border); border-radius: var(--radius-sm); overflow: hidden; }
.log-panel.open { display: flex; }
.log-inner { flex: 1; overflow-y: auto; padding: 10px 12px; font-family: "SF Mono", "Cascadia Code", monospace; font-size: 11px; line-height: 1.7; color: var(--text-muted); }
.log-line { display: flex; gap: 8px; padding: 1px 0; }
.log-time { color: var(--text-dim); flex-shrink: 0; font-size: 10px; }
.log-text { white-space: pre-wrap; word-break: break-all; flex: 1; }
.log-line.info .log-text { color: var(--text-muted); }
.log-line.error .log-text { color: #e05a5a; }
.log-line.ok .log-text { color: #5ab870; }
.log-line.cmd .log-text { color: #7c8fd8; }
.log-empty { color: var(--text-dim); font-style: italic; font-size: 11px; text-align: center; padding: 12px 0; }
.log-panel-header { display: flex; align-items: center; justify-content: space-between; padding: 5px 10px 5px 12px; border-bottom: 1px solid var(--log-border); }
.log-panel-title { font-size: 10px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-dim); }
.log-clear-btn { font-size: 10px; padding: 2px 8px; color: var(--text-dim); background: transparent; border: 1px solid rgba(155,150,196,0.15); border-radius: 4px; cursor: pointer; font-family: inherit; }
</style>
</head>
<body>
<div class="shell">
<div class="logo-wrap">
<img src="../assets/images/presenton_logo.png" alt="Presenton" onerror="this.style.display='none'; document.getElementById('logo-fb').style.display='block';" />
<span id="logo-fb" class="logo-fallback" style="display:none;">Presenton</span>
</div>
<div class="step-badge" id="step-badge">Setup</div>
<div class="card">
<div class="dots"></div>
<div id="state-prompt" class="state active">
<div class="icon-wrap purple">📦</div>
<p class="heading" id="prompt-heading">Dependencies required</p>
<p class="sub" id="prompt-sub">Presenton needs LibreOffice, Chrome, and ImageMagick to create and export presentations reliably. Install them now so everything works.</p>
<div class="btn-row">
<button class="btn-primary" id="btn-install">Install</button>
<button class="btn-ghost" id="btn-skip">Skip for now</button>
</div>
</div>
<div id="state-downloading" class="state">
<div class="spinner"></div>
<p class="heading" id="dl-heading">Downloading</p>
<p class="sub" id="dl-filename">Preparing…</p>
<div class="progress-wrap">
<div class="progress-meta">
<span id="dl-label">0%</span>
<span id="dl-size"></span>
</div>
<div class="progress-track"><div class="progress-fill" id="dl-bar"></div></div>
</div>
<p class="phase-label" id="dl-phase">This may take a few minutes</p>
</div>
<div id="state-installing" class="state">
<div class="spinner"></div>
<p class="heading" id="install-heading">Installing</p>
<p class="sub">Please wait…</p>
<div class="progress-wrap">
<div class="progress-track"><div class="progress-fill indeterminate" id="install-bar"></div></div>
</div>
</div>
<div id="state-success" class="state">
<div class="icon-wrap green"></div>
<p class="heading" id="success-heading">Installed</p>
<p class="sub" id="success-sub">Continuing in a moment…</p>
<div class="progress-wrap">
<div class="progress-track"><div class="progress-fill" id="success-bar" style="width:0%; transition: width 2s linear;"></div></div>
</div>
</div>
<div id="state-error" class="state">
<div class="icon-wrap red"></div>
<p class="heading">Installation failed</p>
<p class="sub" id="err-msg">Something went wrong. You can try again or skip.</p>
<div class="btn-row">
<button class="btn-primary" id="btn-retry">Try again</button>
<button class="btn-ghost" id="btn-skip-error">Skip</button>
</div>
</div>
</div>
<div class="log-section" id="log-section" style="display:none;">
<button class="log-toggle" id="log-toggle" onclick="toggleLog()">
<span class="log-toggle-chevron"></span>
<span id="log-toggle-label">Show details</span>
<span class="log-count" id="log-count"></span>
</button>
<div class="log-panel" id="log-panel">
<div class="log-panel-header">
<span class="log-panel-title">Log</span>
<button class="log-clear-btn" onclick="clearLog()">Clear</button>
</div>
<div class="log-inner" id="log-inner">
<div class="log-empty" id="log-empty">No output yet.</div>
</div>
</div>
</div>
</div>
<script>
const STATES = ['prompt','downloading','installing','success','error'];
const setupInstallerApi = window.setupInstaller;
let logLines = 0;
let currentStep = null; // 'libreoffice' | 'chrome' | 'imagemagick'
let status = { needsLibreOffice: false, needsChrome: false, needsImageMagick: false };
let steps = [];
let logOpen = false;
function showState(name) {
STATES.forEach(s => {
const el = document.getElementById('state-' + s);
if (el) el.classList.toggle('active', s === name);
});
const logSection = document.getElementById('log-section');
if (logSection) logSection.style.display = (name === 'downloading' || name === 'installing' || name === 'error') ? 'flex' : 'none';
}
function setStepBadge(stepNum, total, label) {
const el = document.getElementById('step-badge');
if (el) el.textContent = total > 1 ? `Step ${stepNum} of ${total}: ${label}` : label;
}
function escHtml(str) {
return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function appendLog(level, text) {
const inner = document.getElementById('log-inner');
if (!inner) return;
const placeholder = document.getElementById('log-empty');
if (placeholder) placeholder.remove();
const now = new Date();
const ts = now.toTimeString().slice(0,8);
const line = document.createElement('div');
line.className = `log-line ${level}`;
line.innerHTML = `<span class="log-time">${ts}</span><span class="log-text">${escHtml(text)}</span>`;
inner.appendChild(line);
logLines++;
const countEl = document.getElementById('log-count');
if (countEl) countEl.textContent = logLines > 0 ? `${logLines} lines` : '';
const nearBottom = inner.scrollHeight - inner.scrollTop - inner.clientHeight < 80;
if (nearBottom) line.scrollIntoView({ behavior: 'smooth', block: 'end' });
}
function clearLog() {
const inner = document.getElementById('log-inner');
if (!inner) return;
inner.innerHTML = '<div class="log-empty" id="log-empty">Log cleared.</div>';
logLines = 0;
document.getElementById('log-count').textContent = '';
}
function toggleLog() {
logOpen = !logOpen;
document.getElementById('log-toggle').classList.toggle('open', logOpen);
document.getElementById('log-panel').classList.toggle('open', logOpen);
document.getElementById('log-toggle-label').textContent = logOpen ? 'Hide details' : 'Show details';
}
function getStepsFromStatus() {
const queue = [];
if (status.needsLibreOffice) queue.push('libreoffice');
if (status.needsChrome) queue.push('chrome');
if (status.needsImageMagick) queue.push('imagemagick');
return queue;
}
function showPromptForStep(step) {
currentStep = step;
const total = steps.length || 1;
const stepNum = Math.max(1, steps.indexOf(step) + 1);
const stepLabel = step === 'libreoffice' ? 'LibreOffice' : step === 'chrome' ? 'Chromium' : 'ImageMagick';
setStepBadge(stepNum, total, stepLabel);
document.getElementById('prompt-heading').textContent =
step === 'libreoffice' ? 'LibreOffice required' :
step === 'chrome' ? 'Chromium required' :
'ImageMagick required';
document.getElementById('prompt-sub').innerHTML =
step === 'libreoffice'
? '<strong>Presenton</strong> uses LibreOffice to generate custom templates from PPTX files.'
: step === 'chrome'
? '<strong>Presenton</strong> uses Chromium for export and slide rendering. Download it now (~150 MB).'
: '<strong>Presenton</strong> uses ImageMagick for OCR/document conversion support. Linux uses apt, macOS installs Homebrew first (if needed) and then runs brew install imagemagick, and Windows downloads and installs it directly into the Presenton runtime.';
document.getElementById('btn-install').onclick = () => startInstall(step);
document.getElementById('btn-skip').onclick = () => handleSkip();
showState('prompt');
}
function startInstall(step) {
if (!setupInstallerApi) {
showState('error');
document.getElementById('err-msg').textContent = 'Installer bridge is unavailable. Restart Presenton and try again.';
return;
}
currentStep = step;
showState('downloading');
if (!logOpen) toggleLog();
if (step === 'libreoffice') {
document.getElementById('dl-heading').textContent = 'Downloading LibreOffice';
document.getElementById('dl-phase').textContent = 'This may take a few minutes (~300 MB)';
setupInstallerApi.installLibreOffice();
} else if (step === 'chrome') {
document.getElementById('dl-heading').textContent = 'Downloading Chromium';
document.getElementById('dl-phase').textContent = 'This may take a few minutes (~150 MB)';
setupInstallerApi.installChrome().then(res => {
if (!res.ok && currentStep === 'chrome') {
document.getElementById('err-msg').textContent = res.error || 'Chrome download failed.';
showState('error');
document.getElementById('btn-retry').onclick = () => startInstall('chrome');
document.getElementById('btn-skip-error').onclick = () => nextOrDone();
}
});
} else {
document.getElementById('dl-heading').textContent = 'Installing ImageMagick';
document.getElementById('dl-phase').textContent = 'Linux: apt-get | macOS: Homebrew + brew install | Windows: direct installer (Presenton runtime)';
setupInstallerApi.installImageMagick().then((installResult) => {
if (!installResult || !installResult.ok) {
if (currentStep !== 'imagemagick') return;
document.getElementById('err-msg').textContent = installResult?.error || 'ImageMagick installation needs manual completion. Follow the shown commands and then click Retry.';
showState('error');
document.getElementById('btn-retry').onclick = () => startInstall('imagemagick');
document.getElementById('btn-skip-error').onclick = () => nextOrDone();
return;
}
setupInstallerApi.checkImageMagick().then(res => {
if (!res.ok && currentStep === 'imagemagick') {
document.getElementById('err-msg').textContent = res.error || 'ImageMagick is not installed yet.';
showState('error');
document.getElementById('btn-retry').onclick = () => startInstall('imagemagick');
document.getElementById('btn-skip-error').onclick = () => nextOrDone();
}
});
});
}
}
function nextOrDone() {
if (!setupInstallerApi) {
return;
}
const idx = steps.indexOf(currentStep);
const nextStep = idx >= 0 ? steps[idx + 1] : null;
if (nextStep) {
showPromptForStep(nextStep);
} else {
setupInstallerApi.done();
}
}
function handleSkip() {
nextOrDone();
}
function onProgress(which, data) {
const { phase, percent, message } = data;
if (which !== currentStep) return;
if (phase === 'downloading') {
showState('downloading');
const bar = document.getElementById('dl-bar');
const label = document.getElementById('dl-label');
const size = document.getElementById('dl-size');
const fname = document.getElementById('dl-filename');
if (bar) bar.style.width = (percent || 0) + '%';
if (label) label.textContent = (percent || 0) + '%';
if (message) {
const parts = String(message).split('|');
if (fname && parts[0]) fname.textContent = parts[0];
if (size && parts[1]) size.textContent = parts[1];
}
return;
}
if (phase === 'installing' || phase === 'extracting') {
showState('installing');
document.getElementById('install-heading').textContent = phase === 'extracting' ? 'Extracting…' : 'Installing…';
return;
}
if (phase === 'done') {
showState('success');
document.getElementById('success-heading').textContent =
currentStep === 'libreoffice' ? 'LibreOffice installed' :
currentStep === 'chrome' ? 'Chromium installed' :
'ImageMagick ready';
const idx = steps.indexOf(currentStep);
const nextStep = idx >= 0 ? steps[idx + 1] : null;
document.getElementById('success-sub').textContent = nextStep ? 'Continuing with next step…' : 'Continuing in a moment…';
const bar = document.getElementById('success-bar');
if (bar) bar.style.width = '100%';
setTimeout(() => {
nextOrDone();
}, 2200);
return;
}
if (phase === 'error') {
showState('error');
document.getElementById('err-msg').textContent = message || 'Installation failed.';
document.getElementById('btn-retry').onclick = () => startInstall(currentStep);
document.getElementById('btn-skip-error').onclick = () => nextOrDone();
if (!logOpen) toggleLog();
}
}
function onLog(which, data) {
if (which !== currentStep) return;
appendLog(data.level || 'info', data.text || '');
}
document.getElementById('btn-retry').onclick = () => startInstall(currentStep);
document.getElementById('btn-skip-error').onclick = () => nextOrDone();
if (!setupInstallerApi) {
showState('error');
document.getElementById('err-msg').textContent = 'Installer bridge failed to initialize. Please restart Presenton and try again.';
document.getElementById('btn-retry').onclick = () => location.reload();
document.getElementById('btn-skip-error').onclick = () => window.close();
appendLog('error', 'setupInstaller API is unavailable in this renderer.');
} else {
setupInstallerApi.onLibreOfficeProgress((data) => onProgress('libreoffice', data));
setupInstallerApi.onLibreOfficeLog((data) => onLog('libreoffice', data));
setupInstallerApi.onChromeProgress((data) => onProgress('chrome', data));
setupInstallerApi.onChromeLog((data) => onLog('chrome', data));
setupInstallerApi.onImageMagickProgress((data) => onProgress('imagemagick', data));
setupInstallerApi.onImageMagickLog((data) => onLog('imagemagick', data));
setupInstallerApi.getStatus().then(s => {
status = s;
steps = getStepsFromStatus();
if (steps.length === 0) {
setupInstallerApi.done();
return;
}
showPromptForStep(steps[0]);
});
}
</script>
</body>
</html>