diff --git a/plugins/dagu-ocr/CHANGELOG.md b/plugins/dagu-ocr/CHANGELOG.md index a61fb84e2..21330837f 100644 --- a/plugins/dagu-ocr/CHANGELOG.md +++ b/plugins/dagu-ocr/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## 1.2.3 - 2026-11-12 + +### 新增 + +- 固定截图支持右键菜单:复制、删除、编辑、OCR 与翻译;编辑以当前固定图片重新进入标注器,OCR / 翻译会回到主页面处理。 + +### 修复 + +- 修复固定截图被普通窗口遮挡和拖动不跟随的问题:创建固定窗口后重新声明置顶,并禁用图片原生拖拽,让指针拖动始终移动窗口。 +- 修复固定截图右键 OCR / 翻译可能无响应的问题:固定期间不再退出插件主视图,并等待识别处理完成后再关闭固定子窗口,避免宿主回收插件进程或把主窗口保持在 0 高度。 + ## 1.2.2 - 2026-11-11 ### 修复 diff --git a/plugins/dagu-ocr/README.md b/plugins/dagu-ocr/README.md index e92e70bbf..5dcb8d67e 100644 --- a/plugins/dagu-ocr/README.md +++ b/plugins/dagu-ocr/README.md @@ -5,7 +5,7 @@ 常见用法举几个例子: - 想给别人圈重点 → 截图后直接在全屏画面上框选、画矩形/箭头、加文字,再复制成图片发出去。 -- 想把截图钉在屏幕上做参考 → 截图后点「固定」,图片会置顶显示,可拖动、滚轮缩放。 +- 想把截图钉在屏幕上做参考 → 截图后点「固定」,图片会置顶显示,可拖动、滚轮缩放,右键还能继续复制、编辑、OCR 或翻译。 - 随手截了张图想裁剪、打马赛克 → 用「编辑图片」就地处理。 - 截图里的字看不清,想变成可复制文本 → 截图后点「图片 OCR」。 - 遇到一段英文/日文,想看懂 → 选中图片点「翻译」,或直接在输入框里粘贴文字翻译。 @@ -20,6 +20,10 @@ ![图片编辑:同一套标注界面](./docs/screenshots/editor.png) +固定截图:右键可直接复制、删除、编辑、OCR 或翻译;「编辑」会以这张固定图重新进入标注器。 + +![固定截图右键菜单](./docs/screenshots/pin-context-menu.png) + 主工作区:左边是当前图片(可滚轮放大、拖动查看),右边是识别结果(可直接编辑),左右等宽;图片标题行可复制图片,下方可以复制结果、重新识别或翻译。 ![主工作区:图片识别与结果](./docs/screenshots/main-ocr.png) diff --git a/plugins/dagu-ocr/docs/screenshots/pin-context-menu.png b/plugins/dagu-ocr/docs/screenshots/pin-context-menu.png new file mode 100644 index 000000000..947ccebe7 Binary files /dev/null and b/plugins/dagu-ocr/docs/screenshots/pin-context-menu.png differ diff --git a/plugins/dagu-ocr/pin.html b/plugins/dagu-ocr/pin.html index 49e63a34d..7d43e354b 100644 --- a/plugins/dagu-ocr/pin.html +++ b/plugins/dagu-ocr/pin.html @@ -12,7 +12,10 @@ cursor: move; user-select: none; } - #pin-image { width: 100%; height: 100%; display: block; object-fit: fill; } + #pin-image { + width: 100%; height: 100%; display: block; object-fit: fill; + touch-action: none; -webkit-user-drag: none; + } #pin-close { position: fixed; top: 6px; right: 6px; width: 26px; height: 26px; @@ -32,12 +35,35 @@ opacity: 0; transition: opacity .18s ease; pointer-events: none; } #pin-hint.visible { opacity: 1; } + #pin-menu { + position: fixed; z-index: 20; min-width: 108px; padding: 4px; + max-height: calc(100% - 8px); overflow-y: auto; + display: flex; flex-direction: column; gap: 2px; + border: 1px solid rgba(148, 163, 184, .32); border-radius: 10px; + background: rgba(17, 20, 24, .94); box-shadow: 0 12px 30px rgba(15, 23, 42, .32); + backdrop-filter: blur(8px); + } + #pin-menu[hidden] { display: none; } + #pin-menu button { + display: flex; align-items: center; min-height: 26px; padding: 0 10px; + border: none; border-radius: 7px; background: transparent; color: #e5e7eb; + font: 400 12px/1 Inter, "Segoe UI", "Microsoft YaHei", sans-serif; + cursor: pointer; text-align: left; + } + #pin-menu button:hover, #pin-menu button:focus-visible { background: #0ea5e9; color: #fff; outline: none; } 固定截图
滚轮缩放 · 拖动移动 · 双击或 Esc 关闭
+ diff --git a/plugins/dagu-ocr/plugin.json b/plugins/dagu-ocr/plugin.json index 623e3b9b9..4ae7cead0 100644 --- a/plugins/dagu-ocr/plugin.json +++ b/plugins/dagu-ocr/plugin.json @@ -1,7 +1,7 @@ { "name": "dagu-ocr", "title": "大古截图", - "version": "1.2.2", + "version": "1.2.3", "description": "截图、OCR 文字识别与图片翻译,从图片中提取并翻译文字", "author": "beierweiwei", "logo": "icon.png", diff --git a/plugins/dagu-ocr/src/capture/pin.js b/plugins/dagu-ocr/src/capture/pin.js index 8c6245f28..ff7a4c6c1 100644 --- a/plugins/dagu-ocr/src/capture/pin.js +++ b/plugins/dagu-ocr/src/capture/pin.js @@ -1,6 +1,7 @@ // 固定到屏幕的图片窗口:拖动移动、滚轮缩放、双击/Esc 关闭。 import { readEditorImage } from '../editor-image-handoff.js'; +import { copyImageDataUrl } from '../clipboard-image.js'; const win = globalThis.window; const params = new URLSearchParams(win.location.search); @@ -10,10 +11,14 @@ const pinKey = params.get('pinKey') || ''; const image = document.getElementById('pin-image'); const closeButton = document.getElementById('pin-close'); const hint = document.getElementById('pin-hint'); +const menu = document.getElementById('pin-menu'); let dragging = false; let dragOffset = { x: 0, y: 0 }; let hintTimer = null; +let imageDataUrl = ''; + +image.draggable = false; function send(event, payload = {}) { const host = win.ztools || win.utools; @@ -46,11 +51,47 @@ function showHint(message) { hintTimer = setTimeout(() => hint.classList.remove('visible'), 1800); } +function hideMenu() { + menu.hidden = true; +} + +function showMenu(x, y) { + if (!imageDataUrl) return; + menu.hidden = false; + const rect = menu.getBoundingClientRect(); + const left = Math.max(4, Math.min(x, win.innerWidth - rect.width - 4)); + const top = Math.max(4, Math.min(y, win.innerHeight - rect.height - 4)); + menu.style.left = `${left}px`; + menu.style.top = `${top}px`; + menu.querySelector('button')?.focus?.(); +} + +async function handleMenuAction(action) { + hideMenu(); + if (action === 'copy') { + showHint(await copyImageDataUrl(win, imageDataUrl) ? '已复制到剪贴板' : '复制失败'); + return; + } + if (action === 'delete') { + closeSelf(); + return; + } + if (action === 'edit' || action === 'ocr' || action === 'translate') { + send('pinAction', { action }); + } +} + image.addEventListener('pointerdown', (event) => { if (event.button !== 0) return; + // Chromium 里 img 默认可原生拖拽;不阻止默认行为会抢占 pointermove,表现为拖图片而不是拖窗口。 + event.preventDefault(); dragging = true; dragOffset = { x: event.screenX - win.screenX, y: event.screenY - win.screenY }; - image.setPointerCapture?.(event.pointerId); + try { + image.setPointerCapture?.(event.pointerId); + } catch { + // 合成指针没有活动指针时允许测试/辅助技术触发同样逻辑。 + } }); win.addEventListener('pointermove', (event) => { @@ -65,7 +106,28 @@ win.addEventListener('pointerup', () => { dragging = false; }); +image.addEventListener('contextmenu', (event) => { + event.preventDefault(); + showMenu(event.clientX, event.clientY); +}); + +win.addEventListener('contextmenu', (event) => { + if (event.target === image) return; + event.preventDefault(); + hideMenu(); +}); + +document.addEventListener('pointerdown', (event) => { + if (!menu.hidden && !menu.contains(event.target)) hideMenu(); +}, true); + +menu.addEventListener('click', (event) => { + const action = event.target instanceof Element ? event.target.dataset.action : ''; + if (action) void handleMenuAction(action); +}); + win.addEventListener('wheel', (event) => { + if (!menu.hidden) return; event.preventDefault(); send('pinZoom', { factor: event.deltaY < 0 ? 1.1 : 1 / 1.1, @@ -77,6 +139,10 @@ win.addEventListener('wheel', (event) => { win.addEventListener('dblclick', () => closeSelf()); win.addEventListener('keydown', (event) => { + if (event.key === 'Escape' && !menu.hidden) { + hideMenu(); + return; + } if (event.key === 'Escape') closeSelf(); }); @@ -87,6 +153,7 @@ readEditorImage(pinKey, false).then((dataUrl) => { closeSelf(); return; } + imageDataUrl = dataUrl; image.src = dataUrl; showHint('滚轮缩放 · 拖动移动 · 双击或 Esc 关闭'); }); diff --git a/plugins/dagu-ocr/src/main.js b/plugins/dagu-ocr/src/main.js index 611147445..055cdd8c0 100644 --- a/plugins/dagu-ocr/src/main.js +++ b/plugins/dagu-ocr/src/main.js @@ -24,6 +24,7 @@ let controller; // ── 覆盖层窗口:截图与图片编辑共用同一套窗口生命周期 ── let activeOverlay = null; const pinWindows = new Map(); +let pinMainWindowRestored = false; function editorWindowSize(imageUrl) { const clamp = (width, height) => ({ @@ -213,7 +214,8 @@ function openPinWindow({ imageUrl, bounds }) { fullscreenable: false, skipTaskbar: true, hasShadow: false, - alwaysOnTop: true, + // Windows/Electron 从构造参数进入置顶态可能不落 native z-order,先显式关闭再打开。 + alwaysOnTop: false, title: '固定截图', webPreferences: { preload: 'preload.js', @@ -224,9 +226,18 @@ function openPinWindow({ imageUrl, bounds }) { console.warn('创建固定窗口失败:', error); } if (!child) return false; + try { + // ZTools 插件子窗口会弱化创建参数里的置顶状态,创建后需重新声明。 + child.setAlwaysOnTop?.(false); + child.setAlwaysOnTop?.(true); + child.moveTop?.(); + } catch (error) { + console.warn('固定窗口置顶失败:', error); + } pinWindows.set(pinId, { child, + imageUrl, base: { width, height }, zoom: 1, bounds: { ...position, width, height } @@ -243,6 +254,17 @@ function applyPinBounds(entry, nextBounds) { } } +function closePinWindow(pinId) { + const entry = pinWindows.get(pinId); + if (!entry) return; + try { + entry.child?.close?.(); + } catch { + // 窗口已关闭时忽略。 + } + pinWindows.delete(pinId); +} + function handlePinMessage(message) { const entry = pinWindows.get(message.pinId); if (!entry) return; @@ -273,17 +295,39 @@ function handlePinMessage(message) { return; } if (message.event === 'pinClose') { - try { - entry.child?.close?.(); - } catch { - // 窗口已关闭时忽略。 + closePinWindow(message.pinId); + if (pinWindows.size === 0 && !pinMainWindowRestored) controller.exitPlugin(); + return; + } + if (message.event === 'pinAction') { + const action = message.action; + if (action === 'edit') { + void openEditorWindow(entry.imageUrl).finally(() => closePinWindow(message.pinId)); + return; + } + if (action === 'ocr' || action === 'translate') { + // OCR / 翻译会异步调用 Provider;固定子窗口必须等主页面处理完成后再关闭, + // 否则最后一个子窗口销毁后宿主可能回收插件进程,让识别请求静默中断。 + void (async () => { + const restore = win?.ztools?.showMainWindow || win?.utools?.showMainWindow; + if (typeof restore === 'function') { + try { + await restore(); + pinMainWindowRestored = true; + } catch (error) { + console.warn('恢复主窗口失败:', error); + } + } + await controller.handleEditedImage(entry.imageUrl, action); + })().finally(() => closePinWindow(message.pinId)); + return; } - pinWindows.delete(message.pinId); } } // ── 截图:沉浸式覆盖层直接在全屏冻结画面上框选、标注 ── function startScreenshotFlow() { + pinMainWindowRestored = false; hideMainWindow(); controller.showStatus('正在准备截图...'); closeOverlayWindow(); @@ -418,7 +462,7 @@ function handleEditorMessage(message) { if (message.event === 'closed') { closeOverlayWindow(); if (message.returnInput || message.returnToInput) showMainWindow(); - else controller.exitPlugin(); + else if (pinWindows.size === 0) controller.exitPlugin(); return true; } return false;