dsh-image-gen 0.6.10 → 0.7.0

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
@@ -515,6 +515,31 @@ window.__ModuleLoader__.load({
515
515
  notifyListeners();
516
516
  }
517
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
+ /**
518
543
  * Standardize path format across Windows and POSIX (lowercase, forward slashes, no trailing slash).
519
544
  */
520
545
  function normalizeWorkspacePath(rawPath) {
@@ -1254,6 +1279,19 @@ window.__ModuleLoader__.load({
1254
1279
  * This source code is licensed under the ISC license.
1255
1280
  * See the LICENSE file in the root directory of this source tree.
1256
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
+ */
1257
1295
  const Star = createLucideIcon("star", [["path", {
1258
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",
1259
1297
  key: "r04s7s"
@@ -1563,6 +1601,89 @@ window.__ModuleLoader__.load({
1563
1601
  const d = new Date(timestamp);
1564
1602
  return `${d.getFullYear()}/${d.getMonth() + 1}/${d.getDate()}`;
1565
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
+ }
1566
1687
  //#endregion
1567
1688
  //#region lib/types/client/multi-model-compare.js
1568
1689
  /** Map one shared output intent to settings accepted by every target model. */
@@ -116106,6 +116227,7 @@ img.ProseMirror-separator {
116106
116227
  const GRID_GAP = 40;
116107
116228
  const GRID_COLS = 3;
116108
116229
  const mountedCanvases = /* @__PURE__ */ new Set();
116230
+ const mountedEditors = /* @__PURE__ */ new Set();
116109
116231
  /** Reset canvas-owned data only. Gallery records and host attachments are untouched. */
116110
116232
  function clearStudioTlCanvases() {
116111
116233
  if (mountedCanvases.size === 0) return false;
@@ -116115,6 +116237,17 @@ img.ProseMirror-separator {
116115
116237
  return true;
116116
116238
  }
116117
116239
  /**
116240
+ * Cancel in-editor selection on every mounted canvas, keeping shapes and sync
116241
+ * untouched. Selection is interaction state tied to the surface: leaving the
116242
+ * studio page cancels it so a stale selection can neither resurface on return
116243
+ * nor linger in the host mirror.
116244
+ */
116245
+ function deselectStudioTlCanvases() {
116246
+ if (mountedEditors.size === 0) return false;
116247
+ for (const editor of mountedEditors) editor.run(() => editor.selectNone());
116248
+ return true;
116249
+ }
116250
+ /**
116118
116251
  * Dock the stock toolbar vertically along the left edge (Figma-style) instead
116119
116252
  * of tldraw's signature bottom-center pill. It is still the same
116120
116253
  * DefaultToolbar with the same tools and overflow handling — only the
@@ -116289,6 +116422,8 @@ img.ProseMirror-separator {
116289
116422
  const retrySync = (0, react.useRef)(() => {});
116290
116423
  const onMount = (0, react.useCallback)((editor) => {
116291
116424
  let active = true;
116425
+ editor.run(() => editor.selectNone());
116426
+ mountedEditors.add(editor);
116292
116427
  applyBrandTheme(editor);
116293
116428
  editor.updateInstanceState({ isGridMode: true });
116294
116429
  console.info(`[dsh-image-gen] tldraw mounted (instance ${editor.id})`);
@@ -116322,6 +116457,7 @@ img.ProseMirror-separator {
116322
116457
  mountedCanvases.add(clear);
116323
116458
  return () => {
116324
116459
  active = false;
116460
+ mountedEditors.delete(editor);
116325
116461
  mountedCanvases.delete(clear);
116326
116462
  stopLanding();
116327
116463
  stopSync();
@@ -119654,12 +119790,22 @@ img.ProseMirror-separator {
119654
119790
  close: "关闭 (Esc)",
119655
119791
  prevImage: "上一张 (←)",
119656
119792
  nextImage: "下一张 (→)",
119657
- manage: "批量删除",
119793
+ manage: "批量管理",
119658
119794
  exitManage: "退出选择",
119659
119795
  selectedCount: "已选 {n} 项",
119660
119796
  selectAll: "全选",
119661
119797
  invertSelect: "反选",
119662
119798
  clearSelect: "清空",
119799
+ batchFavorite: "批量收藏",
119800
+ batchUnfavorite: "批量取消收藏",
119801
+ batchFavoritedToast: "已将 {count} 张图片加入收藏",
119802
+ batchUnfavoritedToast: "已取消 {count} 张图片的收藏",
119803
+ batchFavoriteFailed: "收藏操作失败,请重试",
119804
+ batchDownload: "批量下载",
119805
+ batchDownloading: "打包中 ({current}/{total})…",
119806
+ batchDownloadSingleToast: "已下载 1 张图片",
119807
+ batchDownloadZipToast: "已打包下载 {count} 张图片",
119808
+ batchDownloadFailed: "批量下载失败,请重试",
119663
119809
  batchDelete: "批量删除",
119664
119810
  batchDeleteTitle: "确认批量删除选中的 {count} 张图片?",
119665
119811
  batchDeleteTitleSingle: "确认从画廊中删除这张图片?",
@@ -119740,12 +119886,22 @@ img.ProseMirror-separator {
119740
119886
  close: "Close (Esc)",
119741
119887
  prevImage: "Previous (←)",
119742
119888
  nextImage: "Next (→)",
119743
- manage: "Batch Delete",
119889
+ manage: "Batch Manage",
119744
119890
  exitManage: "Done",
119745
119891
  selectedCount: "{n} selected",
119746
119892
  selectAll: "Select All",
119747
119893
  invertSelect: "Invert",
119748
119894
  clearSelect: "Clear",
119895
+ batchFavorite: "Batch Favorite",
119896
+ batchUnfavorite: "Batch Unfavorite",
119897
+ batchFavoritedToast: "Added {count} images to favorites",
119898
+ batchUnfavoritedToast: "Removed {count} images from favorites",
119899
+ batchFavoriteFailed: "Favorite operation failed, please retry",
119900
+ batchDownload: "Batch Download",
119901
+ batchDownloading: "Packaging ({current}/{total})…",
119902
+ batchDownloadSingleToast: "Downloaded 1 image",
119903
+ batchDownloadZipToast: "Packaged & downloaded {count} images",
119904
+ batchDownloadFailed: "Batch download failed, please retry",
119749
119905
  batchDelete: "Batch Delete",
119750
119906
  batchDeleteTitle: "Delete {count} selected images?",
119751
119907
  batchDeleteTitleSingle: "Delete this image from gallery?",
@@ -119828,7 +119984,7 @@ img.ProseMirror-separator {
119828
119984
  return "1:1";
119829
119985
  }
119830
119986
  const GalleryViewTab = (props) => {
119831
- const { locale, sessionId, useSessions, useWorkspaces, credentialEvents, inSidebar, defaultTab, initialCanvasSurface } = props;
119987
+ const { locale, sessionId, useSessions, useWorkspaces, credentialEvents, inSidebar, defaultTab, initialCanvasSurface, useTabInfo } = props;
119832
119988
  const [activeTab, setActiveTab] = (0, react.useState)(defaultTab ?? "gallery");
119833
119989
  const [studioDraft, setStudioDraft] = (0, react.useState)(void 0);
119834
119990
  const [items, setItems] = (0, react.useState)([]);
@@ -119851,6 +120007,10 @@ img.ProseMirror-separator {
119851
120007
  localStorage.setItem("dsh-ig-only-current-workspace", String(onlyCurrentWorkspace));
119852
120008
  } catch {}
119853
120009
  }, [onlyCurrentWorkspace]);
120010
+ const tabVisible = useTabInfo?.().tab?.visible;
120011
+ (0, react.useEffect)(() => {
120012
+ if (tabVisible === false) deselectStudioTlCanvases();
120013
+ }, [tabVisible]);
119854
120014
  (0, react.useEffect)(() => {
119855
120015
  let mounted = true;
119856
120016
  fetch(STUDIO_ROUTE).then((res) => res.ok ? res.json() : null).then((data) => {
@@ -120273,6 +120433,91 @@ img.ProseMirror-separator {
120273
120433
  setSelectedIds(/* @__PURE__ */ new Set());
120274
120434
  lastSelectedIndexRef.current = null;
120275
120435
  };
120436
+ const selectedItems = (0, react.useMemo)(() => {
120437
+ if (selectedIds.size === 0) return [];
120438
+ return items.filter((item) => selectedIds.has(item.id));
120439
+ }, [items, selectedIds]);
120440
+ const allSelectedAreFavorites = (0, react.useMemo)(() => {
120441
+ if (selectedItems.length === 0) return false;
120442
+ return selectedItems.every((item) => Boolean(item.isFavorite));
120443
+ }, [selectedItems]);
120444
+ const handleBatchToggleFavorite = async () => {
120445
+ if (selectedIds.size === 0) return;
120446
+ const targetFavorite = !allSelectedAreFavorites;
120447
+ const targetIds = Array.from(selectedIds);
120448
+ setItems((prev) => prev.map((item) => selectedIds.has(item.id) ? {
120449
+ ...item,
120450
+ isFavorite: targetFavorite
120451
+ } : item));
120452
+ try {
120453
+ await bulkSetFavoriteGalleryItems(targetIds, targetFavorite);
120454
+ showToast(targetFavorite ? t("batchFavoritedToast", { count: String(targetIds.length) }) : t("batchUnfavoritedToast", { count: String(targetIds.length) }));
120455
+ } catch (err) {
120456
+ console.error("[dsh-image-gen] Batch favorite failed:", err);
120457
+ getGalleryItems().then((res) => setItems(res));
120458
+ showToast(t("batchFavoriteFailed"));
120459
+ }
120460
+ };
120461
+ const [isDownloadingBatch, setIsDownloadingBatch] = (0, react.useState)(false);
120462
+ const [batchDownloadProgress, setBatchDownloadProgress] = (0, react.useState)(null);
120463
+ const handleBatchDownload = async () => {
120464
+ if (selectedIds.size === 0 || isDownloadingBatch) return;
120465
+ const itemsToDownload = selectedItems;
120466
+ if (itemsToDownload.length === 0) return;
120467
+ setIsDownloadingBatch(true);
120468
+ try {
120469
+ if (itemsToDownload.length === 1) {
120470
+ const single = itemsToDownload[0];
120471
+ if (!single) return;
120472
+ let blob = blobCache.get(single.id);
120473
+ if (!blob) {
120474
+ blob = await fetchAttachmentBlob(single.attachment);
120475
+ blobCache.set(single.id, blob);
120476
+ }
120477
+ const objectUrl = URL.createObjectURL(blob);
120478
+ downloadBlobUrl(objectUrl, `dsh-${single.provider}-${single.id}.png`);
120479
+ URL.revokeObjectURL(objectUrl);
120480
+ showToast(t("batchDownloadSingleToast"));
120481
+ return;
120482
+ }
120483
+ const zipFiles = [];
120484
+ const usedNames = /* @__PURE__ */ new Set();
120485
+ for (let i = 0; i < itemsToDownload.length; i++) {
120486
+ const it = itemsToDownload[i];
120487
+ if (!it) continue;
120488
+ setBatchDownloadProgress(`${i + 1}/${itemsToDownload.length}`);
120489
+ let blob = blobCache.get(it.id);
120490
+ if (!blob) {
120491
+ blob = await fetchAttachmentBlob(it.attachment);
120492
+ blobCache.set(it.id, blob);
120493
+ }
120494
+ const arrayBuffer = await blob.arrayBuffer();
120495
+ const ext = blob.type === "image/jpeg" ? "jpg" : blob.type === "image/webp" ? "webp" : "png";
120496
+ let baseName = `dsh-${it.provider}-${it.id.slice(0, 8)}.${ext}`;
120497
+ let count = 1;
120498
+ while (usedNames.has(baseName)) {
120499
+ baseName = `dsh-${it.provider}-${it.id.slice(0, 8)}-${count}.${ext}`;
120500
+ count++;
120501
+ }
120502
+ usedNames.add(baseName);
120503
+ zipFiles.push({
120504
+ name: baseName,
120505
+ data: new Uint8Array(arrayBuffer)
120506
+ });
120507
+ }
120508
+ const zipBlob = createZipBlob(zipFiles);
120509
+ const zipUrl = URL.createObjectURL(zipBlob);
120510
+ downloadBlobUrl(zipUrl, `dsh-gallery-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10).replace(/-/g, "")}.zip`);
120511
+ URL.revokeObjectURL(zipUrl);
120512
+ showToast(t("batchDownloadZipToast", { count: String(itemsToDownload.length) }));
120513
+ } catch (err) {
120514
+ console.error("[dsh-image-gen] Batch download failed:", err);
120515
+ showToast(t("batchDownloadFailed"));
120516
+ } finally {
120517
+ setIsDownloadingBatch(false);
120518
+ setBatchDownloadProgress(null);
120519
+ }
120520
+ };
120276
120521
  const requestSingleDelete = (item, e) => {
120277
120522
  if (e) e.stopPropagation();
120278
120523
  setSelectedIds(/* @__PURE__ */ new Set([item.id]));
@@ -120505,13 +120750,13 @@ img.ProseMirror-separator {
120505
120750
  })]
120506
120751
  }), (0, react_jsx_runtime.jsxs)("button", {
120507
120752
  type: "button",
120508
- className: `dsh-ig-studio-btn dsh-ig-studio-btn-danger ${isManageMode ? "is-active" : ""}`,
120753
+ className: `dsh-ig-studio-btn ${isManageMode ? "is-active" : ""}`,
120509
120754
  title: isManageMode ? t("exitManage") : t("manage"),
120510
120755
  onClick: () => {
120511
120756
  if (isManageMode) handleExitManage();
120512
120757
  else setIsManageMode(true);
120513
120758
  },
120514
- children: [(0, react_jsx_runtime.jsx)(Trash2, { size: 13 }), (0, react_jsx_runtime.jsx)("span", { children: isManageMode ? t("exitManage") : t("manage") })]
120759
+ children: [(0, react_jsx_runtime.jsx)(SquareCheckBig, { size: 13 }), (0, react_jsx_runtime.jsx)("span", { children: isManageMode ? t("exitManage") : t("manage") })]
120515
120760
  })]
120516
120761
  })]
120517
120762
  }),
