669 lines
21 KiB
HTML
669 lines
21 KiB
HTML
<!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, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"');
|
||
}
|
||
|
||
// ── 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>
|