dsh-image-gen 0.6.9 → 0.6.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +347 -130
- package/lib/client.js.map +1 -1
- package/lib/index.js +287 -217
- package/lib/types/canvas-asset-route.d.ts +12 -0
- package/lib/types/canvas-state.d.ts +3 -0
- package/lib/types/client/gallery-store.d.ts +3 -1
- package/lib/types/client/gallery-view.d.ts +2 -0
- package/lib/types/client/studio-style.d.ts +1 -1
- package/lib/types/client/tl/canvas-selection.d.ts +13 -0
- package/lib/types/client/tl/canvas-sync.d.ts +9 -1
- package/lib/types/client/tl/studio-tl-canvas.d.ts +3 -1
- package/lib/types/shared.d.ts +10 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -87,6 +87,8 @@ window.__ModuleLoader__.load({
|
|
|
87
87
|
const TEST_CONNECTION_ROUTE = "/plugins/dsh-image-gen/test";
|
|
88
88
|
/** Browser route the workbench infinite canvas pushes live state through. */
|
|
89
89
|
const CANVAS_STATE_ROUTE = "/plugins/dsh-image-gen/canvas-state";
|
|
90
|
+
/** Upload a selected canvas original to the host attachment store. */
|
|
91
|
+
const CANVAS_ASSET_ROUTE = "/plugins/dsh-image-gen/canvas-asset";
|
|
90
92
|
const SUBSCRIPTION_LOGIN_ROUTE = "/plugins/dsh-image-gen/subscription-login";
|
|
91
93
|
const SUBSCRIPTION_STATUS_ROUTE = "/plugins/dsh-image-gen/subscription-status";
|
|
92
94
|
/** Namespace persisted through DSH Settings. */
|
|
@@ -320,6 +322,7 @@ window.__ModuleLoader__.load({
|
|
|
320
322
|
resolve(request.result);
|
|
321
323
|
};
|
|
322
324
|
request.onerror = () => {
|
|
325
|
+
dbPromise = null;
|
|
323
326
|
reject(request.error);
|
|
324
327
|
};
|
|
325
328
|
});
|
|
@@ -370,11 +373,13 @@ window.__ModuleLoader__.load({
|
|
|
370
373
|
* Save or update a gallery record by attachmentId.
|
|
371
374
|
* Skipped if the item was previously deleted (tombstoned).
|
|
372
375
|
* Preserves existing isFavorite, tags, and original createdAt on re-renders.
|
|
376
|
+
* Returns false when the record was not persisted (e.g. IndexedDB unavailable)
|
|
377
|
+
* so callers can surface the failure and let the user retry.
|
|
373
378
|
*/
|
|
374
379
|
async function saveGalleryItem(item) {
|
|
375
380
|
try {
|
|
376
381
|
const db = await getDB();
|
|
377
|
-
if ((await loadTombstones(db)).has(item.id)) return;
|
|
382
|
+
if ((await loadTombstones(db)).has(item.id)) return true;
|
|
378
383
|
await new Promise((resolve, reject) => {
|
|
379
384
|
const store = db.transaction(STORE_NAME, "readwrite").objectStore(STORE_NAME);
|
|
380
385
|
const getReq = store.get(item.id);
|
|
@@ -398,8 +403,10 @@ window.__ModuleLoader__.load({
|
|
|
398
403
|
getReq.onerror = () => reject(getReq.error);
|
|
399
404
|
});
|
|
400
405
|
notifyListeners();
|
|
406
|
+
return true;
|
|
401
407
|
} catch (err) {
|
|
402
408
|
console.warn("[dsh-image-gen] Failed to save gallery item to IndexedDB:", err);
|
|
409
|
+
return false;
|
|
403
410
|
}
|
|
404
411
|
}
|
|
405
412
|
/**
|
|
@@ -115633,16 +115640,113 @@ img.ProseMirror-separator {
|
|
|
115633
115640
|
});
|
|
115634
115641
|
}
|
|
115635
115642
|
//#endregion
|
|
115643
|
+
//#region lib/types/client/tl/canvas-selection.js
|
|
115644
|
+
/** Bound raster dimensions before exporting; large multi-image PNGs get a
|
|
115645
|
+
* JPEG preview instead of being silently discarded. Originals are separate. */
|
|
115646
|
+
async function exportSelectionPreview(editor, shapes, maxBytes, signal) {
|
|
115647
|
+
const bounds = editor.getSelectionPageBounds();
|
|
115648
|
+
const scale = Math.min(1, 2048 / Math.max(1, (bounds?.w ?? 2048) + 32, (bounds?.h ?? 2048) + 32));
|
|
115649
|
+
const attempts = [
|
|
115650
|
+
{
|
|
115651
|
+
format: "png",
|
|
115652
|
+
scale,
|
|
115653
|
+
quality: 1
|
|
115654
|
+
},
|
|
115655
|
+
{
|
|
115656
|
+
format: "jpeg",
|
|
115657
|
+
scale,
|
|
115658
|
+
quality: .85
|
|
115659
|
+
},
|
|
115660
|
+
{
|
|
115661
|
+
format: "jpeg",
|
|
115662
|
+
scale: scale * .75,
|
|
115663
|
+
quality: .7
|
|
115664
|
+
},
|
|
115665
|
+
{
|
|
115666
|
+
format: "jpeg",
|
|
115667
|
+
scale: scale * .5,
|
|
115668
|
+
quality: .6
|
|
115669
|
+
},
|
|
115670
|
+
{
|
|
115671
|
+
format: "jpeg",
|
|
115672
|
+
scale: scale * .25,
|
|
115673
|
+
quality: .5
|
|
115674
|
+
}
|
|
115675
|
+
];
|
|
115676
|
+
const maxChars = Math.floor(Math.min(maxBytes, 4e6) / 3) * 4;
|
|
115677
|
+
for (const attempt of attempts) {
|
|
115678
|
+
signal.throwIfAborted();
|
|
115679
|
+
const { url } = await editor.toImageDataUrl(shapes, {
|
|
115680
|
+
background: true,
|
|
115681
|
+
padding: 16,
|
|
115682
|
+
pixelRatio: 1,
|
|
115683
|
+
...attempt
|
|
115684
|
+
});
|
|
115685
|
+
signal.throwIfAborted();
|
|
115686
|
+
if (url.length - url.indexOf(",") - 1 <= maxChars) return url;
|
|
115687
|
+
}
|
|
115688
|
+
throw new Error("preview-too-large");
|
|
115689
|
+
}
|
|
115690
|
+
async function readCanvasImageLimit(signal) {
|
|
115691
|
+
const response = await fetch(CANVAS_ASSET_ROUTE, {
|
|
115692
|
+
credentials: "same-origin",
|
|
115693
|
+
signal
|
|
115694
|
+
});
|
|
115695
|
+
if (!response.ok) throw new Error(`host-unavailable (${response.status})`);
|
|
115696
|
+
const value = await response.json();
|
|
115697
|
+
if (typeof value.maxImageBytes !== "number" || !Number.isSafeInteger(value.maxImageBytes) || value.maxImageBytes <= 0) throw new Error("host-unavailable");
|
|
115698
|
+
return value.maxImageBytes;
|
|
115699
|
+
}
|
|
115700
|
+
/** Use tldraw's resolver for IndexedDB-backed imports, not just asset.props.src.
|
|
115701
|
+
* Upload each original once per mount/source, with individual byte limits. */
|
|
115702
|
+
async function syncCanvasOriginal(editor, shape, maxBytes, signal, cache) {
|
|
115703
|
+
if (shape.type !== "image" || shape.props.assetId === null) throw new Error("original-unavailable");
|
|
115704
|
+
const asset = editor.getAsset(shape.props.assetId);
|
|
115705
|
+
if (asset?.type !== "image") throw new Error("original-unavailable");
|
|
115706
|
+
const cached = cache.get(asset.id);
|
|
115707
|
+
if (cached?.src === asset.props.src) return cached.attachment;
|
|
115708
|
+
const url = await editor.resolveAssetUrl(asset.id, { shouldResolveToOriginal: true });
|
|
115709
|
+
signal.throwIfAborted();
|
|
115710
|
+
if (url === null) throw new Error("original-unavailable");
|
|
115711
|
+
const response = await fetch(url, { signal });
|
|
115712
|
+
if (!response.ok) throw new Error("original-unavailable");
|
|
115713
|
+
const blob = await response.blob();
|
|
115714
|
+
if (blob.size > maxBytes) throw new Error("original-too-large");
|
|
115715
|
+
const mediaType = blob.type || asset.props.mimeType || "";
|
|
115716
|
+
if (![
|
|
115717
|
+
"image/png",
|
|
115718
|
+
"image/jpeg",
|
|
115719
|
+
"image/webp",
|
|
115720
|
+
"image/gif"
|
|
115721
|
+
].includes(mediaType)) throw new Error("original-format-unsupported");
|
|
115722
|
+
const uploaded = await fetch(CANVAS_ASSET_ROUTE, {
|
|
115723
|
+
method: "POST",
|
|
115724
|
+
credentials: "same-origin",
|
|
115725
|
+
headers: { "content-type": mediaType },
|
|
115726
|
+
body: blob,
|
|
115727
|
+
signal
|
|
115728
|
+
});
|
|
115729
|
+
if (!uploaded.ok) throw new Error(uploaded.status === 413 ? "original-too-large" : `original-upload-failed (${uploaded.status})`);
|
|
115730
|
+
const ref = (await uploaded.json()).attachment;
|
|
115731
|
+
if (!ref || typeof ref.attachmentId !== "string" || ![
|
|
115732
|
+
"image/png",
|
|
115733
|
+
"image/jpeg",
|
|
115734
|
+
"image/webp",
|
|
115735
|
+
"image/gif"
|
|
115736
|
+
].includes(ref.mediaType) || !Number.isSafeInteger(ref.bytes) || ref.bytes <= 0 || !Number.isInteger(ref.width) || ref.width <= 0 || !Number.isInteger(ref.height) || ref.height <= 0) throw new Error("original-upload-failed");
|
|
115737
|
+
cache.set(asset.id, {
|
|
115738
|
+
src: asset.props.src,
|
|
115739
|
+
attachment: ref
|
|
115740
|
+
});
|
|
115741
|
+
return ref;
|
|
115742
|
+
}
|
|
115743
|
+
//#endregion
|
|
115636
115744
|
//#region lib/types/client/tl/canvas-sync.js
|
|
115637
115745
|
/** Debounce for ordinary document/session changes. */
|
|
115638
115746
|
const STATE_DEBOUNCE_MS = 600;
|
|
115639
115747
|
/** Extra settle time before an expensive selection screenshot export. */
|
|
115640
115748
|
const SELECTION_EXPORT_DELAY_MS = 1200;
|
|
115641
|
-
/** Minimum spacing between two HTTP pushes. */
|
|
115642
|
-
const MIN_PUSH_INTERVAL_MS = 400;
|
|
115643
115749
|
const MAX_TEXT_CHARS = 96;
|
|
115644
|
-
/** Hard cap on the exported screenshot payload (base64 chars, ~5MB binary). */
|
|
115645
|
-
const MAX_SCREENSHOT_DATAURL_CHARS = 7e6;
|
|
115646
115750
|
/** Map a tldraw shape type onto the coarse model-facing node kind. */
|
|
115647
115751
|
function kindOf(shapeType) {
|
|
115648
115752
|
switch (shapeType) {
|
|
@@ -115749,69 +115853,66 @@ img.ProseMirror-separator {
|
|
|
115749
115853
|
const ids = editor.getSelectedShapeIds().map(String).sort().join("|");
|
|
115750
115854
|
return ids.length === 0 ? "none" : ids;
|
|
115751
115855
|
}
|
|
115752
|
-
|
|
115753
|
-
|
|
115754
|
-
* Returns a disposer that stops listening and marks the instance offline.
|
|
115755
|
-
*/
|
|
115756
|
-
function startCanvasSync(editor) {
|
|
115757
|
-
const clientInstance = String(editor.id);
|
|
115856
|
+
function startCanvasSync(editor, onStatus = () => {}) {
|
|
115857
|
+
const clientInstance = `${String(editor.id).slice(0, 36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
115758
115858
|
let disposed = false;
|
|
115759
115859
|
let stateTimer;
|
|
115760
115860
|
let exportTimer;
|
|
115761
115861
|
let lastPushKey = "";
|
|
115762
|
-
let lastSelectionKey =
|
|
115763
|
-
let
|
|
115764
|
-
let
|
|
115765
|
-
|
|
115862
|
+
let lastSelectionKey = selectionKeyOf(editor);
|
|
115863
|
+
let revision = 0;
|
|
115864
|
+
let sequence = 0;
|
|
115865
|
+
let imageLimit;
|
|
115866
|
+
let inFlight = new AbortController();
|
|
115867
|
+
let exporting = false;
|
|
115868
|
+
let exportAgain = false;
|
|
115869
|
+
let prepared = {};
|
|
115870
|
+
const originalCache = /* @__PURE__ */ new Map();
|
|
115871
|
+
let preparedItems;
|
|
115872
|
+
const stopListening = editor.store.listen((event) => {
|
|
115766
115873
|
if (disposed) return;
|
|
115767
|
-
const
|
|
115768
|
-
|
|
115769
|
-
|
|
115770
|
-
|
|
115771
|
-
|
|
115772
|
-
|
|
115773
|
-
|
|
115774
|
-
|
|
115874
|
+
const key = selectionKeyOf(editor);
|
|
115875
|
+
const changes = event.changes;
|
|
115876
|
+
const documentChanged = [
|
|
115877
|
+
...Object.values(changes.added),
|
|
115878
|
+
...Object.values(changes.removed),
|
|
115879
|
+
...Object.values(changes.updated).map((pair) => pair[1])
|
|
115880
|
+
].some((record) => record.typeName === "shape" || record.typeName === "asset" || record.typeName === "page");
|
|
115881
|
+
if (key !== lastSelectionKey || documentChanged) {
|
|
115882
|
+
lastSelectionKey = key;
|
|
115883
|
+
invalidate();
|
|
115775
115884
|
}
|
|
115776
115885
|
if (stateTimer === void 0) stateTimer = setTimeout(() => {
|
|
115777
115886
|
stateTimer = void 0;
|
|
115778
|
-
|
|
115887
|
+
pushState();
|
|
115779
115888
|
}, STATE_DEBOUNCE_MS);
|
|
115780
115889
|
}, {
|
|
115781
115890
|
source: "all",
|
|
115782
115891
|
scope: "all"
|
|
115783
115892
|
});
|
|
115784
|
-
|
|
115893
|
+
function invalidate() {
|
|
115894
|
+
revision++;
|
|
115895
|
+
inFlight.abort();
|
|
115896
|
+
inFlight = new AbortController();
|
|
115897
|
+
prepared = {};
|
|
115898
|
+
preparedItems = void 0;
|
|
115899
|
+
const count = editor.getSelectedShapeIds().length;
|
|
115900
|
+
onStatus({
|
|
115901
|
+
phase: count > 0 ? "preparing" : "idle",
|
|
115902
|
+
count
|
|
115903
|
+
});
|
|
115904
|
+
if (exportTimer !== void 0) clearTimeout(exportTimer);
|
|
115905
|
+
if (count > 0) exportTimer = setTimeout(() => {
|
|
115906
|
+
exportTimer = void 0;
|
|
115907
|
+
prepare();
|
|
115908
|
+
}, SELECTION_EXPORT_DELAY_MS);
|
|
115909
|
+
}
|
|
115910
|
+
async function pushState(force = false) {
|
|
115785
115911
|
if (disposed) return;
|
|
115912
|
+
const currentRevision = revision;
|
|
115786
115913
|
const summary = summarize(editor);
|
|
115787
|
-
|
|
115788
|
-
|
|
115789
|
-
if (elapsed < MIN_PUSH_INTERVAL_MS) {
|
|
115790
|
-
if (stateTimer !== void 0) clearTimeout(stateTimer);
|
|
115791
|
-
stateTimer = setTimeout(() => {
|
|
115792
|
-
stateTimer = void 0;
|
|
115793
|
-
flush(withScreenshot);
|
|
115794
|
-
}, MIN_PUSH_INTERVAL_MS - elapsed);
|
|
115795
|
-
return;
|
|
115796
|
-
}
|
|
115797
|
-
let selectionImage;
|
|
115798
|
-
if (withScreenshot && summary.selectionCount > 0) try {
|
|
115799
|
-
const selectedBefore = editor.getSelectedShapes();
|
|
115800
|
-
const { url } = await editor.toImageDataUrl(selectedBefore, {
|
|
115801
|
-
background: true,
|
|
115802
|
-
padding: 16,
|
|
115803
|
-
scale: 1,
|
|
115804
|
-
pixelRatio: 2
|
|
115805
|
-
});
|
|
115806
|
-
if (disposed) return;
|
|
115807
|
-
const selectedAfter = editor.getSelectedShapes();
|
|
115808
|
-
const idsBefore = new Set(selectedBefore.map((shape) => String(shape.id)));
|
|
115809
|
-
if (!(selectedBefore.length === selectedAfter.length && selectedAfter.every((shape) => idsBefore.has(String(shape.id))))) return;
|
|
115810
|
-
if (url.length > MAX_SCREENSHOT_DATAURL_CHARS) console.warn("[dsh-image-gen] canvas selection screenshot skipped: too large");
|
|
115811
|
-
else selectionImage = url;
|
|
115812
|
-
} catch (error) {
|
|
115813
|
-
console.warn("[dsh-image-gen] canvas selection screenshot failed:", error);
|
|
115814
|
-
}
|
|
115914
|
+
const key = `${revision}:${summary.stateKey}`;
|
|
115915
|
+
if (!force && key === lastPushKey) return;
|
|
115815
115916
|
const payload = {
|
|
115816
115917
|
clientInstance,
|
|
115817
115918
|
connected: true,
|
|
@@ -115820,43 +115921,104 @@ img.ProseMirror-separator {
|
|
|
115820
115921
|
selection: {
|
|
115821
115922
|
count: summary.selectionCount,
|
|
115822
115923
|
kinds: summary.selectionKinds,
|
|
115823
|
-
items: summary.selectionItems
|
|
115924
|
+
items: preparedItems ?? summary.selectionItems
|
|
115824
115925
|
},
|
|
115926
|
+
selectionRevision: String(revision),
|
|
115927
|
+
...summary.selectionCount > 0 ? { selectionStatus: "preparing" } : {},
|
|
115928
|
+
...prepared,
|
|
115929
|
+
sequence: ++sequence,
|
|
115825
115930
|
updatedAt: Date.now()
|
|
115826
115931
|
};
|
|
115827
|
-
|
|
115828
|
-
lastPushKey = summary.stateKey;
|
|
115829
|
-
lastPostAt = Date.now();
|
|
115830
|
-
const controller = new AbortController();
|
|
115831
|
-
inFlight = controller;
|
|
115932
|
+
const requestSequence = sequence;
|
|
115832
115933
|
try {
|
|
115833
|
-
await fetch(CANVAS_STATE_ROUTE, {
|
|
115934
|
+
const response = await fetch(CANVAS_STATE_ROUTE, {
|
|
115834
115935
|
method: "POST",
|
|
115835
115936
|
credentials: "same-origin",
|
|
115836
115937
|
headers: { "content-type": "application/json" },
|
|
115837
115938
|
body: JSON.stringify(payload),
|
|
115838
|
-
signal:
|
|
115939
|
+
signal: AbortSignal.any([inFlight.signal, AbortSignal.timeout(3e4)])
|
|
115940
|
+
});
|
|
115941
|
+
if (!response.ok) throw new Error(`state-sync-failed (${response.status})`);
|
|
115942
|
+
if (disposed || revision !== currentRevision || sequence !== requestSequence) return;
|
|
115943
|
+
lastPushKey = key;
|
|
115944
|
+
onStatus({
|
|
115945
|
+
phase: payload.selectionStatus ?? "idle",
|
|
115946
|
+
count: summary.selectionCount,
|
|
115947
|
+
...payload.selectionError ? { error: payload.selectionError } : {}
|
|
115839
115948
|
});
|
|
115840
115949
|
} catch (error) {
|
|
115841
|
-
if (
|
|
115950
|
+
if (disposed || revision !== currentRevision || sequence !== requestSequence) return;
|
|
115842
115951
|
lastPushKey = "";
|
|
115952
|
+
onStatus({
|
|
115953
|
+
phase: "error",
|
|
115954
|
+
count: summary.selectionCount,
|
|
115955
|
+
error: error instanceof Error ? error.message : "state-sync-failed"
|
|
115956
|
+
});
|
|
115957
|
+
}
|
|
115958
|
+
}
|
|
115959
|
+
async function prepare() {
|
|
115960
|
+
if (disposed) return;
|
|
115961
|
+
if (exporting) {
|
|
115962
|
+
exportAgain = true;
|
|
115963
|
+
return;
|
|
115964
|
+
}
|
|
115965
|
+
exporting = true;
|
|
115966
|
+
const currentRevision = revision;
|
|
115967
|
+
const signal = AbortSignal.any([inFlight.signal, AbortSignal.timeout(3e4)]);
|
|
115968
|
+
const shapes = editor.getSelectedShapes();
|
|
115969
|
+
const items = shapes.slice(0, 16).map((shape) => describeShape(editor, shape));
|
|
115970
|
+
let selectionImage;
|
|
115971
|
+
let failure;
|
|
115972
|
+
try {
|
|
115973
|
+
imageLimit ??= await readCanvasImageLimit(signal);
|
|
115974
|
+
try {
|
|
115975
|
+
selectionImage = await exportSelectionPreview(editor, shapes, imageLimit, signal);
|
|
115976
|
+
} catch (error) {
|
|
115977
|
+
signal.throwIfAborted();
|
|
115978
|
+
failure = error instanceof Error ? error.message : "preview-export-failed";
|
|
115979
|
+
}
|
|
115980
|
+
if (shapes.length > 16 && shapes.some((shape) => shape.type === "image")) throw new Error("too-many-selected-shapes");
|
|
115981
|
+
for (const [index, shape] of shapes.entries()) {
|
|
115982
|
+
signal.throwIfAborted();
|
|
115983
|
+
if (shape.type !== "image") continue;
|
|
115984
|
+
const item = items[index];
|
|
115985
|
+
if (item === void 0) continue;
|
|
115986
|
+
const attachment = await syncCanvasOriginal(editor, shape, imageLimit, signal, originalCache);
|
|
115987
|
+
item.attachment = attachment;
|
|
115988
|
+
item.attachmentId = attachment.attachmentId;
|
|
115989
|
+
}
|
|
115990
|
+
} catch (error) {
|
|
115991
|
+
failure = error instanceof Error ? error.message : "selection-sync-failed";
|
|
115992
|
+
} finally {
|
|
115993
|
+
exporting = false;
|
|
115994
|
+
}
|
|
115995
|
+
if (!disposed && revision === currentRevision) {
|
|
115996
|
+
preparedItems = items;
|
|
115997
|
+
prepared = {
|
|
115998
|
+
selectionStatus: failure === void 0 ? "ready" : "error",
|
|
115999
|
+
...failure === void 0 ? {} : { selectionError: failure.slice(0, 300) },
|
|
116000
|
+
...selectionImage === void 0 ? {} : { selectionImage }
|
|
116001
|
+
};
|
|
116002
|
+
await pushState(true);
|
|
116003
|
+
}
|
|
116004
|
+
if (exportAgain && !disposed) {
|
|
116005
|
+
exportAgain = false;
|
|
116006
|
+
prepare();
|
|
115843
116007
|
}
|
|
115844
116008
|
}
|
|
116009
|
+
invalidate();
|
|
115845
116010
|
stateTimer = setTimeout(() => {
|
|
115846
116011
|
stateTimer = void 0;
|
|
115847
|
-
|
|
116012
|
+
pushState();
|
|
115848
116013
|
}, STATE_DEBOUNCE_MS);
|
|
115849
|
-
|
|
115850
|
-
exportTimer = void 0;
|
|
115851
|
-
flush(true);
|
|
115852
|
-
}, SELECTION_EXPORT_DELAY_MS);
|
|
115853
|
-
return () => {
|
|
116014
|
+
const stop = () => {
|
|
115854
116015
|
if (disposed) return;
|
|
115855
116016
|
disposed = true;
|
|
115856
116017
|
if (stateTimer !== void 0) clearTimeout(stateTimer);
|
|
115857
116018
|
if (exportTimer !== void 0) clearTimeout(exportTimer);
|
|
115858
116019
|
stopListening();
|
|
115859
|
-
inFlight
|
|
116020
|
+
inFlight.abort();
|
|
116021
|
+
originalCache.clear();
|
|
115860
116022
|
fetch(CANVAS_STATE_ROUTE, {
|
|
115861
116023
|
method: "POST",
|
|
115862
116024
|
credentials: "same-origin",
|
|
@@ -115866,10 +116028,15 @@ img.ProseMirror-separator {
|
|
|
115866
116028
|
clientInstance,
|
|
115867
116029
|
connected: false,
|
|
115868
116030
|
nodeCount: 0,
|
|
116031
|
+
sequence: ++sequence,
|
|
115869
116032
|
updatedAt: Date.now()
|
|
115870
116033
|
})
|
|
115871
116034
|
}).catch(() => {});
|
|
115872
116035
|
};
|
|
116036
|
+
return Object.assign(stop, { retry: () => {
|
|
116037
|
+
invalidate();
|
|
116038
|
+
pushState(true);
|
|
116039
|
+
} });
|
|
115873
116040
|
}
|
|
115874
116041
|
//#endregion
|
|
115875
116042
|
//#region lib/types/client/tl/tl-canvas-bridge.js
|
|
@@ -116112,56 +116279,84 @@ img.ProseMirror-separator {
|
|
|
116112
116279
|
}
|
|
116113
116280
|
return true;
|
|
116114
116281
|
}
|
|
116115
|
-
const
|
|
116282
|
+
const TL_COMPONENTS = { Toolbar: StudioToolbar };
|
|
116283
|
+
const StudioTlCanvas = (0, react.memo)(function StudioTlCanvas({ lang = "zh" }) {
|
|
116116
116284
|
const sessionId = (0, react.useId)();
|
|
116117
|
-
|
|
116285
|
+
const [syncStatus, setSyncStatus] = (0, react.useState)({
|
|
116286
|
+
phase: "idle",
|
|
116287
|
+
count: 0
|
|
116288
|
+
});
|
|
116289
|
+
const retrySync = (0, react.useRef)(() => {});
|
|
116290
|
+
const onMount = (0, react.useCallback)((editor) => {
|
|
116291
|
+
let active = true;
|
|
116292
|
+
applyBrandTheme(editor);
|
|
116293
|
+
editor.updateInstanceState({ isGridMode: true });
|
|
116294
|
+
console.info(`[dsh-image-gen] tldraw mounted (instance ${editor.id})`);
|
|
116295
|
+
const stopLanding = registerTlLandingConsumer((items) => {
|
|
116296
|
+
const generation = getTlLandingGeneration();
|
|
116297
|
+
return landTlItems(editor, items, () => active && generation === getTlLandingGeneration());
|
|
116298
|
+
});
|
|
116299
|
+
let stopSync = startCanvasSync(editor, setSyncStatus);
|
|
116300
|
+
retrySync.current = () => stopSync.retry();
|
|
116301
|
+
const clear = () => {
|
|
116302
|
+
stopSync();
|
|
116303
|
+
editor.complete();
|
|
116304
|
+
editor.run(() => {
|
|
116305
|
+
editor.selectNone();
|
|
116306
|
+
const pages = editor.getPages();
|
|
116307
|
+
for (const page of pages) editor.deleteShapes([...editor.getPageShapeIds(page.id)]);
|
|
116308
|
+
for (const page of pages.slice(1)) editor.deletePage(page.id);
|
|
116309
|
+
editor.deleteAssets(editor.getAssets());
|
|
116310
|
+
editor.setCamera({
|
|
116311
|
+
x: 0,
|
|
116312
|
+
y: 0,
|
|
116313
|
+
z: 1
|
|
116314
|
+
});
|
|
116315
|
+
}, {
|
|
116316
|
+
history: "ignore",
|
|
116317
|
+
ignoreShapeLock: true
|
|
116318
|
+
});
|
|
116319
|
+
editor.clearHistory();
|
|
116320
|
+
stopSync = startCanvasSync(editor, setSyncStatus);
|
|
116321
|
+
};
|
|
116322
|
+
mountedCanvases.add(clear);
|
|
116323
|
+
return () => {
|
|
116324
|
+
active = false;
|
|
116325
|
+
mountedCanvases.delete(clear);
|
|
116326
|
+
stopLanding();
|
|
116327
|
+
stopSync();
|
|
116328
|
+
retrySync.current = () => {};
|
|
116329
|
+
};
|
|
116330
|
+
}, []);
|
|
116331
|
+
const statusText = lang === "en" ? syncStatus.phase === "ready" ? `Selection ready · ${syncStatus.count} shapes` : syncStatus.phase === "preparing" ? `Preparing selection · ${syncStatus.count} shapes…` : "Selection sync failed" : syncStatus.phase === "ready" ? `选区已就绪 · ${syncStatus.count} 个元素` : syncStatus.phase === "preparing" ? `正在准备选区 · ${syncStatus.count} 个元素…` : "选区同步失败";
|
|
116332
|
+
return (0, react_jsx_runtime.jsxs)("div", {
|
|
116118
116333
|
className: "dsh-ig-tl-canvas",
|
|
116119
|
-
children: (0, react_jsx_runtime.jsx)(Tldraw, {
|
|
116334
|
+
children: [(0, react_jsx_runtime.jsx)(Tldraw, {
|
|
116120
116335
|
persistenceKey: "dsh-image-gen-workbench-v1",
|
|
116121
116336
|
sessionId,
|
|
116122
|
-
components:
|
|
116123
|
-
onMount
|
|
116124
|
-
|
|
116125
|
-
|
|
116126
|
-
|
|
116127
|
-
|
|
116128
|
-
|
|
116129
|
-
|
|
116130
|
-
|
|
116131
|
-
|
|
116132
|
-
|
|
116133
|
-
|
|
116134
|
-
|
|
116135
|
-
editor.complete();
|
|
116136
|
-
editor.run(() => {
|
|
116137
|
-
editor.selectNone();
|
|
116138
|
-
const pages = editor.getPages();
|
|
116139
|
-
for (const page of pages) editor.deleteShapes([...editor.getPageShapeIds(page.id)]);
|
|
116140
|
-
for (const page of pages.slice(1)) editor.deletePage(page.id);
|
|
116141
|
-
editor.deleteAssets(editor.getAssets());
|
|
116142
|
-
editor.setCamera({
|
|
116143
|
-
x: 0,
|
|
116144
|
-
y: 0,
|
|
116145
|
-
z: 1
|
|
116146
|
-
});
|
|
116147
|
-
}, {
|
|
116148
|
-
history: "ignore",
|
|
116149
|
-
ignoreShapeLock: true
|
|
116150
|
-
});
|
|
116151
|
-
editor.clearHistory();
|
|
116152
|
-
stopSync = startCanvasSync(editor);
|
|
116153
|
-
};
|
|
116154
|
-
mountedCanvases.add(clear);
|
|
116155
|
-
return () => {
|
|
116156
|
-
active = false;
|
|
116157
|
-
mountedCanvases.delete(clear);
|
|
116158
|
-
stopLanding();
|
|
116159
|
-
stopSync();
|
|
116160
|
-
};
|
|
116161
|
-
}
|
|
116162
|
-
})
|
|
116337
|
+
components: TL_COMPONENTS,
|
|
116338
|
+
onMount
|
|
116339
|
+
}), syncStatus.phase !== "idle" && (0, react_jsx_runtime.jsxs)("div", {
|
|
116340
|
+
className: "dsh-ig-canvas-sync-status",
|
|
116341
|
+
"data-phase": syncStatus.phase,
|
|
116342
|
+
role: "status",
|
|
116343
|
+
"aria-live": "polite",
|
|
116344
|
+
children: [(0, react_jsx_runtime.jsxs)("span", { children: [statusText, syncStatus.phase === "error" ? `:${canvasSyncError(syncStatus.error, lang)}` : ""] }), syncStatus.phase === "error" && (0, react_jsx_runtime.jsx)("button", {
|
|
116345
|
+
type: "button",
|
|
116346
|
+
onClick: () => retrySync.current(),
|
|
116347
|
+
children: lang === "en" ? "Retry" : "重试"
|
|
116348
|
+
})]
|
|
116349
|
+
})]
|
|
116163
116350
|
});
|
|
116164
116351
|
});
|
|
116352
|
+
function canvasSyncError(error, lang) {
|
|
116353
|
+
const en = lang === "en";
|
|
116354
|
+
if (error?.includes("too-many-selected")) return en ? "Select at most 16 shapes to edit with originals." : "使用原图编辑时,请一次选择不超过 16 个元素。";
|
|
116355
|
+
if (error?.includes("too-large")) return en ? "An image exceeds the size limit. Reduce its size and retry." : "图片超出大小限制,请缩小后重试。";
|
|
116356
|
+
if (error?.includes("unsupported")) return en ? "Use PNG, JPEG, WebP or GIF images." : "请使用 PNG、JPEG、WebP 或 GIF 图片。";
|
|
116357
|
+
if (error?.includes("host-unavailable")) return en ? "Restart DSH to load the updated plugin, then retry." : "请重启 DSH 加载新版插件后重试。";
|
|
116358
|
+
return en ? "Check the connection or re-import an unavailable image, then retry." : "请检查连接;若原图已失效,请重新导入后重试。";
|
|
116359
|
+
}
|
|
116165
116360
|
//#endregion
|
|
116166
116361
|
//#region lib/types/client/studio-view.js
|
|
116167
116362
|
/** Below this workbench width the recent-generations rail folds automatically. */
|
|
@@ -116246,6 +116441,7 @@ img.ProseMirror-separator {
|
|
|
116246
116441
|
favorited: "已收藏",
|
|
116247
116442
|
favoriteAdded: "已添加到收藏",
|
|
116248
116443
|
favoriteRemoved: "已取消收藏",
|
|
116444
|
+
gallerySaveFailed: "保存到图库失败,请重试",
|
|
116249
116445
|
loadMore: "加载更多 ({n})",
|
|
116250
116446
|
deleteModalTitle: "从图库删除",
|
|
116251
116447
|
deleteModalDesc: "确定从图库中删除这张图片吗?(原聊天记录不会受影响)",
|
|
@@ -116354,6 +116550,7 @@ img.ProseMirror-separator {
|
|
|
116354
116550
|
favorited: "Favorited",
|
|
116355
116551
|
favoriteAdded: "Added to favorites",
|
|
116356
116552
|
favoriteRemoved: "Removed from favorites",
|
|
116553
|
+
gallerySaveFailed: "Failed to save to the gallery, please retry",
|
|
116357
116554
|
loadMore: "Load more ({n})",
|
|
116358
116555
|
deleteModalTitle: "Delete from gallery",
|
|
116359
116556
|
deleteModalDesc: "Remove this image from the local gallery? (Chat history remains unaffected)",
|
|
@@ -117018,7 +117215,7 @@ img.ProseMirror-separator {
|
|
|
117018
117215
|
...targetRoot ? { workspacePath: targetRoot } : {},
|
|
117019
117216
|
...workspace?.workspaceId ? { workspaceId: workspace.workspaceId } : {}
|
|
117020
117217
|
};
|
|
117021
|
-
await saveGalleryItem(updatedItem);
|
|
117218
|
+
if (!await saveGalleryItem(updatedItem)) throw new Error(t("gallerySaveFailed"));
|
|
117022
117219
|
return updatedItem;
|
|
117023
117220
|
};
|
|
117024
117221
|
const handleSaveToGallery = async () => {
|
|
@@ -117112,16 +117309,29 @@ img.ProseMirror-separator {
|
|
|
117112
117309
|
} : i) : null);
|
|
117113
117310
|
flash(nextFav ? t("favoriteAdded") : t("favoriteRemoved"));
|
|
117114
117311
|
} else {
|
|
117115
|
-
|
|
117116
|
-
|
|
117117
|
-
|
|
117118
|
-
|
|
117119
|
-
|
|
117120
|
-
|
|
117121
|
-
|
|
117122
|
-
|
|
117123
|
-
|
|
117124
|
-
|
|
117312
|
+
if (!!selected.isFavorite) {
|
|
117313
|
+
setSelected((curr) => curr !== null && curr.id === targetId ? {
|
|
117314
|
+
...curr,
|
|
117315
|
+
isFavorite: false
|
|
117316
|
+
} : curr);
|
|
117317
|
+
if (currentBatch) setCurrentBatch((prev) => prev ? prev.map((i) => i.id === targetId ? {
|
|
117318
|
+
...i,
|
|
117319
|
+
isFavorite: false
|
|
117320
|
+
} : i) : null);
|
|
117321
|
+
flash(t("favoriteRemoved"));
|
|
117322
|
+
return;
|
|
117323
|
+
}
|
|
117324
|
+
try {
|
|
117325
|
+
const updated = await saveGalleryEntry({
|
|
117326
|
+
...selected,
|
|
117327
|
+
isFavorite: true
|
|
117328
|
+
});
|
|
117329
|
+
setSelected((curr) => curr !== null && curr.id === targetId ? updated : curr);
|
|
117330
|
+
if (currentBatch) setCurrentBatch((prev) => prev ? prev.map((i) => i.id === targetId ? updated : i) : prev);
|
|
117331
|
+
flash(t("favoriteAdded"));
|
|
117332
|
+
} catch (err) {
|
|
117333
|
+
setError(err instanceof Error ? err.message : t("gallerySaveFailed"));
|
|
117334
|
+
}
|
|
117125
117335
|
}
|
|
117126
117336
|
};
|
|
117127
117337
|
const stepLightbox = (delta) => {
|
|
@@ -117353,7 +117563,7 @@ img.ProseMirror-separator {
|
|
|
117353
117563
|
})
|
|
117354
117564
|
]
|
|
117355
117565
|
}),
|
|
117356
|
-
canvasSurface === "infinite" ? (0, react_jsx_runtime.jsx)(StudioTlCanvas, {}) : (0, react_jsx_runtime.jsx)("div", {
|
|
117566
|
+
canvasSurface === "infinite" ? (0, react_jsx_runtime.jsx)(StudioTlCanvas, { lang }) : (0, react_jsx_runtime.jsx)("div", {
|
|
117357
117567
|
className: `dsh-ig-canvas ${isDragging ? "is-dragging" : ""}`,
|
|
117358
117568
|
ref: canvasRef,
|
|
117359
117569
|
onMouseDown: handleCanvasMouseDown,
|
|
@@ -119434,6 +119644,7 @@ img.ProseMirror-separator {
|
|
|
119434
119644
|
confirmRegenerate: "开始生成",
|
|
119435
119645
|
regenerating: "正在重新生成…",
|
|
119436
119646
|
regenerateSuccess: "已生成新图片并收录到画廊",
|
|
119647
|
+
regenerateSaveFailed: "图片已生成,但保存到图库失败,请重试",
|
|
119437
119648
|
regenerateFailed: "重新生成失败",
|
|
119438
119649
|
delete: "从画廊删除",
|
|
119439
119650
|
confirmDelete: "确定要从画廊中删除这张图片吗?(不会影响原聊天记录)",
|
|
@@ -119519,6 +119730,7 @@ img.ProseMirror-separator {
|
|
|
119519
119730
|
confirmRegenerate: "Generate",
|
|
119520
119731
|
regenerating: "Regenerating…",
|
|
119521
119732
|
regenerateSuccess: "New image generated and added to gallery",
|
|
119733
|
+
regenerateSaveFailed: "Image generated, but saving to the gallery failed. Please retry.",
|
|
119522
119734
|
regenerateFailed: "Regeneration failed",
|
|
119523
119735
|
delete: "Delete from gallery",
|
|
119524
119736
|
confirmDelete: "Are you sure you want to remove this image from the gallery? (Chat history will not be affected)",
|
|
@@ -119934,11 +120146,11 @@ img.ProseMirror-separator {
|
|
|
119934
120146
|
...activeWorkspace?.workspaceId ? { workspaceId: activeWorkspace.workspaceId } : {},
|
|
119935
120147
|
...currentSessionId ? { sessionId: currentSessionId } : {}
|
|
119936
120148
|
};
|
|
119937
|
-
await saveGalleryItem(newItem);
|
|
120149
|
+
const savedOk = await saveGalleryItem(newItem);
|
|
119938
120150
|
if (activeTab !== "gallery") setActiveTab("gallery");
|
|
119939
120151
|
if (search.trim().length > 0) setSearch("");
|
|
119940
120152
|
openPreviewItem(newItem);
|
|
119941
|
-
showToast(t("regenerateSuccess"));
|
|
120153
|
+
showToast(savedOk ? t("regenerateSuccess") : t("regenerateSaveFailed"));
|
|
119942
120154
|
} catch (err) {
|
|
119943
120155
|
if (controller.signal.aborted) return;
|
|
119944
120156
|
showToast(err instanceof Error ? err.message : String(err));
|
|
@@ -121284,6 +121496,9 @@ img.ProseMirror-separator {
|
|
|
121284
121496
|
|
|
121285
121497
|
/* tldraw infinite canvas surface (P0 spike) */
|
|
121286
121498
|
.dsh-ig-tl-canvas{position:relative;flex:1 1 0;height:0;min-height:0;overflow:hidden;isolation:isolate}
|
|
121499
|
+
.dsh-ig-canvas-sync-status{position:absolute;z-index:500;bottom:48px;left:60px;right:16px;display:flex;align-items:center;justify-content:center;gap:8px;max-width:560px;margin:auto;padding:7px 12px;border:1px solid var(--dsh-ig-border);border-radius:10px;background:var(--dsh-ig-surface,#fff);color:var(--dsh-ig-text,#475569);box-shadow:0 2px 8px #0001;font-size:12px;pointer-events:none}
|
|
121500
|
+
.dsh-ig-canvas-sync-status[data-phase='error']{color:#b42318;border-color:#fecdca}
|
|
121501
|
+
.dsh-ig-canvas-sync-status button{pointer-events:auto;flex-shrink:0;border:1px solid currentColor;border-radius:6px;padding:3px 9px;background:transparent;color:inherit;cursor:pointer}
|
|
121287
121502
|
|
|
121288
121503
|
/* ---- Right-sidebar seat variant (DSH 0.1.5 'sidebar.right.pane.tab') ----
|
|
121289
121504
|
The seat is a narrow panel beside the conversation column, not a full
|
|
@@ -122005,7 +122220,7 @@ img.ProseMirror-separator {
|
|
|
122005
122220
|
`;
|
|
122006
122221
|
//#endregion
|
|
122007
122222
|
//#region lib/types/client/index.js
|
|
122008
|
-
console.info(`[dsh-image-gen] canvas bundle 2026-09-
|
|
122223
|
+
console.info(`[dsh-image-gen] canvas bundle 2026-09-20T16:35:15.504Z`);
|
|
122009
122224
|
/** Right-sidebar tab-type registration id (DSH 0.1.5 `sidebarRightTabs`). */
|
|
122010
122225
|
const SIDEBAR_STUDIO_TAB_ID = "dsh-image-gen/studio";
|
|
122011
122226
|
/** The tab kind: a page type opened by kind, recognizing no resource address. */
|
|
@@ -122149,6 +122364,7 @@ img.ProseMirror-separator {
|
|
|
122149
122364
|
confirmRegenerate: "确认生成",
|
|
122150
122365
|
regenerating: "重新生成中…",
|
|
122151
122366
|
regenerateFailed: "重新生成失败",
|
|
122367
|
+
regenerateSaveFailed: "已重新生成,但保存到图库失败,请重试",
|
|
122152
122368
|
versionPrevious: "上一版本",
|
|
122153
122369
|
versionNext: "下一版本",
|
|
122154
122370
|
versionLabel: "图片版本 {current}/{total}"
|
|
@@ -122291,6 +122507,7 @@ img.ProseMirror-separator {
|
|
|
122291
122507
|
confirmRegenerate: "Regenerate",
|
|
122292
122508
|
regenerating: "Regenerating…",
|
|
122293
122509
|
regenerateFailed: "Regeneration failed",
|
|
122510
|
+
regenerateSaveFailed: "Regenerated, but saving to the gallery failed. Please retry.",
|
|
122294
122511
|
versionPrevious: "Previous version",
|
|
122295
122512
|
versionNext: "Next version",
|
|
122296
122513
|
versionLabel: "Image version {current}/{total}"
|
|
@@ -124602,7 +124819,7 @@ img.ProseMirror-separator {
|
|
|
124602
124819
|
ratio: request.ratio,
|
|
124603
124820
|
quality: request.quality
|
|
124604
124821
|
};
|
|
124605
|
-
await saveGalleryItem({
|
|
124822
|
+
const savedOk = await saveGalleryItem({
|
|
124606
124823
|
id: String(revision.attachment.attachmentId),
|
|
124607
124824
|
attachment: revision.attachment,
|
|
124608
124825
|
prompt: revision.prompt,
|
|
@@ -124617,7 +124834,7 @@ img.ProseMirror-separator {
|
|
|
124617
124834
|
});
|
|
124618
124835
|
const next = appendConversationImageRevision(String(originId), revision);
|
|
124619
124836
|
setRevisionChain(next);
|
|
124620
|
-
setToast(t("regenerate"));
|
|
124837
|
+
setToast(savedOk ? t("regenerate") : t("regenerateSaveFailed"));
|
|
124621
124838
|
setTimeout(() => {
|
|
124622
124839
|
setToast(void 0);
|
|
124623
124840
|
}, 2e3);
|