mirror of
https://github.com/PR0M3TH3AN/Artstr-Studio.git
synced 2026-10-27 08:40:39 +00:00
book(D2 polish): manifest dirty tracking + feed-card preview
Two pieces that close out Phase D2: Dirty tracking - isBookManifestDirty() compares the current bookManifestPayload serial to state.book.lastPublishedManifestSerial; a missing snapshot but any existing ref counts as dirty so the sidebar prompts the first publish. - markBookManifestClean() snapshots the serial on success; called from both the public and private manifest publish paths in publishCurrentTemplateToNostr. - syncBookSidebarSummary renders a one-line hint — "⚠ Manifest has unsaved changes" or "✓ Manifest is in sync" — so authors see when sidecar publishes have outpaced the manifest publish. Feed-card preview - buildFeedCard now has a `book` branch: a chapbook-shaped card (cream-to-amber gradient, Georgia serif title, page + chapter counts) so book manifests don't fall through to a blank "Preview unavailable" tile in the community browser. - MODE_LABELS gets 'book': 'Book' so the type chip + fallback label read correctly. Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
1 parent
5b19c25e2b
commit
55f53aeb49
1 file changed
+73
-2
+73
-2
@@ -10531,6 +10531,36 @@
|
||||
return summary;
|
||||
}
|
||||
|
||||
// Manifest dirty tracking. After a successful manifest publish
|
||||
// we snapshot the serialised payload onto state.book.lastPublishedManifestSerial;
|
||||
// isBookManifestDirty() compares the current serialisation to
|
||||
// that snapshot. Used by the sidebar hint so the user knows when
|
||||
// the manifest needs another publish.
|
||||
function isBookManifestDirty() {
|
||||
const book = state.book;
|
||||
if (!book) return false;
|
||||
const snapshot = book.lastPublishedManifestSerial;
|
||||
if (!snapshot) {
|
||||
// No snapshot but we have refs → counted as dirty so the
|
||||
// sidebar prods the user to publish for the first time.
|
||||
const hasAnyRef = !!book.cover?.ref
|
||||
|| (book.masters || []).some(m => m.ref)
|
||||
|| (book.pages || []).some(p => p.ref || p.textRef);
|
||||
return hasAnyRef;
|
||||
}
|
||||
try {
|
||||
const current = JSON.stringify(bookManifestPayload());
|
||||
return current !== snapshot;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
function markBookManifestClean() {
|
||||
const book = state.book;
|
||||
if (!book) return;
|
||||
try { book.lastPublishedManifestSerial = JSON.stringify(bookManifestPayload()); } catch {}
|
||||
}
|
||||
|
||||
// Build the casewrap-book manifest payload. Lean — references
|
||||
// sibling page / master / cover / chapter events via {kind, pubkey,
|
||||
// dTag} coords instead of inlining their layers. Phase D1 hands
|
||||
@@ -11157,11 +11187,24 @@
|
||||
? `derived from ${pageCount} page${pageCount === 1 ? '' : 's'}`
|
||||
: 'manual';
|
||||
const fmt = (n) => Number(n).toFixed(n % 1 === 0 ? 0 : 3);
|
||||
// Manifest dirty hint — sidecars publish independently, so the
|
||||
// manifest can be "out of sync" with the per-page events. Tell
|
||||
// the user explicitly so they don't think Publish-page was the
|
||||
// last step.
|
||||
let dirtyHint = '';
|
||||
try {
|
||||
if (isBookManifestDirty()) {
|
||||
dirtyHint = '<br><span style="color:#92400e">⚠ Manifest has unsaved changes — Publish to update the casewrap-book event.</span>';
|
||||
} else if (state.book.lastPublishedManifestSerial) {
|
||||
dirtyHint = '<br><span style="color:#15803d">✓ Manifest is in sync with the published casewrap-book event.</span>';
|
||||
}
|
||||
} catch {}
|
||||
el.innerHTML = ''
|
||||
+ `<b>${s.bookSize || 'Custom'}</b> · ${fmt(s.trimW)} × ${fmt(s.trimH)} in`
|
||||
+ `<br>Bleed ${fmt(s.bleed)} in · Paper ${fmt(s.paperThickness)} in/page`
|
||||
+ `<br>Spine ${fmt(spineW)} in <span class="hint">(${spineNote})</span>`
|
||||
+ `<br>${pageCount} page${pageCount === 1 ? '' : 's'} · ${(state.book.masters || []).length} master${(state.book.masters || []).length === 1 ? '' : 's'}`;
|
||||
+ `<br>${pageCount} page${pageCount === 1 ? '' : 's'} · ${(state.book.masters || []).length} master${(state.book.masters || []).length === 1 ? '' : 's'}`
|
||||
+ dirtyHint;
|
||||
syncBookVisibilityChip();
|
||||
}
|
||||
// Sync the Public/Private chip in the book sidebar from
|
||||
@@ -21648,6 +21691,9 @@
|
||||
const signed = await publishEncryptedDesign(payload);
|
||||
const label = isEditing() ? `Updated "${state.meta.title}" (private)` : `Private "${state.meta.title}"`;
|
||||
await finalizePublishedTemplateEvent(signed, { label });
|
||||
if (state.templateMode === 'book' && typeof markBookManifestClean === 'function') {
|
||||
try { markBookManifestClean(); } catch {}
|
||||
}
|
||||
if (isEditing()) exitEditMode();
|
||||
return signed;
|
||||
}
|
||||
@@ -21677,6 +21723,12 @@
|
||||
const signed = await publishCommunityEvent(evt);
|
||||
const label = isEditing() ? `Updated "${state.meta.title}"` : `Template "${state.meta.title}"`;
|
||||
await finalizePublishedTemplateEvent(signed, { label });
|
||||
// Book manifest: snapshot the published serialisation so the
|
||||
// sidebar's dirty hint flips to "Saved" until the user makes
|
||||
// another manifest-affecting change.
|
||||
if (state.templateMode === 'book' && typeof markBookManifestClean === 'function') {
|
||||
try { markBookManifestClean(); } catch {}
|
||||
}
|
||||
if (isEditing()) exitEditMode();
|
||||
return signed;
|
||||
}
|
||||
@@ -29098,6 +29150,25 @@
|
||||
else if (row.mode === 'customart' || row.mode === 'slide') renderCustomArtPreviewDOM(previewWrap, payload, { compact: true });
|
||||
else if (row.mode === 'deck') renderDeckPreviewDOM(previewWrap, payload, { compact: true });
|
||||
else if (row.mode === 'pixelart') renderPixelArtPreviewDOM(previewWrap, payload, { compact: true });
|
||||
else if (row.mode === 'book') {
|
||||
// Book manifests don't ship layer data — render a
|
||||
// chapbook-shaped card with title + entry counts so the
|
||||
// feed has something to display instead of "Preview
|
||||
// unavailable".
|
||||
const spec = payload.book?.spec || {};
|
||||
const pages = payload.book?.pages || [];
|
||||
const designed = pages.filter(p => p.type === 'design').length;
|
||||
const reflow = pages.filter(p => p.type === 'reflow').length;
|
||||
const trimLabel = spec.bookSize || `${spec.trimW || 6} × ${spec.trimH || 9} in`;
|
||||
previewWrap.style.cssText = 'width:100%;height:100%;display:flex;align-items:center;justify-content:center;padding:14px;box-sizing:border-box;background:linear-gradient(135deg,#fef3c7 0%,#fde68a 50%,#f59e0b 100%)';
|
||||
previewWrap.innerHTML = `
|
||||
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;font-family:Georgia,serif;color:#1f2937;padding:18px 14px;background:rgba(255,255,255,.85);border:1px solid #d97706;border-radius:4px;max-width:90%">
|
||||
<div style="font-size:1rem;font-weight:700;line-height:1.2;margin-bottom:6px;word-break:break-word">${escapeHtml(payload.meta?.title || 'Untitled book')}</div>
|
||||
<div style="font-size:.72rem;color:#92400e">${escapeHtml(trimLabel)}</div>
|
||||
<div style="font-size:.7rem;color:#92400e;margin-top:8px">${designed} page${designed === 1 ? '' : 's'}${reflow ? ` · ${reflow} chapter${reflow === 1 ? '' : 's'}` : ''}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
else renderTemplatePreviewDOM(previewWrap, payload, { compact: true });
|
||||
} catch {}
|
||||
}
|
||||
@@ -29865,7 +29936,7 @@
|
||||
return btn;
|
||||
}
|
||||
|
||||
const MODE_LABELS = { cover: 'Case cover', disc: 'Disc sheet', jewel: 'CD jewel inserts', 'disc-design': 'Disc design', customart: 'Custom art', slide: 'Slide', deck: 'Slide deck', pixelart: 'Pixel art' };
|
||||
const MODE_LABELS = { cover: 'Case cover', disc: 'Disc sheet', jewel: 'CD jewel inserts', 'disc-design': 'Disc design', customart: 'Custom art', slide: 'Slide', deck: 'Slide deck', pixelart: 'Pixel art', book: 'Book' };
|
||||
|
||||
function buildDiscSheetThumb(thumb, payload, compact = false) {
|
||||
const slots = payload?.discTemplate?.discs || payload?.discs || {};
|
||||
|
||||
Reference in new issue
Block a user