diff --git a/README.md b/README.md index 806a86d..ef08fe9 100644 --- a/README.md +++ b/README.md @@ -166,8 +166,8 @@ Open `convert.html` on the served site (or `node tools/pptx-to-html.mjs talk.ppt 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. + decks appear under "Your decks" on the main page, with Open and Remove. They are kept in + this browser on this computer until you remove them or clear site data. 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/assets/saved-decks.mjs b/assets/saved-decks.mjs new file mode 100644 index 0000000..e864057 --- /dev/null +++ b/assets/saved-decks.mjs @@ -0,0 +1,62 @@ +/** + * Decks converted in the browser and saved there, shared by the landing page and + * the converter. Each is kept as its original .pptx in IndexedDB (too large for + * sessionStorage) and converted again when opened. Nothing leaves the browser. + */ +const DB_NAME = 'decks-session'; +const STORE = 'decks'; + +export const hasStorage = typeof indexedDB !== 'undefined'; + +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 run(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(); + } +} + +/** Newest first. Never throws: blocked storage just means an empty list. */ +export async function listSaved() { + if (!hasStorage) return []; + try { + return ((await run('readonly', (s) => s.getAll())) ?? []).sort((a, b) => b.savedAt - a.savedAt); + } catch { + return []; + } +} + +export const getSaved = (id) => run('readonly', (s) => s.get(id)); +export const removeSaved = (id) => run('readwrite', (s) => s.delete(id)); + +/** `bytes` is the original .pptx as a Uint8Array. */ +export async function saveDeck({ name, bytes, title, slug, author, slides }) { + // Ask the browser not to evict saved decks under storage pressure; harmless if refused. + navigator.storage?.persist?.().catch(() => {}); + const record = { + id: `${name}:${bytes.byteLength}`, + name, + title, + slug, + author, + slides, + bytes: bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength), + savedAt: Date.now(), + }; + await run('readwrite', (s) => s.put(record)); + return record.id; +} diff --git a/convert.html b/convert.html index a051ef0..3fc2c5d 100644 --- a/convert.html +++ b/convert.html @@ -118,23 +118,6 @@ #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; } @@ -158,19 +141,6 @@

Convert a PowerPoint to HTML

-