dsh-image-gen 0.6.9 → 0.6.11
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 +620 -146
- package/lib/client.js.map +1 -1
- package/lib/index.js +304 -218
- package/lib/types/canvas-asset-route.d.ts +12 -0
- package/lib/types/canvas-state.d.ts +3 -0
- package/lib/types/client/browser-image-utils.d.ts +9 -0
- package/lib/types/client/gallery-store.d.ts +7 -1
- package/lib/types/client/gallery-view.d.ts +24 -2
- 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
|
/**
|
|
@@ -508,6 +515,31 @@ window.__ModuleLoader__.load({
|
|
|
508
515
|
notifyListeners();
|
|
509
516
|
}
|
|
510
517
|
/**
|
|
518
|
+
* Bulk update favorite status for multiple gallery records in a single transaction.
|
|
519
|
+
*/
|
|
520
|
+
async function bulkSetFavoriteGalleryItems(ids, isFavorite) {
|
|
521
|
+
if (ids.length === 0) return;
|
|
522
|
+
const db = await getDB();
|
|
523
|
+
await new Promise((resolve, reject) => {
|
|
524
|
+
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
525
|
+
const store = tx.objectStore(STORE_NAME);
|
|
526
|
+
for (const id of ids) {
|
|
527
|
+
const getReq = store.get(id);
|
|
528
|
+
getReq.onsuccess = () => {
|
|
529
|
+
const item = getReq.result;
|
|
530
|
+
if (item && item.isFavorite !== isFavorite) {
|
|
531
|
+
item.isFavorite = isFavorite;
|
|
532
|
+
store.put(item);
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
tx.oncomplete = () => resolve();
|
|
537
|
+
tx.onerror = () => reject(tx.error ?? /* @__PURE__ */ new Error("IndexedDB transaction failed"));
|
|
538
|
+
tx.onabort = () => reject(tx.error ?? /* @__PURE__ */ new Error("IndexedDB transaction aborted"));
|
|
539
|
+
});
|
|
540
|
+
notifyListeners();
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
511
543
|
* Standardize path format across Windows and POSIX (lowercase, forward slashes, no trailing slash).
|
|
512
544
|
*/
|
|
513
545
|
function normalizeWorkspacePath(rawPath) {
|
|
@@ -1247,6 +1279,19 @@ window.__ModuleLoader__.load({
|
|
|
1247
1279
|
* This source code is licensed under the ISC license.
|
|
1248
1280
|
* See the LICENSE file in the root directory of this source tree.
|
|
1249
1281
|
*/
|
|
1282
|
+
const SquareCheckBig = createLucideIcon("square-check-big", [["path", {
|
|
1283
|
+
d: "M21 10.656V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h12.344",
|
|
1284
|
+
key: "2acyp4"
|
|
1285
|
+
}], ["path", {
|
|
1286
|
+
d: "m9 11 3 3L22 4",
|
|
1287
|
+
key: "1pflzl"
|
|
1288
|
+
}]]);
|
|
1289
|
+
/**
|
|
1290
|
+
* @license lucide-react v1.38.0 - ISC
|
|
1291
|
+
*
|
|
1292
|
+
* This source code is licensed under the ISC license.
|
|
1293
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
1294
|
+
*/
|
|
1250
1295
|
const Star = createLucideIcon("star", [["path", {
|
|
1251
1296
|
d: "M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z",
|
|
1252
1297
|
key: "r04s7s"
|
|
@@ -1556,6 +1601,89 @@ window.__ModuleLoader__.load({
|
|
|
1556
1601
|
const d = new Date(timestamp);
|
|
1557
1602
|
return `${d.getFullYear()}/${d.getMonth() + 1}/${d.getDate()}`;
|
|
1558
1603
|
}
|
|
1604
|
+
const CRC_TABLE = /* @__PURE__ */ new Uint32Array(256);
|
|
1605
|
+
for (let i = 0; i < 256; i++) {
|
|
1606
|
+
let c = i;
|
|
1607
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
1608
|
+
CRC_TABLE[i] = c;
|
|
1609
|
+
}
|
|
1610
|
+
function crc32(buf) {
|
|
1611
|
+
let crc = 4294967295;
|
|
1612
|
+
for (let i = 0; i < buf.length; i++) {
|
|
1613
|
+
const byte = buf[i] ?? 0;
|
|
1614
|
+
crc = (CRC_TABLE[(crc ^ byte) & 255] ?? 0) ^ crc >>> 8;
|
|
1615
|
+
}
|
|
1616
|
+
return (crc ^ 4294967295) >>> 0;
|
|
1617
|
+
}
|
|
1618
|
+
/**
|
|
1619
|
+
* Create a standard zero-compression (Store mode 0) PKZip Blob from an array of files.
|
|
1620
|
+
* Zero external dependencies, pure browser ArrayBuffer/Blob, fast and universally compatible with OS extractors.
|
|
1621
|
+
*/
|
|
1622
|
+
function createZipBlob(files) {
|
|
1623
|
+
const parts = [];
|
|
1624
|
+
const centralRecords = [];
|
|
1625
|
+
let offset = 0;
|
|
1626
|
+
const encoder = new TextEncoder();
|
|
1627
|
+
for (const file of files) {
|
|
1628
|
+
const nameBytes = encoder.encode(file.name);
|
|
1629
|
+
const data = file.data;
|
|
1630
|
+
const crc = crc32(data);
|
|
1631
|
+
const size = data.length;
|
|
1632
|
+
const localHeader = /* @__PURE__ */ new Uint8Array(30);
|
|
1633
|
+
const lv = new DataView(localHeader.buffer);
|
|
1634
|
+
lv.setUint32(0, 67324752, true);
|
|
1635
|
+
lv.setUint16(4, 20, true);
|
|
1636
|
+
lv.setUint16(6, 0, true);
|
|
1637
|
+
lv.setUint16(8, 0, true);
|
|
1638
|
+
lv.setUint16(10, 0, true);
|
|
1639
|
+
lv.setUint16(12, 0, true);
|
|
1640
|
+
lv.setUint32(14, crc, true);
|
|
1641
|
+
lv.setUint32(18, size, true);
|
|
1642
|
+
lv.setUint32(22, size, true);
|
|
1643
|
+
lv.setUint16(26, nameBytes.length, true);
|
|
1644
|
+
lv.setUint16(28, 0, true);
|
|
1645
|
+
parts.push(localHeader, nameBytes, data);
|
|
1646
|
+
const cdRecord = /* @__PURE__ */ new Uint8Array(46);
|
|
1647
|
+
const cv = new DataView(cdRecord.buffer);
|
|
1648
|
+
cv.setUint32(0, 33639248, true);
|
|
1649
|
+
cv.setUint16(4, 20, true);
|
|
1650
|
+
cv.setUint16(6, 20, true);
|
|
1651
|
+
cv.setUint16(8, 0, true);
|
|
1652
|
+
cv.setUint16(10, 0, true);
|
|
1653
|
+
cv.setUint16(12, 0, true);
|
|
1654
|
+
cv.setUint16(14, 0, true);
|
|
1655
|
+
cv.setUint32(16, crc, true);
|
|
1656
|
+
cv.setUint32(20, size, true);
|
|
1657
|
+
cv.setUint32(24, size, true);
|
|
1658
|
+
cv.setUint16(28, nameBytes.length, true);
|
|
1659
|
+
cv.setUint16(30, 0, true);
|
|
1660
|
+
cv.setUint16(32, 0, true);
|
|
1661
|
+
cv.setUint16(34, 0, true);
|
|
1662
|
+
cv.setUint16(36, 0, true);
|
|
1663
|
+
cv.setUint32(38, 0, true);
|
|
1664
|
+
cv.setUint32(42, offset, true);
|
|
1665
|
+
centralRecords.push(cdRecord, nameBytes);
|
|
1666
|
+
offset += localHeader.length + nameBytes.length + data.length;
|
|
1667
|
+
}
|
|
1668
|
+
const cdOffset = offset;
|
|
1669
|
+
let cdSize = 0;
|
|
1670
|
+
for (const part of centralRecords) cdSize += part.byteLength;
|
|
1671
|
+
const eocd = /* @__PURE__ */ new Uint8Array(22);
|
|
1672
|
+
const ev = new DataView(eocd.buffer);
|
|
1673
|
+
ev.setUint32(0, 101010256, true);
|
|
1674
|
+
ev.setUint16(4, 0, true);
|
|
1675
|
+
ev.setUint16(6, 0, true);
|
|
1676
|
+
ev.setUint16(8, files.length, true);
|
|
1677
|
+
ev.setUint16(10, files.length, true);
|
|
1678
|
+
ev.setUint32(12, cdSize, true);
|
|
1679
|
+
ev.setUint32(16, cdOffset, true);
|
|
1680
|
+
ev.setUint16(20, 0, true);
|
|
1681
|
+
return new Blob([
|
|
1682
|
+
...parts,
|
|
1683
|
+
...centralRecords,
|
|
1684
|
+
eocd
|
|
1685
|
+
], { type: "application/zip" });
|
|
1686
|
+
}
|
|
1559
1687
|
//#endregion
|
|
1560
1688
|
//#region lib/types/client/multi-model-compare.js
|
|
1561
1689
|
/** Map one shared output intent to settings accepted by every target model. */
|
|
@@ -115633,16 +115761,113 @@ img.ProseMirror-separator {
|
|
|
115633
115761
|
});
|
|
115634
115762
|
}
|
|
115635
115763
|
//#endregion
|
|
115764
|
+
//#region lib/types/client/tl/canvas-selection.js
|
|
115765
|
+
/** Bound raster dimensions before exporting; large multi-image PNGs get a
|
|
115766
|
+
* JPEG preview instead of being silently discarded. Originals are separate. */
|
|
115767
|
+
async function exportSelectionPreview(editor, shapes, maxBytes, signal) {
|
|
115768
|
+
const bounds = editor.getSelectionPageBounds();
|
|
115769
|
+
const scale = Math.min(1, 2048 / Math.max(1, (bounds?.w ?? 2048) + 32, (bounds?.h ?? 2048) + 32));
|
|
115770
|
+
const attempts = [
|
|
115771
|
+
{
|
|
115772
|
+
format: "png",
|
|
115773
|
+
scale,
|
|
115774
|
+
quality: 1
|
|
115775
|
+
},
|
|
115776
|
+
{
|
|
115777
|
+
format: "jpeg",
|
|
115778
|
+
scale,
|
|
115779
|
+
quality: .85
|
|
115780
|
+
},
|
|
115781
|
+
{
|
|
115782
|
+
format: "jpeg",
|
|
115783
|
+
scale: scale * .75,
|
|
115784
|
+
quality: .7
|
|
115785
|
+
},
|
|
115786
|
+
{
|
|
115787
|
+
format: "jpeg",
|
|
115788
|
+
scale: scale * .5,
|
|
115789
|
+
quality: .6
|
|
115790
|
+
},
|
|
115791
|
+
{
|
|
115792
|
+
format: "jpeg",
|
|
115793
|
+
scale: scale * .25,
|
|
115794
|
+
quality: .5
|
|
115795
|
+
}
|
|
115796
|
+
];
|
|
115797
|
+
const maxChars = Math.floor(Math.min(maxBytes, 4e6) / 3) * 4;
|
|
115798
|
+
for (const attempt of attempts) {
|
|
115799
|
+
signal.throwIfAborted();
|
|
115800
|
+
const { url } = await editor.toImageDataUrl(shapes, {
|
|
115801
|
+
background: true,
|
|
115802
|
+
padding: 16,
|
|
115803
|
+
pixelRatio: 1,
|
|
115804
|
+
...attempt
|
|
115805
|
+
});
|
|
115806
|
+
signal.throwIfAborted();
|
|
115807
|
+
if (url.length - url.indexOf(",") - 1 <= maxChars) return url;
|
|
115808
|
+
}
|
|
115809
|
+
throw new Error("preview-too-large");
|
|
115810
|
+
}
|
|
115811
|
+
async function readCanvasImageLimit(signal) {
|
|
115812
|
+
const response = await fetch(CANVAS_ASSET_ROUTE, {
|
|
115813
|
+
credentials: "same-origin",
|
|
115814
|
+
signal
|
|
115815
|
+
});
|
|
115816
|
+
if (!response.ok) throw new Error(`host-unavailable (${response.status})`);
|
|
115817
|
+
const value = await response.json();
|
|
115818
|
+
if (typeof value.maxImageBytes !== "number" || !Number.isSafeInteger(value.maxImageBytes) || value.maxImageBytes <= 0) throw new Error("host-unavailable");
|
|
115819
|
+
return value.maxImageBytes;
|
|
115820
|
+
}
|
|
115821
|
+
/** Use tldraw's resolver for IndexedDB-backed imports, not just asset.props.src.
|
|
115822
|
+
* Upload each original once per mount/source, with individual byte limits. */
|
|
115823
|
+
async function syncCanvasOriginal(editor, shape, maxBytes, signal, cache) {
|
|
115824
|
+
if (shape.type !== "image" || shape.props.assetId === null) throw new Error("original-unavailable");
|
|
115825
|
+
const asset = editor.getAsset(shape.props.assetId);
|
|
115826
|
+
if (asset?.type !== "image") throw new Error("original-unavailable");
|
|
115827
|
+
const cached = cache.get(asset.id);
|
|
115828
|
+
if (cached?.src === asset.props.src) return cached.attachment;
|
|
115829
|
+
const url = await editor.resolveAssetUrl(asset.id, { shouldResolveToOriginal: true });
|
|
115830
|
+
signal.throwIfAborted();
|
|
115831
|
+
if (url === null) throw new Error("original-unavailable");
|
|
115832
|
+
const response = await fetch(url, { signal });
|
|
115833
|
+
if (!response.ok) throw new Error("original-unavailable");
|
|
115834
|
+
const blob = await response.blob();
|
|
115835
|
+
if (blob.size > maxBytes) throw new Error("original-too-large");
|
|
115836
|
+
const mediaType = blob.type || asset.props.mimeType || "";
|
|
115837
|
+
if (![
|
|
115838
|
+
"image/png",
|
|
115839
|
+
"image/jpeg",
|
|
115840
|
+
"image/webp",
|
|
115841
|
+
"image/gif"
|
|
115842
|
+
].includes(mediaType)) throw new Error("original-format-unsupported");
|
|
115843
|
+
const uploaded = await fetch(CANVAS_ASSET_ROUTE, {
|
|
115844
|
+
method: "POST",
|
|
115845
|
+
credentials: "same-origin",
|
|
115846
|
+
headers: { "content-type": mediaType },
|
|
115847
|
+
body: blob,
|
|
115848
|
+
signal
|
|
115849
|
+
});
|
|
115850
|
+
if (!uploaded.ok) throw new Error(uploaded.status === 413 ? "original-too-large" : `original-upload-failed (${uploaded.status})`);
|
|
115851
|
+
const ref = (await uploaded.json()).attachment;
|
|
115852
|
+
if (!ref || typeof ref.attachmentId !== "string" || ![
|
|
115853
|
+
"image/png",
|
|
115854
|
+
"image/jpeg",
|
|
115855
|
+
"image/webp",
|
|
115856
|
+
"image/gif"
|
|
115857
|
+
].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");
|
|
115858
|
+
cache.set(asset.id, {
|
|
115859
|
+
src: asset.props.src,
|
|
115860
|
+
attachment: ref
|
|
115861
|
+
});
|
|
115862
|
+
return ref;
|
|
115863
|
+
}
|
|
115864
|
+
//#endregion
|
|
115636
115865
|
//#region lib/types/client/tl/canvas-sync.js
|
|
115637
115866
|
/** Debounce for ordinary document/session changes. */
|
|
115638
115867
|
const STATE_DEBOUNCE_MS = 600;
|
|
115639
115868
|
/** Extra settle time before an expensive selection screenshot export. */
|
|
115640
115869
|
const SELECTION_EXPORT_DELAY_MS = 1200;
|
|
115641
|
-
/** Minimum spacing between two HTTP pushes. */
|
|
115642
|
-
const MIN_PUSH_INTERVAL_MS = 400;
|
|
115643
115870
|
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
115871
|
/** Map a tldraw shape type onto the coarse model-facing node kind. */
|
|
115647
115872
|
function kindOf(shapeType) {
|
|
115648
115873
|
switch (shapeType) {
|
|
@@ -115749,69 +115974,66 @@ img.ProseMirror-separator {
|
|
|
115749
115974
|
const ids = editor.getSelectedShapeIds().map(String).sort().join("|");
|
|
115750
115975
|
return ids.length === 0 ? "none" : ids;
|
|
115751
115976
|
}
|
|
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);
|
|
115977
|
+
function startCanvasSync(editor, onStatus = () => {}) {
|
|
115978
|
+
const clientInstance = `${String(editor.id).slice(0, 36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
115758
115979
|
let disposed = false;
|
|
115759
115980
|
let stateTimer;
|
|
115760
115981
|
let exportTimer;
|
|
115761
115982
|
let lastPushKey = "";
|
|
115762
|
-
let lastSelectionKey =
|
|
115763
|
-
let
|
|
115764
|
-
let
|
|
115765
|
-
|
|
115983
|
+
let lastSelectionKey = selectionKeyOf(editor);
|
|
115984
|
+
let revision = 0;
|
|
115985
|
+
let sequence = 0;
|
|
115986
|
+
let imageLimit;
|
|
115987
|
+
let inFlight = new AbortController();
|
|
115988
|
+
let exporting = false;
|
|
115989
|
+
let exportAgain = false;
|
|
115990
|
+
let prepared = {};
|
|
115991
|
+
const originalCache = /* @__PURE__ */ new Map();
|
|
115992
|
+
let preparedItems;
|
|
115993
|
+
const stopListening = editor.store.listen((event) => {
|
|
115766
115994
|
if (disposed) return;
|
|
115767
|
-
const
|
|
115768
|
-
|
|
115769
|
-
|
|
115770
|
-
|
|
115771
|
-
|
|
115772
|
-
|
|
115773
|
-
|
|
115774
|
-
|
|
115995
|
+
const key = selectionKeyOf(editor);
|
|
115996
|
+
const changes = event.changes;
|
|
115997
|
+
const documentChanged = [
|
|
115998
|
+
...Object.values(changes.added),
|
|
115999
|
+
...Object.values(changes.removed),
|
|
116000
|
+
...Object.values(changes.updated).map((pair) => pair[1])
|
|
116001
|
+
].some((record) => record.typeName === "shape" || record.typeName === "asset" || record.typeName === "page");
|
|
116002
|
+
if (key !== lastSelectionKey || documentChanged) {
|
|
116003
|
+
lastSelectionKey = key;
|
|
116004
|
+
invalidate();
|
|
115775
116005
|
}
|
|
115776
116006
|
if (stateTimer === void 0) stateTimer = setTimeout(() => {
|
|
115777
116007
|
stateTimer = void 0;
|
|
115778
|
-
|
|
116008
|
+
pushState();
|
|
115779
116009
|
}, STATE_DEBOUNCE_MS);
|
|
115780
116010
|
}, {
|
|
115781
116011
|
source: "all",
|
|
115782
116012
|
scope: "all"
|
|
115783
116013
|
});
|
|
115784
|
-
|
|
116014
|
+
function invalidate() {
|
|
116015
|
+
revision++;
|
|
116016
|
+
inFlight.abort();
|
|
116017
|
+
inFlight = new AbortController();
|
|
116018
|
+
prepared = {};
|
|
116019
|
+
preparedItems = void 0;
|
|
116020
|
+
const count = editor.getSelectedShapeIds().length;
|
|
116021
|
+
onStatus({
|
|
116022
|
+
phase: count > 0 ? "preparing" : "idle",
|
|
116023
|
+
count
|
|
116024
|
+
});
|
|
116025
|
+
if (exportTimer !== void 0) clearTimeout(exportTimer);
|
|
116026
|
+
if (count > 0) exportTimer = setTimeout(() => {
|
|
116027
|
+
exportTimer = void 0;
|
|
116028
|
+
prepare();
|
|
116029
|
+
}, SELECTION_EXPORT_DELAY_MS);
|
|
116030
|
+
}
|
|
116031
|
+
async function pushState(force = false) {
|
|
115785
116032
|
if (disposed) return;
|
|
116033
|
+
const currentRevision = revision;
|
|
115786
116034
|
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
|
-
}
|
|
116035
|
+
const key = `${revision}:${summary.stateKey}`;
|
|
116036
|
+
if (!force && key === lastPushKey) return;
|
|
115815
116037
|
const payload = {
|
|
115816
116038
|
clientInstance,
|
|
115817
116039
|
connected: true,
|
|
@@ -115820,43 +116042,104 @@ img.ProseMirror-separator {
|
|
|
115820
116042
|
selection: {
|
|
115821
116043
|
count: summary.selectionCount,
|
|
115822
116044
|
kinds: summary.selectionKinds,
|
|
115823
|
-
items: summary.selectionItems
|
|
116045
|
+
items: preparedItems ?? summary.selectionItems
|
|
115824
116046
|
},
|
|
116047
|
+
selectionRevision: String(revision),
|
|
116048
|
+
...summary.selectionCount > 0 ? { selectionStatus: "preparing" } : {},
|
|
116049
|
+
...prepared,
|
|
116050
|
+
sequence: ++sequence,
|
|
115825
116051
|
updatedAt: Date.now()
|
|
115826
116052
|
};
|
|
115827
|
-
|
|
115828
|
-
lastPushKey = summary.stateKey;
|
|
115829
|
-
lastPostAt = Date.now();
|
|
115830
|
-
const controller = new AbortController();
|
|
115831
|
-
inFlight = controller;
|
|
116053
|
+
const requestSequence = sequence;
|
|
115832
116054
|
try {
|
|
115833
|
-
await fetch(CANVAS_STATE_ROUTE, {
|
|
116055
|
+
const response = await fetch(CANVAS_STATE_ROUTE, {
|
|
115834
116056
|
method: "POST",
|
|
115835
116057
|
credentials: "same-origin",
|
|
115836
116058
|
headers: { "content-type": "application/json" },
|
|
115837
116059
|
body: JSON.stringify(payload),
|
|
115838
|
-
signal:
|
|
116060
|
+
signal: AbortSignal.any([inFlight.signal, AbortSignal.timeout(3e4)])
|
|
116061
|
+
});
|
|
116062
|
+
if (!response.ok) throw new Error(`state-sync-failed (${response.status})`);
|
|
116063
|
+
if (disposed || revision !== currentRevision || sequence !== requestSequence) return;
|
|
116064
|
+
lastPushKey = key;
|
|
116065
|
+
onStatus({
|
|
116066
|
+
phase: payload.selectionStatus ?? "idle",
|
|
116067
|
+
count: summary.selectionCount,
|
|
116068
|
+
...payload.selectionError ? { error: payload.selectionError } : {}
|
|
115839
116069
|
});
|
|
115840
116070
|
} catch (error) {
|
|
115841
|
-
if (
|
|
116071
|
+
if (disposed || revision !== currentRevision || sequence !== requestSequence) return;
|
|
115842
116072
|
lastPushKey = "";
|
|
116073
|
+
onStatus({
|
|
116074
|
+
phase: "error",
|
|
116075
|
+
count: summary.selectionCount,
|
|
116076
|
+
error: error instanceof Error ? error.message : "state-sync-failed"
|
|
116077
|
+
});
|
|
116078
|
+
}
|
|
116079
|
+
}
|
|
116080
|
+
async function prepare() {
|
|
116081
|
+
if (disposed) return;
|
|
116082
|
+
if (exporting) {
|
|
116083
|
+
exportAgain = true;
|
|
116084
|
+
return;
|
|
116085
|
+
}
|
|
116086
|
+
exporting = true;
|
|
116087
|
+
const currentRevision = revision;
|
|
116088
|
+
const signal = AbortSignal.any([inFlight.signal, AbortSignal.timeout(3e4)]);
|
|
116089
|
+
const shapes = editor.getSelectedShapes();
|
|
116090
|
+
const items = shapes.slice(0, 16).map((shape) => describeShape(editor, shape));
|
|
116091
|
+
let selectionImage;
|
|
116092
|
+
let failure;
|
|
116093
|
+
try {
|
|
116094
|
+
imageLimit ??= await readCanvasImageLimit(signal);
|
|
116095
|
+
try {
|
|
116096
|
+
selectionImage = await exportSelectionPreview(editor, shapes, imageLimit, signal);
|
|
116097
|
+
} catch (error) {
|
|
116098
|
+
signal.throwIfAborted();
|
|
116099
|
+
failure = error instanceof Error ? error.message : "preview-export-failed";
|
|
116100
|
+
}
|
|
116101
|
+
if (shapes.length > 16 && shapes.some((shape) => shape.type === "image")) throw new Error("too-many-selected-shapes");
|
|
116102
|
+
for (const [index, shape] of shapes.entries()) {
|
|
116103
|
+
signal.throwIfAborted();
|
|
116104
|
+
if (shape.type !== "image") continue;
|
|
116105
|
+
const item = items[index];
|
|
116106
|
+
if (item === void 0) continue;
|
|
116107
|
+
const attachment = await syncCanvasOriginal(editor, shape, imageLimit, signal, originalCache);
|
|
116108
|
+
item.attachment = attachment;
|
|
116109
|
+
item.attachmentId = attachment.attachmentId;
|
|
116110
|
+
}
|
|
116111
|
+
} catch (error) {
|
|
116112
|
+
failure = error instanceof Error ? error.message : "selection-sync-failed";
|
|
116113
|
+
} finally {
|
|
116114
|
+
exporting = false;
|
|
116115
|
+
}
|
|
116116
|
+
if (!disposed && revision === currentRevision) {
|
|
116117
|
+
preparedItems = items;
|
|
116118
|
+
prepared = {
|
|
116119
|
+
selectionStatus: failure === void 0 ? "ready" : "error",
|
|
116120
|
+
...failure === void 0 ? {} : { selectionError: failure.slice(0, 300) },
|
|
116121
|
+
...selectionImage === void 0 ? {} : { selectionImage }
|
|
116122
|
+
};
|
|
116123
|
+
await pushState(true);
|
|
116124
|
+
}
|
|
116125
|
+
if (exportAgain && !disposed) {
|
|
116126
|
+
exportAgain = false;
|
|
116127
|
+
prepare();
|
|
115843
116128
|
}
|
|
115844
116129
|
}
|
|
116130
|
+
invalidate();
|
|
115845
116131
|
stateTimer = setTimeout(() => {
|
|
115846
116132
|
stateTimer = void 0;
|
|
115847
|
-
|
|
116133
|
+
pushState();
|
|
115848
116134
|
}, STATE_DEBOUNCE_MS);
|
|
115849
|
-
|
|
115850
|
-
exportTimer = void 0;
|
|
115851
|
-
flush(true);
|
|
115852
|
-
}, SELECTION_EXPORT_DELAY_MS);
|
|
115853
|
-
return () => {
|
|
116135
|
+
const stop = () => {
|
|
115854
116136
|
if (disposed) return;
|
|
115855
116137
|
disposed = true;
|
|
115856
116138
|
if (stateTimer !== void 0) clearTimeout(stateTimer);
|
|
115857
116139
|
if (exportTimer !== void 0) clearTimeout(exportTimer);
|
|
115858
116140
|
stopListening();
|
|
115859
|
-
inFlight
|
|
116141
|
+
inFlight.abort();
|
|
116142
|
+
originalCache.clear();
|
|
115860
116143
|
fetch(CANVAS_STATE_ROUTE, {
|
|
115861
116144
|
method: "POST",
|
|
115862
116145
|
credentials: "same-origin",
|
|
@@ -115866,10 +116149,15 @@ img.ProseMirror-separator {
|
|
|
115866
116149
|
clientInstance,
|
|
115867
116150
|
connected: false,
|
|
115868
116151
|
nodeCount: 0,
|
|
116152
|
+
sequence: ++sequence,
|
|
115869
116153
|
updatedAt: Date.now()
|
|
115870
116154
|
})
|
|
115871
116155
|
}).catch(() => {});
|
|
115872
116156
|
};
|
|
116157
|
+
return Object.assign(stop, { retry: () => {
|
|
116158
|
+
invalidate();
|
|
116159
|
+
pushState(true);
|
|
116160
|
+
} });
|
|
115873
116161
|
}
|
|
115874
116162
|
//#endregion
|
|
115875
116163
|
//#region lib/types/client/tl/tl-canvas-bridge.js
|
|
@@ -116112,56 +116400,84 @@ img.ProseMirror-separator {
|
|
|
116112
116400
|
}
|
|
116113
116401
|
return true;
|
|
116114
116402
|
}
|
|
116115
|
-
const
|
|
116403
|
+
const TL_COMPONENTS = { Toolbar: StudioToolbar };
|
|
116404
|
+
const StudioTlCanvas = (0, react.memo)(function StudioTlCanvas({ lang = "zh" }) {
|
|
116116
116405
|
const sessionId = (0, react.useId)();
|
|
116117
|
-
|
|
116406
|
+
const [syncStatus, setSyncStatus] = (0, react.useState)({
|
|
116407
|
+
phase: "idle",
|
|
116408
|
+
count: 0
|
|
116409
|
+
});
|
|
116410
|
+
const retrySync = (0, react.useRef)(() => {});
|
|
116411
|
+
const onMount = (0, react.useCallback)((editor) => {
|
|
116412
|
+
let active = true;
|
|
116413
|
+
applyBrandTheme(editor);
|
|
116414
|
+
editor.updateInstanceState({ isGridMode: true });
|
|
116415
|
+
console.info(`[dsh-image-gen] tldraw mounted (instance ${editor.id})`);
|
|
116416
|
+
const stopLanding = registerTlLandingConsumer((items) => {
|
|
116417
|
+
const generation = getTlLandingGeneration();
|
|
116418
|
+
return landTlItems(editor, items, () => active && generation === getTlLandingGeneration());
|
|
116419
|
+
});
|
|
116420
|
+
let stopSync = startCanvasSync(editor, setSyncStatus);
|
|
116421
|
+
retrySync.current = () => stopSync.retry();
|
|
116422
|
+
const clear = () => {
|
|
116423
|
+
stopSync();
|
|
116424
|
+
editor.complete();
|
|
116425
|
+
editor.run(() => {
|
|
116426
|
+
editor.selectNone();
|
|
116427
|
+
const pages = editor.getPages();
|
|
116428
|
+
for (const page of pages) editor.deleteShapes([...editor.getPageShapeIds(page.id)]);
|
|
116429
|
+
for (const page of pages.slice(1)) editor.deletePage(page.id);
|
|
116430
|
+
editor.deleteAssets(editor.getAssets());
|
|
116431
|
+
editor.setCamera({
|
|
116432
|
+
x: 0,
|
|
116433
|
+
y: 0,
|
|
116434
|
+
z: 1
|
|
116435
|
+
});
|
|
116436
|
+
}, {
|
|
116437
|
+
history: "ignore",
|
|
116438
|
+
ignoreShapeLock: true
|
|
116439
|
+
});
|
|
116440
|
+
editor.clearHistory();
|
|
116441
|
+
stopSync = startCanvasSync(editor, setSyncStatus);
|
|
116442
|
+
};
|
|
116443
|
+
mountedCanvases.add(clear);
|
|
116444
|
+
return () => {
|
|
116445
|
+
active = false;
|
|
116446
|
+
mountedCanvases.delete(clear);
|
|
116447
|
+
stopLanding();
|
|
116448
|
+
stopSync();
|
|
116449
|
+
retrySync.current = () => {};
|
|
116450
|
+
};
|
|
116451
|
+
}, []);
|
|
116452
|
+
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} 个元素…` : "选区同步失败";
|
|
116453
|
+
return (0, react_jsx_runtime.jsxs)("div", {
|
|
116118
116454
|
className: "dsh-ig-tl-canvas",
|
|
116119
|
-
children: (0, react_jsx_runtime.jsx)(Tldraw, {
|
|
116455
|
+
children: [(0, react_jsx_runtime.jsx)(Tldraw, {
|
|
116120
116456
|
persistenceKey: "dsh-image-gen-workbench-v1",
|
|
116121
116457
|
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
|
-
})
|
|
116458
|
+
components: TL_COMPONENTS,
|
|
116459
|
+
onMount
|
|
116460
|
+
}), syncStatus.phase !== "idle" && (0, react_jsx_runtime.jsxs)("div", {
|
|
116461
|
+
className: "dsh-ig-canvas-sync-status",
|
|
116462
|
+
"data-phase": syncStatus.phase,
|
|
116463
|
+
role: "status",
|
|
116464
|
+
"aria-live": "polite",
|
|
116465
|
+
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", {
|
|
116466
|
+
type: "button",
|
|
116467
|
+
onClick: () => retrySync.current(),
|
|
116468
|
+
children: lang === "en" ? "Retry" : "重试"
|
|
116469
|
+
})]
|
|
116470
|
+
})]
|
|
116163
116471
|
});
|
|
116164
116472
|
});
|
|
116473
|
+
function canvasSyncError(error, lang) {
|
|
116474
|
+
const en = lang === "en";
|
|
116475
|
+
if (error?.includes("too-many-selected")) return en ? "Select at most 16 shapes to edit with originals." : "使用原图编辑时,请一次选择不超过 16 个元素。";
|
|
116476
|
+
if (error?.includes("too-large")) return en ? "An image exceeds the size limit. Reduce its size and retry." : "图片超出大小限制,请缩小后重试。";
|
|
116477
|
+
if (error?.includes("unsupported")) return en ? "Use PNG, JPEG, WebP or GIF images." : "请使用 PNG、JPEG、WebP 或 GIF 图片。";
|
|
116478
|
+
if (error?.includes("host-unavailable")) return en ? "Restart DSH to load the updated plugin, then retry." : "请重启 DSH 加载新版插件后重试。";
|
|
116479
|
+
return en ? "Check the connection or re-import an unavailable image, then retry." : "请检查连接;若原图已失效,请重新导入后重试。";
|
|
116480
|
+
}
|
|
116165
116481
|
//#endregion
|
|
116166
116482
|
//#region lib/types/client/studio-view.js
|
|
116167
116483
|
/** Below this workbench width the recent-generations rail folds automatically. */
|
|
@@ -116246,6 +116562,7 @@ img.ProseMirror-separator {
|
|
|
116246
116562
|
favorited: "已收藏",
|
|
116247
116563
|
favoriteAdded: "已添加到收藏",
|
|
116248
116564
|
favoriteRemoved: "已取消收藏",
|
|
116565
|
+
gallerySaveFailed: "保存到图库失败,请重试",
|
|
116249
116566
|
loadMore: "加载更多 ({n})",
|
|
116250
116567
|
deleteModalTitle: "从图库删除",
|
|
116251
116568
|
deleteModalDesc: "确定从图库中删除这张图片吗?(原聊天记录不会受影响)",
|
|
@@ -116354,6 +116671,7 @@ img.ProseMirror-separator {
|
|
|
116354
116671
|
favorited: "Favorited",
|
|
116355
116672
|
favoriteAdded: "Added to favorites",
|
|
116356
116673
|
favoriteRemoved: "Removed from favorites",
|
|
116674
|
+
gallerySaveFailed: "Failed to save to the gallery, please retry",
|
|
116357
116675
|
loadMore: "Load more ({n})",
|
|
116358
116676
|
deleteModalTitle: "Delete from gallery",
|
|
116359
116677
|
deleteModalDesc: "Remove this image from the local gallery? (Chat history remains unaffected)",
|
|
@@ -117018,7 +117336,7 @@ img.ProseMirror-separator {
|
|
|
117018
117336
|
...targetRoot ? { workspacePath: targetRoot } : {},
|
|
117019
117337
|
...workspace?.workspaceId ? { workspaceId: workspace.workspaceId } : {}
|
|
117020
117338
|
};
|
|
117021
|
-
await saveGalleryItem(updatedItem);
|
|
117339
|
+
if (!await saveGalleryItem(updatedItem)) throw new Error(t("gallerySaveFailed"));
|
|
117022
117340
|
return updatedItem;
|
|
117023
117341
|
};
|
|
117024
117342
|
const handleSaveToGallery = async () => {
|
|
@@ -117112,16 +117430,29 @@ img.ProseMirror-separator {
|
|
|
117112
117430
|
} : i) : null);
|
|
117113
117431
|
flash(nextFav ? t("favoriteAdded") : t("favoriteRemoved"));
|
|
117114
117432
|
} else {
|
|
117115
|
-
|
|
117116
|
-
|
|
117117
|
-
|
|
117118
|
-
|
|
117119
|
-
|
|
117120
|
-
|
|
117121
|
-
|
|
117122
|
-
|
|
117123
|
-
|
|
117124
|
-
|
|
117433
|
+
if (!!selected.isFavorite) {
|
|
117434
|
+
setSelected((curr) => curr !== null && curr.id === targetId ? {
|
|
117435
|
+
...curr,
|
|
117436
|
+
isFavorite: false
|
|
117437
|
+
} : curr);
|
|
117438
|
+
if (currentBatch) setCurrentBatch((prev) => prev ? prev.map((i) => i.id === targetId ? {
|
|
117439
|
+
...i,
|
|
117440
|
+
isFavorite: false
|
|
117441
|
+
} : i) : null);
|
|
117442
|
+
flash(t("favoriteRemoved"));
|
|
117443
|
+
return;
|
|
117444
|
+
}
|
|
117445
|
+
try {
|
|
117446
|
+
const updated = await saveGalleryEntry({
|
|
117447
|
+
...selected,
|
|
117448
|
+
isFavorite: true
|
|
117449
|
+
});
|
|
117450
|
+
setSelected((curr) => curr !== null && curr.id === targetId ? updated : curr);
|
|
117451
|
+
if (currentBatch) setCurrentBatch((prev) => prev ? prev.map((i) => i.id === targetId ? updated : i) : prev);
|
|
117452
|
+
flash(t("favoriteAdded"));
|
|
117453
|
+
} catch (err) {
|
|
117454
|
+
setError(err instanceof Error ? err.message : t("gallerySaveFailed"));
|
|
117455
|
+
}
|
|
117125
117456
|
}
|
|
117126
117457
|
};
|
|
117127
117458
|
const stepLightbox = (delta) => {
|
|
@@ -117353,7 +117684,7 @@ img.ProseMirror-separator {
|
|
|
117353
117684
|
})
|
|
117354
117685
|
]
|
|
117355
117686
|
}),
|
|
117356
|
-
canvasSurface === "infinite" ? (0, react_jsx_runtime.jsx)(StudioTlCanvas, {}) : (0, react_jsx_runtime.jsx)("div", {
|
|
117687
|
+
canvasSurface === "infinite" ? (0, react_jsx_runtime.jsx)(StudioTlCanvas, { lang }) : (0, react_jsx_runtime.jsx)("div", {
|
|
117357
117688
|
className: `dsh-ig-canvas ${isDragging ? "is-dragging" : ""}`,
|
|
117358
117689
|
ref: canvasRef,
|
|
117359
117690
|
onMouseDown: handleCanvasMouseDown,
|
|
@@ -119434,6 +119765,7 @@ img.ProseMirror-separator {
|
|
|
119434
119765
|
confirmRegenerate: "开始生成",
|
|
119435
119766
|
regenerating: "正在重新生成…",
|
|
119436
119767
|
regenerateSuccess: "已生成新图片并收录到画廊",
|
|
119768
|
+
regenerateSaveFailed: "图片已生成,但保存到图库失败,请重试",
|
|
119437
119769
|
regenerateFailed: "重新生成失败",
|
|
119438
119770
|
delete: "从画廊删除",
|
|
119439
119771
|
confirmDelete: "确定要从画廊中删除这张图片吗?(不会影响原聊天记录)",
|
|
@@ -119443,12 +119775,22 @@ img.ProseMirror-separator {
|
|
|
119443
119775
|
close: "关闭 (Esc)",
|
|
119444
119776
|
prevImage: "上一张 (←)",
|
|
119445
119777
|
nextImage: "下一张 (→)",
|
|
119446
|
-
manage: "
|
|
119778
|
+
manage: "批量管理",
|
|
119447
119779
|
exitManage: "退出选择",
|
|
119448
119780
|
selectedCount: "已选 {n} 项",
|
|
119449
119781
|
selectAll: "全选",
|
|
119450
119782
|
invertSelect: "反选",
|
|
119451
119783
|
clearSelect: "清空",
|
|
119784
|
+
batchFavorite: "批量收藏",
|
|
119785
|
+
batchUnfavorite: "批量取消收藏",
|
|
119786
|
+
batchFavoritedToast: "已将 {count} 张图片加入收藏",
|
|
119787
|
+
batchUnfavoritedToast: "已取消 {count} 张图片的收藏",
|
|
119788
|
+
batchFavoriteFailed: "收藏操作失败,请重试",
|
|
119789
|
+
batchDownload: "批量下载",
|
|
119790
|
+
batchDownloading: "打包中 ({current}/{total})…",
|
|
119791
|
+
batchDownloadSingleToast: "已下载 1 张图片",
|
|
119792
|
+
batchDownloadZipToast: "已打包下载 {count} 张图片",
|
|
119793
|
+
batchDownloadFailed: "批量下载失败,请重试",
|
|
119452
119794
|
batchDelete: "批量删除",
|
|
119453
119795
|
batchDeleteTitle: "确认批量删除选中的 {count} 张图片?",
|
|
119454
119796
|
batchDeleteTitleSingle: "确认从画廊中删除这张图片?",
|
|
@@ -119519,6 +119861,7 @@ img.ProseMirror-separator {
|
|
|
119519
119861
|
confirmRegenerate: "Generate",
|
|
119520
119862
|
regenerating: "Regenerating…",
|
|
119521
119863
|
regenerateSuccess: "New image generated and added to gallery",
|
|
119864
|
+
regenerateSaveFailed: "Image generated, but saving to the gallery failed. Please retry.",
|
|
119522
119865
|
regenerateFailed: "Regeneration failed",
|
|
119523
119866
|
delete: "Delete from gallery",
|
|
119524
119867
|
confirmDelete: "Are you sure you want to remove this image from the gallery? (Chat history will not be affected)",
|
|
@@ -119528,12 +119871,22 @@ img.ProseMirror-separator {
|
|
|
119528
119871
|
close: "Close (Esc)",
|
|
119529
119872
|
prevImage: "Previous (←)",
|
|
119530
119873
|
nextImage: "Next (→)",
|
|
119531
|
-
manage: "Batch
|
|
119874
|
+
manage: "Batch Manage",
|
|
119532
119875
|
exitManage: "Done",
|
|
119533
119876
|
selectedCount: "{n} selected",
|
|
119534
119877
|
selectAll: "Select All",
|
|
119535
119878
|
invertSelect: "Invert",
|
|
119536
119879
|
clearSelect: "Clear",
|
|
119880
|
+
batchFavorite: "Batch Favorite",
|
|
119881
|
+
batchUnfavorite: "Batch Unfavorite",
|
|
119882
|
+
batchFavoritedToast: "Added {count} images to favorites",
|
|
119883
|
+
batchUnfavoritedToast: "Removed {count} images from favorites",
|
|
119884
|
+
batchFavoriteFailed: "Favorite operation failed, please retry",
|
|
119885
|
+
batchDownload: "Batch Download",
|
|
119886
|
+
batchDownloading: "Packaging ({current}/{total})…",
|
|
119887
|
+
batchDownloadSingleToast: "Downloaded 1 image",
|
|
119888
|
+
batchDownloadZipToast: "Packaged & downloaded {count} images",
|
|
119889
|
+
batchDownloadFailed: "Batch download failed, please retry",
|
|
119537
119890
|
batchDelete: "Batch Delete",
|
|
119538
119891
|
batchDeleteTitle: "Delete {count} selected images?",
|
|
119539
119892
|
batchDeleteTitleSingle: "Delete this image from gallery?",
|
|
@@ -119934,11 +120287,11 @@ img.ProseMirror-separator {
|
|
|
119934
120287
|
...activeWorkspace?.workspaceId ? { workspaceId: activeWorkspace.workspaceId } : {},
|
|
119935
120288
|
...currentSessionId ? { sessionId: currentSessionId } : {}
|
|
119936
120289
|
};
|
|
119937
|
-
await saveGalleryItem(newItem);
|
|
120290
|
+
const savedOk = await saveGalleryItem(newItem);
|
|
119938
120291
|
if (activeTab !== "gallery") setActiveTab("gallery");
|
|
119939
120292
|
if (search.trim().length > 0) setSearch("");
|
|
119940
120293
|
openPreviewItem(newItem);
|
|
119941
|
-
showToast(t("regenerateSuccess"));
|
|
120294
|
+
showToast(savedOk ? t("regenerateSuccess") : t("regenerateSaveFailed"));
|
|
119942
120295
|
} catch (err) {
|
|
119943
120296
|
if (controller.signal.aborted) return;
|
|
119944
120297
|
showToast(err instanceof Error ? err.message : String(err));
|
|
@@ -120061,6 +120414,91 @@ img.ProseMirror-separator {
|
|
|
120061
120414
|
setSelectedIds(/* @__PURE__ */ new Set());
|
|
120062
120415
|
lastSelectedIndexRef.current = null;
|
|
120063
120416
|
};
|
|
120417
|
+
const selectedItems = (0, react.useMemo)(() => {
|
|
120418
|
+
if (selectedIds.size === 0) return [];
|
|
120419
|
+
return items.filter((item) => selectedIds.has(item.id));
|
|
120420
|
+
}, [items, selectedIds]);
|
|
120421
|
+
const allSelectedAreFavorites = (0, react.useMemo)(() => {
|
|
120422
|
+
if (selectedItems.length === 0) return false;
|
|
120423
|
+
return selectedItems.every((item) => Boolean(item.isFavorite));
|
|
120424
|
+
}, [selectedItems]);
|
|
120425
|
+
const handleBatchToggleFavorite = async () => {
|
|
120426
|
+
if (selectedIds.size === 0) return;
|
|
120427
|
+
const targetFavorite = !allSelectedAreFavorites;
|
|
120428
|
+
const targetIds = Array.from(selectedIds);
|
|
120429
|
+
setItems((prev) => prev.map((item) => selectedIds.has(item.id) ? {
|
|
120430
|
+
...item,
|
|
120431
|
+
isFavorite: targetFavorite
|
|
120432
|
+
} : item));
|
|
120433
|
+
try {
|
|
120434
|
+
await bulkSetFavoriteGalleryItems(targetIds, targetFavorite);
|
|
120435
|
+
showToast(targetFavorite ? t("batchFavoritedToast", { count: String(targetIds.length) }) : t("batchUnfavoritedToast", { count: String(targetIds.length) }));
|
|
120436
|
+
} catch (err) {
|
|
120437
|
+
console.error("[dsh-image-gen] Batch favorite failed:", err);
|
|
120438
|
+
getGalleryItems().then((res) => setItems(res));
|
|
120439
|
+
showToast(t("batchFavoriteFailed"));
|
|
120440
|
+
}
|
|
120441
|
+
};
|
|
120442
|
+
const [isDownloadingBatch, setIsDownloadingBatch] = (0, react.useState)(false);
|
|
120443
|
+
const [batchDownloadProgress, setBatchDownloadProgress] = (0, react.useState)(null);
|
|
120444
|
+
const handleBatchDownload = async () => {
|
|
120445
|
+
if (selectedIds.size === 0 || isDownloadingBatch) return;
|
|
120446
|
+
const itemsToDownload = selectedItems;
|
|
120447
|
+
if (itemsToDownload.length === 0) return;
|
|
120448
|
+
setIsDownloadingBatch(true);
|
|
120449
|
+
try {
|
|
120450
|
+
if (itemsToDownload.length === 1) {
|
|
120451
|
+
const single = itemsToDownload[0];
|
|
120452
|
+
if (!single) return;
|
|
120453
|
+
let blob = blobCache.get(single.id);
|
|
120454
|
+
if (!blob) {
|
|
120455
|
+
blob = await fetchAttachmentBlob(single.attachment);
|
|
120456
|
+
blobCache.set(single.id, blob);
|
|
120457
|
+
}
|
|
120458
|
+
const objectUrl = URL.createObjectURL(blob);
|
|
120459
|
+
downloadBlobUrl(objectUrl, `dsh-${single.provider}-${single.id}.png`);
|
|
120460
|
+
URL.revokeObjectURL(objectUrl);
|
|
120461
|
+
showToast(t("batchDownloadSingleToast"));
|
|
120462
|
+
return;
|
|
120463
|
+
}
|
|
120464
|
+
const zipFiles = [];
|
|
120465
|
+
const usedNames = /* @__PURE__ */ new Set();
|
|
120466
|
+
for (let i = 0; i < itemsToDownload.length; i++) {
|
|
120467
|
+
const it = itemsToDownload[i];
|
|
120468
|
+
if (!it) continue;
|
|
120469
|
+
setBatchDownloadProgress(`${i + 1}/${itemsToDownload.length}`);
|
|
120470
|
+
let blob = blobCache.get(it.id);
|
|
120471
|
+
if (!blob) {
|
|
120472
|
+
blob = await fetchAttachmentBlob(it.attachment);
|
|
120473
|
+
blobCache.set(it.id, blob);
|
|
120474
|
+
}
|
|
120475
|
+
const arrayBuffer = await blob.arrayBuffer();
|
|
120476
|
+
const ext = blob.type === "image/jpeg" ? "jpg" : blob.type === "image/webp" ? "webp" : "png";
|
|
120477
|
+
let baseName = `dsh-${it.provider}-${it.id.slice(0, 8)}.${ext}`;
|
|
120478
|
+
let count = 1;
|
|
120479
|
+
while (usedNames.has(baseName)) {
|
|
120480
|
+
baseName = `dsh-${it.provider}-${it.id.slice(0, 8)}-${count}.${ext}`;
|
|
120481
|
+
count++;
|
|
120482
|
+
}
|
|
120483
|
+
usedNames.add(baseName);
|
|
120484
|
+
zipFiles.push({
|
|
120485
|
+
name: baseName,
|
|
120486
|
+
data: new Uint8Array(arrayBuffer)
|
|
120487
|
+
});
|
|
120488
|
+
}
|
|
120489
|
+
const zipBlob = createZipBlob(zipFiles);
|
|
120490
|
+
const zipUrl = URL.createObjectURL(zipBlob);
|
|
120491
|
+
downloadBlobUrl(zipUrl, `dsh-gallery-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10).replace(/-/g, "")}.zip`);
|
|
120492
|
+
URL.revokeObjectURL(zipUrl);
|
|
120493
|
+
showToast(t("batchDownloadZipToast", { count: String(itemsToDownload.length) }));
|
|
120494
|
+
} catch (err) {
|
|
120495
|
+
console.error("[dsh-image-gen] Batch download failed:", err);
|
|
120496
|
+
showToast(t("batchDownloadFailed"));
|
|
120497
|
+
} finally {
|
|
120498
|
+
setIsDownloadingBatch(false);
|
|
120499
|
+
setBatchDownloadProgress(null);
|
|
120500
|
+
}
|
|
120501
|
+
};
|
|
120064
120502
|
const requestSingleDelete = (item, e) => {
|
|
120065
120503
|
if (e) e.stopPropagation();
|
|
120066
120504
|
setSelectedIds(/* @__PURE__ */ new Set([item.id]));
|
|
@@ -120293,13 +120731,13 @@ img.ProseMirror-separator {
|
|
|
120293
120731
|
})]
|
|
120294
120732
|
}), (0, react_jsx_runtime.jsxs)("button", {
|
|
120295
120733
|
type: "button",
|
|
120296
|
-
className: `dsh-ig-studio-btn
|
|
120734
|
+
className: `dsh-ig-studio-btn ${isManageMode ? "is-active" : ""}`,
|
|
120297
120735
|
title: isManageMode ? t("exitManage") : t("manage"),
|
|
120298
120736
|
onClick: () => {
|
|
120299
120737
|
if (isManageMode) handleExitManage();
|
|
120300
120738
|
else setIsManageMode(true);
|
|
120301
120739
|
},
|
|
120302
|
-
children: [(0, react_jsx_runtime.jsx)(
|
|
120740
|
+
children: [(0, react_jsx_runtime.jsx)(SquareCheckBig, { size: 13 }), (0, react_jsx_runtime.jsx)("span", { children: isManageMode ? t("exitManage") : t("manage") })]
|
|
120303
120741
|
})]
|
|
120304
120742
|
})]
|
|
120305
120743
|
}),
|
|
@@ -120416,18 +120854,45 @@ img.ProseMirror-separator {
|
|
|
120416
120854
|
]
|
|
120417
120855
|
}), (0, react_jsx_runtime.jsxs)("div", {
|
|
120418
120856
|
className: "dsh-ig-batch-bar-right",
|
|
120419
|
-
children: [
|
|
120420
|
-
|
|
120421
|
-
|
|
120422
|
-
|
|
120423
|
-
|
|
120424
|
-
|
|
120425
|
-
|
|
120426
|
-
|
|
120427
|
-
|
|
120428
|
-
|
|
120429
|
-
|
|
120430
|
-
|
|
120857
|
+
children: [
|
|
120858
|
+
(0, react_jsx_runtime.jsxs)("button", {
|
|
120859
|
+
type: "button",
|
|
120860
|
+
className: `dsh-ig-batch-btn ${allSelectedAreFavorites ? "is-favorited" : ""}`,
|
|
120861
|
+
disabled: selectedIds.size === 0,
|
|
120862
|
+
onClick: () => void handleBatchToggleFavorite(),
|
|
120863
|
+
title: allSelectedAreFavorites ? t("batchUnfavorite") : t("batchFavorite"),
|
|
120864
|
+
children: [(0, react_jsx_runtime.jsx)(Heart, {
|
|
120865
|
+
size: 13,
|
|
120866
|
+
fill: allSelectedAreFavorites ? "#ef4444" : "none",
|
|
120867
|
+
color: allSelectedAreFavorites ? "#ef4444" : "currentColor"
|
|
120868
|
+
}), (0, react_jsx_runtime.jsx)("span", { children: allSelectedAreFavorites ? t("batchUnfavorite") : t("batchFavorite") })]
|
|
120869
|
+
}),
|
|
120870
|
+
(0, react_jsx_runtime.jsxs)("button", {
|
|
120871
|
+
type: "button",
|
|
120872
|
+
className: "dsh-ig-batch-btn dsh-ig-batch-btn-download",
|
|
120873
|
+
disabled: selectedIds.size === 0 || isDownloadingBatch,
|
|
120874
|
+
onClick: () => void handleBatchDownload(),
|
|
120875
|
+
title: t("batchDownload"),
|
|
120876
|
+
children: [(0, react_jsx_runtime.jsx)(Download, { size: 13 }), (0, react_jsx_runtime.jsx)("span", { children: isDownloadingBatch && batchDownloadProgress ? t("batchDownloading", {
|
|
120877
|
+
current: batchDownloadProgress.split("/")[0] ?? "",
|
|
120878
|
+
total: batchDownloadProgress.split("/")[1] ?? ""
|
|
120879
|
+
}) : t("batchDownload") })]
|
|
120880
|
+
}),
|
|
120881
|
+
(0, react_jsx_runtime.jsx)("span", { className: "dsh-ig-batch-divider" }),
|
|
120882
|
+
(0, react_jsx_runtime.jsxs)("button", {
|
|
120883
|
+
type: "button",
|
|
120884
|
+
className: "dsh-ig-batch-btn dsh-ig-batch-btn-danger",
|
|
120885
|
+
disabled: selectedIds.size === 0,
|
|
120886
|
+
onClick: () => setShowDeleteModal(true),
|
|
120887
|
+
children: [(0, react_jsx_runtime.jsx)(Trash2, { size: 13 }), (0, react_jsx_runtime.jsxs)("span", { children: [t("batchDelete"), selectedIds.size > 0 ? ` (${selectedIds.size})` : ""] })]
|
|
120888
|
+
}),
|
|
120889
|
+
(0, react_jsx_runtime.jsx)("button", {
|
|
120890
|
+
type: "button",
|
|
120891
|
+
className: "dsh-ig-batch-btn dsh-ig-batch-btn-exit",
|
|
120892
|
+
onClick: handleExitManage,
|
|
120893
|
+
children: t("exitManage")
|
|
120894
|
+
})
|
|
120895
|
+
]
|
|
120431
120896
|
})]
|
|
120432
120897
|
}),
|
|
120433
120898
|
showDeleteModal && (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -121284,6 +121749,9 @@ img.ProseMirror-separator {
|
|
|
121284
121749
|
|
|
121285
121750
|
/* tldraw infinite canvas surface (P0 spike) */
|
|
121286
121751
|
.dsh-ig-tl-canvas{position:relative;flex:1 1 0;height:0;min-height:0;overflow:hidden;isolation:isolate}
|
|
121752
|
+
.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}
|
|
121753
|
+
.dsh-ig-canvas-sync-status[data-phase='error']{color:#b42318;border-color:#fecdca}
|
|
121754
|
+
.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
121755
|
|
|
121288
121756
|
/* ---- Right-sidebar seat variant (DSH 0.1.5 'sidebar.right.pane.tab') ----
|
|
121289
121757
|
The seat is a narrow panel beside the conversation column, not a full
|
|
@@ -122005,7 +122473,7 @@ img.ProseMirror-separator {
|
|
|
122005
122473
|
`;
|
|
122006
122474
|
//#endregion
|
|
122007
122475
|
//#region lib/types/client/index.js
|
|
122008
|
-
console.info(`[dsh-image-gen] canvas bundle 2026-09-
|
|
122476
|
+
console.info(`[dsh-image-gen] canvas bundle 2026-09-23T16:16:55.812Z`);
|
|
122009
122477
|
/** Right-sidebar tab-type registration id (DSH 0.1.5 `sidebarRightTabs`). */
|
|
122010
122478
|
const SIDEBAR_STUDIO_TAB_ID = "dsh-image-gen/studio";
|
|
122011
122479
|
/** The tab kind: a page type opened by kind, recognizing no resource address. */
|
|
@@ -122149,6 +122617,7 @@ img.ProseMirror-separator {
|
|
|
122149
122617
|
confirmRegenerate: "确认生成",
|
|
122150
122618
|
regenerating: "重新生成中…",
|
|
122151
122619
|
regenerateFailed: "重新生成失败",
|
|
122620
|
+
regenerateSaveFailed: "已重新生成,但保存到图库失败,请重试",
|
|
122152
122621
|
versionPrevious: "上一版本",
|
|
122153
122622
|
versionNext: "下一版本",
|
|
122154
122623
|
versionLabel: "图片版本 {current}/{total}"
|
|
@@ -122291,6 +122760,7 @@ img.ProseMirror-separator {
|
|
|
122291
122760
|
confirmRegenerate: "Regenerate",
|
|
122292
122761
|
regenerating: "Regenerating…",
|
|
122293
122762
|
regenerateFailed: "Regeneration failed",
|
|
122763
|
+
regenerateSaveFailed: "Regenerated, but saving to the gallery failed. Please retry.",
|
|
122294
122764
|
versionPrevious: "Previous version",
|
|
122295
122765
|
versionNext: "Next version",
|
|
122296
122766
|
versionLabel: "Image version {current}/{total}"
|
|
@@ -122537,6 +123007,10 @@ img.ProseMirror-separator {
|
|
|
122537
123007
|
.dsh-ig-batch-btn{appearance:none;border:1px solid rgba(255,255,255,0.15);background:rgba(255,255,255,0.08);color:#fff;border-radius:20px;padding:5px 12px;font-size:12px;cursor:pointer;display:inline-flex;align-items:center;gap:5px;transition:background .15s,border-color .15s,color .15s}
|
|
122538
123008
|
.dsh-ig-batch-btn:hover:not(:disabled){background:rgba(255,255,255,0.2)}
|
|
122539
123009
|
.dsh-ig-batch-btn:disabled{opacity:0.4;cursor:not-allowed}
|
|
123010
|
+
.dsh-ig-batch-btn.is-favorited{background:rgba(239,68,68,0.15);border-color:rgba(239,68,68,0.4);color:#fca5a5}
|
|
123011
|
+
.dsh-ig-batch-btn.is-favorited:hover:not(:disabled){background:rgba(239,68,68,0.25)!important;border-color:rgba(239,68,68,0.6)!important;color:#fff!important}
|
|
123012
|
+
.dsh-ig-batch-btn-download:hover:not(:disabled){background:rgba(59,130,246,0.2)!important;border-color:rgba(59,130,246,0.5)!important;color:#93c5fd!important}
|
|
123013
|
+
.dsh-ig-batch-divider{width:1px;height:16px;background:rgba(255,255,255,0.18);margin:0 2px;display:inline-block}
|
|
122540
123014
|
.dsh-ig-batch-btn-danger{background:rgba(239,68,68,0.2);border-color:rgba(239,68,68,0.5);color:#fca5a5}
|
|
122541
123015
|
.dsh-ig-batch-btn-danger:hover:not(:disabled){background:rgba(239,68,68,0.4)!important;border-color:rgba(239,68,68,0.8)!important;color:#fff!important}
|
|
122542
123016
|
.dsh-ig-batch-btn-exit{border-color:transparent;background:transparent;color:rgba(255,255,255,0.7)}
|
|
@@ -124602,7 +125076,7 @@ img.ProseMirror-separator {
|
|
|
124602
125076
|
ratio: request.ratio,
|
|
124603
125077
|
quality: request.quality
|
|
124604
125078
|
};
|
|
124605
|
-
await saveGalleryItem({
|
|
125079
|
+
const savedOk = await saveGalleryItem({
|
|
124606
125080
|
id: String(revision.attachment.attachmentId),
|
|
124607
125081
|
attachment: revision.attachment,
|
|
124608
125082
|
prompt: revision.prompt,
|
|
@@ -124617,7 +125091,7 @@ img.ProseMirror-separator {
|
|
|
124617
125091
|
});
|
|
124618
125092
|
const next = appendConversationImageRevision(String(originId), revision);
|
|
124619
125093
|
setRevisionChain(next);
|
|
124620
|
-
setToast(t("regenerate"));
|
|
125094
|
+
setToast(savedOk ? t("regenerate") : t("regenerateSaveFailed"));
|
|
124621
125095
|
setTimeout(() => {
|
|
124622
125096
|
setToast(void 0);
|
|
124623
125097
|
}, 2e3);
|