@@ -120628,18 +120873,45 @@ img.ProseMirror-separator {
120628
120873
  ]
120629
120874
  }), (0, react_jsx_runtime.jsxs)("div", {
120630
120875
  className: "dsh-ig-batch-bar-right",
120631
- children: [(0, react_jsx_runtime.jsxs)("button", {
120632
- type: "button",
120633
- className: "dsh-ig-batch-btn dsh-ig-batch-btn-danger",
120634
- disabled: selectedIds.size === 0,
120635
- onClick: () => setShowDeleteModal(true),
120636
- children: [(0, react_jsx_runtime.jsx)(Trash2, { size: 13 }), (0, react_jsx_runtime.jsxs)("span", { children: [t("batchDelete"), selectedIds.size > 0 ? ` (${selectedIds.size})` : ""] })]
120637
- }), (0, react_jsx_runtime.jsx)("button", {
120638
- type: "button",
120639
- className: "dsh-ig-batch-btn dsh-ig-batch-btn-exit",
120640
- onClick: handleExitManage,
120641
- children: t("exitManage")
120642
- })]
120876
+ children: [
120877
+ (0, react_jsx_runtime.jsxs)("button", {
120878
+ type: "button",
120879
+ className: `dsh-ig-batch-btn ${allSelectedAreFavorites ? "is-favorited" : ""}`,
120880
+ disabled: selectedIds.size === 0,
120881
+ onClick: () => void handleBatchToggleFavorite(),
120882
+ title: allSelectedAreFavorites ? t("batchUnfavorite") : t("batchFavorite"),
120883
+ children: [(0, react_jsx_runtime.jsx)(Heart, {
120884
+ size: 13,
120885
+ fill: allSelectedAreFavorites ? "#ef4444" : "none",
120886
+ color: allSelectedAreFavorites ? "#ef4444" : "currentColor"
120887
+ }), (0, react_jsx_runtime.jsx)("span", { children: allSelectedAreFavorites ? t("batchUnfavorite") : t("batchFavorite") })]
120888
+ }),
120889
+ (0, react_jsx_runtime.jsxs)("button", {
120890
+ type: "button",
120891
+ className: "dsh-ig-batch-btn dsh-ig-batch-btn-download",
120892
+ disabled: selectedIds.size === 0 || isDownloadingBatch,
120893
+ onClick: () => void handleBatchDownload(),
120894
+ title: t("batchDownload"),
120895
+ children: [(0, react_jsx_runtime.jsx)(Download, { size: 13 }), (0, react_jsx_runtime.jsx)("span", { children: isDownloadingBatch && batchDownloadProgress ? t("batchDownloading", {
120896
+ current: batchDownloadProgress.split("/")[0] ?? "",
120897
+ total: batchDownloadProgress.split("/")[1] ?? ""
120898
+ }) : t("batchDownload") })]
120899
+ }),
120900
+ (0, react_jsx_runtime.jsx)("span", { className: "dsh-ig-batch-divider" }),
120901
+ (0, react_jsx_runtime.jsxs)("button", {
120902
+ type: "button",
120903
+ className: "dsh-ig-batch-btn dsh-ig-batch-btn-danger",
120904
+ disabled: selectedIds.size === 0,
120905
+ onClick: () => setShowDeleteModal(true),
120906
+ children: [(0, react_jsx_runtime.jsx)(Trash2, { size: 13 }), (0, react_jsx_runtime.jsxs)("span", { children: [t("batchDelete"), selectedIds.size > 0 ? ` (${selectedIds.size})` : ""] })]
120907
+ }),
120908
+ (0, react_jsx_runtime.jsx)("button", {
120909
+ type: "button",
120910
+ className: "dsh-ig-batch-btn dsh-ig-batch-btn-exit",
120911
+ onClick: handleExitManage,
120912
+ children: t("exitManage")
120913
+ })
120914
+ ]
120643
120915
  })]
