minecodex 1.2.0 → 1.2.2
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/features/images/codex-feature.json +11 -2
- package/features/images/src/http-server.mjs +15 -5
- package/features/images/src/image-library.mjs +37 -9
- package/features/images/src/prompt-index.mjs +27 -24
- package/features/images/src/prompt-source.mjs +100 -0
- package/features/images/web/app.js +180 -67
- package/features/images/web/i18n.mjs +5 -0
- package/features/images/web/index.html +29 -24
- package/features/images/web/styles.css +169 -139
- package/features/model-slider/README.md +13 -1
- package/features/session-status/DESIGN.md +2 -0
- package/features/session-status/README.md +7 -1
- package/features/session-status/src/session-status.mjs +77 -5
- package/package.json +5 -2
- package/packages/runtime-host/src/cloud-images.mjs +123 -0
- package/packages/runtime-host/src/codex-cdp.mjs +42 -8
- package/packages/runtime-host/src/codex-design-contract.mjs +10 -0
- package/packages/runtime-host/src/codex-injection.mjs +326 -113
- package/packages/runtime-host/src/codex-runtime.mjs +88 -23
- package/packages/runtime-host/src/feature-registry.mjs +2 -0
- package/packages/runtime-host/src/session-status-reader.mjs +25 -8
|
@@ -5,6 +5,8 @@ const libraryHeader = document.querySelector(".library-header");
|
|
|
5
5
|
const sidebarOpenButton = document.querySelector(".sidebar-open-button");
|
|
6
6
|
const scrollArea = document.querySelector(".library-scroll");
|
|
7
7
|
const grid = document.querySelector(".image-grid");
|
|
8
|
+
const newImagesChip = document.querySelector(".new-images-chip");
|
|
9
|
+
const newImagesLabel = newImagesChip.querySelector(".new-images-label");
|
|
8
10
|
const pageSentinel = document.querySelector(".page-sentinel");
|
|
9
11
|
const empty = document.querySelector(".empty-state");
|
|
10
12
|
const emptyTitle = empty.querySelector("h2");
|
|
@@ -16,7 +18,8 @@ const filterLabel = document.querySelector(".filter-label");
|
|
|
16
18
|
const filterCount = document.querySelector(".filter-count");
|
|
17
19
|
const sourceMenu = document.querySelector(".source-menu");
|
|
18
20
|
const densityButtonValue = document.querySelector(".density-button-value");
|
|
19
|
-
const
|
|
21
|
+
const densityTrigger = document.querySelector(".density-trigger");
|
|
22
|
+
const densityMenu = document.querySelector(".density-menu");
|
|
20
23
|
const groupButton = document.querySelector(".group-button");
|
|
21
24
|
const drawer = document.querySelector(".detail-drawer");
|
|
22
25
|
const drawerImage = drawer.querySelector("img");
|
|
@@ -74,6 +77,8 @@ let activeThreadGroup = null;
|
|
|
74
77
|
let savedBrowseState = null;
|
|
75
78
|
let items = [];
|
|
76
79
|
let totalItems = 0;
|
|
80
|
+
let feedSnapshot = null;
|
|
81
|
+
let pendingImageCount = 0;
|
|
77
82
|
let filteredImageCount = 0;
|
|
78
83
|
let pageRequest = null;
|
|
79
84
|
let loadGeneration = 0;
|
|
@@ -82,6 +87,10 @@ let detailRequest = 0;
|
|
|
82
87
|
let masonryTimer = null;
|
|
83
88
|
let pendingScrollTop = null;
|
|
84
89
|
let appliedHostTokens = new Set();
|
|
90
|
+
let imagesReady = false;
|
|
91
|
+
let syncPromise = null;
|
|
92
|
+
let hasStartedCloudSync = false;
|
|
93
|
+
let missingEditorTokenSignature = "";
|
|
85
94
|
let densityColumns = null;
|
|
86
95
|
let copyFeedbackTimer = null;
|
|
87
96
|
let updateCheckTimer = null;
|
|
@@ -127,7 +136,7 @@ function applyHostTheme(payload) {
|
|
|
127
136
|
for (const name of appliedHostTokens) root.style.removeProperty(name);
|
|
128
137
|
appliedHostTokens = new Set();
|
|
129
138
|
for (const [name, value] of Object.entries(payload.tokens ?? {})) {
|
|
130
|
-
if (!name.startsWith("--") || typeof value !== "string") continue;
|
|
139
|
+
if (!name.startsWith("--") || typeof value !== "string" || !value.trim()) continue;
|
|
131
140
|
root.style.setProperty(name, value);
|
|
132
141
|
appliedHostTokens.add(name);
|
|
133
142
|
}
|
|
@@ -142,6 +151,13 @@ function applyHostTheme(payload) {
|
|
|
142
151
|
"--radius-md", "--radius-xl", "--shadow-md", "--shadow-xl",
|
|
143
152
|
];
|
|
144
153
|
editorControls.hidden = !editorTokens.every((name) => appliedHostTokens.has(name));
|
|
154
|
+
const missingTokens = editorTokens.filter((name) => !appliedHostTokens.has(name));
|
|
155
|
+
const signature = missingTokens.join(",");
|
|
156
|
+
if (signature && signature !== missingEditorTokenSignature) {
|
|
157
|
+
console.warn("[Images] Native editor theme contract is incomplete:", missingTokens);
|
|
158
|
+
}
|
|
159
|
+
missingEditorTokenSignature = signature;
|
|
160
|
+
if (imagesReady && !hasStartedCloudSync) void refreshImages();
|
|
145
161
|
}
|
|
146
162
|
|
|
147
163
|
function announce(message) {
|
|
@@ -155,6 +171,7 @@ function announce(message) {
|
|
|
155
171
|
function applySidebarState(payload) {
|
|
156
172
|
const available = Boolean(payload?.available);
|
|
157
173
|
const open = Boolean(payload?.open);
|
|
174
|
+
root.dataset.sidebarOpen = String(available && open);
|
|
158
175
|
const label = typeof payload?.label === "string" && payload.label.trim()
|
|
159
176
|
? payload.label.trim()
|
|
160
177
|
: t("Show sidebar");
|
|
@@ -310,6 +327,7 @@ function moveSourceOption(delta) {
|
|
|
310
327
|
}
|
|
311
328
|
|
|
312
329
|
function openSourceMenu({ focusIndex = null } = {}) {
|
|
330
|
+
closeDensityMenu();
|
|
313
331
|
sourceMenu.hidden = false;
|
|
314
332
|
filterButton.setAttribute("aria-expanded", "true");
|
|
315
333
|
const options = sourceOptions();
|
|
@@ -332,22 +350,17 @@ async function loadFacets() {
|
|
|
332
350
|
renderSourceMenu();
|
|
333
351
|
}
|
|
334
352
|
|
|
335
|
-
function recommendedColumnsForWidth(width) {
|
|
336
|
-
if (width < 920) return 2;
|
|
337
|
-
if (width < 1900) return 3;
|
|
338
|
-
if (width < 2500) return 4;
|
|
339
|
-
if (width < 3150) return 5;
|
|
340
|
-
return 6;
|
|
341
|
-
}
|
|
342
353
|
|
|
343
354
|
function applyDensity() {
|
|
344
355
|
if (densityColumns === null) {
|
|
345
|
-
densityColumns =
|
|
356
|
+
densityColumns = 4;
|
|
346
357
|
}
|
|
347
358
|
grid.style.setProperty("--grid-columns", String(densityColumns));
|
|
348
|
-
|
|
359
|
+
densityMenu.querySelectorAll("[data-columns]").forEach((option) => {
|
|
360
|
+
option.setAttribute("aria-selected", String(Number(option.dataset.columns) === densityColumns));
|
|
361
|
+
});
|
|
349
362
|
densityButtonValue.textContent = String(densityColumns);
|
|
350
|
-
|
|
363
|
+
|
|
351
364
|
scheduleMasonry();
|
|
352
365
|
}
|
|
353
366
|
|
|
@@ -393,6 +406,7 @@ function scheduleMasonry() {
|
|
|
393
406
|
function createCard(image) {
|
|
394
407
|
const button = document.createElement("button");
|
|
395
408
|
button.className = "image-card";
|
|
409
|
+
button.classList.toggle("is-wide", image.width >= image.height * 2 && image.height > 0);
|
|
396
410
|
button.type = "button";
|
|
397
411
|
button.dataset.imageId = image.id;
|
|
398
412
|
const createdAt = dateFormatter.format(new Date(image.createdAt));
|
|
@@ -408,7 +422,10 @@ function createCard(image) {
|
|
|
408
422
|
preview.width = image.width;
|
|
409
423
|
preview.height = image.height;
|
|
410
424
|
}
|
|
411
|
-
preview.addEventListener("load",
|
|
425
|
+
preview.addEventListener("load", () => {
|
|
426
|
+
button.classList.toggle("is-wide", preview.naturalWidth >= preview.naturalHeight * 2 && preview.naturalHeight > 0);
|
|
427
|
+
scheduleMasonry();
|
|
428
|
+
}, { once: true });
|
|
412
429
|
|
|
413
430
|
const caption = document.createElement("span");
|
|
414
431
|
caption.className = "card-caption";
|
|
@@ -518,8 +535,9 @@ function render() {
|
|
|
518
535
|
scheduleMasonry();
|
|
519
536
|
}
|
|
520
537
|
|
|
521
|
-
function pageUrl({ offset, limit = PAGE_SIZE } = {}) {
|
|
538
|
+
function pageUrl({ offset, limit = PAGE_SIZE, snapshot = feedSnapshot } = {}) {
|
|
522
539
|
const parameters = new URLSearchParams({ limit: String(limit), offset: String(offset ?? 0) });
|
|
540
|
+
if (snapshot !== null) parameters.set("snapshot", String(snapshot));
|
|
523
541
|
if (viewContext === "thread") {
|
|
524
542
|
parameters.set("threadId", activeThreadGroup.threadId);
|
|
525
543
|
parameters.set("sort", "asc");
|
|
@@ -537,7 +555,7 @@ async function loadPage({ reset = false, preserveScroll = false } = {}) {
|
|
|
537
555
|
const offset = reset ? 0 : items.length;
|
|
538
556
|
const preservedScrollTop = reset && preserveScroll ? scrollArea.scrollTop : null;
|
|
539
557
|
const request = (async () => {
|
|
540
|
-
const response = await fetch(pageUrl({ offset }));
|
|
558
|
+
const response = await fetch(pageUrl({ offset, snapshot: reset ? null : feedSnapshot }));
|
|
541
559
|
if (!response.ok) throw new Error(t("Could not load images"));
|
|
542
560
|
const payload = await response.json();
|
|
543
561
|
if (generation !== loadGeneration) return;
|
|
@@ -547,6 +565,8 @@ async function loadPage({ reset = false, preserveScroll = false } = {}) {
|
|
|
547
565
|
filteredImageCount = payload.imageCount ?? payload.count;
|
|
548
566
|
|
|
549
567
|
if (reset) {
|
|
568
|
+
feedSnapshot = payload.snapshot;
|
|
569
|
+
setPendingImages(0);
|
|
550
570
|
items = nextItems;
|
|
551
571
|
if (preservedScrollTop === null) scrollArea.scrollTop = 0;
|
|
552
572
|
else pendingScrollTop = preservedScrollTop;
|
|
@@ -590,25 +610,42 @@ async function toggleThreadGrouping() {
|
|
|
590
610
|
await loadPage({ reset: true });
|
|
591
611
|
}
|
|
592
612
|
|
|
613
|
+
function setPendingImages(count) {
|
|
614
|
+
const changed = pendingImageCount !== count;
|
|
615
|
+
pendingImageCount = count;
|
|
616
|
+
newImagesChip.hidden = count === 0;
|
|
617
|
+
newImagesLabel.textContent = t("{count} new images", { count });
|
|
618
|
+
if (changed && count > 0) announce(newImagesLabel.textContent);
|
|
619
|
+
}
|
|
620
|
+
|
|
593
621
|
async function checkForUpdates() {
|
|
594
|
-
if (
|
|
595
|
-
const
|
|
622
|
+
if (viewContext !== "browse" || feedSnapshot === null) return;
|
|
623
|
+
const generation = loadGeneration;
|
|
624
|
+
const response = await fetch(pageUrl({ offset: 0, limit: 1, snapshot: null }));
|
|
596
625
|
if (!response.ok) throw new Error(t("Could not check for new images"));
|
|
597
626
|
const payload = await response.json();
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
const currentFirst = items[0];
|
|
601
|
-
const changed = groupByThread
|
|
602
|
-
? payload.count !== totalItems
|
|
603
|
-
|| payload.imageCount !== filteredImageCount
|
|
604
|
-
|| nextFirst?.threadId !== currentFirst?.threadId
|
|
605
|
-
|| nextFirst?.updatedAt !== currentFirst?.updatedAt
|
|
606
|
-
: payload.count !== totalItems || nextFirst?.id !== currentFirst?.id;
|
|
607
|
-
if (changed) {
|
|
608
|
-
await Promise.all([loadFacets(), loadPage({ reset: true, preserveScroll: true })]);
|
|
609
|
-
}
|
|
627
|
+
if (generation !== loadGeneration) return;
|
|
628
|
+
setPendingImages(Math.max(0, (payload.imageCount ?? payload.count) - filteredImageCount));
|
|
610
629
|
}
|
|
611
630
|
|
|
631
|
+
newImagesChip.addEventListener("click", async () => {
|
|
632
|
+
newImagesChip.disabled = true;
|
|
633
|
+
try {
|
|
634
|
+
await loadPage({ reset: true, preserveScroll: true });
|
|
635
|
+
await loadFacets();
|
|
636
|
+
// 等待瀑布流恢复高度后再滚动,避免布局回写打断动画。
|
|
637
|
+
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
638
|
+
pendingScrollTop = null;
|
|
639
|
+
scrollArea.scrollTo({ top: 0, behavior: matchMedia("(prefers-reduced-motion: reduce)").matches ? "instant" : "smooth" });
|
|
640
|
+
grid.querySelector("button")?.focus({ preventScroll: true });
|
|
641
|
+
} catch (error) {
|
|
642
|
+
announce(t("Could not load images"));
|
|
643
|
+
console.error(error);
|
|
644
|
+
} finally {
|
|
645
|
+
newImagesChip.disabled = false;
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
|
|
612
649
|
function canPollForUpdates() {
|
|
613
650
|
return hostSurfaceActive
|
|
614
651
|
&& documentVisible
|
|
@@ -652,6 +689,7 @@ function setHostSurfaceActive(active) {
|
|
|
652
689
|
cancelActiveCommentEditor();
|
|
653
690
|
}
|
|
654
691
|
refreshUpdatePolling({ immediate: changed && hostSurfaceActive });
|
|
692
|
+
if (changed && hostSurfaceActive && imagesReady && root.dataset.host === "codex") void refreshImages();
|
|
655
693
|
}
|
|
656
694
|
|
|
657
695
|
function setDocumentVisible(visible) {
|
|
@@ -820,6 +858,9 @@ function showImageDetails(image) {
|
|
|
820
858
|
drawerDate.textContent = dateFormatter.format(new Date(image.createdAt));
|
|
821
859
|
drawerSize.textContent = image.width && image.height ? `${image.width} × ${image.height}` : t("Original");
|
|
822
860
|
drawerFileSize.textContent = formatBytes(image.byteLength);
|
|
861
|
+
const promptHeading = drawer.querySelector(".detail-section-heading h3");
|
|
862
|
+
promptHeading.dataset.i18n = image.promptKind === "request" ? "Original request" : "Generation prompt";
|
|
863
|
+
promptHeading.textContent = t(promptHeading.dataset.i18n);
|
|
823
864
|
drawerPrompt.textContent = image.prompt || t("Prompt is not available for this image.");
|
|
824
865
|
drawerPrompt.classList.toggle("is-missing", !image.prompt);
|
|
825
866
|
promptCopyButton.disabled = !image.prompt;
|
|
@@ -867,6 +908,8 @@ async function openThreadGroup(group, originatingElement = null) {
|
|
|
867
908
|
items: items.slice(),
|
|
868
909
|
totalItems,
|
|
869
910
|
filteredImageCount,
|
|
911
|
+
feedSnapshot,
|
|
912
|
+
pendingImageCount,
|
|
870
913
|
scrollTop: scrollArea.scrollTop,
|
|
871
914
|
nodes: [...grid.children],
|
|
872
915
|
};
|
|
@@ -899,6 +942,8 @@ function leaveThreadGroup() {
|
|
|
899
942
|
items = restore.items;
|
|
900
943
|
totalItems = restore.totalItems;
|
|
901
944
|
filteredImageCount = restore.filteredImageCount;
|
|
945
|
+
feedSnapshot = restore.feedSnapshot;
|
|
946
|
+
setPendingImages(restore.pendingImageCount);
|
|
902
947
|
pendingScrollTop = restore.scrollTop;
|
|
903
948
|
grid.replaceChildren(...restore.nodes);
|
|
904
949
|
grid.classList.toggle("is-grouped", groupByThread);
|
|
@@ -991,14 +1036,49 @@ sourceMenu.addEventListener("keydown", (event) => {
|
|
|
991
1036
|
}
|
|
992
1037
|
});
|
|
993
1038
|
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1039
|
+
function closeDensityMenu(restoreFocus = false) {
|
|
1040
|
+
densityMenu.hidden = true;
|
|
1041
|
+
densityTrigger.setAttribute("aria-expanded", "false");
|
|
1042
|
+
if (restoreFocus) densityTrigger.focus();
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function openDensityMenu() {
|
|
1046
|
+
closeSourceMenu();
|
|
1047
|
+
densityMenu.hidden = false;
|
|
1048
|
+
densityTrigger.setAttribute("aria-expanded", "true");
|
|
1049
|
+
densityMenu.querySelector('[aria-selected="true"]')?.focus();
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
densityTrigger.addEventListener("click", () => {
|
|
1053
|
+
if (densityMenu.hidden) openDensityMenu();
|
|
1054
|
+
else closeDensityMenu();
|
|
1055
|
+
});
|
|
1056
|
+
densityTrigger.addEventListener("keydown", (event) => {
|
|
1057
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
1058
|
+
event.preventDefault();
|
|
1059
|
+
openDensityMenu();
|
|
1000
1060
|
}
|
|
1061
|
+
});
|
|
1062
|
+
densityMenu.addEventListener("click", (event) => {
|
|
1063
|
+
const option = event.target.closest("[data-columns]");
|
|
1064
|
+
if (!option) return;
|
|
1065
|
+
densityColumns = Number(option.dataset.columns);
|
|
1066
|
+
try { localStorage.setItem(DENSITY_STORAGE_KEY, String(densityColumns)); } catch {}
|
|
1001
1067
|
applyDensity();
|
|
1068
|
+
closeDensityMenu(true);
|
|
1069
|
+
});
|
|
1070
|
+
densityMenu.addEventListener("keydown", (event) => {
|
|
1071
|
+
const options = [...densityMenu.querySelectorAll("[data-columns]")];
|
|
1072
|
+
const index = options.indexOf(document.activeElement);
|
|
1073
|
+
if (["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) {
|
|
1074
|
+
event.preventDefault();
|
|
1075
|
+
const next = event.key === "Home" ? 0 : event.key === "End" ? options.length - 1
|
|
1076
|
+
: (index + (event.key === "ArrowDown" ? 1 : -1) + options.length) % options.length;
|
|
1077
|
+
options[next].focus();
|
|
1078
|
+
} else if (event.key === "Escape") {
|
|
1079
|
+
event.preventDefault();
|
|
1080
|
+
closeDensityMenu(true);
|
|
1081
|
+
} else if (event.key === "Tab") closeDensityMenu(true);
|
|
1002
1082
|
});
|
|
1003
1083
|
|
|
1004
1084
|
groupButton.addEventListener("click", () => toggleThreadGrouping().catch(console.error));
|
|
@@ -1016,42 +1096,72 @@ sidebarOpenButton.addEventListener("click", async () => {
|
|
|
1016
1096
|
|
|
1017
1097
|
document.addEventListener("click", (event) => {
|
|
1018
1098
|
if (!event.target.closest(".source-filter")) closeToolbarPopups();
|
|
1099
|
+
if (!event.target.closest(".density-control")) closeDensityMenu();
|
|
1019
1100
|
});
|
|
1020
1101
|
|
|
1021
|
-
|
|
1102
|
+
async function refreshImages() {
|
|
1103
|
+
if (syncPromise) return syncPromise;
|
|
1104
|
+
hasStartedCloudSync = true;
|
|
1022
1105
|
rescan.disabled = true;
|
|
1023
1106
|
rescan.classList.add("is-loading");
|
|
1024
1107
|
rescan.setAttribute("aria-busy", "true");
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1108
|
+
syncPromise = (async () => {
|
|
1109
|
+
let imported = 0, failures = 0;
|
|
1110
|
+
const updateGallery = async () => {
|
|
1111
|
+
await checkForUpdates();
|
|
1112
|
+
};
|
|
1113
|
+
try {
|
|
1114
|
+
const local = await fetch("/api/rescan", { method: "POST" });
|
|
1115
|
+
if (!local.ok) throw new Error(t("Could not refresh images"));
|
|
1116
|
+
await updateGallery();
|
|
1117
|
+
if (root.dataset.host === "codex") {
|
|
1118
|
+
const cachedResponse = await fetch("/api/cloud-image-cache");
|
|
1119
|
+
if (!cachedResponse.ok) throw new Error(t("Could not refresh images"));
|
|
1120
|
+
const cached = new Map((await cachedResponse.json()).items.map(item => [item.libraryId, item.version]));
|
|
1121
|
+
let cursor = null;
|
|
1122
|
+
const cursors = new Set();
|
|
1123
|
+
do {
|
|
1124
|
+
rescan.title = t("Syncing cloud images… {count} added", { count: imported });
|
|
1125
|
+
const page = await requestHostAction("list-cloud-images", { cursor }, 90_000);
|
|
1126
|
+
for (const item of page.items) {
|
|
1127
|
+
if (cached.get(item.libraryId) === item.version) continue;
|
|
1128
|
+
try {
|
|
1129
|
+
const result = await requestHostAction("import-cloud-image", { libraryId: item.libraryId }, 120_000);
|
|
1130
|
+
if (result.imported) {
|
|
1131
|
+
imported += 1;
|
|
1132
|
+
if (imported % 5 === 0) await updateGallery();
|
|
1133
|
+
}
|
|
1134
|
+
} catch (error) {
|
|
1135
|
+
failures += 1;
|
|
1136
|
+
console.warn("[Images] Cloud image could not be synced", item.libraryId, error.message);
|
|
1137
|
+
}
|
|
1138
|
+
rescan.title = t("Syncing cloud images… {count} added", { count: imported });
|
|
1039
1139
|
}
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1140
|
+
await updateGallery();
|
|
1141
|
+
cursor = page.cursor;
|
|
1142
|
+
if (cursor && cursors.has(cursor)) throw new Error("Cloud Library returned a repeated cursor");
|
|
1143
|
+
if (cursor) cursors.add(cursor);
|
|
1144
|
+
} while (cursor);
|
|
1145
|
+
}
|
|
1146
|
+
const status = failures
|
|
1147
|
+
? t("Cloud sync finished: {count} added, {failed} failed. Refresh to retry.", { count: imported, failed: failures })
|
|
1148
|
+
: t("Images refreshed.");
|
|
1149
|
+
rescan.title = status;
|
|
1150
|
+
announce(status);
|
|
1151
|
+
} catch (error) {
|
|
1152
|
+
rescan.title = t("Cloud sync unavailable. Local images are still available.");
|
|
1153
|
+
announce(rescan.title);
|
|
1154
|
+
console.error(error);
|
|
1155
|
+
} finally {
|
|
1156
|
+
rescan.disabled = false;
|
|
1157
|
+
rescan.classList.remove("is-loading");
|
|
1158
|
+
rescan.removeAttribute("aria-busy");
|
|
1159
|
+
syncPromise = null;
|
|
1160
|
+
}
|
|
1161
|
+
})();
|
|
1162
|
+
return syncPromise;
|
|
1163
|
+
}
|
|
1164
|
+
rescan.addEventListener("click", () => void refreshImages());
|
|
1055
1165
|
|
|
1056
1166
|
drawer.querySelector(".drawer-close").addEventListener("click", closeDrawer);
|
|
1057
1167
|
sourceButton.addEventListener("click", () => {
|
|
@@ -1178,14 +1288,14 @@ function resetZoom() {
|
|
|
1178
1288
|
zoomTriggerValue.textContent = `${fitZoomPercent()}%`;
|
|
1179
1289
|
}
|
|
1180
1290
|
|
|
1181
|
-
function requestHostAction(action, payload) {
|
|
1291
|
+
function requestHostAction(action, payload, timeoutMs = 30_000) {
|
|
1182
1292
|
if (window.parent === window) return Promise.reject(new Error(t("Image editing is available inside Codex only.")));
|
|
1183
1293
|
const requestId = crypto.randomUUID?.() ?? `${Date.now()}-${Math.random()}`;
|
|
1184
1294
|
return new Promise((resolve, reject) => {
|
|
1185
1295
|
const timeout = setTimeout(() => {
|
|
1186
1296
|
window.removeEventListener("message", onResult);
|
|
1187
1297
|
reject(new Error(t("Codex did not respond to the image edit request.")));
|
|
1188
|
-
},
|
|
1298
|
+
}, timeoutMs);
|
|
1189
1299
|
function onResult(event) {
|
|
1190
1300
|
if (event.source !== window.parent || event.data?.type !== "codex-personal:host-result" || event.data?.requestId !== requestId) return;
|
|
1191
1301
|
clearTimeout(timeout);
|
|
@@ -1641,9 +1751,12 @@ applyDensity();
|
|
|
1641
1751
|
setLocale(navigator.language);
|
|
1642
1752
|
translateDocument(document);
|
|
1643
1753
|
await Promise.all([loadFacets(), loadPage({ reset: true })]);
|
|
1754
|
+
imagesReady = true;
|
|
1755
|
+
if (root.dataset.host === "codex") void refreshImages();
|
|
1644
1756
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1645
1757
|
|
|
1646
1758
|
new ResizeObserver(() => {
|
|
1759
|
+
root.style.setProperty("--library-scrollbar-width", `${scrollArea.offsetWidth - scrollArea.clientWidth}px`);
|
|
1647
1760
|
scheduleMasonry();
|
|
1648
1761
|
}).observe(scrollArea);
|
|
1649
1762
|
|
|
@@ -10,6 +10,11 @@ export function setLocale(value) {
|
|
|
10
10
|
return true;
|
|
11
11
|
}
|
|
12
12
|
const zh = {
|
|
13
|
+
"{count} new images": "{count} 张新图片",
|
|
14
|
+
"Original request": "原始请求",
|
|
15
|
+
"Syncing cloud images… {count} added": "正在同步云端图片…已新增 {count} 张",
|
|
16
|
+
"Cloud sync finished: {count} added, {failed} failed. Refresh to retry.": "云端同步完成:新增 {count} 张,{failed} 张失败,可刷新重试。",
|
|
17
|
+
"Cloud sync unavailable. Local images are still available.": "云端同步暂不可用,本地图片仍可浏览。",
|
|
13
18
|
"Images": "图片",
|
|
14
19
|
"Images · Codex": "图片 · Codex",
|
|
15
20
|
"Show sidebar": "显示侧边栏",
|
|
@@ -18,20 +18,8 @@
|
|
|
18
18
|
</button>
|
|
19
19
|
<h1 data-i18n="Images">Images</h1>
|
|
20
20
|
</div>
|
|
21
|
-
<button class="icon-button rescan-button" type="button" aria-label="Refresh images" data-i18n-aria-label="Refresh images" title="Refresh images" data-i18n-title="Refresh images">
|
|
22
|
-
<svg class="lucide refresh-icon" viewBox="0 0 24 24" aria-hidden="true">
|
|
23
|
-
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"></path>
|
|
24
|
-
<path d="M21 3v5h-5"></path>
|
|
25
|
-
<path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"></path>
|
|
26
|
-
<path d="M8 16H3v5"></path>
|
|
27
|
-
</svg>
|
|
28
|
-
<svg class="lucide loading-ring-icon" viewBox="0 0 24 24" aria-hidden="true">
|
|
29
|
-
<path d="M21 12a9 9 0 1 1-6.219-8.56"></path>
|
|
30
|
-
</svg>
|
|
31
|
-
</button>
|
|
32
|
-
</header>
|
|
33
|
-
|
|
34
21
|
<nav class="library-toolbar" aria-label="Image library controls" data-i18n-aria-label="Image library controls">
|
|
22
|
+
<div class="toolbar-actions">
|
|
35
23
|
<div class="source-filter">
|
|
36
24
|
<button
|
|
37
25
|
class="toolbar-button source-filter-button"
|
|
@@ -50,24 +38,24 @@
|
|
|
50
38
|
<div class="source-menu" id="source-menu" role="listbox" aria-label="Filter images by source" data-i18n-aria-label="Filter images by source" hidden></div>
|
|
51
39
|
</div>
|
|
52
40
|
|
|
53
|
-
<div class="toolbar-actions">
|
|
54
41
|
<div class="density-control" title="Grid columns" data-i18n-title="Grid columns">
|
|
42
|
+
<button class="toolbar-button density-trigger" type="button" aria-label="Grid columns" data-i18n-aria-label="Grid columns" aria-haspopup="listbox" aria-controls="density-menu" aria-expanded="false">
|
|
55
43
|
<svg class="lucide density-icon" viewBox="0 0 24 24" aria-hidden="true">
|
|
56
44
|
<rect width="7" height="7" x="3" y="3" rx="1"></rect>
|
|
57
45
|
<rect width="7" height="7" x="14" y="3" rx="1"></rect>
|
|
58
46
|
<rect width="7" height="7" x="3" y="14" rx="1"></rect>
|
|
59
47
|
<rect width="7" height="7" x="14" y="14" rx="1"></rect>
|
|
60
48
|
</svg>
|
|
61
|
-
<
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
49
|
+
<span class="density-button-value">4</span>
|
|
50
|
+
<svg class="lucide filter-chevron" viewBox="0 0 24 24" aria-hidden="true"><path d="m6 9 6 6 6-6"></path></svg>
|
|
51
|
+
</button>
|
|
52
|
+
<div class="source-menu density-menu" id="density-menu" role="listbox" aria-label="Grid columns" data-i18n-aria-label="Grid columns" hidden>
|
|
53
|
+
<button class="source-option" type="button" role="option" data-columns="2" aria-selected="false" tabindex="-1">2</button>
|
|
54
|
+
<button class="source-option" type="button" role="option" data-columns="3" aria-selected="false" tabindex="-1">3</button>
|
|
55
|
+
<button class="source-option" type="button" role="option" data-columns="4" aria-selected="true" tabindex="-1">4</button>
|
|
56
|
+
<button class="source-option" type="button" role="option" data-columns="5" aria-selected="false" tabindex="-1">5</button>
|
|
57
|
+
<button class="source-option" type="button" role="option" data-columns="6" aria-selected="false" tabindex="-1">6</button>
|
|
58
|
+
</div>
|
|
71
59
|
</div>
|
|
72
60
|
|
|
73
61
|
<button
|
|
@@ -85,7 +73,23 @@
|
|
|
85
73
|
</button>
|
|
86
74
|
</div>
|
|
87
75
|
</nav>
|
|
76
|
+
<button class="icon-button rescan-button" type="button" aria-label="Refresh images" data-i18n-aria-label="Refresh images" title="Refresh images" data-i18n-title="Refresh images">
|
|
77
|
+
<svg class="lucide refresh-icon" viewBox="0 0 24 24" aria-hidden="true">
|
|
78
|
+
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"></path>
|
|
79
|
+
<path d="M21 3v5h-5"></path>
|
|
80
|
+
<path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"></path>
|
|
81
|
+
<path d="M8 16H3v5"></path>
|
|
82
|
+
</svg>
|
|
83
|
+
<svg class="lucide loading-ring-icon" viewBox="0 0 24 24" aria-hidden="true">
|
|
84
|
+
<path d="M21 12a9 9 0 1 1-6.219-8.56"></path>
|
|
85
|
+
</svg>
|
|
86
|
+
</button>
|
|
87
|
+
</header>
|
|
88
88
|
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
<div class="library-feed">
|
|
92
|
+
<button class="new-images-chip" type="button" hidden><span class="new-images-arrow" aria-hidden="true">↑</span><span class="new-images-label"></span></button>
|
|
89
93
|
<div class="library-scroll">
|
|
90
94
|
<section class="empty-state" hidden>
|
|
91
95
|
<h2 data-i18n="No images yet">No images yet</h2>
|
|
@@ -95,6 +99,7 @@
|
|
|
95
99
|
<section class="image-grid" aria-label="Generated images" data-i18n-aria-label="Generated images"></section>
|
|
96
100
|
<div class="page-sentinel" aria-hidden="true" hidden></div>
|
|
97
101
|
</div>
|
|
102
|
+
</div>
|
|
98
103
|
</section>
|
|
99
104
|
|
|
100
105
|
<div class="status-announcer" role="status" aria-live="polite" aria-atomic="true"></div>
|