minecodex 1.2.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -3
- package/features/images/README.md +16 -98
- package/features/images/codex-feature.json +6 -14
- package/features/images/src/http-server.mjs +15 -5
- package/features/images/src/image-library.mjs +37 -9
- package/features/images/src/library-prompt-page.mjs +239 -0
- 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/model-slider/THIRD_PARTY_NOTICES.md +1 -1
- package/features/notes/DESIGN.md +66 -0
- package/features/notes/README.md +18 -99
- package/features/notes/assets/icons/book-open-nav.svg +15 -0
- package/features/notes/assets/icons/book-open.svg +16 -0
- package/features/notes/assets/native-emojis.json +1 -0
- package/features/notes/assets/native-prompt-icons.json +28 -0
- package/features/notes/codex-feature.json +12 -70
- package/features/notes/src/main.mjs +2 -2
- package/features/notes/src/product.mjs +287 -613
- package/features/notes/src/prompt-schema.mjs +117 -0
- package/features/notes/src/quick-prompts-page.mjs +1559 -0
- package/features/notes/src/quick-prompts-server.mjs +167 -0
- package/features/session-status/DESIGN.md +10 -2
- package/features/session-status/README.md +13 -3
- package/features/session-status/src/session-status.mjs +329 -24
- package/features/sidebar-focus/DESIGN.md +16 -0
- package/features/sidebar-focus/codex-feature.json +20 -0
- package/features/sidebar-focus/src/sidebar-focus.mjs +134 -0
- package/features/sidebar-focus/vendor/lucide-LICENSE +43 -0
- package/features/unread-inbox/DESIGN.md +28 -0
- package/features/unread-inbox/codex-feature.json +20 -0
- package/features/unread-inbox/src/unread-inbox.mjs +360 -0
- package/features/usage-meter/DESIGN.md +49 -0
- package/features/usage-meter/README.md +18 -0
- package/features/usage-meter/codex-feature.json +13 -0
- package/features/usage-meter/src/usage-meter.mjs +200 -0
- package/package.json +21 -6
- package/packages/cli/src/config.mjs +4 -1
- package/packages/cli/src/control-server.mjs +2 -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 +364 -116
- package/packages/runtime-host/src/codex-runtime.mjs +105 -28
- package/packages/runtime-host/src/feature-registry.mjs +3 -0
- package/packages/runtime-host/src/session-status-reader.mjs +65 -24
- package/features/notes/assets/icons/circle.svg +0 -15
- package/features/notes/assets/icons/ellipsis.svg +0 -17
- package/features/notes/assets/icons/external-link.svg +0 -17
- package/features/notes/assets/icons/list-todo.svg +0 -19
- package/features/notes/assets/icons/message-square-quote.svg +0 -17
- package/features/notes/assets/icons/paperclip.svg +0 -15
- package/features/notes/assets/icons/star-off.svg +0 -17
- package/features/notes/assets/icons/star.svg +0 -15
- package/features/notes/assets/icons/sticky-note.svg +0 -16
- package/features/notes/assets/icons/trash-2.svg +0 -19
- package/features/notes/src/http-server.mjs +0 -580
- package/features/notes/web/app.js +0 -1202
- package/features/notes/web/codex-artifact-document.svg +0 -1
- package/features/notes/web/codex-attachment.svg +0 -1
- package/features/notes/web/codex-build.svg +0 -1
- package/features/notes/web/codex-code.svg +0 -1
- package/features/notes/web/codex-cplusplus.svg +0 -1
- package/features/notes/web/codex-css.svg +0 -1
- package/features/notes/web/codex-document.svg +0 -1
- package/features/notes/web/codex-file.svg +0 -1
- package/features/notes/web/codex-folder.svg +0 -1
- package/features/notes/web/codex-hashes.svg +0 -1
- package/features/notes/web/codex-html.svg +0 -1
- package/features/notes/web/codex-image.svg +0 -1
- package/features/notes/web/codex-java.svg +0 -1
- package/features/notes/web/codex-javascript.svg +0 -1
- package/features/notes/web/codex-json.svg +0 -1
- package/features/notes/web/codex-notebook.svg +0 -1
- package/features/notes/web/codex-pdf.svg +0 -1
- package/features/notes/web/codex-pencil.svg +0 -3
- package/features/notes/web/codex-php.svg +0 -1
- package/features/notes/web/codex-plus.svg +0 -3
- package/features/notes/web/codex-presentation.svg +0 -1
- package/features/notes/web/codex-python.svg +0 -1
- package/features/notes/web/codex-react.svg +0 -1
- package/features/notes/web/codex-rust.svg +0 -1
- package/features/notes/web/codex-shell.svg +0 -1
- package/features/notes/web/codex-skill.svg +0 -1
- package/features/notes/web/codex-spreadsheet.svg +0 -1
- package/features/notes/web/codex-task.svg +0 -3
- package/features/notes/web/codex-terminal.svg +0 -1
- package/features/notes/web/codex-toml.svg +0 -1
- package/features/notes/web/codex-typescript.svg +0 -1
- package/features/notes/web/codex-view-all.svg +0 -3
- package/features/notes/web/file-icons.mjs +0 -68
- package/features/notes/web/i18n.mjs +0 -237
- package/features/notes/web/index.html +0 -15
- package/features/notes/web/styles.css +0 -1155
- package/features/notes/web/todo-drag.mjs +0 -18
package/README.md
CHANGED
|
@@ -11,10 +11,12 @@ MineCodex 给 Codex Desktop 增加一组轻量、本地优先的扩展功能。
|
|
|
11
11
|
| 插件 | 说明 |
|
|
12
12
|
| --- | --- |
|
|
13
13
|
| **Model Slider** | 更清晰、更顺滑的模型切换面板:具备品牌标识、常用收藏与流畅交互 |
|
|
14
|
-
| **
|
|
15
|
-
| **
|
|
14
|
+
| **Prompts** | 专页分类管理提示词与调用统计,输入框 Quick Prompts 快速调用 |
|
|
15
|
+
| **Usage Meter** | 在 Help 与 Account 之间显示剩余额度环,悬浮查看周 / 五小时额度,点击进入 Usage 设置 |
|
|
16
|
+
| **Images** | 在原生 Library 图片详情中查看并复制提示词 |
|
|
16
17
|
| **Hide Upsell Banner** | 隐藏输入框上方的用量提示横幅,仅优化界面展示,不影响正常的用量统计 |
|
|
17
18
|
| **Session Status** | 在输入框下方查看当前任务的步骤、观测速度、Token 用量与上下文明细 |
|
|
19
|
+
| **Task Switcher** | 点击聊天标题或标签页前的任务按钮,查看并切换工作中与未读会话 |
|
|
18
20
|
|
|
19
21
|
## 安装
|
|
20
22
|
|
|
@@ -54,7 +56,7 @@ mcx open
|
|
|
54
56
|
|
|
55
57
|
## 工作原理
|
|
56
58
|
|
|
57
|
-
MineCodex 统一管理 Codex 内的菜单入口、主题风格与界面适配。Model Slider、
|
|
59
|
+
MineCodex 统一管理 Codex 内的菜单入口、主题风格与界面适配。Model Slider、Prompts 各自管理操作界面,Images 增强原生 Library 的提示词查看;Session Status 只读汇总本机 Codex 会话事件,Hide Upsell Banner 优化界面展示。所有功能按需开启,关闭后即停止运行。
|
|
58
60
|
|
|
59
61
|
## 开发
|
|
60
62
|
|
|
@@ -69,3 +71,7 @@ npm run pack:audit
|
|
|
69
71
|
## 许可
|
|
70
72
|
|
|
71
73
|
MineCodex 是非官方独立扩展项目,与 OpenAI 无关联。项目自有代码以 MIT License 授权;第三方图标、品牌和兼容资产遵循各自的权利说明,见 [`features/model-slider/THIRD_PARTY_NOTICES.md`](features/model-slider/THIRD_PARTY_NOTICES.md) 与 [`features/session-status/vendor/lucide-LICENSE`](features/session-status/vendor/lucide-LICENSE)。
|
|
74
|
+
|
|
75
|
+
### Sidebar Focus / 侧栏聚焦
|
|
76
|
+
|
|
77
|
+
展开侧栏时,点击铃铛与搜索之间的聚焦按钮,保留当前任务路径并收起其他分支。再次点击恢复原先布局;切换任务时跟随路径,手动调整分支时退出聚焦。
|
|
@@ -1,107 +1,25 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Images · Library 提示词
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Images 在 Codex 原生 **Library → 图片详情**中补充提示词查看能力。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- 有可追溯的提示词时,在缩放胶囊左侧显示 **Prompt / 提示词**。
|
|
6
|
+
- 点击后,使用原生 Version history 相同布局和 design token 的右侧面板展示内容。
|
|
7
|
+
- 面板只展示提示词正文,支持一键复制;优先使用生成提示词,否则追溯该图片的用户输入。
|
|
8
|
+
- 未找到提示词、普通上传图片或来源对话不可用时,不显示按钮。
|
|
9
|
+
- 切换图片或关闭详情会移除旧内容;Escape 先关闭提示词面板。
|
|
6
10
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
- 工具栏内直接提供 2–6 列 Grid 滑条,使用无描边的 4px 轨道;首次打开按可用宽度给一个初始列数,之后只记住用户自己的选择。每张卡片显示来源 Project 或独立 Chat,以及生成时间。
|
|
11
|
-
- 可按 Thread 聚合成图板;进入图板后,左侧图片选择带只保留该 Thread 的图片,关闭详情即返回原图板位置。
|
|
12
|
-
- 标题栏固定在独立滚动区上方;图片按 36 张一批渐进加载,滚动条保持至少 48px 的可抓取高度。
|
|
13
|
-
- 点击图片后,以左侧图片选择带 + 右侧详情区的 20/80 布局查看原图。
|
|
14
|
-
- 按需读取该次 ImageGen 实际使用的 Prompt,并显示 Project、Thread、尺寸、时间与文件大小。
|
|
15
|
-
- 通过 `Save as` 打开 macOS 系统保存面板并保存原始 PNG。
|
|
16
|
-
- 从详情区通过 `Open Thread` 打开真实来源 Thread。
|
|
17
|
-
- 已归档 Thread 的图片仍可查看和 Save as,但 `Open Thread` 会禁用并在悬浮时说明原因。
|
|
18
|
-
- 通过上层同级项目 `CodexRuntimeHost` 启动一个统一宿主,在 Codex 左侧栏注册 Images。
|
|
11
|
+
新版不再在 Sites 下方注入独立 Images 入口,也不启动第二套图库服务。
|
|
12
|
+
图片浏览、编辑与下载使用 Codex 原生能力。读取仅在本机原生 Renderer 内完成,
|
|
13
|
+
沿用当前登录态,不复制图片或持久化来源对话。
|
|
19
14
|
|
|
20
|
-
|
|
15
|
+
## 开发
|
|
21
16
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
需要 Node.js 22.5 或更高版本,不需要安装第三方 npm 依赖。
|
|
25
|
-
|
|
26
|
-
启动统一宿主;它会读取 `codex-feature.json`、启动 Images 服务并注册侧边入口:
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
npm start
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
只启动独立 Web UI:
|
|
33
|
-
|
|
34
|
-
```bash
|
|
35
|
-
npm run service
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
然后打开 <http://127.0.0.1:47831>。
|
|
39
|
-
|
|
40
|
-
运行测试:
|
|
17
|
+
统一宿主通过 `codex-feature.json` 注册 `src/library-prompt-page.mjs` 页面脚本。
|
|
18
|
+
原生 DOM 或 API 契约缺失时不注入。视觉契约见 [DESIGN.md](DESIGN.md)。
|
|
41
19
|
|
|
42
20
|
```bash
|
|
43
|
-
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
服务运行时,路径索引默认保存在 `~/.codex-image-host/library.sqlite`。统一宿主暂时沿用 `~/.codex-image-host/codex-profile`,避免迁移后丢失现有专用 Codex 实例状态。项目不会写入或删除 `~/.codex/generated_images` 中的文件。
|
|
47
|
-
|
|
48
|
-
## 精简结构
|
|
49
|
-
|
|
50
|
-
```text
|
|
51
|
-
~/.codex/generated_images
|
|
52
|
-
│ scan + watch
|
|
53
|
-
▼
|
|
54
|
-
ImageLibrary ── SQLite path index
|
|
55
|
-
│ Codex state + session index
|
|
56
|
-
│ ├─ Project / Chat / Thread 名称与 Archive 状态
|
|
57
|
-
│ └─ revised_prompt(按需读取 Session JSONL)
|
|
58
|
-
▼
|
|
59
|
-
HTTP + Images UI
|
|
60
|
-
│
|
|
61
|
-
└── codex-feature.json
|
|
62
|
-
│ sibling discovery
|
|
63
|
-
▼
|
|
64
|
-
../CodexRuntimeHost
|
|
65
|
-
└── Codex sidebar + Theme Bridge + open Thread
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
- `src/image-library.mjs`:扫描、监听和路径索引,是产品核心。
|
|
69
|
-
- `src/thread-metadata.mjs`:读取 Codex 当前 Project、projectless Chat、Thread 名称与 Archive 状态。
|
|
70
|
-
- `src/prompt-index.mjs`:按 Thread 与 ImageGen item 定位真实 `revised_prompt`。
|
|
71
|
-
- `src/http-server.mjs`:图片列表、来源 facets、Thread 图板、原图读取、系统 Save as 和静态页面。
|
|
72
|
-
- `codex-feature.json`:Images 对统一宿主的最小声明,不包含图片业务逻辑。
|
|
73
|
-
- `../CodexRuntimeHost`:可复用的 CDP 集成,统一负责多个个人功能的入口、Surface、主题与导航。
|
|
74
|
-
- `web/`:零框架 Images 页面。
|
|
75
|
-
|
|
76
|
-
## Codex 设计语言与主题
|
|
77
|
-
|
|
78
|
-
- Images 不维护独立品牌皮肤,而是通过 Theme Bridge 读取当前 Codex Renderer 已解析的 semantic tokens。
|
|
79
|
-
- 支持 Codex 的明亮 / 暗黑主题,并监听根节点 theme class 与 style 变化。
|
|
80
|
-
- 用户修改 accent、ink 或 surface 后,新的 resolved tokens 会同步到 iframe,不需要重新读取或猜测内部配置文件位置。
|
|
81
|
-
- 侧边栏 Images 入口克隆原生 Plugins 控件的布局与状态,再替换为 Codex 图标体系中的独立 Image 图标;iframe 内不导入 Codex 私有 React 模块或 hashed CSS bundle,以免 Codex 更新后直接失效。
|
|
82
|
-
- 原生左侧栏隐藏时,Images 标题左侧显示同源图标入口;点击由 RuntimeHost 转发给原生侧栏开关,恢复后入口自动隐藏。
|
|
83
|
-
|
|
84
|
-
取证、token Interface 和代码复用边界见 [`docs/CODEX_DESIGN_LANGUAGE.md`](docs/CODEX_DESIGN_LANGUAGE.md)。
|
|
85
|
-
|
|
86
|
-
## 当前边界
|
|
87
|
-
|
|
88
|
-
- Images 服务需要保持运行,才会持续监听新图片;侧边栏注册由 `CodexRuntimeHost` 统一维持。
|
|
89
|
-
- 每次服务启动都会先开放 health,再在后台补扫停机期间新增的图片并刷新来源名称;初始补扫完成后,运行期间的新图片由文件监听自动加入。手动 Refresh 会再次补扫来源和 Archive 状态,但不会重置筛选、图板模式、详情、滚动位置或列数。刷新 ring 至少显示 1 秒。
|
|
90
|
-
- 侧边栏使用 Codex 当前未公开的 CDP / DOM 能力,Codex UI 更新后只需调整统一宿主;独立 Web UI 和路径索引不受影响。
|
|
91
|
-
- 当前 Codex 的 CSP 会屏蔽普通本地 iframe。统一宿主会先启用 CSP bypass 并注册 document-start 脚本;尚未完成该初始化的首次或旧版 document 会受控 reload 一次。完成初始化后会留下 document 标记,后续 RuntimeHost 重启只替换旧的 MineCodex runtime,不再刷新 Codex 页面(Issue #1)。
|
|
92
|
-
- 详情打开时会从本机 Session JSONL 按需读取该 ImageGen item 的真实 `revised_prompt` 并缓存到索引;历史记录缺少该事件时明确显示 Prompt 不可用,不从附近文本猜测。
|
|
93
|
-
- 当前没有发现 Codex 会因运行时间增长而自动清理 `generated_images` 的证据。如果未来观察到真实清理,再决定是否增加可选备份。
|
|
94
|
-
|
|
95
|
-
Phase 0 的原图、来源元数据与 Thread 导航证据见 [`spikes/phase0/README.md`](spikes/phase0/README.md)。
|
|
96
|
-
|
|
97
|
-
需要验证 Codex 内的 iframe 是否真正加载时,可在 `npm start` 运行期间执行:
|
|
98
|
-
|
|
99
|
-
```bash
|
|
100
|
-
node spikes/phase0/cdp-probe.mjs frame-check --port 9231 \
|
|
101
|
-
--url http://127.0.0.1:47831/
|
|
102
|
-
|
|
103
|
-
node spikes/phase0/cdp-probe.mjs overlay-check --port 9231 \
|
|
104
|
-
--url http://127.0.0.1:47831/
|
|
21
|
+
node --test features/images/test/library-prompt-page.test.mjs
|
|
105
22
|
```
|
|
106
23
|
|
|
107
|
-
`
|
|
24
|
+
旧 `src/image-library.mjs`、索引、独立 `web/` 与服务脚本暂保留为历史工具,
|
|
25
|
+
不属于当前插件入口;已有本地图片和缓存不会被迁移或删除。
|
|
@@ -7,23 +7,15 @@
|
|
|
7
7
|
"zh-CN": "图片"
|
|
8
8
|
},
|
|
9
9
|
"description": {
|
|
10
|
-
"en": "
|
|
11
|
-
"zh-CN": "
|
|
10
|
+
"en": "View image prompts inside the native Library image viewer.",
|
|
11
|
+
"zh-CN": "在原生 Library 图片详情中查看并复制提示词。"
|
|
12
12
|
},
|
|
13
|
-
"surfaceUrl": "http://127.0.0.1:47831/",
|
|
14
|
-
"healthUrl": "http://127.0.0.1:47831/api/health",
|
|
15
|
-
"runtimePortEnvironment": "CODEX_IMAGE_HOST_PORT",
|
|
16
|
-
"pageScript": {
|
|
17
|
-
"resource": "src/chatgpt-image-source-page.mjs"
|
|
18
|
-
},
|
|
19
|
-
"hostActions": ["import-generated-image", "create-image-edit-thread", "open-sidebar"],
|
|
20
|
-
"placement": {
|
|
21
|
-
"after": "sites",
|
|
22
|
-
"order": 10
|
|
23
|
-
},
|
|
24
|
-
"command": ["node", "src/main.mjs"],
|
|
25
13
|
"icon": {
|
|
26
14
|
"viewBox": "0 0 24 24",
|
|
27
15
|
"markup": "<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\" ry=\"2\"></rect><circle cx=\"9\" cy=\"9\" r=\"2\"></circle><path d=\"m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21\"></path>"
|
|
16
|
+
},
|
|
17
|
+
"entry": {
|
|
18
|
+
"kind": "page-script",
|
|
19
|
+
"script": "src/library-prompt-page.mjs"
|
|
28
20
|
}
|
|
29
21
|
}
|
|
@@ -48,7 +48,7 @@ function publicImage(image, { includePrompt = false } = {}) {
|
|
|
48
48
|
downloadUrl: `/api/images/${image.id}/download`,
|
|
49
49
|
sourceUrl: image.threadId.startsWith("chatgpt:") ? null : `codex://threads/${image.threadId}`,
|
|
50
50
|
};
|
|
51
|
-
if (includePrompt) result.prompt = image.prompt;
|
|
51
|
+
if (includePrompt) { result.prompt = image.prompt; result.promptKind = image.promptKind; }
|
|
52
52
|
return result;
|
|
53
53
|
}
|
|
54
54
|
|
|
@@ -295,11 +295,13 @@ export async function createHttpServer({
|
|
|
295
295
|
|
|
296
296
|
if (request.method === "GET" && url.pathname === "/api/images") {
|
|
297
297
|
const { limit, offset } = paginationFor(url);
|
|
298
|
+
const snapshot = parseIntegerQuery(url.searchParams, "snapshot", { defaultValue: library.snapshot(), min: 0, max: Number.MAX_SAFE_INTEGER });
|
|
298
299
|
const source = url.searchParams.get("source") ?? "all";
|
|
299
300
|
const threadId = url.searchParams.get("threadId") || null;
|
|
300
301
|
const sort = url.searchParams.get("sort") === "asc" ? "asc" : "desc";
|
|
301
302
|
const query = url.searchParams.get("q") ?? "";
|
|
302
303
|
const images = library.list({
|
|
304
|
+
snapshot,
|
|
303
305
|
query,
|
|
304
306
|
source,
|
|
305
307
|
threadId,
|
|
@@ -309,7 +311,8 @@ export async function createHttpServer({
|
|
|
309
311
|
}).map(publicImage);
|
|
310
312
|
json(response, 200, {
|
|
311
313
|
images,
|
|
312
|
-
count: library.count({ query, source, threadId }),
|
|
314
|
+
count: library.count({ query, source, threadId, snapshot }),
|
|
315
|
+
snapshot,
|
|
313
316
|
limit,
|
|
314
317
|
offset,
|
|
315
318
|
});
|
|
@@ -323,18 +326,25 @@ export async function createHttpServer({
|
|
|
323
326
|
|
|
324
327
|
if (request.method === "GET" && url.pathname === "/api/image-groups") {
|
|
325
328
|
const { limit, offset } = paginationFor(url);
|
|
329
|
+
const snapshot = parseIntegerQuery(url.searchParams, "snapshot", { defaultValue: library.snapshot(), min: 0, max: Number.MAX_SAFE_INTEGER });
|
|
326
330
|
const source = url.searchParams.get("source") ?? "all";
|
|
327
|
-
const groups = library.groups({ source, limit, offset }).map(publicGroup);
|
|
331
|
+
const groups = library.groups({ source, limit, offset, snapshot }).map(publicGroup);
|
|
328
332
|
json(response, 200, {
|
|
329
333
|
groups,
|
|
330
|
-
count: library.countGroups({ source }),
|
|
331
|
-
imageCount: library.count({ source }),
|
|
334
|
+
count: library.countGroups({ source, snapshot }),
|
|
335
|
+
imageCount: library.count({ source, snapshot }),
|
|
336
|
+
snapshot,
|
|
332
337
|
limit,
|
|
333
338
|
offset,
|
|
334
339
|
});
|
|
335
340
|
return;
|
|
336
341
|
}
|
|
337
342
|
|
|
343
|
+
if (request.method === "GET" && url.pathname === "/api/cloud-image-cache") {
|
|
344
|
+
json(response, 200, { items: library.cloudCache() });
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
338
348
|
if (request.method === "POST" && url.pathname === "/api/rescan") {
|
|
339
349
|
if (library.rescanStatus().inProgress) {
|
|
340
350
|
json(response, 202, { imported: 0, total: library.count(), inProgress: true });
|
|
@@ -28,10 +28,15 @@ function imageIdFor(sourcePath) {
|
|
|
28
28
|
return createHash("sha256").update(sourcePath).digest("hex").slice(0, 24);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
function buildWhere({ query = "", source = "all", threadId = null } = {}) {
|
|
31
|
+
function buildWhere({ query = "", source = "all", threadId = null, snapshot = null } = {}) {
|
|
32
32
|
const clauses = [];
|
|
33
33
|
const parameters = [];
|
|
34
34
|
|
|
35
|
+
if (snapshot !== null) {
|
|
36
|
+
clauses.push("images.rowid <= ?");
|
|
37
|
+
parameters.push(snapshot);
|
|
38
|
+
}
|
|
39
|
+
|
|
35
40
|
if (threadId) {
|
|
36
41
|
clauses.push("images.thread_id = ?");
|
|
37
42
|
parameters.push(threadId);
|
|
@@ -153,6 +158,7 @@ function rowToImage(row) {
|
|
|
153
158
|
imageGenerationItemId: row.image_generation_item_id,
|
|
154
159
|
sourcePath: row.source_path,
|
|
155
160
|
prompt: row.prompt,
|
|
161
|
+
promptKind: row.prompt_kind ?? "generation",
|
|
156
162
|
width: row.width,
|
|
157
163
|
height: row.height,
|
|
158
164
|
byteLength: row.byte_length,
|
|
@@ -235,6 +241,10 @@ export class ImageLibrary {
|
|
|
235
241
|
updated_at TEXT NOT NULL
|
|
236
242
|
);
|
|
237
243
|
`);
|
|
244
|
+
const imageColumns = new Set(this.database.prepare("PRAGMA table_info(images)").all().map(column => column.name));
|
|
245
|
+
for (const name of ["prompt_kind", "cloud_library_id", "cloud_version"]) {
|
|
246
|
+
if (!imageColumns.has(name)) this.database.exec(`ALTER TABLE images ADD COLUMN ${name} TEXT`);
|
|
247
|
+
}
|
|
238
248
|
const threadSourceColumns = new Set(
|
|
239
249
|
this.database.prepare("PRAGMA table_info(thread_sources)").all().map((column) => column.name),
|
|
240
250
|
);
|
|
@@ -299,6 +309,10 @@ export class ImageLibrary {
|
|
|
299
309
|
});
|
|
300
310
|
}
|
|
301
311
|
|
|
312
|
+
snapshot() {
|
|
313
|
+
return this.database.prepare("SELECT COALESCE(MAX(rowid), 0) AS value FROM images").get().value;
|
|
314
|
+
}
|
|
315
|
+
|
|
302
316
|
count(options = {}) {
|
|
303
317
|
const { sql, parameters } = buildWhere(options);
|
|
304
318
|
return this.database.prepare(`
|
|
@@ -309,8 +323,8 @@ export class ImageLibrary {
|
|
|
309
323
|
`).get(...parameters).count;
|
|
310
324
|
}
|
|
311
325
|
|
|
312
|
-
list({ query = "", source = "all", threadId = null, sort = "desc", limit = null, offset = 0 } = {}) {
|
|
313
|
-
const { sql, parameters } = buildWhere({ query, source, threadId });
|
|
326
|
+
list({ query = "", source = "all", threadId = null, sort = "desc", limit = null, offset = 0, snapshot = null } = {}) {
|
|
327
|
+
const { sql, parameters } = buildWhere({ query, source, threadId, snapshot });
|
|
314
328
|
const pagination = Number.isInteger(limit) ? "LIMIT ? OFFSET ?" : "";
|
|
315
329
|
if (pagination) parameters.push(limit, offset);
|
|
316
330
|
const direction = sort === "asc" ? "ASC" : "DESC";
|
|
@@ -358,8 +372,8 @@ export class ImageLibrary {
|
|
|
358
372
|
return { total: this.count(), chats, works, projects };
|
|
359
373
|
}
|
|
360
374
|
|
|
361
|
-
countGroups({ source = "all" } = {}) {
|
|
362
|
-
const { sql, parameters } = buildWhere({ source });
|
|
375
|
+
countGroups({ source = "all", snapshot = null } = {}) {
|
|
376
|
+
const { sql, parameters } = buildWhere({ source, snapshot });
|
|
363
377
|
return this.database.prepare(`
|
|
364
378
|
SELECT COUNT(DISTINCT images.thread_id) AS count
|
|
365
379
|
FROM images
|
|
@@ -368,8 +382,8 @@ export class ImageLibrary {
|
|
|
368
382
|
`).get(...parameters).count;
|
|
369
383
|
}
|
|
370
384
|
|
|
371
|
-
groups({ source = "all", limit = null, offset = 0, previewLimit = 4 } = {}) {
|
|
372
|
-
const { sql, parameters } = buildWhere({ source });
|
|
385
|
+
groups({ source = "all", limit = null, offset = 0, previewLimit = 4, snapshot = null } = {}) {
|
|
386
|
+
const { sql, parameters } = buildWhere({ source, snapshot });
|
|
373
387
|
const pagination = Number.isInteger(limit) ? "LIMIT ? OFFSET ?" : "";
|
|
374
388
|
if (pagination) parameters.push(limit, offset);
|
|
375
389
|
const rows = this.database.prepare(`
|
|
@@ -400,10 +414,11 @@ export class ImageLibrary {
|
|
|
400
414
|
FROM images
|
|
401
415
|
LEFT JOIN thread_sources ON thread_sources.thread_id = images.thread_id
|
|
402
416
|
WHERE images.thread_id IN (${placeholders})
|
|
417
|
+
${snapshot === null ? "" : "AND images.rowid <= ?"}
|
|
403
418
|
)
|
|
404
419
|
WHERE preview_rank <= ?
|
|
405
420
|
ORDER BY thread_id ASC, created_at ASC, id ASC
|
|
406
|
-
`).all(...threadIds, previewLimit);
|
|
421
|
+
`).all(...threadIds, ...(snapshot === null ? [] : [snapshot]), previewLimit);
|
|
407
422
|
for (const previewRow of previewRows) {
|
|
408
423
|
const previews = previewsByThread.get(previewRow.thread_id) ?? [];
|
|
409
424
|
previews.push(rowToImage(previewRow));
|
|
@@ -621,6 +636,10 @@ export class ImageLibrary {
|
|
|
621
636
|
return true;
|
|
622
637
|
}
|
|
623
638
|
|
|
639
|
+
cloudCache() {
|
|
640
|
+
return this.database.prepare("SELECT cloud_library_id AS libraryId, cloud_version AS version FROM images WHERE cloud_library_id IS NOT NULL").all();
|
|
641
|
+
}
|
|
642
|
+
|
|
624
643
|
async importExternalImage({
|
|
625
644
|
bytes,
|
|
626
645
|
mimeType,
|
|
@@ -630,6 +649,9 @@ export class ImageLibrary {
|
|
|
630
649
|
sourceKind,
|
|
631
650
|
prompt = null,
|
|
632
651
|
createdAt = null,
|
|
652
|
+
promptKind = "generation",
|
|
653
|
+
cloudLibraryId = null,
|
|
654
|
+
cloudVersion = null,
|
|
633
655
|
} = {}) {
|
|
634
656
|
if (!this.isLifecycleActive() || !Buffer.isBuffer(bytes)) return { imported: false, reason: "INVALID_BYTES" };
|
|
635
657
|
const normalizedMime = normalizedImportMime(mimeType);
|
|
@@ -660,10 +682,15 @@ export class ImageLibrary {
|
|
|
660
682
|
const assetDir = path.join(this.dataDir, "external-images");
|
|
661
683
|
const sourcePath = path.join(assetDir, `${id}.${extension}`);
|
|
662
684
|
const existing = this.database.prepare("SELECT source_path, byte_length FROM images WHERE id = ?").get(id);
|
|
685
|
+
const updateCloudMetadata = () => {
|
|
686
|
+
if (typeof cloudLibraryId !== "string" || !/^libfile_[\w-]+$/.test(cloudLibraryId)) return;
|
|
687
|
+
this.database.prepare("UPDATE images SET cloud_library_id = ?, cloud_version = ?, prompt = ?, prompt_kind = ? WHERE id = ?")
|
|
688
|
+
.run(cloudLibraryId, String(cloudVersion ?? "").slice(0, 256), safePrompt, promptKind === "request" ? "request" : "generation", id);
|
|
689
|
+
};
|
|
663
690
|
if (existing && existing.byte_length === bytes.length) {
|
|
664
691
|
try {
|
|
665
692
|
const existingBytes = await readFile(existing.source_path);
|
|
666
|
-
if (existingBytes.equals(bytes)) return { imported: false, id, reason: "DUPLICATE" };
|
|
693
|
+
if (existingBytes.equals(bytes)) { updateCloudMetadata(); return { imported: false, id, reason: "DUPLICATE" }; }
|
|
667
694
|
} catch (error) {
|
|
668
695
|
if (error.code !== "ENOENT") throw error;
|
|
669
696
|
}
|
|
@@ -712,6 +739,7 @@ export class ImageLibrary {
|
|
|
712
739
|
normalizedCreatedAt,
|
|
713
740
|
importedAt,
|
|
714
741
|
);
|
|
742
|
+
updateCloudMetadata();
|
|
715
743
|
this.database
|
|
716
744
|
.prepare(`
|
|
717
745
|
INSERT INTO thread_sources (
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// 仅增强原生 Library 图片详情;不创建第二个图库或侧栏入口。
|
|
2
|
+
const PANEL_ID = "minecodex-library-prompt";
|
|
3
|
+
const zh = () => (document.documentElement.lang || navigator.language || "en").toLowerCase().startsWith("zh");
|
|
4
|
+
const label = (en, cn) => zh() ? cn : en;
|
|
5
|
+
let mounted = null;
|
|
6
|
+
let apiPromise = null;
|
|
7
|
+
let scheduled = false;
|
|
8
|
+
let candidate = null;
|
|
9
|
+
|
|
10
|
+
function libraryImage(header) {
|
|
11
|
+
const key = Object.keys(header).find(key => key.startsWith("__reactFiber$"));
|
|
12
|
+
let fiber = key ? header[key] : null;
|
|
13
|
+
let selected = null;
|
|
14
|
+
for (let depth = 0; fiber && depth < 80; depth++, fiber = fiber.return) {
|
|
15
|
+
const file = fiber.memoizedProps?.file;
|
|
16
|
+
if (!file || !/^libfile_[\w-]+$/.test(file.id ?? "") || !file.mime_type?.startsWith("image/")) continue;
|
|
17
|
+
if (selected && selected.id !== file.id) break;
|
|
18
|
+
selected = file;
|
|
19
|
+
if (file.origination_thread_id && file.origination_message_id) break;
|
|
20
|
+
}
|
|
21
|
+
if (!selected) return null;
|
|
22
|
+
// 只保留关联字段,签名下载地址和图片数据不进入插件。
|
|
23
|
+
return Object.fromEntries(["id", "file_id", "current_version_number", "origination_thread_id", "origination_message_id"].map(key => [key, selected[key]]));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function nativeApi() {
|
|
27
|
+
if (!apiPromise) apiPromise = (async () => {
|
|
28
|
+
const initialUrl = [...document.querySelectorAll('link[rel="modulepreload"],script[src]')]
|
|
29
|
+
.map(element => element.href || element.src).find(url => /\/app-initial-[^/]+\.js/.test(url));
|
|
30
|
+
if (!initialUrl) throw new Error("Library API unavailable");
|
|
31
|
+
const initialSource = await (await fetch(initialUrl, { signal })).text();
|
|
32
|
+
const metadataAsset = initialSource.match(/library-file-metadata-[\w-]+\.js/)?.[0];
|
|
33
|
+
if (!metadataAsset) throw new Error("Library metadata unavailable");
|
|
34
|
+
const metadataUrl = new URL(metadataAsset, initialUrl).href;
|
|
35
|
+
const source = await (await fetch(metadataUrl, { signal })).text();
|
|
36
|
+
const apiLocal = source.match(/([\w$]+)\.safePost\(`\/files\/library`/)?.[1];
|
|
37
|
+
const imports = [...source.matchAll(/import\{([^}]+)\}from["']([^"']+)["']/g)]
|
|
38
|
+
.find(match => match[1].split(",").some(binding => binding.trim().split(/\s+as\s+/).at(-1) === apiLocal));
|
|
39
|
+
const apiExport = imports?.[1].split(",").map(s => s.trim().split(/\s+as\s+/)).find(parts => parts.at(-1) === apiLocal)?.[0];
|
|
40
|
+
const initLocal = source.match(/function ([\w$]+)\(\)\{return\(\1=/)?.[1];
|
|
41
|
+
const exports = source.match(/export\{([^}]+)\}/)?.[1].split(",").map(s => s.trim().split(/\s+as\s+/)) ?? [];
|
|
42
|
+
const initExport = exports.find(parts => parts[0] === initLocal)?.at(-1);
|
|
43
|
+
const metadata = await import(metadataUrl);
|
|
44
|
+
if (typeof metadata[initExport] !== "function" || !apiExport) throw new Error("Library API contract changed");
|
|
45
|
+
metadata[initExport]();
|
|
46
|
+
const api = (await import(new URL(imports[2], metadataUrl).href))[apiExport];
|
|
47
|
+
if (typeof api?.safeGet !== "function") throw new Error("Library API unavailable");
|
|
48
|
+
return api;
|
|
49
|
+
})().catch(error => { apiPromise = null; throw error; });
|
|
50
|
+
return apiPromise;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function promptFromConversation(file, conversation) {
|
|
54
|
+
const node = conversation.mapping?.[file.origination_message_id];
|
|
55
|
+
const part = node?.message?.content?.parts?.find(part => part && typeof part === "object"
|
|
56
|
+
&& part.content_type === "image_asset_pointer"
|
|
57
|
+
&& [file.file_id, `sediment://${file.file_id}`].includes(part.asset_pointer));
|
|
58
|
+
if (!part || !["assistant", "tool"].includes(node.message.author?.role)
|
|
59
|
+
|| !(part.metadata?.generation?.gen_id || part.metadata?.dalle?.gen_id)) return null;
|
|
60
|
+
const prompt = part.metadata?.dalle?.prompt;
|
|
61
|
+
if (typeof prompt === "string" && prompt.trim()) return { text: prompt, kind: "generation" };
|
|
62
|
+
let ancestor = conversation.mapping[node.parent];
|
|
63
|
+
const visited = new Set();
|
|
64
|
+
while (ancestor && !visited.has(ancestor.id)) {
|
|
65
|
+
visited.add(ancestor.id);
|
|
66
|
+
if (ancestor.message?.author?.role === "user") {
|
|
67
|
+
const text = ancestor.message.content?.parts?.filter(part => typeof part === "string").join("\n");
|
|
68
|
+
return text?.trim() ? { text, kind: "request" } : null;
|
|
69
|
+
}
|
|
70
|
+
ancestor = conversation.mapping[ancestor.parent];
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function element(tag, className, text) {
|
|
76
|
+
const node = document.createElement(tag);
|
|
77
|
+
node.className = className;
|
|
78
|
+
if (text != null) node.textContent = text;
|
|
79
|
+
return node;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 复用旧 Images 的 Lucide copy/check 图形,统一为 16px 图标按钮。
|
|
83
|
+
function copyGlyph(copied = false) {
|
|
84
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
85
|
+
for (const [key, value] of Object.entries({ viewBox: "0 0 24 24", width: "16", height: "16", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true" })) svg.setAttribute(key, value);
|
|
86
|
+
svg.innerHTML = copied
|
|
87
|
+
? '<path d="m20 6-11 11-5-5"></path>'
|
|
88
|
+
: '<rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path>';
|
|
89
|
+
return svg;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function closePanel(record, restoreFocus = false) {
|
|
93
|
+
record.request?.abort();
|
|
94
|
+
record.request = null;
|
|
95
|
+
record.panel.hidden = true;
|
|
96
|
+
record.button.setAttribute("aria-expanded", "false");
|
|
97
|
+
if (restoreFocus && record.button.isConnected) record.button.focus();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function showPrompt(record) {
|
|
101
|
+
record.panel.hidden = false;
|
|
102
|
+
record.button.setAttribute("aria-expanded", "true");
|
|
103
|
+
record.close.focus();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function resolvePrompt(file, request) {
|
|
107
|
+
if (!file.origination_thread_id || !file.origination_message_id) return null;
|
|
108
|
+
const api = await nativeApi();
|
|
109
|
+
request.signal.throwIfAborted();
|
|
110
|
+
const conversation = await api.safeGet("/conversation/{conversation_id}", {
|
|
111
|
+
parameters: { path: { conversation_id: file.origination_thread_id } },
|
|
112
|
+
signal: AbortSignal.any([signal, request.signal, AbortSignal.timeout(25_000)]),
|
|
113
|
+
});
|
|
114
|
+
return promptFromConversation(file, conversation);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function unmount() {
|
|
118
|
+
if (!mounted) return;
|
|
119
|
+
closePanel(mounted);
|
|
120
|
+
clearTimeout(mounted.copyTimer);
|
|
121
|
+
mounted.button.remove();
|
|
122
|
+
mounted.panel.remove();
|
|
123
|
+
mounted = null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function scan() {
|
|
127
|
+
scheduled = false;
|
|
128
|
+
if (signal.aborted) return;
|
|
129
|
+
const header = [...document.querySelectorAll('[data-testid="viewer-header"]')].find(header => header.getBoundingClientRect().width > 0 && libraryImage(header));
|
|
130
|
+
const file = header && libraryImage(header);
|
|
131
|
+
const identity = file && JSON.stringify(file);
|
|
132
|
+
if (candidate?.header === header && candidate?.identity === identity
|
|
133
|
+
&& (!mounted || (mounted.button.isConnected && mounted.panel.isConnected))) return;
|
|
134
|
+
candidate?.request.abort();
|
|
135
|
+
unmount();
|
|
136
|
+
candidate = null;
|
|
137
|
+
if (!file) return;
|
|
138
|
+
const next = { header, identity, request: new AbortController() };
|
|
139
|
+
candidate = next;
|
|
140
|
+
let prompt;
|
|
141
|
+
try { prompt = await resolvePrompt(file, next.request); }
|
|
142
|
+
catch { return; }
|
|
143
|
+
// 异步结果只能写回当前图片;没有提示词时不展示入口。
|
|
144
|
+
if (!prompt || signal.aborted || candidate !== next || !header.isConnected) return;
|
|
145
|
+
// 缩放项在图片详情中是百分比菜单;沿用它的胶囊类,不固定构建 hash。
|
|
146
|
+
const zoom = [...header.querySelectorAll('button[aria-haspopup="menu"]')].find(button => /^\d+(?:\.\d+)?%$/.test(button.textContent.trim()));
|
|
147
|
+
const group = zoom?.parentElement?.parentElement;
|
|
148
|
+
const host = header.closest('[role="presentation"]');
|
|
149
|
+
if (!zoom || !group || !host) return;
|
|
150
|
+
const capsuleClass = zoom.className.split(/\s+/).filter(name => !/^_(?:dropdown|zoom)_/.test(name)).join(" ");
|
|
151
|
+
const button = element("button", capsuleClass, label("Prompt", "提示词"));
|
|
152
|
+
button.type = "button";
|
|
153
|
+
button.dataset.minecodexLibraryPrompt = "trigger";
|
|
154
|
+
button.setAttribute("aria-expanded", "false");
|
|
155
|
+
button.setAttribute("aria-controls", PANEL_ID);
|
|
156
|
+
button.style.width = "max-content";
|
|
157
|
+
button.style.minWidth = "0";
|
|
158
|
+
button.style.flex = "0 0 auto";
|
|
159
|
+
button.style.paddingInline = "calc(var(--spacing) * 3)";
|
|
160
|
+
group.prepend(button);
|
|
161
|
+
// 对齐原生 Library Version history 的面板、标题和内容区 token。
|
|
162
|
+
const panel = element("aside", "absolute inset-x-2 top-toolbar bottom-2 z-30 ms-auto mt-2 flex max-w-75 flex-col overflow-hidden rounded-2xl border border-default bg-surface shadow-lg select-none");
|
|
163
|
+
panel.id = PANEL_ID;
|
|
164
|
+
panel.setAttribute("aria-label", label("Image prompt", "图片提示词"));
|
|
165
|
+
panel.hidden = true;
|
|
166
|
+
panel.style.setProperty("-webkit-app-region", "no-drag");
|
|
167
|
+
panel.style.paddingBottom = "calc(var(--spacing) * 3)";
|
|
168
|
+
const heading = element("div", "flex items-center justify-between py-2 ps-4 pe-2");
|
|
169
|
+
const close = element("button", "flex size-8 shrink-0 items-center justify-center rounded-full text-default hover:bg-primary-ghost-hover focus-visible:outline-ring");
|
|
170
|
+
close.type = "button";
|
|
171
|
+
close.setAttribute("aria-label", label("Close prompt", "关闭提示词"));
|
|
172
|
+
// 直接复用原生关闭图标,避免手绘近似图形。
|
|
173
|
+
const nativeClose = [...header.querySelectorAll("button")].at(-1)?.querySelector("svg");
|
|
174
|
+
if (nativeClose) close.append(nativeClose.cloneNode(true));
|
|
175
|
+
else close.textContent = "×";
|
|
176
|
+
const actions = element("div", "flex items-center gap-1");
|
|
177
|
+
const copy = element("button", close.className);
|
|
178
|
+
copy.type = "button";
|
|
179
|
+
function copyFeedback(copied = false, failed = false) {
|
|
180
|
+
const text = failed ? label("Try copying again", "重试复制") : copied ? label("Copied", "已复制") : label("Copy prompt", "复制提示词");
|
|
181
|
+
copy.title = text;
|
|
182
|
+
copy.setAttribute("aria-label", text);
|
|
183
|
+
copy.replaceChildren(copyGlyph(copied));
|
|
184
|
+
}
|
|
185
|
+
copyFeedback();
|
|
186
|
+
actions.append(copy, close);
|
|
187
|
+
heading.append(element("span", "text-base font-semibold text-default", label("Prompt", "提示词")), actions);
|
|
188
|
+
const body = element("div", "min-h-0 flex-1 overflow-y-auto px-3");
|
|
189
|
+
body.style.paddingTop = "var(--spacing)";
|
|
190
|
+
body.append(
|
|
191
|
+
element("p", "px-1 text-sm leading-5 text-default whitespace-pre-wrap break-words select-text", prompt.text),
|
|
192
|
+
);
|
|
193
|
+
panel.append(heading, body);
|
|
194
|
+
host.append(panel);
|
|
195
|
+
const record = { header, file, identity, button, panel, body, close, request: null,
|
|
196
|
+
refreshLocale() {
|
|
197
|
+
button.textContent = label("Prompt", "提示词");
|
|
198
|
+
panel.setAttribute("aria-label", label("Image prompt", "图片提示词"));
|
|
199
|
+
close.setAttribute("aria-label", label("Close prompt", "关闭提示词"));
|
|
200
|
+
heading.firstElementChild.textContent = label("Prompt", "提示词");
|
|
201
|
+
copyFeedback();
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
mounted = record;
|
|
205
|
+
copy.addEventListener("click", async () => {
|
|
206
|
+
try {
|
|
207
|
+
await navigator.clipboard.writeText(prompt.text);
|
|
208
|
+
if (mounted !== record) return;
|
|
209
|
+
copyFeedback(true);
|
|
210
|
+
} catch {
|
|
211
|
+
if (mounted !== record) return;
|
|
212
|
+
copyFeedback(false, true);
|
|
213
|
+
}
|
|
214
|
+
clearTimeout(record.copyTimer);
|
|
215
|
+
record.copyTimer = setTimeout(() => copyFeedback(), 2000);
|
|
216
|
+
});
|
|
217
|
+
button.addEventListener("click", () => panel.hidden ? showPrompt(record) : closePanel(record));
|
|
218
|
+
close.addEventListener("click", () => closePanel(record, true));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const localeObserver = new MutationObserver(() => mounted?.refreshLocale());
|
|
222
|
+
localeObserver.observe(document.documentElement, { attributes:true, attributeFilter:["lang"] });
|
|
223
|
+
const observer = new MutationObserver(() => {
|
|
224
|
+
if (scheduled) return;
|
|
225
|
+
scheduled = true;
|
|
226
|
+
queueMicrotask(scan);
|
|
227
|
+
});
|
|
228
|
+
observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ["src", "data-state", "inert"] });
|
|
229
|
+
function escape(event) {
|
|
230
|
+
if (event.key !== "Escape" || !mounted || mounted.panel.hidden) return;
|
|
231
|
+
event.preventDefault();
|
|
232
|
+
event.stopImmediatePropagation();
|
|
233
|
+
closePanel(mounted, true);
|
|
234
|
+
}
|
|
235
|
+
document.addEventListener("keydown", escape, true);
|
|
236
|
+
const cleanup = () => { candidate?.request.abort(); candidate = null; observer.disconnect(); localeObserver.disconnect(); document.removeEventListener("keydown", escape, true); unmount(); };
|
|
237
|
+
signal.addEventListener("abort", cleanup, { once: true });
|
|
238
|
+
scan();
|
|
239
|
+
return cleanup;
|