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 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
- * Start mirroring one editor into the host canvas-state route.
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 = "init";
115763
- let lastPostAt = 0;
115764
- let inFlight;
115765
- const stopListening = editor.store.listen(() => {
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 selectionKey = selectionKeyOf(editor);
115768
- if (selectionKey !== lastSelectionKey) {
115769
- lastSelectionKey = selectionKey;
115770
- if (exportTimer !== void 0) clearTimeout(exportTimer);
115771
- exportTimer = setTimeout(() => {
115772
- exportTimer = void 0;
115773
- flush(true);
115774
- }, SELECTION_EXPORT_DELAY_MS);
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
- flush(false);
116008
+ pushState();
115779
116009
  }, STATE_DEBOUNCE_MS);
115780
116010
  }, {
115781
116011
  source: "all",
115782
116012
  scope: "all"
115783
116013
  });
115784
- async function flush(withScreenshot) {
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
- if (!withScreenshot && summary.stateKey === lastPushKey) return;
115788
- const elapsed = Date.now() - lastPostAt;
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
- if (selectionImage !== void 0) payload.selectionImage = selectionImage;
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: controller.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 (!disposed) console.warn("[dsh-image-gen] canvas state push failed:", error);
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
- flush(false);
116133
+ pushState();
115848
116134
  }, STATE_DEBOUNCE_MS);
115849
- if (editor.getSelectedShapeIds().length > 0) exportTimer = setTimeout(() => {
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?.abort();
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 StudioTlCanvas = (0, react.memo)(function StudioTlCanvas() {
116403
+ const TL_COMPONENTS = { Toolbar: StudioToolbar };
116404
+ const StudioTlCanvas = (0, react.memo)(function StudioTlCanvas({ lang = "zh" }) {
116116
116405
  const sessionId = (0, react.useId)();
116117
- return (0, react_jsx_runtime.jsx)("div", {
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: { Toolbar: StudioToolbar },
116123
- onMount: (editor) => {
116124
- let active = true;
116125
- applyBrandTheme(editor);
116126
- editor.updateInstanceState({ isGridMode: true });
116127
- console.info(`[dsh-image-gen] tldraw mounted (instance ${editor.id})`);
116128
- const stopLanding = registerTlLandingConsumer((items) => {
116129
- const generation = getTlLandingGeneration();
116130
- return landTlItems(editor, items, () => active && generation === getTlLandingGeneration());
116131
- });
116132
- let stopSync = startCanvasSync(editor);
116133
- const clear = () => {
116134
- stopSync();
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
- const nextFav = !selected.isFavorite;
117116
- setSelected((curr) => curr !== null && curr.id === targetId ? {
117117
- ...curr,
117118
- isFavorite: nextFav
117119
- } : curr);
117120
- if (currentBatch) setCurrentBatch((prev) => prev ? prev.map((i) => i.id === targetId ? {
117121
- ...i,
117122
- isFavorite: nextFav
117123
- } : i) : null);
117124
- flash(nextFav ? t("favoriteAdded") : t("favoriteRemoved"));
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 Delete",
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 dsh-ig-studio-btn-danger ${isManageMode ? "is-active" : ""}`,
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)(Trash2, { size: 13 }), (0, react_jsx_runtime.jsx)("span", { children: isManageMode ? t("exitManage") : t("manage") })]
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: [(0, react_jsx_runtime.jsxs)("button", {
120420
- type: "button",
120421
- className: "dsh-ig-batch-btn dsh-ig-batch-btn-danger",
120422
- disabled: selectedIds.size === 0,
120423
- onClick: () => setShowDeleteModal(true),
120424
- children: [(0, react_jsx_runtime.jsx)(Trash2, { size: 13 }), (0, react_jsx_runtime.jsxs)("span", { children: [t("batchDelete"), selectedIds.size > 0 ? ` (${selectedIds.size})` : ""] })]
120425
- }), (0, react_jsx_runtime.jsx)("button", {
120426
- type: "button",
120427
- className: "dsh-ig-batch-btn dsh-ig-batch-btn-exit",
120428
- onClick: handleExitManage,
120429
- children: t("exitManage")
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-20T15:13:07.204Z`);
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);