diff --git a/src/index.html b/src/index.html
index 7ae2bfb..22ddfb4 100644
--- a/src/index.html
+++ b/src/index.html
@@ -4108,6 +4108,11 @@
+
+
+
+
+
Publish visibility:
@@ -9672,6 +9677,142 @@
}
}
+ // Import a whole book by pasting an naddr / nevent that points
+ // at a casewrap-book manifest event. The manifest is lean — just
+ // refs to cover / pages / masters / chapters — so once we feed
+ // its payload to loadProjectFromText the existing book branch
+ // restores state.book + kicks off resolveBookSidecars in the
+ // background. Status reports back into #bookResolveStatus.
+ async function importBookManifestFromNostr() {
+ const raw = (typeof prompt === 'function')
+ ? prompt('Paste an naddr / nevent pointing at a casewrap-book manifest event:')
+ : '';
+ if (!raw) return;
+ const decoded = decodeNostrId(raw.trim());
+ if (!decoded) { toastError && toastError('Could not decode that id.'); return; }
+ const status = document.getElementById('bookResolveStatus');
+ const setStatus = (text, kind) => {
+ if (!status) return;
+ status.textContent = text || '';
+ status.style.color = kind === 'error' ? '#b91c1c' : kind === 'ok' ? '#15803d' : 'var(--muted)';
+ };
+ setStatus('Resolving manifest event…');
+ const decodedForResolver = decoded.type === 'naddr'
+ ? decoded
+ : { type: 'nevent', eventId: decoded.eventId, relays: decoded.relays || [], author: decoded.author || '', kind: decoded.kind || null };
+ try {
+ const ev = await _resolveEmbedEvent(decodedForResolver);
+ if (!ev) { setStatus('No event found on your relays.', 'error'); toastError?.('No event found.'); return; }
+ if (ev.kind !== 30078) { setStatus(`Event is kind ${ev.kind}, not a casewrap-book.`, 'error'); toastError?.('Wrong event kind.'); return; }
+ // Try decrypt-first (private manifest), then plain JSON.
+ let payload = null;
+ try {
+ const decrypted = await _decryptBookSidecarEvent(ev);
+ if (decrypted) payload = decrypted;
+ } catch {}
+ if (!payload) {
+ try { payload = JSON.parse(ev.content || ''); } catch {}
+ }
+ if (!payload?.book) { setStatus('Event payload does not contain a book manifest.', 'error'); toastError?.('Not a book manifest.'); return; }
+ // Inject the publish ref so future "Publish manifest" replaces
+ // this event in place, and the encrypted flag so visibility
+ // rides through correctly.
+ const dTag = (ev.tags || []).find(t => t[0] === 'd' && t[1])?.[1] || (decoded.dTag || '');
+ if (dTag) {
+ payload.book = payload.book || {};
+ payload.book.manifestRef = {
+ kind: 30078, pubkey: ev.pubkey, dTag,
+ relays: (decoded.relays || []).slice(0, 4),
+ encrypted: (ev.tags || []).some(t => t[0] === 'encrypted') || undefined,
+ };
+ }
+ if ((ev.tags || []).some(t => t[0] === 'encrypted')) {
+ payload.visibility = 'private';
+ payload.privateDTag = dTag || '';
+ }
+ if (typeof loadProjectFromText === 'function') {
+ loadProjectFromText(JSON.stringify(payload));
+ }
+ setStatus(`Manifest loaded. Resolving sidecars…`);
+ // resolveBookSidecars already fires from the book branch of
+ // loadProjectFromText (setTimeout 0). Surface a summary when
+ // it settles.
+ setTimeout(async () => {
+ try {
+ const summary = await resolveBookSidecars(state.book);
+ const total = (summary?.resolved || 0) + (summary?.failed || 0);
+ const ok = summary?.resolved || 0;
+ const failed = summary?.failed || 0;
+ setStatus(
+ failed
+ ? `Resolved ${ok} / ${total} sidecars; ${failed} failed (check relays).`
+ : (total ? `Resolved ${ok} / ${total} sidecars.` : 'Manifest loaded.'),
+ failed ? 'error' : 'ok'
+ );
+ } catch (err) {
+ setStatus('Resolver error — open DevTools for details.', 'error');
+ }
+ }, 100);
+ toastSuccess?.('Book manifest loaded — sidecars resolving in background.');
+ } catch (err) {
+ console.warn('importBookManifestFromNostr failed', err);
+ setStatus('Import failed: ' + (err?.message || 'unknown error'), 'error');
+ }
+ }
+
+ // Import a whole book from a local JSON file. Accepts the format
+ // bookPayload() emits (Save Project in book mode) — has
+ // templateMode === 'book' + a `book` field at the top level.
+ async function importBookManifestFromJsonFile(file) {
+ if (!file) return;
+ let parsed;
+ try {
+ const text = await file.text();
+ parsed = JSON.parse(text);
+ } catch (err) {
+ toastError && toastError('Not a valid JSON file.');
+ return;
+ }
+ if (parsed?.templateMode !== 'book' || !parsed?.book) {
+ toastError && toastError('Not a book project JSON (expected templateMode "book").');
+ return;
+ }
+ const status = document.getElementById('bookResolveStatus');
+ const setStatus = (text, kind) => {
+ if (!status) return;
+ status.textContent = text || '';
+ status.style.color = kind === 'error' ? '#b91c1c' : kind === 'ok' ? '#15803d' : 'var(--muted)';
+ };
+ try {
+ loadProjectFromText(JSON.stringify(parsed));
+ // The whole-book JSON inlines page / master / cover layers
+ // already (it's a backup format), so the resolver has less
+ // network work — but still call it to pull any chapter
+ // markdown that comes via textRef.
+ setStatus('Book loaded from JSON. Resolving any referenced sidecars…');
+ setTimeout(async () => {
+ try {
+ const summary = await resolveBookSidecars(state.book);
+ const total = (summary?.resolved || 0) + (summary?.failed || 0);
+ const ok = summary?.resolved || 0;
+ const failed = summary?.failed || 0;
+ setStatus(
+ total
+ ? (failed ? `Resolved ${ok} / ${total} sidecars; ${failed} failed.` : `Resolved ${ok} / ${total} sidecars.`)
+ : 'Book loaded.',
+ failed ? 'error' : 'ok'
+ );
+ } catch (err) {
+ setStatus('Resolver error — open DevTools for details.', 'error');
+ }
+ }, 100);
+ toastSuccess?.('Book loaded from JSON.');
+ } catch (err) {
+ console.warn('importBookManifestFromJsonFile failed', err);
+ toastError && toastError('Load failed: ' + (err?.message || 'unknown error'));
+ }
+ }
+
// Load a cover spread from a local JSON file (matching the format
// bookCoverFileExportPayload emits). Replaces the active book's
// cover layers + cover-spread spec hints; clears book.cover.ref
@@ -39133,6 +39274,16 @@
_checkDocsHash();
on('bookDocumentSettingsBtn', 'click', openBookDocumentSettings);
+ on('bookImportManifestNostrBtn', 'click', importBookManifestFromNostr);
+ on('bookImportManifestJsonBtn', 'click', () => {
+ const input = document.getElementById('bookImportManifestJsonFile');
+ if (input) input.click();
+ });
+ on('bookImportManifestJsonFile', 'change', async (e) => {
+ const file = e.target?.files?.[0];
+ e.target.value = '';
+ if (file) await importBookManifestFromJsonFile(file);
+ });
on('bookVisibilityChip', 'click', toggleBookVisibility);
on('bookResolveBtn', 'click', async () => {
const status = document.getElementById('bookResolveStatus');