minecodex 2.0.5 → 2.0.7
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/DESIGN.md +19 -0
- package/README.md +5 -1
- package/apps/dashboard/DESIGN.md +53 -31
- package/apps/dashboard/app.js +55 -39
- package/apps/dashboard/assets/OFL.txt +93 -0
- package/apps/dashboard/assets/info.svg +15 -0
- package/apps/dashboard/assets/moon.svg +13 -0
- package/apps/dashboard/assets/refresh-cw.svg +16 -0
- package/apps/dashboard/assets/silkscreen.ttf +0 -0
- package/apps/dashboard/assets/sun.svg +21 -0
- package/apps/dashboard/assets/terrain.js +89 -0
- package/apps/dashboard/assets/triangle-alert.svg +15 -0
- package/apps/dashboard/favicon.svg +12 -17
- package/apps/dashboard/index.html +24 -18
- package/apps/dashboard/styles.css +147 -181
- package/assets/brand/README.md +19 -0
- package/assets/brand/minecodex-icon-dark.svg +9 -0
- package/assets/brand/minecodex-icon-light.svg +9 -0
- package/assets/brand/minecodex-icon.svg +8 -0
- package/assets/brand/references/lucide-LICENSE.txt +43 -0
- package/assets/readme/hero.svg +93 -0
- package/features/hide-upsell-banner/README.md +7 -5
- package/features/hide-upsell-banner/codex-feature.json +2 -2
- package/features/hide-upsell-banner/src/hide-upsell-banner.mjs +30 -12
- package/features/images/DESIGN.md +18 -3
- package/features/images/README.md +4 -3
- package/features/images/codex-feature.json +2 -2
- package/features/images/src/adapters/codex/native-image-modules.mjs +220 -0
- package/features/images/src/adapters/codex/native-image-source.mjs +113 -198
- package/features/images/src/assets/prompts-menu.svg +15 -0
- package/features/images/src/frontend/cloud-image-actions.mjs +8 -6
- package/features/images/src/frontend/local-image-source.mjs +2 -1
- package/features/images/src/library-prompt-page.mjs +10 -4
- package/features/images/src/library-prompt-source.mjs +4 -5
- package/features/model-slider/codex-feature.json +2 -2
- package/features/notes/DESIGN.md +1 -1
- package/features/notes/codex-feature.json +2 -2
- package/features/notes/src/frontend/entry.mjs +1 -1
- package/features/session-status/codex-feature.json +2 -2
- package/features/sidebar-focus/DESIGN.md +4 -4
- package/features/sidebar-focus/codex-feature.json +3 -2
- package/features/sidebar-focus/src/adapters/codex/renderer.mjs +1 -1
- package/features/sidebar-focus/src/sidebar-focus.mjs +9 -7
- package/features/sites/DESIGN.md +25 -3
- package/features/sites/README.md +6 -2
- package/features/sites/codex-feature.json +3 -2
- package/features/sites/src/adapters/codex/renderer.mjs +25 -4
- package/features/sites/src/backend/project-sites.mjs +18 -6
- package/features/sites/src/backend/sites-service.mjs +22 -10
- package/features/sites/src/domain/discovery-candidates.mjs +53 -0
- package/features/sites/src/frontend/entry.mjs +76 -38
- package/features/sites/src/frontend/styles.css +3 -3
- package/features/unread-inbox/DESIGN.md +24 -25
- package/features/unread-inbox/codex-feature.json +3 -2
- package/features/unread-inbox/src/adapters/codex/modules.mjs +52 -0
- package/features/unread-inbox/src/adapters/codex/renderer.mjs +90 -0
- package/features/unread-inbox/src/domain/notifications.mjs +21 -0
- package/features/unread-inbox/src/injection-source.mjs +5 -2
- package/features/unread-inbox/src/unread-inbox.mjs +212 -297
- package/features/usage-meter/codex-feature.json +2 -2
- package/package.json +11 -3
- package/packages/cli/native/MineCodex.app/Contents/Info.plist +4 -2
- package/packages/cli/native/MineCodex.app/Contents/MacOS/MineCodexServiceManager +0 -0
- package/packages/cli/native/MineCodex.app/Contents/Resources/MineCodex.icns +0 -0
- package/packages/cli/native/MineCodex.app/Contents/_CodeSignature/CodeResources +11 -0
- package/packages/cli/native/README.md +31 -0
- package/packages/cli/src/browser-control-panel.mjs +48 -0
- package/packages/cli/src/commands.mjs +3 -1
- package/packages/cli/src/control-server.mjs +6 -3
- package/packages/cli/src/platform.mjs +5 -0
- package/packages/runtime-host/README.md +10 -5
- package/packages/runtime-host/src/codex-injection.mjs +12 -3
- package/packages/runtime-host/src/codex-runtime.mjs +10 -0
- package/packages/ui/DESIGN.md +5 -1
- package/packages/ui/assets/arrow-up-right.svg +13 -0
- package/packages/ui/assets/lucide-LICENSE.txt +43 -0
- package/packages/ui/assets/minecodex-menu-16.svg +7 -0
- package/packages/ui/assets/minecodex-menu-20.svg +7 -0
- package/packages/ui/src/components/account-menu.mjs +88 -0
- package/packages/ui/src/components/page.mjs +2 -2
- package/packages/ui/src/foundation/layers.mjs +1 -0
- package/packages/ui/src/install.mjs +3 -0
- package/packages/ui/src/native/account-menu.mjs +21 -0
- package/packages/ui/src/native/page.mjs +17 -2
- package/packages/ui/src/native/tokens.mjs +1 -0
|
@@ -1,152 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {createNativeImageModules} from "./native-image-modules.mjs";
|
|
2
|
+
|
|
3
|
+
// 原生呈现与上下文只在此适配;构建产物发现由专用模块提供。
|
|
4
|
+
export function createNativeImageSource(codex, { requestHostAction, libraryApi, createCloudImageActions, promptGlyphSource, signal, modules = createNativeImageModules() } = {}) {
|
|
5
|
+
const connect = modules.gallery;
|
|
4
6
|
const opened = new Map();
|
|
5
7
|
|
|
6
|
-
function
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
throw new Error(`Native Images import contract changed: ${local}`);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
async function connect() {
|
|
17
|
-
if (!connecting) connecting = (async () => {
|
|
18
|
-
const urls = [...document.querySelectorAll('link[rel="modulepreload"],script[src]')].map(node => node.href || node.src);
|
|
19
|
-
const initialUrl = urls.find(url => /\/app-initial-[^/]+\.js/.test(url));
|
|
20
|
-
const mainUrl = urls.find(url => /\/app-main-[^/]+\.js/.test(url));
|
|
21
|
-
if (!initialUrl || !mainUrl) throw new Error("Native Images modules are unavailable");
|
|
22
|
-
const [initialSource, mainSource] = await Promise.all([read(initialUrl), read(mainUrl)]);
|
|
23
|
-
const asset = (name, references = initialSource) => {
|
|
24
|
-
const file = references.match(new RegExp(`(?:["'./])(${name}-[\\w-]+\\.js)`))?.[1];
|
|
25
|
-
if (!file) throw new Error(`Native Images capability is unavailable: ${name}`);
|
|
26
|
-
return new URL(file, initialUrl).href;
|
|
27
|
-
};
|
|
28
|
-
const module = async (name,references) => {
|
|
29
|
-
const url = asset(name,references);
|
|
30
|
-
const [source, exports] = await Promise.all([read(url), load(url)]);
|
|
31
|
-
// 调用产物公开的惰性初始化器,不扫描或执行任意导出函数。
|
|
32
|
-
for (const value of Object.values(exports)) {
|
|
33
|
-
if (typeof value === "function" && /^function ([\w$]+)\(\)\{return\(\1=/.test(String(value))) value();
|
|
34
|
-
}
|
|
35
|
-
return {url, source, exports};
|
|
36
|
-
};
|
|
37
|
-
const [masonry, layout, sourceImage, menu, selection, initial] = await Promise.all([
|
|
38
|
-
module("library-masonry"), module("library-item-layout"), module("generated-image-source"),
|
|
39
|
-
module("generated-image-context-menu"), module("library-selection-toolbar"), load(initialUrl),
|
|
40
|
-
]);
|
|
41
|
-
const [moveDialog,dialogs] = await Promise.all([module("library-move-dialog",selection.source),module("library-item-dialogs",selection.source)]);
|
|
42
|
-
const [canvas,canvasLayout,filePreview] = await Promise.all([
|
|
43
|
-
module("image-side-panel"),module("preview-layout"),module("library-cloud-file-preview"),
|
|
44
|
-
]);
|
|
45
|
-
const component = (exports, predicate, name) => {
|
|
46
|
-
const matches = Object.values(exports).filter(value => typeof value === "function" && predicate(String(value)));
|
|
47
|
-
if (matches.length !== 1) throw new Error(`Native Images contract changed: ${name}`);
|
|
48
|
-
return matches[0];
|
|
49
|
-
};
|
|
50
|
-
const binding = async (source, url, local) => {
|
|
51
|
-
if (!local) throw new Error("Native Images component contract changed");
|
|
52
|
-
const importedBinding = imported(source, local);
|
|
53
|
-
return (await load(new URL(importedBinding.file, url).href))[importedBinding.name];
|
|
54
|
-
};
|
|
55
|
-
const routerLocal = selection.source.match(/get\(([\w$]+)\)\.prepareNavigation\(/)?.[1];
|
|
56
|
-
const routerAtom = await binding(selection.source, selection.url, routerLocal);
|
|
57
|
-
const switchLocal = selection.source.match(/([\w$]+)\(e,i\);let _=t.length===1/)?.[1];
|
|
58
|
-
const switchMode = await binding(selection.source, selection.url, switchLocal);
|
|
59
|
-
const draftLocal = selection.source.match(/let u=([\w$]+)\(e,\[\]\);e.set/)?.[1];
|
|
60
|
-
const createChatDraft = await binding(selection.source, selection.url, draftLocal);
|
|
61
|
-
const rootOwner = mainSource.match(/\(0,([\w$]+)\.createRoot\)/)?.[1];
|
|
62
|
-
const rootInit = rootOwner && mainSource.match(new RegExp(`${rootOwner}=([\\w$]+)\\(\\)`))?.[1];
|
|
63
|
-
const reactOwner = masonry.source.match(/([\w$]+)\.useRef\b/)?.[1];
|
|
64
|
-
const reactInit = reactOwner && masonry.source.match(new RegExp(`${reactOwner}=([\\w$]+)\\(\\)`))?.[1];
|
|
65
|
-
const [reactFactory, rootFactory] = await Promise.all([
|
|
66
|
-
binding(masonry.source, masonry.url, reactInit), binding(mainSource, mainUrl, rootInit),
|
|
67
|
-
]);
|
|
68
|
-
const React = reactFactory(), ReactDOM = rootFactory();
|
|
69
|
-
const portalOwner=canvas.source.match(/\(0,([\w$]+)\.createPortal\)/)?.[1];
|
|
70
|
-
const portalInit=portalOwner&&canvas.source.match(new RegExp(`${portalOwner}=([\\w$]+)\\(\\)`))?.[1];
|
|
71
|
-
const createPortal=(await binding(canvas.source,canvas.url,portalInit))().createPortal;
|
|
72
|
-
const dropdownLocal = selection.source.match(/\(0,[\w$]+\.jsxs?\)\(([\w$]+),\{contentWidth:`menu`/)?.[1];
|
|
73
|
-
const itemsLocal = selection.source.match(/\(0,[\w$]+\.jsxs?\)\(([\w$]+)\.Item,\{/)?.[1];
|
|
74
|
-
const buttonLocal = selection.source.match(/\(0,[\w$]+\.jsx\)\(([\w$]+),\{ref:[^,]+,color:`mediaOverlay`/)?.[1];
|
|
75
|
-
const iconLocal = selection.source.match(/color:`mediaOverlay`[^;]+?children:\(0,[\w$]+\.jsx\)\(([\w$]+),\{\}\)/)?.[1];
|
|
76
|
-
const [Dropdown, Menu, Button, MoreIcon] = await Promise.all([dropdownLocal, itemsLocal, buttonLocal, iconLocal].map(local => binding(selection.source, selection.url, local)));
|
|
77
|
-
const checkboxFunction = component(selection.exports,source=>/onCheckedChange:/.test(source)&&/viewMode:/.test(source),"selection checkbox");
|
|
78
|
-
const checkboxLocal = String(checkboxFunction).match(/\(0,[\w$]+\.jsx\)\(([\w$]+),\{className:`cursor-interaction`,variant:/)?.[1];
|
|
79
|
-
const Checkbox = await binding(selection.source,selection.url,checkboxLocal);
|
|
80
|
-
const rangeSelection = component(selection.exports,source=>/^function [\w$]+\(\{orderedIds:/.test(source)&&/extendRange:/.test(source),"range selection");
|
|
81
|
-
const selectionLocal = selection.source.match(/\.set\(([\w$]+),new Set\)/)?.[1];
|
|
82
|
-
const selectionExport = selection.source.match(/export\{([^}]+)\}/)?.[1].split(",").map(value=>value.trim().split(/\s+as\s+/)).find(parts=>parts[0]===selectionLocal)?.at(-1);
|
|
83
|
-
const nativeSelectedIds = selection.exports[selectionExport];
|
|
84
|
-
if (!nativeSelectedIds) throw new Error("Native selection state contract changed");
|
|
85
|
-
const clearPrefix = selection.source.slice(selection.source.indexOf("id:`libraryNext.selection.clear`"));
|
|
86
|
-
const clearLocal = clearPrefix.match(/\(0,[\w$]+\.jsx\)\(([\w$]+),\{\}\)/)?.[1];
|
|
87
|
-
const ClearIcon = await binding(selection.source,selection.url,clearLocal);
|
|
88
|
-
const dialogLocal = dialogs.source.match(/\(0,[\w$]+\.jsx\)\(([\w$]+),\{open:[^,]+,showDialogClose:!1/)?.[1];
|
|
89
|
-
const contentLocal = dialogs.source.match(/\(0,[\w$]+\.jsx\)\(([\w$]+),\{as:`form`,onSubmit:/)?.[1];
|
|
90
|
-
const [Dialog,DialogContent] = await Promise.all([dialogLocal,contentLocal].map(local=>binding(dialogs.source,dialogs.url,local)));
|
|
91
|
-
const menuCoreFile = menu.source.match(/from["']([^"']*generated-image-context-menu-[^"']+)["']/)?.[1];
|
|
92
|
-
const menuCoreUrl = menuCoreFile ? new URL(menuCoreFile,menu.url).href : menu.url;
|
|
93
|
-
const menuCoreSource = menuCoreFile ? await read(menuCoreUrl) : menu.source;
|
|
94
|
-
const imageActions = component(menu.exports,source=>/onOpenedInNewTab:/.test(source),"image actions");
|
|
95
|
-
const imageActionSource = String(imageActions);
|
|
96
|
-
const imageFileLocal = imageActionSource.match(/let\{file:[\w$]+\}=([\w$]+)\(/)?.[1];
|
|
97
|
-
const attachLocal = imageActionSource.match(/await ([\w$]+)\(e,i,\[n\],/)?.[1];
|
|
98
|
-
const downloadLocal = imageActionSource.match(/await ([\w$]+)\(\{downloadSrc:/)?.[1];
|
|
99
|
-
const [imageFile,attachChatImages,downloadImage] = await Promise.all([imageFileLocal,attachLocal,downloadLocal].map(local=>binding(menuCoreSource,menuCoreUrl,local)));
|
|
100
|
-
const toolbarClass = selection.source.match(/className:`(pointer-events-auto flex h-toolbar[^`]+)`/)?.[1];
|
|
101
|
-
const toolbarPositionClass = selection.source.match(/className:`(pointer-events-none fixed inset-x-4 bottom-6[^`]+)`/)?.[1];
|
|
102
|
-
if (!toolbarClass || !toolbarPositionClass) throw new Error("Native selection toolbar layout changed");
|
|
103
|
-
const libraryIcon = async label => {
|
|
104
|
-
const marker = selection.source.indexOf(`defaultMessage:\`${label}\``);
|
|
105
|
-
const prefix = selection.source.slice(Math.max(0, marker - 500), marker);
|
|
106
|
-
const match = [...prefix.matchAll(/leftIcon:\(0,[\w$]+\.jsx\)\(([\w$]+),\{icon:\{(assets:\{)?16:([\w$]+),20:([\w$]+)/g)].at(-1);
|
|
107
|
-
if (!match) throw new Error(`Native Library icon contract changed: ${label}`);
|
|
108
|
-
const [Icon, small, medium] = await Promise.all([match[1], match[3], match[4]].map(local => binding(selection.source, selection.url, local)));
|
|
109
|
-
const sizes = {16:small, 20:medium};
|
|
110
|
-
return React.createElement(Icon, {icon:match[2] ? {assets:sizes} : sizes});
|
|
111
|
-
};
|
|
112
|
-
const upload = component(initial, source => /async function/.test(source) && /libraryPersistenceMode:/.test(source) && /uploadPurpose:/.test(source), "Library upload");
|
|
113
|
-
const setFavorite = component(initial, source => /target_ref:/.test(source) && /showToast:/.test(source), "Library favorite");
|
|
114
|
-
const auth = String(setFavorite).match(/accountId:e.get\(([\w$]+)\),userId:e.get\(([\w$]+)\)/);
|
|
115
|
-
const [accountAtom,userAtom] = await Promise.all([auth?.[1],auth?.[2]].map(local => binding(initialSource,initialUrl,local)));
|
|
116
|
-
if (!accountAtom || !userAtom) throw new Error("Native Library account contract changed");
|
|
117
|
-
const toastLocal = selection.source.match(/get\(([\w$]+)\)\.danger\(/)?.[1];
|
|
118
|
-
const toastAtom = await binding(selection.source, selection.url, toastLocal);
|
|
119
|
-
const favoritePrefix = selection.source.slice(Math.max(0,selection.source.indexOf("id:`library.favorites.add`")-2000),selection.source.indexOf("id:`library.favorites.add`")+500);
|
|
120
|
-
const favoriteAsset = favoritePrefix.match(/\{16:([\w$]+),20:([\w$]+)\}:\{16:([\w$]+),20:([\w$]+)\}/);
|
|
121
|
-
if (!favoriteAsset) throw new Error("Native Library favorite icon contract changed");
|
|
122
|
-
const favoriteIconLocal = favoritePrefix.match(/icon:\{assets:/)?.index;
|
|
123
|
-
const favoriteIconOwner = favoritePrefix.slice(0,favoriteIconLocal).match(/\(0,[\w$]+\.jsx\)\(([\w$]+),\{$/)?.[1];
|
|
124
|
-
const [FavoriteIcon, savedSmall, savedMedium, emptySmall, emptyMedium] = await Promise.all([favoriteIconOwner,...favoriteAsset.slice(1)].map(local => binding(selection.source,selection.url,local)));
|
|
125
|
-
const favoriteIcon = saved => React.createElement(FavoriteIcon,{icon:{assets:saved?{16:savedSmall,20:savedMedium}:{16:emptySmall,20:emptyMedium}}});
|
|
126
|
-
const [chatIcon, downloadIcon, folderIcon, deleteIcon] = await Promise.all([
|
|
127
|
-
libraryIcon("Chat about this"), libraryIcon("Download"), libraryIcon("Add to folder"), libraryIcon("{kind, select, directory {Delete folder} other {Delete}}"),
|
|
128
|
-
]);
|
|
129
|
-
// 文件画布使用原生 Home Composer 的控制器、附件 scope 和输入组件。
|
|
130
|
-
const composerLocals=["kr","Qn","$n","rr","ke","hn","tn","P"];
|
|
131
|
-
const [useComposerController,useHomeComposer,Composer,ComposerSurface,ScopeProvider,composerScope,createComposerValue,composerRouteScope] = await Promise.all(composerLocals.map(local=>binding(filePreview.source,filePreview.url,local)));
|
|
132
|
-
if (!React.createElement || !ReactDOM.createRoot || !Menu.Item) throw new Error("Native Images rendering contract changed");
|
|
133
|
-
return {
|
|
134
|
-
React, ReactDOM, createPortal, Dropdown, Menu, Button, MoreIcon, chatIcon, downloadIcon, folderIcon, favoriteIcon, routerAtom, switchMode, createChatDraft, upload, setFavorite, toastAtom, accountAtom, userAtom,
|
|
135
|
-
MoveDialog:moveDialog.exports.MoveLibraryItemsDialog,
|
|
136
|
-
Checkbox,rangeSelection,nativeSelectedIds,ClearIcon,Dialog,DialogContent,ConfirmationContent:dialogs.exports.LibraryItemConfirmationContent,
|
|
137
|
-
Canvas:canvas.exports.t,CanvasLayout:canvasLayout.exports.r,
|
|
138
|
-
useComposerController,useHomeComposer,Composer,ComposerSurface,ScopeProvider,composerScope,createComposerValue,composerRouteScope,
|
|
139
|
-
editImage:component(initial,source=>/async function/.test(source)&&/requireDispatchAcceptance:/.test(source)&&/sourceAttachment:/.test(source)&&/imageVariant/.test(source),"image edit submission"),
|
|
140
|
-
imageFile,attachChatImages,downloadImage,toolbarClass,toolbarPositionClass,deleteIcon,
|
|
141
|
-
Masonry: component(masonry.exports, source => /\{items:[^}]+animate:/.test(source), "masonry"),
|
|
142
|
-
Skeleton: component(masonry.exports, source => /square:/.test(source) && /Loading library/.test(source), "skeleton"),
|
|
143
|
-
Card: component(layout.exports, source => /actionIndicator:/.test(source) && /documentHeader:/.test(source), "card"),
|
|
144
|
-
useImage: component(sourceImage.exports, source => /shouldLoadFileDataUrl:/.test(source), "image resolver"),
|
|
145
|
-
imageMenu: component(menu.exports, source => /onOpenedInNewTab:/.test(source), "image actions"),
|
|
146
|
-
openImage: component(initial, source => /availableImageCount:/.test(source) && /openInEditor:/.test(source) && /entrypoint:/.test(source), "image preview"),
|
|
147
|
-
};
|
|
148
|
-
})().catch(error => { connecting = null; throw error; });
|
|
149
|
-
return connecting;
|
|
8
|
+
function uploadFile({base64,name}) {
|
|
9
|
+
const decoded=atob(base64),bytes=new Uint8Array(decoded.length);
|
|
10
|
+
// 直接写入字节,避免大图经字符串迭代器创建中间数组。
|
|
11
|
+
for(let index=0;index<decoded.length;index++)bytes[index]=decoded.charCodeAt(index);
|
|
12
|
+
return new File([bytes],name,{type:"image/png"});
|
|
150
13
|
}
|
|
151
14
|
|
|
152
15
|
function environment(anchor) {
|
|
@@ -189,7 +52,7 @@ export function createNativeImageSource(codex, { read = async url => (await fetc
|
|
|
189
52
|
}
|
|
190
53
|
|
|
191
54
|
async function mountNavigation(container, anchor, onSelect) {
|
|
192
|
-
const native = await
|
|
55
|
+
const native = await modules.navigation();
|
|
193
56
|
const context = environment(anchor);
|
|
194
57
|
const root = native.ReactDOM.createRoot(container);
|
|
195
58
|
return {
|
|
@@ -200,7 +63,6 @@ export function createNativeImageSource(codex, { read = async url => (await fetc
|
|
|
200
63
|
selectedKey,
|
|
201
64
|
tabs:[...contract.props.tabs, {key:"imagegen", name:"Image Gen"}],
|
|
202
65
|
onSelect:key => {
|
|
203
|
-
if (key === "imagegen") context.libraryScope?.set(native.nativeSelectedIds,new Set());
|
|
204
66
|
onSelect(key);
|
|
205
67
|
if (key !== "imagegen") navigationContract(anchor).props.onSelect(key);
|
|
206
68
|
},
|
|
@@ -216,20 +78,21 @@ export function createNativeImageSource(codex, { read = async url => (await fetc
|
|
|
216
78
|
}
|
|
217
79
|
|
|
218
80
|
async function mount(container, anchor, {onOpen, onPrompt, onRetry, onHidden}) {
|
|
219
|
-
|
|
81
|
+
let native = await connect();
|
|
220
82
|
const context = environment(anchor);
|
|
221
83
|
const {React, ReactDOM} = native;
|
|
222
84
|
const h = React.createElement;
|
|
85
|
+
const promptIcon=h("span",{className:"inline-flex size-4 items-center justify-center","aria-hidden":true,dangerouslySetInnerHTML:{__html:promptGlyphSource}});
|
|
223
86
|
let failure, currentContent, folderDialog, removeDialog, canvasImage, disposed=false, batchBusy=false;
|
|
224
|
-
let currentImages=[],currentLoading=true,selectedIds=new Set(),selectionAnchor=null;
|
|
87
|
+
let currentImages=[],currentLoading=true,selectedIds=new Set(),selectionAnchor=null,openRevision=0;
|
|
225
88
|
const scope = context.libraryScope;
|
|
226
89
|
if (!scope) throw new Error("Native Library context is unavailable");
|
|
227
90
|
const {accountId,userId} = scope.value;
|
|
228
91
|
const message = (id,defaultMessage) => context.intl.formatMessage({id,defaultMessage});
|
|
229
92
|
const toast = () => scope.get(native.toastAtom);
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
};
|
|
93
|
+
const sameAccount = () => scope.get(native.accountAtom) === accountId && scope.get(native.userAtom) === userId;
|
|
94
|
+
const isCurrent = () => !disposed && sameAccount();
|
|
95
|
+
const assertAccount = () => {if (!isCurrent()) throw new Error("Library action is no longer current");};
|
|
233
96
|
const reference = (image,operation,extra={}) => requestHostAction("local-image-source",{operation,id:image.id,accountId,userId,...extra});
|
|
234
97
|
async function favoriteStatus(id) {
|
|
235
98
|
const result = await libraryApi.favoriteStatus(id);
|
|
@@ -239,13 +102,12 @@ export function createNativeImageSource(codex, { read = async url => (await fetc
|
|
|
239
102
|
}
|
|
240
103
|
const cloud = createCloudImageActions({
|
|
241
104
|
reference, assertAccount,
|
|
242
|
-
|
|
105
|
+
favoriteStatus,
|
|
243
106
|
setFavorite:(libraryFileId,favoriteId,isFavorited) => native.setFavorite(scope,{libraryFileId,favoriteId,isFavorited,surface:"library_grid",showToast:true}),
|
|
244
107
|
upload:async file => {
|
|
245
|
-
const bytes = Uint8Array.from(atob(file.base64),value => value.charCodeAt(0));
|
|
246
108
|
const pendingToast = toast().info(message("minecodex.images.saving","Saving image to Library…"),{duration:0,hasCloseButton:false});
|
|
247
109
|
try {
|
|
248
|
-
const uploaded = await native.upload(scope,
|
|
110
|
+
const uploaded = await native.upload(scope,uploadFile(file),{
|
|
249
111
|
libraryPersistenceMode:"required",storeInLibrary:true,uploadPurpose:"library",beforeRequest:assertAccount,
|
|
250
112
|
});
|
|
251
113
|
return uploaded.libraryFileId;
|
|
@@ -255,13 +117,21 @@ export function createNativeImageSource(codex, { read = async url => (await fetc
|
|
|
255
117
|
async function folder(image) {
|
|
256
118
|
const images=Array.isArray(image)?image:[image];
|
|
257
119
|
const registered=[];
|
|
258
|
-
|
|
259
|
-
const
|
|
120
|
+
const entries=[];
|
|
121
|
+
for (const item of images) {
|
|
122
|
+
const result=await cloud.registered(item);
|
|
123
|
+
const node=await libraryApi.fileNode(result.libraryFileId);
|
|
124
|
+
assertAccount();
|
|
125
|
+
registered.push(result);
|
|
126
|
+
entries.push({node});
|
|
127
|
+
}
|
|
128
|
+
const created=registered.some(item=>item.created);
|
|
260
129
|
let moved = false;
|
|
261
130
|
folderDialog = h(native.MoveDialog,{
|
|
262
131
|
accountId,userId,entries,initialPath:[],
|
|
263
132
|
onMoved:() => {moved=true;},
|
|
264
133
|
onClose:() => {
|
|
134
|
+
if (!isCurrent()) return;
|
|
265
135
|
folderDialog=null;render(currentContent);
|
|
266
136
|
if (!moved) toast().info(created
|
|
267
137
|
? message("minecodex.images.folderCanceledSaved","Image saved to Library, but not added to a folder.")
|
|
@@ -271,7 +141,7 @@ export function createNativeImageSource(codex, { read = async url => (await fetc
|
|
|
271
141
|
render(currentContent);
|
|
272
142
|
}
|
|
273
143
|
function operationError(error) {
|
|
274
|
-
if (
|
|
144
|
+
if (!isCurrent()) return;
|
|
275
145
|
toast().danger(error.code === "UPLOAD_UNCONFIRMED"
|
|
276
146
|
? message("minecodex.images.uploadUnconfirmed","Image upload is pending or could not be confirmed. Check Library before uploading again.")
|
|
277
147
|
: message("minecodex.images.libraryActionFailed","Couldn’t complete the Library action. Please try again."));
|
|
@@ -279,6 +149,7 @@ export function createNativeImageSource(codex, { read = async url => (await fetc
|
|
|
279
149
|
}
|
|
280
150
|
const root = ReactDOM.createRoot(container, {onUncaughtError:error => {failure=error;onRetry(error);}});
|
|
281
151
|
function render(content) {
|
|
152
|
+
if (!isCurrent()) return;
|
|
282
153
|
currentContent = content;
|
|
283
154
|
let tree = h(React.Fragment,null,content,folderDialog,removeDialog,selectionToolbar(),canvasImage?h(ImageCanvas,{key:canvasImage.id,image:canvasImage}):null);
|
|
284
155
|
for (const provider of context.providers) tree = h(provider.type, {value:provider.value}, tree);
|
|
@@ -293,7 +164,7 @@ export function createNativeImageSource(codex, { read = async url => (await fetc
|
|
|
293
164
|
selectedIds=next.selectedIds;selectionAnchor=next.anchor;refresh();
|
|
294
165
|
}
|
|
295
166
|
const selectedImages = () => currentImages.filter(image=>selectedIds.has(image.id));
|
|
296
|
-
function clearSelection() {selectedIds=new Set();selectionAnchor=null;removeDialog=null;refresh();}
|
|
167
|
+
function clearSelection() {openRevision++;selectedIds=new Set();selectionAnchor=null;removeDialog=null;refresh();}
|
|
297
168
|
async function performBatch(action) {
|
|
298
169
|
if (batchBusy) return;
|
|
299
170
|
batchBusy=true;refresh();
|
|
@@ -307,13 +178,13 @@ export function createNativeImageSource(codex, { read = async url => (await fetc
|
|
|
307
178
|
h(native.DialogContent,{as:"form",onSubmit:event=>{
|
|
308
179
|
event.preventDefault();if(pending)return;pending=true;update();refresh();
|
|
309
180
|
void requestHostAction("local-image-source",{operation:"hide",ids:images.map(image=>image.id)}).then(result=>{
|
|
310
|
-
if(
|
|
181
|
+
if(!isCurrent())return;
|
|
311
182
|
const hidden=new Set(result.hiddenIds);
|
|
312
183
|
currentImages=currentImages.filter(image=>!hidden.has(image.id));
|
|
313
184
|
selectedIds=new Set([...selectedIds].filter(id=>!hidden.has(id)));
|
|
314
185
|
pending=false;removeDialog=null;onHidden?.(result.hiddenIds);refresh();
|
|
315
186
|
toast().info(message("minecodex.images.hidden","Hidden from Image Gen"));
|
|
316
|
-
}).catch(error=>{pending=false;update();refresh();operationError(error);});
|
|
187
|
+
}).catch(error=>{if(!isCurrent())return;pending=false;update();refresh();operationError(error);});
|
|
317
188
|
}},h(native.ConfirmationContent,{
|
|
318
189
|
title:context.intl.formatMessage({id:"minecodex.images.hideTitle",defaultMessage:"Delete {count, plural, one {image} other {# images}} from Image Gen?"},{count:images.length}),
|
|
319
190
|
description:message("minecodex.images.hideDescription","These images will only be hidden from Image Gen. Local originals and cloud copies will be kept."),
|
|
@@ -347,11 +218,13 @@ export function createNativeImageSource(codex, { read = async url => (await fetc
|
|
|
347
218
|
}
|
|
348
219
|
document.addEventListener("keydown",onSelectionKey);
|
|
349
220
|
async function startChat(images) {
|
|
221
|
+
assertAccount();
|
|
350
222
|
const conversationId=native.createChatDraft(context.scope,[]);
|
|
351
223
|
native.switchMode(context.scope,"chat");
|
|
352
224
|
const files=[];
|
|
353
|
-
for(const image of images){const file=await reference(image,"upload-file");files.push(native.imageFile(`data:image/png;base64,${file.base64}`,file.name).file);}
|
|
225
|
+
for(const image of images){const file=await reference(image,"upload-file");assertAccount();files.push(native.imageFile(`data:image/png;base64,${file.base64}`,file.name).file);}
|
|
354
226
|
if (!await native.attachChatImages(context.scope,conversationId,files,{isTemporaryChat:false})) throw new Error("Could not attach selected images");
|
|
227
|
+
assertAccount();
|
|
355
228
|
const navigate=context.scope.get(native.routerAtom).prepareNavigation("/",{state:{codexAppMode:"chat",initialChatGptConversationId:conversationId,focusComposerNonce:Date.now()}});
|
|
356
229
|
if(!navigate)throw new Error("Native Chat navigation is unavailable");
|
|
357
230
|
clearSelection();await navigate();
|
|
@@ -359,9 +232,19 @@ export function createNativeImageSource(codex, { read = async url => (await fetc
|
|
|
359
232
|
function localImage(image) {
|
|
360
233
|
return {id:image.imageGenerationItemId, src:image.sourcePath, previewSrc:image.sourcePath, alt:image.name, tabTitle:image.name, width:image.width, height:image.height};
|
|
361
234
|
}
|
|
362
|
-
function open(image, options) {
|
|
235
|
+
async function open(image, options) {
|
|
236
|
+
if (!options) {
|
|
237
|
+
assertAccount();
|
|
238
|
+
const revision=++openRevision;
|
|
239
|
+
const canvas=await modules.canvas();
|
|
240
|
+
// 切换来源或重置列表后,不让晚到的画布覆盖当前页面。
|
|
241
|
+
if(revision!==openRevision || !isCurrent())return false;
|
|
242
|
+
native={...native,...canvas};
|
|
243
|
+
opened.set(image.sourcePath,image);
|
|
244
|
+
canvasImage=image;refresh();return true;
|
|
245
|
+
}
|
|
246
|
+
if (signal?.aborted || !sameAccount()) return;
|
|
363
247
|
opened.set(image.sourcePath, image);
|
|
364
|
-
if (!options) {canvasImage=image;refresh();return;}
|
|
365
248
|
const {readOnly=false,scope=context.scope,chatGptConversationId}=options;
|
|
366
249
|
const result = native.openImage(scope, {
|
|
367
250
|
chatGptConversationId, alt:image.name, title:image.name, attachmentId:`image-playground:${image.imageGenerationItemId}`,
|
|
@@ -369,7 +252,7 @@ export function createNativeImageSource(codex, { read = async url => (await fetc
|
|
|
369
252
|
imageId:image.imageGenerationItemId, sourceConversationId:image.threadId,
|
|
370
253
|
imageSourceContext:{conversationId:image.threadId, hostId:"local"}, imageSource:"generated",
|
|
371
254
|
readOnly, openInEditor:true,
|
|
372
|
-
renderHeaderMenuActions:({renderMenu}) => renderMenu(h(native.Menu.Item, {onSelect:() => onPrompt(image), children:context.intl.formatMessage({id:"minecodex.images.viewPrompts",defaultMessage:"View prompts"})})),
|
|
255
|
+
renderHeaderMenuActions:({renderMenu}) => renderMenu(h(native.Menu.Item, {leftIcon:promptIcon,onSelect:() => onPrompt(image), children:context.intl.formatMessage({id:"minecodex.images.viewPrompts",defaultMessage:"View prompts"})})),
|
|
373
256
|
}, {addToHistory:false});
|
|
374
257
|
if (!result) throw new Error("Native image preview could not open");
|
|
375
258
|
}
|
|
@@ -389,15 +272,28 @@ export function createNativeImageSource(codex, { read = async url => (await fetc
|
|
|
389
272
|
h(native.ScopeProvider,{scope:native.composerRouteScope,value:{pathname:"/",routeKind:"home",routeTemplate:"/",search:"",projectContext:null}},
|
|
390
273
|
h(native.ScopeProvider,{scope:native.composerScope,value:composerValue},h(CanvasSurface,{image,resolved,close}))));
|
|
391
274
|
// 原生文件预览占据主内容 viewport,保留导航、二级侧栏和窗口栏。
|
|
392
|
-
return native.createPortal(h("div",{className:"absolute inset-0 z-40 overflow-hidden", "data-minecodex-image-canvas":image.id,children:
|
|
275
|
+
return native.createPortal(h("div",{className:"absolute inset-0 z-40 overflow-hidden", "data-minecodex-image-canvas":image.id,children:[
|
|
276
|
+
// 画布在主内容内,原生窗口级 overlay 的交通灯避让不适用于此处。
|
|
277
|
+
h("style",{children:'[data-minecodex-image-canvas] .shrink-0:has(>[data-testid="viewer-header"]){padding-left:0!important;padding-right:0!important}'}),content,
|
|
278
|
+
]}),viewport);
|
|
393
279
|
}
|
|
394
280
|
function CanvasSurface({image,resolved,close}) {
|
|
395
281
|
const controller=native.useComposerController();
|
|
282
|
+
const navigate=native.useFileNavigate();
|
|
396
283
|
const home=native.useHomeComposer({homeComposerController:controller,homeComposerMode:"chat"});
|
|
397
284
|
const [abort]=React.useState(()=>new AbortController());
|
|
398
285
|
const [markup,setMarkup]=React.useState(false);
|
|
399
286
|
const [manualZoomPercent,setManualZoomPercent]=React.useState(null);
|
|
400
287
|
const [submitting,setSubmitting]=React.useState(false);
|
|
288
|
+
const [composerHeight,setComposerHeight]=React.useState(0);
|
|
289
|
+
const measureComposer=React.useCallback(node=>{
|
|
290
|
+
const element=node?.querySelector("[data-codex-composer-root]");
|
|
291
|
+
if(!element)return;
|
|
292
|
+
const measure=()=>setComposerHeight(element.getBoundingClientRect().height);
|
|
293
|
+
const observer=new ResizeObserver(measure);
|
|
294
|
+
observer.observe(element);measure();
|
|
295
|
+
return ()=>{observer.disconnect();setComposerHeight(0);};
|
|
296
|
+
},[]);
|
|
401
297
|
const editorRef=React.useRef(null);
|
|
402
298
|
const attachment=React.useRef(null);
|
|
403
299
|
const submissionAccepted=React.useRef(false);
|
|
@@ -408,8 +304,7 @@ export function createNativeImageSource(codex, { read = async url => (await fetc
|
|
|
408
304
|
assertCurrent();
|
|
409
305
|
if(!attachment.current)attachment.current=(async()=>{
|
|
410
306
|
const file=await reference(image,"upload-file");assertCurrent();
|
|
411
|
-
const
|
|
412
|
-
const uploaded=await native.upload(context.scope,new File([bytes],file.name,{type:"image/png"}),{storeInLibrary:false,beforeRequest:assertCurrent,signal:abort.signal});
|
|
307
|
+
const uploaded=await native.upload(context.scope,uploadFile(file),{storeInLibrary:false,beforeRequest:assertCurrent,signal:abort.signal});
|
|
413
308
|
return {...uploaded,width:image.width,height:image.height,imageReference:true};
|
|
414
309
|
})().catch(error=>{attachment.current=null;throw error;});
|
|
415
310
|
return attachment.current;
|
|
@@ -438,31 +333,47 @@ export function createNativeImageSource(codex, { read = async url => (await fetc
|
|
|
438
333
|
}catch(error){if(!abort.signal.aborted)operationError(error);return false;}
|
|
439
334
|
finally {if(!abort.signal.aborted)setSubmitting(false);}
|
|
440
335
|
};
|
|
441
|
-
const composer=h(native.ComposerSurface,{placement:{kind:"home"}},h(native.
|
|
442
|
-
|
|
443
|
-
className:"mx-auto w-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
336
|
+
const composer=h("div",{ref:measureComposer,className:"contents"},h(native.ComposerSurface,{placement:{kind:"home"}},h(native.FileComposer,{
|
|
337
|
+
composerController:controller,composerLayoutMode:"auto-single-line",
|
|
338
|
+
className:"mx-auto w-[min(48rem,calc(100vw-2rem))] max-w-full",
|
|
339
|
+
composerModeAvailability:native.fileComposerAvailability,
|
|
340
|
+
hideRunLocationDropdownOverride:true,showUtilityBar:false,surfaceVariant:"inline",
|
|
341
|
+
placeholderText:message("libraryFilePreview.composerPlaceholder","Ask about this file"),
|
|
342
|
+
submitDisabled:submitting,selectedProject:null,
|
|
343
|
+
// 原图仅在发送时成为本地附件,不伪造 Library 身份,也不上传云端。
|
|
344
|
+
prepareLocalConversationAttachments:async ({fileAttachments,hostId})=>{
|
|
345
|
+
assertCurrent();
|
|
346
|
+
const file=await reference(image,"upload-file");assertCurrent();
|
|
347
|
+
const attached=await native.localAttachmentService(context.scope,hostId).createFileAttachment({contents:uploadFile(file),label:file.name});
|
|
348
|
+
if(abort.signal.aborted||!isCurrent()){
|
|
349
|
+
await native.deleteLocalAttachment(context.scope,hostId,attached.path);
|
|
350
|
+
assertCurrent();
|
|
351
|
+
}
|
|
352
|
+
return [...fileAttachments,attached];
|
|
353
|
+
},
|
|
354
|
+
onLocalFirstTurnAccepted:()=>{submissionAccepted.current=true;},
|
|
355
|
+
onLocalConversationCreated:(conversationId,state)=>{submissionAccepted.current=true;close();navigate(native.localThreadRoute(conversationId),{state});},
|
|
356
|
+
})));
|
|
449
357
|
const dataUrl=resolved.dataUrl??resolved.previewSrc;
|
|
450
358
|
const canvas=h(native.Canvas,{
|
|
451
359
|
alt:image.name,title:image.name,src:dataUrl??image.sourcePath,downloadSrc:dataUrl??image.sourcePath,downloadFilename:image.name,
|
|
452
360
|
attachmentId:`image-playground:${image.imageGenerationItemId}`,attachmentSrc:image.sourcePath,
|
|
453
361
|
chatGptConversationId:props.conversationId,
|
|
454
362
|
imageSource:"generated",imageSourceContext:{conversationId:image.threadId,hostId:"local"},
|
|
455
|
-
readOnly:false,presentation:"
|
|
363
|
+
readOnly:false,presentation:"overlay",standalone:true,showViewerHeader:true,onClose:requestClose,
|
|
456
364
|
markupMode:{active:markup,onActiveChange:setMarkup,editorRef,isSubmitting:submitting,standaloneComposer:true},
|
|
457
365
|
onImageEditSubmit:submitEdit,
|
|
458
366
|
zoom:{manualZoomPercent,onZoomPercentChange:setManualZoomPercent,onZoomToFit:()=>setManualZoomPercent(null)},
|
|
459
367
|
renderComposer:visible=>h(React.Activity,{mode:visible?"visible":"hidden",children:composer}),
|
|
460
|
-
renderHeaderMenuActions:({renderMenu})=>renderMenu(h(native.Menu.Item,{onSelect:()=>onPrompt(image),children:message("minecodex.images.viewPrompts","View prompts")})),
|
|
368
|
+
renderHeaderMenuActions:({renderMenu})=>renderMenu(h(native.Menu.Item,{leftIcon:promptIcon,onSelect:()=>onPrompt(image),children:message("minecodex.images.viewPrompts","View prompts")})),
|
|
461
369
|
});
|
|
462
|
-
// 原生 overlay
|
|
463
|
-
|
|
370
|
+
// 原生 overlay 的 118px 下限属于旧输入框;画布和拖拽边界跟随实际高度。
|
|
371
|
+
const reserve=composerHeight>0?`calc(${composerHeight}px + var(--spacing)*4)`:"0px";
|
|
372
|
+
return h("div",{className:"contents",style:{"--right-panel-composer-overlay-reserve":reserve}},
|
|
373
|
+
h(native.CanvasLayout,{assetName:image.name,contentOwnsHeader:true,onClose:requestClose,children:canvas}));
|
|
464
374
|
}
|
|
465
375
|
async function chatAbout(image) {
|
|
376
|
+
assertAccount();
|
|
466
377
|
const conversationId = native.createChatDraft(context.scope, []);
|
|
467
378
|
native.switchMode(context.scope, "chat");
|
|
468
379
|
const navigate = context.scope.get(native.routerAtom).prepareNavigation("/", {
|
|
@@ -471,24 +382,31 @@ export function createNativeImageSource(codex, { read = async url => (await fetc
|
|
|
471
382
|
if (!navigate) throw new Error("Native Chat navigation is unavailable");
|
|
472
383
|
await navigate();
|
|
473
384
|
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
474
|
-
|
|
385
|
+
// 导航会正常卸载图库;只把面板移交给目标 Chat 的可见输入框。
|
|
386
|
+
if (signal?.aborted || !sameAccount()) return;
|
|
387
|
+
const composer = codex.composer(null,{purpose:"insertion"})?.editable;
|
|
475
388
|
if (!composer) throw new Error("Native Chat composer is unavailable");
|
|
476
389
|
const chat = environment(composer);
|
|
477
|
-
open(image, {readOnly:true, scope:chat.scope, chatGptConversationId:conversationId});
|
|
390
|
+
await open(image, {readOnly:true, scope:chat.scope, chatGptConversationId:conversationId});
|
|
478
391
|
}
|
|
479
392
|
function ImageCard({image}) {
|
|
480
393
|
const [saved,setSaved] = React.useState(false);
|
|
481
394
|
const [busy,setBusy] = React.useState(false);
|
|
395
|
+
const active=React.useRef(true);
|
|
396
|
+
const favoriteRevision=React.useRef(0);
|
|
397
|
+
React.useEffect(()=>{active.current=true;return()=>{active.current=false;favoriteRevision.current++;};},[]);
|
|
482
398
|
async function perform(action) {
|
|
483
399
|
if (busy) return;
|
|
484
400
|
setBusy(true);
|
|
485
|
-
try {await action();} catch(error) {operationError(error);} finally {setBusy(false);}
|
|
401
|
+
try {assertAccount();await action();} catch(error) {operationError(error);} finally {if(active.current&&isCurrent())setBusy(false);}
|
|
486
402
|
}
|
|
487
403
|
async function refreshFavorite(open) {
|
|
404
|
+
const revision=++favoriteRevision.current;
|
|
488
405
|
if (!open) return;
|
|
489
406
|
try {
|
|
490
407
|
const cached = await reference(image,"reference");
|
|
491
|
-
|
|
408
|
+
const value=cached.libraryFileId ? !!(await favoriteStatus(cached.libraryFileId)) : false;
|
|
409
|
+
if (active.current && isCurrent() && revision===favoriteRevision.current) setSaved(value);
|
|
492
410
|
} catch(error) {console.error("MineCodex favorite status",error);}
|
|
493
411
|
}
|
|
494
412
|
const resolved = native.useImage({src:image.sourcePath, conversationId:image.threadId, hostId:"local", shouldLoadFileDataUrl:true});
|
|
@@ -500,22 +418,19 @@ export function createNativeImageSource(codex, { read = async url => (await fetc
|
|
|
500
418
|
const item = byId.get(id);
|
|
501
419
|
return item && {...item, icon, text:context.intl.formatMessage(message ?? item.message)};
|
|
502
420
|
};
|
|
503
|
-
const promptGlyph = document.querySelector('[aria-label="Prompts"] svg');
|
|
504
|
-
const menuGlyph = promptGlyph?.cloneNode(true);
|
|
505
|
-
if (menuGlyph) {
|
|
506
|
-
menuGlyph.setAttribute("width","16");menuGlyph.setAttribute("height","16");
|
|
507
|
-
menuGlyph.style.width="16px";menuGlyph.style.height="16px";
|
|
508
|
-
}
|
|
509
|
-
const promptIcon = menuGlyph ? h("span", {className:"inline-flex size-4 items-center justify-center", "aria-hidden":true, dangerouslySetInnerHTML:{__html:menuGlyph.outerHTML}}) : null;
|
|
510
421
|
const menuItems = [
|
|
511
422
|
{id:"chat-about", icon:native.chatIcon, text:context.intl.formatMessage({id:"libraryNext.actions.chatAboutItem",defaultMessage:"Chat about this"}),onSelect:() => {void chatAbout(image).catch(onRetry);}},
|
|
423
|
+
{id:"view-prompts", icon:promptIcon, text:context.intl.formatMessage({id:"minecodex.images.viewPrompts",defaultMessage:"View prompts"}),onSelect:() => {void open(image).then(opened=>{if(opened&&isCurrent())onPrompt(image);}).catch(operationError);}},
|
|
512
424
|
{id:"favorite",icon:native.favoriteIcon(saved),text:saved
|
|
513
425
|
? message("library.favorites.remove","Remove from Favorites")
|
|
514
|
-
: message("library.favorites.add","Add to Favorites"),onSelect:() => {void perform(async () =>
|
|
426
|
+
: message("library.favorites.add","Add to Favorites"),onSelect:() => {void perform(async () => {
|
|
427
|
+
favoriteRevision.current++;
|
|
428
|
+
const value=await cloud.favorite(image);
|
|
429
|
+
if(active.current&&isCurrent()){favoriteRevision.current++;setSaved(value);}
|
|
430
|
+
});}},
|
|
515
431
|
action("download-a-copy", native.downloadIcon, {id:"libraryNext.actions.download", defaultMessage:"Download"}),
|
|
516
432
|
{id:"folder",icon:native.folderIcon,text:message("libraryNext.actions.addToFolder","Add to folder"),onSelect:() => {void perform(() => folder(image));}},
|
|
517
433
|
{id:"delete",icon:native.deleteIcon,tone:"danger",text:message("libraryNext.actions.delete","Delete"),onSelect:()=>confirmRemove([image])},
|
|
518
|
-
{id:"view-prompts", icon:promptIcon, text:context.intl.formatMessage({id:"minecodex.images.viewPrompts",defaultMessage:"View prompts"}),onSelect:() => {open(image);onPrompt(image);}},
|
|
519
434
|
].filter(Boolean);
|
|
520
435
|
const dropdown = h(native.Dropdown, {
|
|
521
436
|
contentWidth:"menu", align:"end", onOpenChange:refreshFavorite,
|
|
@@ -534,17 +449,17 @@ export function createNativeImageSource(codex, { read = async url => (await fetc
|
|
|
534
449
|
},children:h(native.Checkbox,{className:"cursor-interaction",variant:"overlay",checked,disabled:batchBusy,"aria-label":context.intl.formatMessage({id:"libraryNext.selection.item",defaultMessage:"Select {name}"},{name:image.name}),onCheckedChange:value=>select(image,value===true)})});
|
|
535
450
|
return h(native.Card,{name:image.name,selection,selected:checked,selectionActive:selectedIds.size>0,onSelect:event=>{
|
|
536
451
|
if(selectedIds.size || event.shiftKey){select(image,!checked,event.shiftKey);return true;}return false;
|
|
537
|
-
},onOpen:() => onOpen(image,open),actions:dropdown,children:h("span",{className:"relative flex min-h-0 w-full flex-1 items-center justify-center",children:h("span",{className:"relative flex w-full items-center justify-center",style:{aspectRatio:cardRatio},children:picture})})});
|
|
452
|
+
},onOpen:() => {void Promise.resolve(onOpen(image,open)).catch(operationError);},actions:dropdown,children:h("span",{className:"relative flex min-h-0 w-full flex-1 items-center justify-center",children:h("span",{className:"relative flex w-full items-center justify-center",style:{aspectRatio:cardRatio},children:picture})})});
|
|
538
453
|
}
|
|
539
454
|
return {
|
|
540
455
|
render({images,loading}) {
|
|
541
456
|
if (failure) throw failure;
|
|
542
457
|
currentImages=images;currentLoading=loading;
|
|
543
|
-
if(loading){selectedIds=new Set();selectionAnchor=null;removeDialog=null;}
|
|
458
|
+
if(loading){openRevision++;selectedIds=new Set();selectionAnchor=null;removeDialog=null;scope.set(native.nativeSelectedIds,new Set());}
|
|
544
459
|
refresh();
|
|
545
460
|
},
|
|
546
461
|
clearSelection,
|
|
547
|
-
dispose() {disposed=true;document.removeEventListener("keydown",onSelectionKey);root.unmount();},
|
|
462
|
+
dispose() {disposed=true;openRevision++;document.removeEventListener("keydown",onSelectionKey);root.unmount();},
|
|
548
463
|
};
|
|
549
464
|
}
|
|
550
465
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!-- Derived from Lucide book-open (ISC): merged stroke subpaths for uniform alpha. -->
|
|
2
|
+
<svg
|
|
3
|
+
class="icon-xs shrink-0"
|
|
4
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
+
width="16"
|
|
6
|
+
height="16"
|
|
7
|
+
viewBox="-1.5 -1.5 27 27"
|
|
8
|
+
fill="none"
|
|
9
|
+
stroke="currentColor"
|
|
10
|
+
stroke-width="1.8"
|
|
11
|
+
stroke-linecap="round"
|
|
12
|
+
stroke-linejoin="round"
|
|
13
|
+
>
|
|
14
|
+
<path d="M12 7v14 M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z" />
|
|
15
|
+
</svg>
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
// 工作流只管理云端身份与操作顺序;上传协议和原生组件由 Adapter 提供。
|
|
2
|
-
export function createCloudImageActions({reference,upload,
|
|
2
|
+
export function createCloudImageActions({reference,upload,favoriteStatus,setFavorite,assertAccount}) {
|
|
3
3
|
const pending = new Map();
|
|
4
4
|
function registered(image) {
|
|
5
5
|
if (!pending.has(image.id)) {
|
|
6
6
|
const task = (async () => {
|
|
7
7
|
assertAccount();
|
|
8
8
|
const cached = await reference(image,"reference");
|
|
9
|
-
|
|
9
|
+
assertAccount();
|
|
10
|
+
if (cached.libraryFileId) return {libraryFileId:cached.libraryFileId,created:false};
|
|
10
11
|
const unconfirmed = () => Object.assign(new Error("Library upload is pending or could not be confirmed"),{code:"UPLOAD_UNCONFIRMED"});
|
|
11
12
|
if (cached.status === "uploading") throw unconfirmed();
|
|
12
13
|
const file = await reference(image,"upload-file");
|
|
@@ -18,7 +19,7 @@ export function createCloudImageActions({reference,upload,fileNode,favoriteStatu
|
|
|
18
19
|
// 先保存身份;收藏失败或取消文件夹选择后仍可复用上传结果。
|
|
19
20
|
await reference(image,"register",{libraryFileId,fingerprint:file.fingerprint});
|
|
20
21
|
assertAccount();
|
|
21
|
-
return {
|
|
22
|
+
return {libraryFileId,created:true};
|
|
22
23
|
})();
|
|
23
24
|
pending.set(image.id,task);
|
|
24
25
|
const release = () => {if (pending.get(image.id) === task) pending.delete(image.id);};
|
|
@@ -29,10 +30,11 @@ export function createCloudImageActions({reference,upload,fileNode,favoriteStatu
|
|
|
29
30
|
return {
|
|
30
31
|
registered,
|
|
31
32
|
async favorite(image) {
|
|
32
|
-
const {
|
|
33
|
-
const favoriteId = await favoriteStatus(
|
|
33
|
+
const {libraryFileId} = await registered(image);
|
|
34
|
+
const favoriteId = await favoriteStatus(libraryFileId);
|
|
35
|
+
assertAccount();
|
|
36
|
+
if (!await setFavorite(libraryFileId,favoriteId,!favoriteId)) throw new Error("Library favorite could not be changed");
|
|
34
37
|
assertAccount();
|
|
35
|
-
if (!await setFavorite(entry.node.id,favoriteId,!favoriteId)) throw new Error("Library favorite could not be changed");
|
|
36
38
|
return !favoriteId;
|
|
37
39
|
},
|
|
38
40
|
};
|
|
@@ -183,7 +183,8 @@ export function installLocalImageSource(config, signal, sources, native) {
|
|
|
183
183
|
});
|
|
184
184
|
observer.observe(document.body, {childList:true, subtree:true, attributes:true, attributeFilter:["aria-selected"]});
|
|
185
185
|
void scan();
|
|
186
|
-
|
|
186
|
+
let cleaned = false;
|
|
187
|
+
const cleanup = () => {if(cleaned)return;cleaned=true;observer.disconnect();unmount();pendingPrompt=null;};
|
|
187
188
|
signal.addEventListener("abort", cleanup, {once:true});
|
|
188
189
|
return {
|
|
189
190
|
cleanup,
|