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

669 lines
21 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 Install LibreOffice</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;
}
/* ── Outer layout ── */
.shell {
display: flex;
flex-direction: column;
height: 100vh;
padding: 24px 32px 0;
}
/* ── Logo ── */
.logo-wrap {
text-align: center;
margin-bottom: 20px;
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;
letter-spacing: -0.5px;
}
/* ── Card ── */
.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 panels ── */
.state { display: none; flex-direction: column; align-items: center; gap: 14px; width: 100%; }
.state.active { display: flex; }
/* ── Icons ── */
.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);
}
/* ── Typography ── */
.heading {
font-size: 16px;
font-weight: 600;
color: var(--text);
text-align: center;
letter-spacing: -0.2px;
}
.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; }
/* ── Buttons ── */
.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 bar ── */
.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 ── */
.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 decoration ── */
.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 ── */
.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%;
transition: color 0.15s;
user-select: none;
}
.log-toggle:hover { color: var(--text-muted); }
.log-toggle:active { transform: none; }
.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;
overflow-x: hidden;
padding: 10px 12px;
font-family: "SF Mono", "Cascadia Code", "Fira Code", "Consolas", "Monaco", monospace;
font-size: 11px;
line-height: 1.7;
color: var(--text-muted);
scroll-behavior: smooth;
scrollbar-width: thin;
scrollbar-color: rgba(145,52,234,0.3) transparent;
}
.log-inner::-webkit-scrollbar { width: 4px; }
.log-inner::-webkit-scrollbar-thumb { background: rgba(145,52,234,0.3); border-radius: 2px; }
.log-inner::-webkit-scrollbar-track { background: transparent; }
.log-line {
display: flex;
gap: 8px;
padding: 1px 0;
}
.log-time {
color: var(--text-dim);
flex-shrink: 0;
font-size: 10px;
padding-top: 1px;
}
.log-text { white-space: pre-wrap; word-break: break-all; flex: 1; }
.log-line.info .log-text { color: var(--text-muted); }
.log-line.warn .log-text { color: #c9a54a; }
.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;
}
/* ── Clear log button in panel header ── */
.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;
}
.log-clear-btn:hover { color: var(--text-muted); }
</style>
</head>
<body>
<div class="shell">
<!-- Logo -->
<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>
<!-- Card -->
<div class="card">
<div class="dots"></div>
<!-- ── STATE: prompt ── -->
<div id="state-prompt" class="state active">
<div class="icon-wrap purple">📦</div>
<p class="heading">LibreOffice Required</p>
<p class="sub">
<strong>Presenton</strong> uses LibreOffice to generate custom presentation
templates from uploaded PPTX files. Without it, this feature won't work.
</p>
<div class="btn-row">
<button class="btn-primary" onclick="handleInstall()">Install LibreOffice</button>
<button class="btn-ghost" onclick="handleSkip()">Skip for now</button>
</div>
</div>
<!-- ── STATE: downloading ── -->
<div id="state-downloading" class="state">
<div class="spinner"></div>
<p class="heading">Downloading LibreOffice</p>
<p class="sub" id="dl-filename">Preparing download…</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">This may take a few minutes (~300 MB)</p>
</div>
<!-- ── STATE: installing ── -->
<div id="state-installing" class="state">
<div class="spinner"></div>
<p class="heading">Installing LibreOffice</p>
<p class="sub">Running the installer — this won't take long…</p>
<div class="progress-wrap">
<div class="progress-meta" id="install-meta" style="display:none;">
<span id="install-label">0%</span>
</div>
<div class="progress-track">
<div class="progress-fill indeterminate" id="install-bar"></div>
</div>
</div>
</div>
<!-- ── STATE: success ── -->
<div id="state-success" class="state">
<div class="icon-wrap green"></div>
<p class="heading">LibreOffice Installed</p>
<p class="sub">
Custom template generation from PPTX is now available.
Presenton will continue in a moment…
</p>
<div class="progress-wrap" style="margin-top:2px;">
<div class="progress-track">
<div class="progress-fill" id="success-bar" style="width:0%;transition:width 2s linear;"></div>
</div>
</div>
</div>
<!-- ── STATE: error ── -->
<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 and install LibreOffice manually later.</p>
<div class="btn-row">
<button class="btn-primary" onclick="handleInstall()">Try Again</button>
<button class="btn-ghost" onclick="handleSkip()">Skip</button>
</div>
</div>
</div><!-- /card -->
<!-- ── Log section ── -->
<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">Installation 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><!-- /shell -->
<script>
// ── State machine ───────────────────────────────────────────
const STATES = ['prompt','downloading','installing','success','error'];
let logLines = 0;
function showState(name) {
STATES.forEach(s => {
const el = document.getElementById('state-' + s);
if (el) el.classList.toggle('active', s === name);
});
// show log section for active phases
const logSection = document.getElementById('log-section');
if (logSection) {
logSection.style.display =
(name === 'downloading' || name === 'installing' || name === 'error') ? 'flex' : 'none';
}
// When entering installing state fresh, reset to indeterminate until a
// real percent arrives (e.g. Windows msiexec never sends one).
if (name === 'installing') {
const bar = document.getElementById('install-bar');
const meta = document.getElementById('install-meta');
const label = document.getElementById('install-label');
if (bar && !bar.classList.contains('indeterminate')) {
bar.classList.add('indeterminate');
bar.style.width = '';
}
if (meta) meta.style.display = 'none';
if (label) label.textContent = '0%';
}
}
// ── Log panel ───────────────────────────────────────────────
let logOpen = false;
function toggleLog() {
logOpen = !logOpen;
const toggle = document.getElementById('log-toggle');
const panel = document.getElementById('log-panel');
const label = document.getElementById('log-toggle-label');
if (toggle) toggle.classList.toggle('open', logOpen);
if (panel) panel.classList.toggle('open', logOpen);
if (label) label.textContent = logOpen ? 'Hide details' : 'Show details';
}
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;
updateLogCount();
}
function updateLogCount() {
const el = document.getElementById('log-count');
if (el) el.textContent = logLines > 0 ? `${logLines} lines` : '';
}
function appendLog(level, text) {
const inner = document.getElementById('log-inner');
if (!inner) return;
// remove empty placeholder
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++;
updateLogCount();
// Auto-scroll to keep the latest log visible (if user is following)
const threshold = 80;
const nearBottom = inner.scrollHeight - inner.scrollTop - inner.clientHeight < threshold;
if (nearBottom) {
line.scrollIntoView({ behavior: 'smooth', block: 'end' });
}
}
function escHtml(str) {
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
// ── Button handlers ─────────────────────────────────────────
function handleInstall() {
showState('downloading');
// auto-open log on install start
if (!logOpen) toggleLog();
if (window.loInstaller) {
window.loInstaller.startInstall();
}
}
function handleSkip() {
if (window.loInstaller) {
window.loInstaller.skip();
}
}
// ── Progress handler ─────────────────────────────────────────
function onProgress(data) {
const { phase, percent, message } = data;
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 = message.split('|');
if (fname && parts[0]) fname.textContent = parts[0];
if (size && parts[1]) size.textContent = parts[1];
}
return;
}
if (phase === 'installing') {
showState('installing');
const bar = document.getElementById('install-bar');
const meta = document.getElementById('install-meta');
const label = document.getElementById('install-label');
if (bar) {
if (typeof percent === 'number') {
bar.classList.remove('indeterminate');
bar.style.width = Math.min(percent, 100) + '%';
if (meta) meta.style.display = 'flex';
if (label) label.textContent = percent + '%';
} else if (!bar.classList.contains('indeterminate')) {
bar.classList.add('indeterminate');
bar.style.width = '';
}
}
return;
}
if (phase === 'done') {
showState('success');
appendLog('ok', 'Installation complete.');
setTimeout(() => {
const bar = document.getElementById('success-bar');
if (bar) bar.style.width = '100%';
}, 50);
setTimeout(() => {
if (window.loInstaller) window.loInstaller.skip();
}, 2200);
return;
}
if (phase === 'error') {
showState('error');
const msg = document.getElementById('err-msg');
if (msg && message) msg.textContent = message;
appendLog('error', message || 'Installation failed.');
// auto-open log on error so user can see what happened
if (!logOpen) toggleLog();
return;
}
}
// ── Log handler ──────────────────────────────────────────────
function onLog(data) {
const { level, text } = data;
appendLog(level || 'info', text || '');
}
// ── Wire up IPC ──────────────────────────────────────────────
if (window.loInstaller) {
window.loInstaller.onProgress(onProgress);
window.loInstaller.onLog(onLog);
}
</script>
</body>
</html>