mirror of
https://github.com/PR0M3TH3AN/Artstr-Studio.git
synced 2026-10-27 08:40:39 +00:00
book(F3): import a single page from another book
New "+ Import page" button in the Pages overview toolbar takes a naddr / nevent / note / hex pointing at a casewrap-page event and adds it to the manifest as a design entry referencing that event. - Resolves through the shared _resolveEmbedEvent helper (works for both authors' own and third-party events; honours encrypted envelopes via _decryptBookSidecarEvent for sidecars private path). - Validates kind 30078 + warns when role isn't "interior" before adding. - De-duplicates: refuses to add a page whose (pubkey, dTag) is already in the manifest. - Seeds the disk cache with the resolved event and applies the page's layers immediately so the new tile paints without a second relay round-trip. - Carries the event's `title` tag into entry.label as a sensible default sorter label. Phase F is feature-complete per the spec: starter templates, deck → book conversion, single-page import. Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
1 parent
461ab85728
commit
ea3fa6f060
1 file changed
+75
@@ -4444,6 +4444,7 @@
|
|||||||
<button type="button" id="bookAddPageBtn" class="primary">+ Add page</button>
|
<button type="button" id="bookAddPageBtn" class="primary">+ Add page</button>
|
||||||
<button type="button" id="bookAddTenBtn">+ Add 10 pages</button>
|
<button type="button" id="bookAddTenBtn">+ Add 10 pages</button>
|
||||||
<button type="button" id="bookAddChapterBtn">+ Add chapter</button>
|
<button type="button" id="bookAddChapterBtn">+ Add chapter</button>
|
||||||
|
<button type="button" id="bookImportPageBtn" title="Import a page from another author's book by naddr / nevent">+ Import page</button>
|
||||||
<button type="button" id="bookExportPdfBtn" title="Export this book as a PDF (interior pages + cover spread)">Export PDF…</button>
|
<button type="button" id="bookExportPdfBtn" title="Export this book as a PDF (interior pages + cover spread)">Export PDF…</button>
|
||||||
<span id="bookExportPdfStatus" class="hint" style="margin-left:6px;font-size:.74rem"></span>
|
<span id="bookExportPdfStatus" class="hint" style="margin-left:6px;font-size:.74rem"></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -8852,6 +8853,79 @@
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Import a single page from another book (Phase F3). Accepts a
|
||||||
|
// naddr / nevent / note / hex pointing at a casewrap-page event
|
||||||
|
// and creates a design entry whose `ref` points to it. The
|
||||||
|
// resolver (D1.5) paints the page's layers under the new entry's
|
||||||
|
// target prefix.
|
||||||
|
async function importBookPageFromNostr() {
|
||||||
|
const book = ensureBookState();
|
||||||
|
const raw = (typeof prompt === 'function')
|
||||||
|
? prompt('Paste an naddr / nevent / note / hex pointing at a casewrap-page event:')
|
||||||
|
: '';
|
||||||
|
if (!raw) return;
|
||||||
|
const decoded = decodeNostrId(raw.trim());
|
||||||
|
if (!decoded) { toastError && toastError('Could not decode that id.'); return; }
|
||||||
|
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) { toastError && toastError('No event found on your relays.'); return; }
|
||||||
|
if (ev.kind !== 30078) { toastError && toastError(`Event is kind ${ev.kind}, not a casewrap-page.`); return; }
|
||||||
|
// Validate it's a page event (role tag preferred; templateMode in content as fallback).
|
||||||
|
const roleTag = (ev.tags || []).find(t => t[0] === 'role')?.[1] || '';
|
||||||
|
let payload = null;
|
||||||
|
try {
|
||||||
|
const decrypted = await _decryptBookSidecarEvent(ev);
|
||||||
|
if (decrypted) payload = decrypted;
|
||||||
|
} catch {}
|
||||||
|
if (!payload) {
|
||||||
|
try { payload = JSON.parse(ev.content || ''); } catch {}
|
||||||
|
}
|
||||||
|
const role = roleTag || payload?.page?.role || '';
|
||||||
|
if (role && role !== 'interior') {
|
||||||
|
const ok = confirm(`This event's role is "${role}", not "interior". Add as a page anyway?`);
|
||||||
|
if (!ok) return;
|
||||||
|
}
|
||||||
|
const dTag = (ev.tags || []).find(t => t[0] === 'd' && t[1])?.[1] || (decoded.dTag || '');
|
||||||
|
if (!dTag || !ev.pubkey) { toastError && toastError('Event is missing d-tag / pubkey.'); return; }
|
||||||
|
// Bail if already in this manifest.
|
||||||
|
const already = (book.pages || []).some(p => p.ref?.pubkey === ev.pubkey && p.ref?.dTag === dTag);
|
||||||
|
if (already) { toastError && toastError('That page is already in this book.'); return; }
|
||||||
|
pushHistory('book: import page');
|
||||||
|
const entryId = makeBookEntryId();
|
||||||
|
const newEntry = {
|
||||||
|
id: entryId,
|
||||||
|
type: 'design',
|
||||||
|
ref: {
|
||||||
|
kind: 30078, pubkey: ev.pubkey, dTag,
|
||||||
|
relays: (decoded.relays || []).slice(0, 4),
|
||||||
|
encrypted: (ev.tags || []).some(t => t[0] === 'encrypted') || undefined,
|
||||||
|
},
|
||||||
|
label: (ev.tags || []).find(t => t[0] === 'title' && t[1])?.[1] || '',
|
||||||
|
masterRef: null,
|
||||||
|
masterId: null,
|
||||||
|
skipNumber: false,
|
||||||
|
anchor: null,
|
||||||
|
gating: null,
|
||||||
|
};
|
||||||
|
book.pages.push(newEntry);
|
||||||
|
// Seed the cache + apply the resolved layers immediately so
|
||||||
|
// the tile renders without waiting on a second relay round-trip.
|
||||||
|
_bookCacheWrite(newEntry.ref, ev);
|
||||||
|
if (payload?.page) {
|
||||||
|
_applyResolvedBookPage(entryId, payload);
|
||||||
|
}
|
||||||
|
syncInputs();
|
||||||
|
render();
|
||||||
|
if (typeof toastSuccess === 'function') toastSuccess('Page imported.');
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('book: importBookPageFromNostr failed', err);
|
||||||
|
toastError && toastError('Import failed: ' + (err?.message || 'unknown error'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Starter book templates (Phase F1). Each preset seeds the spec
|
// Starter book templates (Phase F1). Each preset seeds the spec
|
||||||
// + a default master with a text-frame + a starter chapter so the
|
// + a default master with a text-frame + a starter chapter so the
|
||||||
// user lands on a working book instead of an empty pages overview.
|
// user lands on a working book instead of an empty pages overview.
|
||||||
@@ -35338,6 +35412,7 @@
|
|||||||
on('bookAddPageBtn', 'click', addBookDesignedPage);
|
on('bookAddPageBtn', 'click', addBookDesignedPage);
|
||||||
on('bookAddTenBtn', 'click', addBookTenPages);
|
on('bookAddTenBtn', 'click', addBookTenPages);
|
||||||
on('bookAddChapterBtn', 'click', addBookChapter);
|
on('bookAddChapterBtn', 'click', addBookChapter);
|
||||||
|
on('bookImportPageBtn', 'click', importBookPageFromNostr);
|
||||||
// Chapter editor panes.
|
// Chapter editor panes.
|
||||||
on('bookChapterEditorBack', 'click', closeBookChapter);
|
on('bookChapterEditorBack', 'click', closeBookChapter);
|
||||||
on('bookChapterEditorTextarea', 'input', (e) => updateBookChapterMarkdown(e.target.value));
|
on('bookChapterEditorTextarea', 'input', (e) => updateBookChapterMarkdown(e.target.value));
|
||||||
|
|||||||
Reference in new issue
Block a user