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

384 lines
19 KiB
HTML
Raw 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 and Chrome to create and export presentations. 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'];
let logLines = 0;
let currentStep = null; // 'libreoffice' | 'chrome'
let status = { needsLibreOffice: false, needsChrome: false };
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 showPromptForStep(step) {
currentStep = step;
const total = (status.needsLibreOffice ? 1 : 0) + (status.needsChrome ? 1 : 0);
const stepNum = step === 'libreoffice' ? 1 : 2;
setStepBadge(stepNum, total, step === 'libreoffice' ? 'LibreOffice' : 'Chromium');
document.getElementById('prompt-heading').textContent = step === 'libreoffice' ? 'LibreOffice required' : 'Chromium required';
document.getElementById('prompt-sub').innerHTML = step === 'libreoffice'
? '<strong>Presenton</strong> uses LibreOffice to generate custom templates from PPTX files.'
: '<strong>Presenton</strong> uses Chromium for export and slide rendering. Download it now (~150 MB).';
document.getElementById('btn-install').onclick = () => startInstall(step);
document.getElementById('btn-skip').onclick = () => handleSkip();
showState('prompt');
}
function startInstall(step) {
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)';
window.setupInstaller.installLibreOffice();
} else {
document.getElementById('dl-heading').textContent = 'Downloading Chromium';
document.getElementById('dl-phase').textContent = 'This may take a few minutes (~150 MB)';
window.setupInstaller.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();
}
});
}
}
function nextOrDone() {
if (currentStep === 'libreoffice' && status.needsChrome) {
showPromptForStep('chrome');
} else {
window.setupInstaller.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' : 'Chromium installed';
document.getElementById('success-sub').textContent = status.needsChrome && currentStep === 'libreoffice' ? 'Next: Chrome.' : 'Continuing in a moment…';
const bar = document.getElementById('success-bar');
if (bar) bar.style.width = '100%';
setTimeout(() => {
if (currentStep === 'libreoffice' && status.needsChrome) showPromptForStep('chrome');
else window.setupInstaller.done();
}, 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 || '');
}
window.setupInstaller.onLibreOfficeProgress((data) => onProgress('libreoffice', data));
window.setupInstaller.onLibreOfficeLog((data) => onLog('libreoffice', data));
window.setupInstaller.onChromeProgress((data) => onProgress('chrome', data));
window.setupInstaller.onChromeLog((data) => onLog('chrome', data));
document.getElementById('btn-retry').onclick = () => startInstall(currentStep);
document.getElementById('btn-skip-error').onclick = () => nextOrDone();
window.setupInstaller.getStatus().then(s => {
status = s;
if (!status.needsLibreOffice && !status.needsChrome) {
window.setupInstaller.done();
return;
}
if (status.needsLibreOffice) showPromptForStep('libreoffice');
else showPromptForStep('chrome');
});
</script>
</body>
</html>