diff --git a/README.md b/README.md
index 93d0fcd..806a86d 100644
--- a/README.md
+++ b/README.md
@@ -165,6 +165,9 @@ Open `convert.html` on the served site (or `node tools/pptx-to-html.mjs talk.ppt
- **Look through** the generated `slides.html` and the string table, and see what the
converter could not handle.
- **Download** a single HTML file you can double-click, or the deck folder as a ZIP.
+- **Save in this browser** to come back to a conversion without downloading it. Saved
+ decks are listed on the page and kept in this browser on this computer until you remove
+ them or clear site data. They are not synced anywhere.
The folder holds `slides.html`, `strings.json` and `images/`. `strings.json` lists
every paragraph, speaker note and alt text under a stable ID, in the format described
diff --git a/convert.html b/convert.html
index 39150a3..a051ef0 100644
--- a/convert.html
+++ b/convert.html
@@ -116,7 +116,25 @@
#preview { position: fixed; inset: 0; z-index: 50; background: var(--brand-bg); }
#preview.hidden { display: none; }
+ #preview iframe { width: 100%; height: 100%; border: 0; display: block; }
#closePreview { position: fixed; top: 14px; right: 16px; z-index: 60; background: rgba(0, 0, 0, 0.6); }
+
+ .saved-head { display: flex; align-items: baseline; gap: 12px; margin-bottom: 14px; }
+ .saved-head h2 { margin: 0; flex: 1; }
+ .saved-head button { padding: 5px 11px; font-size: 0.82rem; }
+ .saved { list-style: none; margin: 0; padding: 0; }
+ .saved li {
+ display: flex;
+ align-items: center;
+ gap: 14px;
+ padding: 10px 0;
+ border-top: 1px solid var(--brand-border);
+ }
+ .saved li:first-child { border-top: 0; padding-top: 0; }
+ .saved .what { flex: 1; min-width: 0; }
+ .saved .what b { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+ .saved .what span { color: var(--brand-muted); font-size: 0.82rem; }
+ .saved button { padding: 6px 12px; font-size: 0.85rem; }
@@ -140,6 +158,19 @@
Convert a PowerPoint to HTML
+
+
+
Saved in this browser
+
+
+
+
+
+ Kept in this browser only, on this computer. They stay until you remove them or
+ clear site data, and are not synced anywhere, so download anything you need
+ elsewhere.
+
+
Converted
@@ -161,6 +192,7 @@
Converted
+
@@ -210,7 +242,7 @@
This page needs a web server
-
+
@@ -292,23 +324,21 @@
This page needs a web server
render();
}
- async function load(file) {
- if (!file.name.toLowerCase().endsWith('.pptx')) {
- setStatus('That is not a .pptx file.', 'error');
- return;
- }
- setStatus(`Reading ${file.name}…`);
+ /** Convert a .pptx already in memory. `fields` restores the form of a saved deck. */
+ async function loadBytes(name, bytes, fields = null) {
+ setStatus(`Reading ${name}…`);
try {
- const files = await readPptx(new Uint8Array(await file.arrayBuffer()));
- current = { name: file.name, files };
- const guess = slugify(file.name.replace(/\.pptx$/i, ''));
+ const files = await readPptx(bytes);
+ current = { name, files, bytes };
+ const guess = fields?.slug || slugify(name.replace(/\.pptx$/i, ''));
$('slug').value = guess;
- const probe = pptxToHtml(files, { slug: guess, description: `Converted from ${file.name}.` });
- $('title').value = probe.deck.title;
- $('author').value = probe.deck.author;
+ const probe = pptxToHtml(files, { slug: guess, description: `Converted from ${name}.` });
+ $('title').value = fields?.title || probe.deck.title;
+ $('author').value = fields?.author ?? probe.deck.author;
current.result = probe;
+ $('saveBtn').textContent = 'Save in this browser';
render();
- setStatus(`Converted ${probe.stats.slides} slides from ${file.name}.`, 'ok');
+ setStatus(`Converted ${probe.stats.slides} slides from ${name}.`, 'ok');
} catch (error) {
current = null;
$('result').classList.add('hidden');
@@ -316,6 +346,121 @@
This page needs a web server
}
}
+ async function load(file) {
+ if (!file.name.toLowerCase().endsWith('.pptx')) {
+ setStatus('That is not a .pptx file.', 'error');
+ return;
+ }
+ await loadBytes(file.name, new Uint8Array(await file.arrayBuffer()));
+ }
+
+ /* ------------------------------------------- saved in this session -- */
+
+ /*
+ * Decks are kept as the original .pptx in IndexedDB (large files do not fit
+ * in sessionStorage) and converted again when opened, which takes well under
+ * a second. They stay until the user removes them or clears site data for this
+ * origin. They live in this browser only, and are never sent anywhere.
+ */
+ const DB_NAME = 'decks-session';
+ const STORE = 'decks';
+ const hasStorage = typeof indexedDB !== 'undefined';
+ // Ask the browser not to evict saved decks under storage pressure; harmless if refused.
+ navigator.storage?.persist?.().catch(() => {});
+
+ const openDb = () => new Promise((resolve, reject) => {
+ const request = indexedDB.open(DB_NAME, 1);
+ request.onupgradeneeded = () => request.result.createObjectStore(STORE, { keyPath: 'id' });
+ request.onsuccess = () => resolve(request.result);
+ request.onerror = () => reject(request.error);
+ });
+
+ async function dbRun(mode, action) {
+ const db = await openDb();
+ try {
+ return await new Promise((resolve, reject) => {
+ const transaction = db.transaction(STORE, mode);
+ const request = action(transaction.objectStore(STORE));
+ transaction.oncomplete = () => resolve(request?.result);
+ transaction.onerror = () => reject(transaction.error);
+ transaction.onabort = () => reject(transaction.error);
+ });
+ } finally {
+ db.close();
+ }
+ }
+
+ async function listSaved() {
+ const all = (await dbRun('readonly', (s) => s.getAll())) ?? [];
+ return all.sort((a, b) => b.savedAt - a.savedAt);
+ }
+
+ async function renderSaved() {
+ if (!hasStorage) return;
+ let decks = [];
+ try { decks = await listSaved(); } catch { /* storage blocked: no list */ }
+ $('saved').classList.toggle('hidden', decks.length === 0);
+ const total = decks.reduce((n, d) => n + d.bytes.byteLength, 0);
+ $('savedTotal').textContent = decks.length ? `${(total / 1048576).toFixed(1)} MB used` : '';
+ $('savedList').innerHTML = decks.map((d) => `
+