feat(console): 媒体库抓手排序——每行缩略图与垃圾桶之间加六点抓手,按住上下拖实时重排、松手持久化(photoStore.reorder + env)

This commit is contained in:
Aisland
2026-09-02 00:46:38 +08:00
parent 064f4644ec
commit 3f2b80f347
14 changed files with 189 additions and 3 deletions
+89 -1
View File
@@ -8219,6 +8219,9 @@ var css = xterm_default + "\n" + [
".dsh-wt_dropGear:hover{background:var(--wt-chip);color:var(--wt-text)}",
".dsh-wt_dropWide{width:264px}",
".dsh-wt_dropTrash{flex:none;width:22px;height:22px;margin-right:2px;padding:0;border:none;border-radius:6px;background:transparent;color:var(--wt-text3);cursor:pointer;display:inline-flex;align-items:center;justify-content:center;transition:background .15s ease,color .15s ease}",
".dsh-wt_dragHandle{flex:none;width:18px;height:22px;padding:0;border:none;border-radius:6px;background:transparent;color:var(--wt-text3);cursor:grab;display:inline-flex;align-items:center;justify-content:center;transition:background .15s ease,color .15s ease;touch-action:none}",
".dsh-wt_dragHandle:hover{background:var(--wt-chip);color:var(--wt-text2)}",
".dsh-wt_dragHandleOn{background:var(--wt-chip);color:var(--wt-text);cursor:grabbing}",
".dsh-wt_dropTrash:hover{background:rgba(244,63,94,.16);color:#f87171}",
".dsh-wt_gridLabel{flex:none;white-space:nowrap;font-size:12px;line-height:17px;color:var(--wt-text2);padding:2px 2px 2px 4px}",
".dsh-wt_gridHalf .dsh-wt_switch{margin-right:2px}",
@@ -8515,6 +8518,7 @@ var zh = {
"console.bgPhotoUse": "\u4F7F\u7528\u6B64\u7167\u7247",
"console.bgPhotoCurrent": "\u5F53\u524D\u4F7F\u7528",
"console.bgPhotoNone": "\u8FD8\u6CA1\u6709\u4E0A\u4F20\u8FC7\u7167\u7247",
"console.bgMediaDrag": "\u62D6\u52A8\u8C03\u6574\u987A\u5E8F",
"console.bgPhotoDelete": "\u5220\u9664\u8FD9\u6761\u5A92\u4F53",
"console.bgGridLabel": "\u7F51\u683C\u80CC\u666F",
"console.bgTipB": "\u8D34\u7247\u6A21\u7CCA\u7A0B\u5EA6",
@@ -8729,6 +8733,7 @@ var en = {
"console.bgPhotoUse": "Use this photo",
"console.bgPhotoCurrent": "In use",
"console.bgPhotoNone": "No photos yet",
"console.bgMediaDrag": "Drag to reorder",
"console.bgPhotoDelete": "Delete this media",
"console.bgGridLabel": "Background grid",
"console.bgTipB": "Card blur",
@@ -18080,6 +18085,42 @@ function ConsolePane() {
setAutoCheck(next);
if (next) void runUpdateCheck(false);
};
const dragRowRef = (0, import_react.useRef)(null);
const [dragRowId, setDragRowId] = (0, import_react.useState)(null);
const onHandleDown = (id, e) => {
if (e.button !== 0) return;
e.preventDefault();
e.stopPropagation();
dragRowRef.current = id;
setDragRowId(id);
const onMove = (ev) => {
if (dragRowRef.current !== id) return;
const rows = Array.from(document.querySelectorAll(".dsh-wt_photoRow"));
let target = 0;
for (let i = 0; i < rows.length; i++) {
const r = rows[i].getBoundingClientRect();
if (ev.clientY > r.top + r.height / 2) target = i + 1;
}
const from = photoList.findIndex((x) => x.id === id);
if (from < 0) return;
const next = [...photoList];
const moved = next.splice(from, 1)[0];
const to = Math.max(0, Math.min(target > from ? target - 1 : target, next.length));
next.splice(to, 0, moved);
setPhotoList(next);
};
const onUp = () => {
dragRowRef.current = null;
setDragRowId(null);
window.removeEventListener("pointermove", onMove);
window.removeEventListener("pointerup", onUp);
const ids = photoList.map((x) => x.id);
void splitEnv?.console?.reorderPhotos?.(ids).catch(() => {
});
};
window.addEventListener("pointermove", onMove);
window.addEventListener("pointerup", onUp);
};
const selectPhoto = (p) => {
setPhotoIdLocal(p.id);
splitEnv?.console?.setPhotoId?.(p.id);
@@ -18484,6 +18525,14 @@ function ConsolePane() {
] }) })
] }),
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-wt_photoName", children: photoId === p.id ? T("console.bgPhotoCurrent") : T("console.bgPhotoUse") }),
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "dsh-wt_dragHandle" + (dragRowId === p.id ? " dsh-wt_dragHandleOn" : ""), title: T("console.bgMediaDrag"), "aria-label": T("console.bgMediaDrag"), onPointerDown: (e) => onHandleDown(p.id, e), children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { width: "10", height: "12", viewBox: "0 0 10 12", "aria-hidden": true, children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "2.5", cy: "2", r: "1.1", fill: "currentColor" }),
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "7.5", cy: "2", r: "1.1", fill: "currentColor" }),
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "2.5", cy: "6", r: "1.1", fill: "currentColor" }),
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "7.5", cy: "6", r: "1.1", fill: "currentColor" }),
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "2.5", cy: "10", r: "1.1", fill: "currentColor" }),
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "7.5", cy: "10", r: "1.1", fill: "currentColor" })
] }) }),
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "dsh-wt_dropTrash", title: T("console.bgPhotoDelete"), "aria-label": T("console.bgPhotoDelete"), onClick: (e) => {
e.stopPropagation();
removePhotoById(p);
@@ -19594,7 +19643,9 @@ function getAll(db) {
arr.push({ id: r.id, createdAt: r.createdAt, kind: kindOf(r.blob), blob: r.blob });
}
}
arr.sort((a, b) => b.createdAt - a.createdAt);
const allOrdered = arr.every((r) => typeof r.order === "number");
if (allOrdered) arr.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
else arr.sort((a, b) => b.createdAt - a.createdAt);
db.close();
resolve(arr);
};
@@ -19657,6 +19708,40 @@ var photoStore = {
};
});
},
/** 排序:ids 按给定顺序写入 order(0..n),其余保持原顺序追加在后 */
async reorder(ids) {
const db = await openDb();
const cur = await getAll(db);
const idx = /* @__PURE__ */ new Map();
ids.forEach((id, i) => idx.set(id, i));
const ordered = [];
const restSorted = [...cur].sort((a, b) => b.createdAt - a.createdAt);
for (const r of cur) {
const n = idx.get(r.id);
if (typeof n === "number") ordered.push({ ...r, order: n });
}
let tail = ordered.length;
for (const r of restSorted) {
if (!idx.has(r.id)) ordered.push({ ...r, order: tail++ });
}
return new Promise((resolve, reject) => {
const tx = db.transaction(STORE, "readwrite");
const store = tx.objectStore(STORE);
for (const r of ordered) store.put(r, r.id);
tx.oncomplete = () => {
db.close();
resolve();
};
tx.onerror = () => {
db.close();
reject(tx.error);
};
tx.onabort = () => {
db.close();
reject(tx.error);
};
});
},
/** 删除一张记录 */
async remove(id) {
const db = await openDb();
@@ -20958,6 +21043,9 @@ function WorktableSection(props) {
removePhoto: async (id) => {
await photoStore.remove(id);
},
reorderPhotos: async (ids) => {
await photoStore.reorder(ids);
},
getPhotoGrid: () => viewRef.current.consoleBgPhotoGrid !== false,
setPhotoGrid: (v) => persistView({ consoleBgPhotoGrid: v }),
getGridOpacity: () => viewRef.current.consoleBgGridOpacity ?? 8,
+1
View File
@@ -1425,6 +1425,7 @@ function WorktableSection(props: any) {
},
setPhotoId: (id: string) => persistView({ consoleBgPhotoId: id }),
removePhoto: async (id: string) => { await photoStore.remove(id) },
reorderPhotos: async (ids: string[]) => { await photoStore.reorder(ids) },
getPhotoGrid: () => viewRef.current.consoleBgPhotoGrid !== false,
setPhotoGrid: (v: boolean) => persistView({ consoleBgPhotoGrid: v }),
getGridOpacity: () => viewRef.current.consoleBgGridOpacity ?? 8,
+2
View File
@@ -134,6 +134,7 @@ export const zh = {
'console.bgPhotoUse': '使用此照片',
'console.bgPhotoCurrent': '当前使用',
'console.bgPhotoNone': '还没有上传过照片',
'console.bgMediaDrag': '拖动调整顺序',
'console.bgPhotoDelete': '删除这条媒体',
'console.bgGridLabel': '网格背景',
'console.bgTipB': '贴片模糊程度',
@@ -353,6 +354,7 @@ export const en = {
'console.bgPhotoUse': 'Use this photo',
'console.bgPhotoCurrent': 'In use',
'console.bgPhotoNone': 'No photos yet',
'console.bgMediaDrag': 'Drag to reorder',
'console.bgPhotoDelete': 'Delete this media',
'console.bgGridLabel': 'Background grid',
'console.bgTipB': 'Card blur',
+29 -2
View File
@@ -7,7 +7,7 @@ const LEGACY_STORE = 'consoleBgPhoto'
const LEGACY_KEY = 'original'
export type MediaKind = 'photo' | 'video'
export type PhotoRecord = { id: string; createdAt: number; kind: MediaKind; blob: Blob }
export type PhotoRecord = { id: string; createdAt: number; kind: MediaKind; blob: Blob; order?: number }
export const kindOf = (blob: Blob): MediaKind => (blob.type && blob.type.indexOf('video/') === 0 ? 'video' : 'photo')
@@ -35,7 +35,9 @@ function getAll(db: IDBDatabase): Promise<PhotoRecord[]> {
arr.push({ id: r.id, createdAt: r.createdAt, kind: kindOf(r.blob), blob: r.blob })
}
}
arr.sort((a, b) => b.createdAt - a.createdAt)
const allOrdered = arr.every((r) => typeof r.order === 'number')
if (allOrdered) arr.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
else arr.sort((a, b) => b.createdAt - a.createdAt)
db.close()
resolve(arr)
}
@@ -87,6 +89,31 @@ export const photoStore = {
tx.onabort = () => { db.close(); reject(tx.error) }
})
},
/** 排序:ids 按给定顺序写入 order(0..n),其余保持原顺序追加在后 */
async reorder(ids: string[]): Promise<void> {
const db = await openDb()
const cur = await getAll(db)
const idx = new Map<string, number>()
ids.forEach((id, i) => idx.set(id, i))
const ordered: PhotoRecord[] = []
const restSorted = [...cur].sort((a, b) => b.createdAt - a.createdAt)
for (const r of cur) {
const n = idx.get(r.id)
if (typeof n === 'number') ordered.push({ ...r, order: n })
}
let tail = ordered.length
for (const r of restSorted) {
if (!idx.has(r.id)) ordered.push({ ...r, order: tail++ })
}
return new Promise((resolve, reject) => {
const tx = db.transaction(STORE, 'readwrite')
const store = tx.objectStore(STORE)
for (const r of ordered) store.put(r, r.id)
tx.oncomplete = () => { db.close(); resolve() }
tx.onerror = () => { db.close(); reject(tx.error) }
tx.onabort = () => { db.close(); reject(tx.error) }
})
},
/** 删除一张记录 */
async remove(id: string): Promise<void> {
const db = await openDb()
+38
View File
@@ -255,6 +255,7 @@ type SplitEnv = {
getPhotoHsl: (id: string) => { h: number; s: number; l: number }
setPhotoHsl: (id: string, v: { h: number; s: number; l: number }) => void
removePhoto: (id: string) => Promise<void>
reorderPhotos: (ids: string[]) => Promise<void>
getPhotoGrid: () => boolean
setPhotoGrid: (v: boolean) => void
getGridOpacity: () => number
@@ -1571,6 +1572,42 @@ function ConsolePane() {
storeAutoCheck(next)
if (next) void runUpdateCheck(false)
}
/** 抓手拖拽排序:按住抓手上下移动 → 行实时重排,松手持久化 */
const dragRowRef = useRef<string | null>(null)
const [dragRowId, setDragRowId] = useState<string | null>(null)
const onHandleDown = (id: string, e: { button: number; preventDefault: () => void; stopPropagation: () => void }) => {
if (e.button !== 0) return
e.preventDefault()
e.stopPropagation()
dragRowRef.current = id
setDragRowId(id)
const onMove = (ev: PointerEvent) => {
if (dragRowRef.current !== id) return
const rows = Array.from(document.querySelectorAll<HTMLElement>('.dsh-wt_photoRow'))
let target = 0
for (let i = 0; i < rows.length; i++) {
const r = rows[i].getBoundingClientRect()
if (ev.clientY > r.top + r.height / 2) target = i + 1
}
const from = photoList.findIndex((x) => x.id === id)
if (from < 0) return
const next = [...photoList]
const moved = next.splice(from, 1)[0]
const to = Math.max(0, Math.min(target > from ? target - 1 : target, next.length))
next.splice(to, 0, moved)
setPhotoList(next)
}
const onUp = () => {
dragRowRef.current = null
setDragRowId(null)
window.removeEventListener('pointermove', onMove)
window.removeEventListener('pointerup', onUp)
const ids = photoList.map((x) => x.id)
void splitEnv?.console?.reorderPhotos?.(ids).catch(() => {})
}
window.addEventListener('pointermove', onMove)
window.addEventListener('pointerup', onUp)
}
/** 在媒体库中选中某条 */
const selectPhoto = (p: { id: string; kind: 'photo' | 'video'; url: string }) => {
setPhotoIdLocal(p.id)
@@ -1889,6 +1926,7 @@ function ConsolePane() {
: <svg width="8" height="8" viewBox="0 0 16 16"><rect x="2.5" y="3.5" width="11" height="9" rx="1.5" fill="none" stroke="currentColor" strokeWidth="1.5" /><path d="M3.4 11l2.6-2.6 2.1 2.1 1.7-1.7 2.8 2.4" fill="none" stroke="currentColor" strokeWidth="1.5" /></svg>}</span>
</span>
<span className="dsh-wt_photoName">{photoId === p.id ? T('console.bgPhotoCurrent') : T('console.bgPhotoUse')}</span>
<button type="button" className={'dsh-wt_dragHandle' + (dragRowId === p.id ? ' dsh-wt_dragHandleOn' : '')} title={T('console.bgMediaDrag')} aria-label={T('console.bgMediaDrag')} onPointerDown={(e) => onHandleDown(p.id, e)}><svg width="10" height="12" viewBox="0 0 10 12" aria-hidden><circle cx="2.5" cy="2" r="1.1" fill="currentColor" /><circle cx="7.5" cy="2" r="1.1" fill="currentColor" /><circle cx="2.5" cy="6" r="1.1" fill="currentColor" /><circle cx="7.5" cy="6" r="1.1" fill="currentColor" /><circle cx="2.5" cy="10" r="1.1" fill="currentColor" /><circle cx="7.5" cy="10" r="1.1" fill="currentColor" /></svg></button>
<button type="button" className="dsh-wt_dropTrash" title={T('console.bgPhotoDelete')} aria-label={T('console.bgPhotoDelete')} onClick={(e) => { e.stopPropagation(); removePhotoById(p) }}><svg width="11" height="11" viewBox="0 0 16 16" aria-hidden><path d="M2.5 4.2h11M6.5 4.2V2.9c0-.6.4-1 .9-1h1.2c.5 0 .9.4.9 1v1.3M4.2 4.2l.5 8.1c0 .7.5 1.2 1.2 1.2h4.2c.7 0 1.2-.5 1.2-1.2l.5-8.1" fill="none" stroke="currentColor" strokeWidth="1.1" strokeLinecap="round" /></svg></button>
</div>
))
+3
View File
@@ -467,6 +467,9 @@ export const css = xtermCss + '\n' + [
'.dsh-wt_dropGear:hover{background:var(--wt-chip);color:var(--wt-text)}',
'.dsh-wt_dropWide{width:264px}',
'.dsh-wt_dropTrash{flex:none;width:22px;height:22px;margin-right:2px;padding:0;border:none;border-radius:6px;background:transparent;color:var(--wt-text3);cursor:pointer;display:inline-flex;align-items:center;justify-content:center;transition:background .15s ease,color .15s ease}',
'.dsh-wt_dragHandle{flex:none;width:18px;height:22px;padding:0;border:none;border-radius:6px;background:transparent;color:var(--wt-text3);cursor:grab;display:inline-flex;align-items:center;justify-content:center;transition:background .15s ease,color .15s ease;touch-action:none}',
'.dsh-wt_dragHandle:hover{background:var(--wt-chip);color:var(--wt-text2)}',
'.dsh-wt_dragHandleOn{background:var(--wt-chip);color:var(--wt-text);cursor:grabbing}',
'.dsh-wt_dropTrash:hover{background:rgba(244,63,94,.16);color:#f87171}',
'.dsh-wt_gridLabel{flex:none;white-space:nowrap;font-size:12px;line-height:17px;color:var(--wt-text2);padding:2px 2px 2px 4px}',
'.dsh-wt_gridHalf .dsh-wt_switch{margin-right:2px}',
+4
View File
@@ -0,0 +1,4 @@
$ErrorActionPreference = 'Stop'
opencli --profile 74u3g3us browser wt eval "(() => { window.__t = 1; setTimeout(() => { window.__t = 2; }, 400); return 's'; })()"
Start-Sleep -Milliseconds 900
opencli --profile 74u3g3us browser wt eval "window.__t"
+4
View File
@@ -0,0 +1,4 @@
$ErrorActionPreference = 'Stop'
opencli --profile 74u3g3us browser wt eval "(() => { window.__idb = 'pending'; const o = indexedDB.open('dsh-worktable', 2); o.onsuccess = () => { try { const db = o.result; const names = Array.from(db.objectStoreNames).join(','); const r = db.transaction('photoRecords', 'readonly').objectStore('photoRecords').get('default1'); r.onsuccess = () => { window.__idb = 'names=' + names + ';get-default1=' + (r.target.result ? 'found ' + r.target.result.blob.type + ' ' + r.target.result.blob.size : 'null'); }; r.onerror = () => { window.__idb = 'get-err'; }; } catch (e) { window.__idb = 'tx-err:' + e.message; } }; o.onerror = () => { window.__idb = 'open-err'; }; return 's'; })()"
Start-Sleep -Milliseconds 1500
opencli --profile 74u3g3us browser wt eval "window.__idb"
+2
View File
@@ -0,0 +1,2 @@
$ErrorActionPreference = 'Stop'
opencli --profile 74u3g3us browser wt eval "(() => { const o = indexedDB.open('dsh-worktable', 2); o.onsuccess = () => { const r = o.result.transaction('photoRecords', 'readonly').objectStore('photoRecords').getAll(); r.onsuccess = () => { const out = r.target.result.map(x => ({ id: x.id, createdAt: x.createdAt, type: x.blob && x.blob.type, size: x.blob && x.blob.size })); console.log('DATA:' + JSON.stringify(out)); }; }; return 'go'; })()"
+2
View File
@@ -0,0 +1,2 @@
$ErrorActionPreference = 'Stop'
opencli --profile 74u3g3us browser wt eval "(async () => { const o = indexedDB.open('dsh-worktable', 2); const db = await new Promise((res, rej) => { o.onsuccess = () => res(o.result); o.onerror = () => rej(o.error); }); const r = db.transaction('photoRecords', 'readonly').objectStore('photoRecords').getAll(); const list = await new Promise((res, rej) => { r.onsuccess = () => res(r.target.result); r.onerror = () => rej(r.error); }); return JSON.stringify(list.map(x => ({ id: x.id, type: x.blob && x.blob.type, size: x.blob && x.blob.size }))); })()"
+4
View File
@@ -0,0 +1,4 @@
$ErrorActionPreference = 'Stop'
opencli --profile 74u3g3us browser wt eval "(() => { const o = indexedDB.open('dsh-worktable', 2); o.onsuccess = () => { const r = o.result.transaction('photoRecords', 'readonly').objectStore('photoRecords').getAll(); r.onsuccess = () => { window.__mediaList = JSON.stringify(r.target.result.map(x => ({ id: x.id, type: x.blob && x.blob.type, size: x.blob && x.blob.size }))); }; }; return 'scheduled'; })()"
Start-Sleep -Milliseconds 1200
opencli --profile 74u3g3us browser wt eval "window.__mediaList || 'none'"
+4
View File
@@ -0,0 +1,4 @@
$ErrorActionPreference = 'Stop'
opencli --profile 74u3g3us browser wt eval "(() => { try { const o = indexedDB.open('dsh-worktable', 2); o.onerror = () => { window.__mediaList = 'open-err'; }; o.onsuccess = () => { const db = o.result; try { const tx = db.transaction('photoRecords', 'readonly'); const r = tx.objectStore('photoRecords').getAll(); r.onsuccess = () => { window.__mediaList = JSON.stringify(r.target.result.map(x => ({ id: x.id, type: x.blob && x.blob.type, size: x.blob && x.blob.size }))); }; r.onerror = () => { window.__mediaList = 'get-err'; }; } catch (e) { window.__mediaList = 'tx-err:' + e.message; } }; return 'go2'; })()"
Start-Sleep -Milliseconds 3000
opencli --profile 74u3g3us browser wt eval "window.__mediaList || 'none2'"
+4
View File
@@ -0,0 +1,4 @@
$ErrorActionPreference = 'Stop'
opencli --profile 74u3g3us browser wt eval "(() => { window.__mediaList = 'pending'; const o = indexedDB.open('dsh-worktable', 2); o.onerror = () => { window.__mediaList = 'open-err'; }; o.onsuccess = () => { const db = o.result; const r = db.transaction('photoRecords', 'readonly').objectStore('photoRecords').getAll(); r.onsuccess = () => { window.__mediaList = JSON.stringify(r.target.result.map(x => ({ id: x.id, type: x.blob && x.blob.type, size: x.blob && x.blob.size }))); }; r.onerror = () => { window.__mediaList = 'get-err'; }; }; return 'go3'; })()"
Start-Sleep -Milliseconds 2500
opencli --profile 74u3g3us browser wt eval "window.__mediaList || 'none3'"
+3
View File
@@ -0,0 +1,3 @@
$ErrorActionPreference = 'Stop'
opencli --profile 74u3g3us browser wt eval "(() => { try { const id = 'default1'; const svg = atob('PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMjgwIiBoZWlnaHQ9IjcyMCIgdmlld0JveD0iMCAwIDEyODAgNzIwIj48ZGVmcz48bGluZWFyR3JhZGllbnQgaWQ9ImciIHgxPSIwIiB5MT0iMCIgeDI9IjAiIHkyPSIxIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiMwYTBkMTMiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMxMDFhMmUiLz48L2xpbmVhckdyYWRpZW50PjxyYWRpYWxHcmFkaWVudCBpZD0iYjEiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzRmOGVmNyIgc3RvcC1vcGFjaXR5PSIwLjU1Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjNGY4ZWY3IiBzdG9wLW9wYWNpdHk9IjAiLz48L3JhZGlhbEdyYWRpZW50PjxyYWRpYWxHcmFkaWVudCBpZD0iYjIiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzdhNWNmZiIgc3RvcC1vcGFjaXR5PSIwLjQ1Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjN2E1Y2ZmIiBzdG9wLW9wYWNpdHk9IjAiLz48L3JhZGlhbEdyYWRpZW50PjxyYWRpYWxHcmFkaWVudCBpZD0iYjMiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzAwYzJmZiIgc3RvcC1vcGFjaXR5PSIwLjQiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMwMGMyZmYiIHN0b3Atb3BhY2l0eT0iMCIvPjwvcmFkaWFsR3JhZGllbnQ+PHJhZGlhbEdyYWRpZW50IGlkPSJiNCI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZmY5YTNkIiBzdG9wLW9wYWNpdHk9IjAuMjgiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiNmZjlhM2QiIHN0b3Atb3BhY2l0eT0iMCIvPjwvcmFkaWFsR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHdpZHRoPSIxMjgwIiBoZWlnaHQ9IjcyMCIgZmlsbD0idXJsKCNnKSIvPjxlbGxpcHNlIGN4PSIzMDAiIGN5PSIxODAiIHJ4PSI0MjAiIHJ5PSIyNjAiIGZpbGw9InVybCgjYjEpIi8+PGVsbGlwc2UgY3g9Ijk4MCIgY3k9IjIyMCIgcng9IjM4MCIgcnk9IjI0MCIgZmlsbD0idXJsKCNiMikiLz48ZWxsaXBzZSBjeD0iNzYwIiBjeT0iNTYwIiByeD0iNDIwIiByeT0iMjUwIiBmaWxsPSJ1cmwoI2IzKSIvPjxlbGxpcHNlIGN4PSIxODAiIGN5PSI2MDAiIHJ4PSIzMDAiIHJ5PSIyMDAiIGZpbGw9InVybCgjYjQpIi8+PC9zdmc+'); const o = indexedDB.open('dsh-worktable', 2); o.onsuccess = () => { const db = o.result; const tx = db.transaction('photoRecords', 'readwrite'); tx.objectStore('photoRecords').put({ id: id, createdAt: Date.now(), blob: new Blob([svg], { type: 'image/svg+xml' }) }, id); tx.oncomplete = () => { db.close(); }; tx.onerror = () => {}; }; return 'seeded-ok'; } catch (e) { return 'err:' + e.message; } })()"
opencli --profile 74u3g3us browser wt eval "(() => { const o = indexedDB.open('dsh-worktable', 2); let n = -1; o.onsuccess = () => { o.result.transaction('photoRecords', 'readonly').objectStore('photoRecords').getAll().onsuccess = (r) => { n = r.target.result.length; }; }; return 'count-scheduled'; })()"