mirror of
https://github.com/PR0M3TH3AN/Artstr-Studio.git
synced 2026-10-27 08:40:39 +00:00
Layer extract: edit linked source + transparent group canvas
Three bugs in the freshly-shipped extract flow:
1. Group extract baked customArt.background='#ffffff' so the
extracted design lost any transparency the group had. Now
defaults to 'transparent' so the extracted design composites
cleanly into any host. Users can pick a solid fill later via
the customart background control if they want one.
2. Double-clicking a linked layer ran beginDesignObjectEdit
which loads layer.design.payload — but linked layers have no
local payload, just a ref. loadProjectFromText('{}') then
fell back to the default cover/casewrap mode with empty
layers, hence "opens the case wrap editor" and "design is
missing completely."
beginDesignObjectEdit now detects design.kind === 'linked',
pulls the resolved payload from linkedDesigns.peek, and opens
THAT for editing. The edit session is tagged with
{ isLinked, linkedContext } so finishDesignObjectEdit knows
to take the republish path on save instead of mutating
layer.design.payload (which doesn't exist for linked
layers).
3. Guard rails on the linked-edit path:
- Refuse if the resolver hasn't cached the source yet
("Refresh the layer and try again").
- Refuse if the layer is pinned ("Unpin to edit — pinned
versions are frozen").
- Refuse if the source author isn't the current user
("Only the source author can edit; fork or unlink it for
a local copy"). Soft permission — relays decide who can
replace a kind-30078 by pubkey + d-tag, so non-authors
couldn't republish even if we let them try.
On save, _republishLinkedDesignSource:
- Strips linked-design ephemera + builds attribution tags
(same as host publish).
- Publishes a fresh kind-30078 under the same author + d-tag,
which is NIP-33 in-place replace.
- clearCache(key) + seed both resolver caches from the new
signed event so every linked layer on this canvas paints the
updated version on next render.
- Refreshes the layer name from the (possibly edited) payload
title with the 🔗 prefix preserved.
finishDesignObjectEdit is now async; existing call sites are
fire-and-forget which is fine — user feedback flows through
toasts, not the return value.
This commit is contained in:
1 parent
d1d4118666
commit
81e33fab9f
1 file changed
+122
-13
+122
-13
@@ -11732,7 +11732,10 @@
|
||||
customArt: {
|
||||
width: Math.round(bbox.w * 96),
|
||||
height: Math.round(bbox.h * 96),
|
||||
background: '#ffffff',
|
||||
// Transparent so the extracted design composites cleanly
|
||||
// into any host. Users can pick a solid fill later via
|
||||
// the customart background control if they want one.
|
||||
background: 'transparent',
|
||||
},
|
||||
layers: clonedLayers,
|
||||
layerGroups: {},
|
||||
@@ -11939,23 +11942,57 @@
|
||||
toastError('Finish the current embedded-design edit first.');
|
||||
return;
|
||||
}
|
||||
// Linked layers don't carry a payload locally — the source
|
||||
// lives on relays. Open the cached resolved payload for
|
||||
// editing and remember the linked context so finishDesignObjectEdit
|
||||
// can republish the source under the same d-tag on save.
|
||||
const isLinked = layer.design?.kind === 'linked';
|
||||
let editingPayload = null;
|
||||
let linkedContext = null;
|
||||
if (isLinked) {
|
||||
const spec = { ref: layer.design.ref, pinnedEventId: layer.design.pinnedEventId };
|
||||
const cached = linkedDesigns.peek(spec);
|
||||
if (!cached?.payload) {
|
||||
toastError('Linked source is still loading or unavailable. Refresh the layer and try again.');
|
||||
return;
|
||||
}
|
||||
if (layer.design.pinnedEventId) {
|
||||
toastError('Unpin this layer to edit its source — pinned versions are frozen.');
|
||||
return;
|
||||
}
|
||||
const sourcePubkey = cached.sourceEvent?.pubkey;
|
||||
const myPubkey = state.community?.myPubkey || '';
|
||||
if (sourcePubkey && myPubkey && sourcePubkey !== myPubkey) {
|
||||
toastError("Only the source author can edit a linked design. Fork or unlink it to edit a local copy.");
|
||||
return;
|
||||
}
|
||||
editingPayload = cached.payload;
|
||||
linkedContext = { ref: layer.design.ref, sourceEvent: cached.sourceEvent };
|
||||
} else {
|
||||
editingPayload = layer.design?.payload || {};
|
||||
}
|
||||
_designObjectEdit = {
|
||||
hostPayload: currentEditableDesignPayload(),
|
||||
layerId: layer.id,
|
||||
layerName: layer.name || designObjectTitle(layer.design?.payload)
|
||||
layerName: layer.name || designObjectTitle(editingPayload),
|
||||
isLinked,
|
||||
linkedContext,
|
||||
};
|
||||
try {
|
||||
loadProjectFromText(JSON.stringify(layer.design?.payload || {}));
|
||||
loadProjectFromText(JSON.stringify(editingPayload));
|
||||
} catch (err) {
|
||||
_designObjectEdit = null;
|
||||
toastError(`Could not open embedded design: ${err?.message || err}`);
|
||||
toastError(`Could not open design: ${err?.message || err}`);
|
||||
return;
|
||||
}
|
||||
paintDesignObjectBanner();
|
||||
toastSuccess('Editing embedded design object. Use Done editing object to write it back.');
|
||||
toastSuccess(isLinked
|
||||
? '🔗 Editing linked source — Done republishes it to every host that links it.'
|
||||
: 'Editing embedded design object. Use Done editing object to write it back.'
|
||||
);
|
||||
}
|
||||
|
||||
function finishDesignObjectEdit(saveChanges) {
|
||||
async function finishDesignObjectEdit(saveChanges) {
|
||||
if (!_designObjectEdit) return;
|
||||
const edit = _designObjectEdit;
|
||||
const updatedPayload = saveChanges ? currentEditableDesignPayload() : null;
|
||||
@@ -11967,19 +12004,91 @@
|
||||
}
|
||||
_designObjectEdit = null;
|
||||
const layer = getLayer(edit.layerId);
|
||||
if (saveChanges && layer && layer.type === 'design') {
|
||||
pushHistory('update design object');
|
||||
layer.design = { mode: normalizeTemplateMode(updatedPayload), payload: clonePayload(updatedPayload) };
|
||||
layer.name = updatedPayload?.meta?.title || edit.layerName || layer.name;
|
||||
setLayerSelection([layer.id], layer.id);
|
||||
render();
|
||||
toastSuccess('Embedded design object updated.');
|
||||
if (saveChanges && layer && layer.type === 'design' && updatedPayload) {
|
||||
if (edit.isLinked && edit.linkedContext) {
|
||||
await _republishLinkedDesignSource(layer, updatedPayload, edit.linkedContext, edit.layerName);
|
||||
} else {
|
||||
pushHistory('update design object');
|
||||
layer.design = { mode: normalizeTemplateMode(updatedPayload), payload: clonePayload(updatedPayload) };
|
||||
layer.name = updatedPayload?.meta?.title || edit.layerName || layer.name;
|
||||
setLayerSelection([layer.id], layer.id);
|
||||
render();
|
||||
toastSuccess('Embedded design object updated.');
|
||||
}
|
||||
} else {
|
||||
toastSuccess('Returned to parent design.');
|
||||
}
|
||||
paintDesignObjectBanner();
|
||||
}
|
||||
|
||||
// Republish a linked-design source under the same d-tag with
|
||||
// the edited payload, then seed the resolver caches with the
|
||||
// new signed event so every linked layer on this canvas paints
|
||||
// the updated version on the next render. Only safe for the
|
||||
// source author — beginDesignObjectEdit blocks non-authors
|
||||
// from reaching this path.
|
||||
async function _republishLinkedDesignSource(layer, updatedPayload, linkedContext, prevLayerName) {
|
||||
const myPubkey = state.community?.myPubkey || '';
|
||||
if (!myPubkey) {
|
||||
toastError('Sign in with Nostr first.');
|
||||
return;
|
||||
}
|
||||
const ref = linkedContext.ref;
|
||||
const parsed = linkedDesigns._parseAddressCoord(ref);
|
||||
if (!parsed || parsed.pubkey !== myPubkey) {
|
||||
toastError('Only the source author can republish a linked design.');
|
||||
return;
|
||||
}
|
||||
const dTag = parsed.dTag;
|
||||
const mode = normalizeTemplateMode(updatedPayload);
|
||||
const payload = clonePayload(updatedPayload);
|
||||
stripLinkedDesignEphemera(payload);
|
||||
let attributionTags = [];
|
||||
try {
|
||||
const built = await _buildLinkedAttributionTags(payload, myPubkey);
|
||||
attributionTags = [...built.contributorTags, ...built.linkTags];
|
||||
} catch (err) {
|
||||
console.warn('[linked-edit] attribution build failed', err);
|
||||
}
|
||||
const typeTag = typeTagForMode(mode);
|
||||
const tags = [
|
||||
['d', dTag],
|
||||
...getTemplateTags(payload, typeTag),
|
||||
...attributionTags,
|
||||
];
|
||||
const evt = {
|
||||
kind: 30078,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags,
|
||||
content: JSON.stringify(payload),
|
||||
};
|
||||
const publishingToast = toast('Re-publishing linked source…', { duration: 30000 });
|
||||
let signed;
|
||||
try { signed = await publishCommunityEvent(evt); }
|
||||
catch (err) {
|
||||
publishingToast?.dismiss?.();
|
||||
toastError(err?.message || 'Could not re-publish the linked source.');
|
||||
return;
|
||||
}
|
||||
publishingToast?.dismiss?.();
|
||||
if (!signed?.id) {
|
||||
toastError('Republish succeeded but the relay did not return the event id.');
|
||||
return;
|
||||
}
|
||||
pushHistory('republish linked source');
|
||||
const key = linkedDesigns.keyFor({ ref, pinnedEventId: null });
|
||||
if (key) linkedDesigns.clearCache(key);
|
||||
try { await linkedDesigns.seed({ ref, pinnedEventId: null }, signed); }
|
||||
catch (err) { console.warn('[linked-edit] cache seed failed', err); }
|
||||
// Refresh the layer name from the (possibly edited) payload title.
|
||||
const newTitle = updatedPayload?.meta?.title?.trim();
|
||||
if (newTitle) layer.name = '🔗 ' + newTitle;
|
||||
else if (prevLayerName) layer.name = prevLayerName;
|
||||
setLayerSelection([layer.id], layer.id);
|
||||
render();
|
||||
toastSuccess('🔗 Linked source updated — every host that links it shows the new version on next refresh.');
|
||||
}
|
||||
|
||||
function paintDesignObjectBanner() {
|
||||
const banner = $('designObjectBanner');
|
||||
const title = $('designObjectTitle');
|
||||
|
||||
Reference in new issue
Block a user