120644
120916
  }),
120645
120917
  showDeleteModal && (0, react_jsx_runtime.jsx)("div", {
@@ -121785,6 +122057,12 @@ img.ProseMirror-separator {
121785
122057
  return typeof candidate.attachmentId === "string" && (candidate.mediaType === "image/png" || candidate.mediaType === "image/jpeg" || candidate.mediaType === "image/webp" || candidate.mediaType === "image/gif") && typeof candidate.bytes === "number" && typeof candidate.width === "number" && typeof candidate.height === "number";
121786
122058
  }
121787
122059
  //#endregion
122060
+ //#region lib/types/client/settings-write.js
122061
+ /** DSH 0.1.7 resolves refused writes to false; older settings scopes resolve to void. */
122062
+ async function writeSetting(scope, field, value, rejectedMessage) {
122063
+ if (await scope.set(field, value) === false) throw new Error(rejectedMessage);
122064
+ }
122065
+ //#endregion
121788
122066
  //#region lib/types/client/provider-pill.js
121789
122067
  /**
121790
122068
  * Composer tool-row pill: switch the default image provider without leaving
@@ -121805,7 +122083,8 @@ img.ProseMirror-separator {
121805
122083
  subSignedOut: "未登录",
121806
122084
  subStatusUnknown: "状态未知",
121807
122085
  readOnly: "图像设置为只读,无法在此切换",
121808
- switchFailed: "切换失败"
122086
+ switchFailed: "切换失败",
122087
+ writeRejected: "设置未保存,请重试"
121809
122088
  },
121810
122089
  en: {
121811
122090
  pillLabel: "Image",
@@ -121819,7 +122098,8 @@ img.ProseMirror-separator {
121819
122098
  subSignedOut: "Signed out",
121820
122099
  subStatusUnknown: "Unknown",
121821
122100
  readOnly: "Image settings are read-only; switch them in the config source",
121822
- switchFailed: "Switch failed"
122101
+ switchFailed: "Switch failed",
122102
+ writeRejected: "Setting was not saved. Please try again"
121823
122103
  }
121824
122104
  };
121825
122105
  /** Provider row names, per locale. Brand words stay untranslated; only the
@@ -122027,7 +122307,7 @@ img.ProseMirror-separator {
122027
122307
  }
122028
122308
  setError("");
122029
122309
  setPending(provider);
122030
- props.scope.set("provider", provider).then(() => {
122310
+ writeSetting(props.scope, "provider", provider, t("writeRejected")).then(() => {
122031
122311
  setOpen(false);
122032
122312
  }).catch((cause) => {
122033
122313
  setError(cause instanceof Error ? cause.message : String(cause));
@@ -122220,7 +122500,7 @@ img.ProseMirror-separator {
122220
122500
  `;
122221
122501
  //#endregion
122222
122502
  //#region lib/types/client/index.js
122223
- console.info(`[dsh-image-gen] canvas bundle 2026-09-20T16:35:15.504Z`);
122503
+ console.info(`[dsh-image-gen] canvas bundle 2026-09-26T07:20:53.102Z`);
122224
122504
  /** Right-sidebar tab-type registration id (DSH 0.1.5 `sidebarRightTabs`). */
122225
122505
  const SIDEBAR_STUDIO_TAB_ID = "dsh-image-gen/studio";
122226
122506
  /** The tab kind: a page type opened by kind, recognizing no resource address. */
@@ -122345,6 +122625,7 @@ img.ProseMirror-separator {
122345
122625
  saving: "保存中…",
122346
122626
  save: "保存",
122347
122627
  saved: "已保存",
122628
+ settingsWriteRejected: "有一项设置未能保存,请检查后重试。",
122348
122629
  savedToPath: "已保存到",
122349
122630
  generating: "正在生成图片…",
122350
122631
  loading: "正在加载图片…",
@@ -122488,6 +122769,7 @@ img.ProseMirror-separator {
122488
122769
  saving: "Saving…",
122489
122770
  save: "Save",
122490
122771
  saved: "Saved",
122772
+ settingsWriteRejected: "A setting could not be saved. Please check and try again.",
122491
122773
  savedToPath: "Saved to",
122492
122774
  generating: "Generating image…",
122493
122775
  loading: "Loading image…",
@@ -122754,6 +123036,10 @@ img.ProseMirror-separator {
122754
123036
  .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}
122755
123037
  .dsh-ig-batch-btn:hover:not(:disabled){background:rgba(255,255,255,0.2)}
122756
123038
  .dsh-ig-batch-btn:disabled{opacity:0.4;cursor:not-allowed}
123039
+ .dsh-ig-batch-btn.is-favorited{background:rgba(239,68,68,0.15);border-color:rgba(239,68,68,0.4);color:#fca5a5}
123040
+ .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}
123041
+ .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}
123042
+ .dsh-ig-batch-divider{width:1px;height:16px;background:rgba(255,255,255,0.18);margin:0 2px;display:inline-block}
122757
123043
  .dsh-ig-batch-btn-danger{background:rgba(239,68,68,0.2);border-color:rgba(239,68,68,0.5);color:#fca5a5}
122758
123044
  .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}
122759
123045
  .dsh-ig-batch-btn-exit{border-color:transparent;background:transparent;color:rgba(255,255,255,0.7)}
@@ -122800,19 +123086,40 @@ img.ProseMirror-separator {
122800
123086
  :has(.dsh-ig-gallery-page:not(.dsh-ig-page-in-sidebar)) [class*="widthHandle"],
122801
123087
  .root:has(.dsh-ig-gallery-page:not(.dsh-ig-page-in-sidebar)) [class*="widthHandle"]{display:none!important}
122802
123088
  `;
122803
- /** Required browser services. */
123089
+ /**
123090
+ * Required browser services. The settings form reader is deliberately absent:
123091
+ * DSH 0.1.7 serves it as `configForms` (keyed by the Loader entry id) while
123092
+ * 0.1.5/0.1.6 expose `settingsScope` (keyed by the legacy namespace), so it is
123093
+ * probed at runtime in `apply`. A static inject of either name would park this
123094
+ * fiber forever on hosts from the other generation — the issue #55 failure.
123095
+ */
122804
123096
  const inject = [
122805
123097
  "slots",
122806
123098
  "connection",
122807
123099
  "remote",
122808
- "settingsScope",
122809
123100
  "locale"
122810
123101
  ];
123102
+ /** Host with neither settings generation: reads empty, writes refuse, never throws. */
123103
+ function degradedScope() {
123104
+ return {
123105
+ getSnapshot: () => ({
123106
+ value: void 0,
123107
+ writable: false
123108
+ }),
123109
+ subscribe: () => () => {},
123110
+ set: async () => false
123111
+ };
123112
+ }
122811
123113
  /** Mount the settings card, generated-image card, and native conversation gallery view. */
122812
123114
  function apply(ctx) {
122813
- const scope = ctx.settingsScope.bind({ namespace: IMAGE_GENERATION_NAMESPACE });
122814
- const chatScope = ctx.settingsScope.bind({ namespace: "ui-chat" });
122815
- const isCompactTranscript = () => chatScope.getSnapshot().value?.transcriptView !== "normal";
123115
+ const configForms = ctx.get("configForms");
123116
+ const settingsScopeApi = configForms === void 0 ? ctx.get("settingsScope") : void 0;
123117
+ const scope = configForms?.get("image-gen") ?? settingsScopeApi?.bind({ namespace: "image-generation" }) ?? degradedScope();
123118
+ const chatScope = configForms?.get("ui-chat") ?? settingsScopeApi?.bind({ namespace: "ui-chat" }) ?? degradedScope();
123119
+ const isCompactTranscript = () => {
123120
+ const mode = chatScope.getSnapshot().value?.transcriptView;
123121
+ return configForms === void 0 ? mode !== "normal" : mode !== "standard" && mode !== "detailed" && mode !== "verbose";
123122
+ };
122816
123123
  const locale = ctx.get("locale");
122817
123124
  const promotion = { enabled: false };
122818
123125
  ctx.inject(["sessions"], (owner) => {
@@ -123305,6 +123612,7 @@ img.ProseMirror-separator {
123305
123612
  if (params) for (const [k, v] of Object.entries(params)) text = text.replace(`{${k}}`, v);
123306
123613
  return text;
123307
123614
  };
123615
+ const saveSetting = (field, value) => writeSetting(props.scope, field, value, t("settingsWriteRejected"));
123308
123616
  const providerLabels = {
123309
123617
  google: t("providerGoogle"),
123310
123618
  openai: t("providerOpenAI"),
@@ -123416,7 +123724,7 @@ img.ProseMirror-separator {
123416
123724
  setDefaultProvider(provider);
123417
123725
  setProviderMessage("");
123418
123726
  setProviderMessageIsError(false);
123419
- props.scope.set("provider", provider).catch((cause) => {
123727
+ saveSetting("provider", provider).catch((cause) => {
123420
123728
  setDefaultProvider(snapshot.value?.provider ?? "google");
123421
123729
  setProviderMessage(cause instanceof Error ? cause.message : String(cause));
123422
123730
  setProviderMessageIsError(true);
@@ -123442,12 +123750,12 @@ img.ProseMirror-separator {
123442
123750
  }
123443
123751
  if (new Set(entries.map((entry) => entry.name)).size !== entries.length) throw new Error(t("workflowDuplicateName"));
123444
123752
  const activeEntry = entries.find((entry) => entry.name === row.activeWorkflow) ?? entries[0];
123445
- await props.scope.set("comfyuiBaseURL", row.baseURL);
123446
- await props.scope.set("comfyuiWorkflows", entries);
123447
- await props.scope.set("comfyuiActiveWorkflow", activeEntry === void 0 ? "" : activeEntry.name);
123448
- await props.scope.set("comfyuiWorkflowJson", activeEntry === void 0 ? "" : activeEntry.json);
123449
- await props.scope.set("comfyuiWorkflowName", activeEntry === void 0 ? "" : activeEntry.name);
123450
- await props.scope.set("comfyuiTimeoutMs", Math.max(1, Math.round(row.timeoutSeconds)) * 1e3);
123753
+ await saveSetting("comfyuiBaseURL", row.baseURL);
123754
+ await saveSetting("comfyuiWorkflows", entries);
123755
+ await saveSetting("comfyuiActiveWorkflow", activeEntry === void 0 ? "" : activeEntry.name);
123756
+ await saveSetting("comfyuiWorkflowJson", activeEntry === void 0 ? "" : activeEntry.json);
123757
+ await saveSetting("comfyuiWorkflowName", activeEntry === void 0 ? "" : activeEntry.name);
123758
+ await saveSetting("comfyuiTimeoutMs", Math.max(1, Math.round(row.timeoutSeconds)) * 1e3);
123451
123759
  } else if (isSubscriptionProvider(provider)) await Promise.resolve();
123452
123760
  else {
123453
123761
  let editExtra = {};
@@ -123464,16 +123772,16 @@ img.ProseMirror-separator {
123464
123772
  editExtra = parsed;
123465
123773
  }
123466
123774
  }
123467
- await props.scope.set(modelFieldOf(provider), row.model);
123468
- await props.scope.set(baseURLFieldOf(provider), row.baseURL);
123775
+ await saveSetting(modelFieldOf(provider), row.model);
123776
+ await saveSetting(baseURLFieldOf(provider), row.baseURL);
123469
123777
  if (provider === "openai-compat") {
123470
- await props.scope.set("openaiCompatEditFormat", row.editFormat);
123471
- await props.scope.set("openaiCompatEditExtra", editExtra);
123778
+ await saveSetting("openaiCompatEditFormat", row.editFormat);
123779
+ await saveSetting("openaiCompatEditExtra", editExtra);
123472
123780
  }
123473
123781
  if (provider === "seedream") {
123474
- await props.scope.set("seedreamOutputFormat", row.outputFormat);
123475
- await props.scope.set("seedreamWatermark", row.watermark);
123476
- await props.scope.set("seedreamBackground", row.background);
123782
+ await saveSetting("seedreamOutputFormat", row.outputFormat);
123783
+ await saveSetting("seedreamWatermark", row.watermark);
123784
+ await saveSetting("seedreamBackground", row.background);
123477
123785
  }
123478
123786
  if (row.keyInput.trim().length > 0) {
123479
123787
  const keyRef = cloudCredentialRef(provider);
@@ -123764,7 +124072,7 @@ img.ProseMirror-separator {
123764
124072
  setSaveToWorkspace(next);
123765
124073
  setWorkspaceMessage("");
123766
124074
  setWorkspaceMessageIsError(false);
123767
- props.scope.set("saveToWorkspace", next).catch((cause) => {
124075
+ saveSetting("saveToWorkspace", next).catch((cause) => {
123768
124076
  setSaveToWorkspace(snapshot.value?.saveToWorkspace ?? true);
123769
124077
  setWorkspaceMessage(cause instanceof Error ? cause.message : String(cause));
123770
124078
  setWorkspaceMessageIsError(true);
@@ -123776,7 +124084,7 @@ img.ProseMirror-separator {
123776
124084
  if (next === (snapshot.value?.workspaceFolder ?? "dsh-image-gen")) return;
123777
124085
  setWorkspaceMessage("");
123778
124086
  setWorkspaceMessageIsError(false);
123779
- props.scope.set("workspaceFolder", next).then(() => {
124087
+ saveSetting("workspaceFolder", next).then(() => {
123780
124088
  setWorkspaceMessage(t("saved"));
123781
124089
  }).catch((cause) => {
123782
124090
  setWorkspaceFolder(snapshot.value?.workspaceFolder ?? "dsh-image-gen");
@@ -123789,7 +124097,7 @@ img.ProseMirror-separator {
123789
124097
  setShowPill(next);
123790
124098
  setUiMessage("");
123791
124099
  setUiMessageIsError(false);
123792
- props.scope.set("showProviderPill", next).catch((cause) => {
124100
+ saveSetting("showProviderPill", next).catch((cause) => {
123793
124101
  setShowPill(snapshot.value?.showProviderPill === true);
123794
124102
  setUiMessage(cause instanceof Error ? cause.message : String(cause));
123795
124103
  setUiMessageIsError(true);