Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions plugins/dagu-ocr/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

## 1.2.3 - 2026-11-12

### 新增

- 固定截图支持右键菜单:复制、删除、编辑、OCR 与翻译;编辑以当前固定图片重新进入标注器,OCR / 翻译会回到主页面处理。

### 修复

- 修复固定截图被普通窗口遮挡和拖动不跟随的问题:创建固定窗口后重新声明置顶,并禁用图片原生拖拽,让指针拖动始终移动窗口。
- 修复固定截图右键 OCR / 翻译可能无响应的问题:固定期间不再退出插件主视图,并等待识别处理完成后再关闭固定子窗口,避免宿主回收插件进程或把主窗口保持在 0 高度。

## 1.2.2 - 2026-11-11

### 修复
Expand Down
6 changes: 5 additions & 1 deletion plugins/dagu-ocr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
常见用法举几个例子:

- 想给别人圈重点 → 截图后直接在全屏画面上框选、画矩形/箭头、加文字,再复制成图片发出去。
- 想把截图钉在屏幕上做参考 → 截图后点「固定」,图片会置顶显示,可拖动、滚轮缩放。
- 想把截图钉在屏幕上做参考 → 截图后点「固定」,图片会置顶显示,可拖动、滚轮缩放,右键还能继续复制、编辑、OCR 或翻译。
- 随手截了张图想裁剪、打马赛克 → 用「编辑图片」就地处理。
- 截图里的字看不清,想变成可复制文本 → 截图后点「图片 OCR」。
- 遇到一段英文/日文,想看懂 → 选中图片点「翻译」,或直接在输入框里粘贴文字翻译。
Expand All @@ -20,6 +20,10 @@

![图片编辑:同一套标注界面](./docs/screenshots/editor.png)

固定截图:右键可直接复制、删除、编辑、OCR 或翻译;「编辑」会以这张固定图重新进入标注器。

![固定截图右键菜单](./docs/screenshots/pin-context-menu.png)

主工作区:左边是当前图片(可滚轮放大、拖动查看),右边是识别结果(可直接编辑),左右等宽;图片标题行可复制图片,下方可以复制结果、重新识别或翻译。

![主工作区:图片识别与结果](./docs/screenshots/main-ocr.png)
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 27 additions & 1 deletion plugins/dagu-ocr/pin.html
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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; }
</style>
</head>
<body>
<img id="pin-image" alt="固定截图">
<button id="pin-close" title="关闭 (Esc)">✕</button>
<div id="pin-hint">滚轮缩放 · 拖动移动 · 双击或 Esc 关闭</div>
<div id="pin-menu" role="menu" aria-label="固定截图操作" hidden>
<button type="button" role="menuitem" data-action="copy">复制</button>
<button type="button" role="menuitem" data-action="delete">删除</button>
<button type="button" role="menuitem" data-action="edit">编辑</button>
<button type="button" role="menuitem" data-action="ocr">OCR</button>
<button type="button" role="menuitem" data-action="translate">翻译</button>
</div>
<script type="module" src="/src/capture/pin.js"></script>
</body>
</html>
2 changes: 1 addition & 1 deletion plugins/dagu-ocr/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "dagu-ocr",
"title": "大古截图",
"version": "1.2.2",
"version": "1.2.3",
"description": "截图、OCR 文字识别与图片翻译,从图片中提取并翻译文字",
"author": "beierweiwei",
"logo": "icon.png",
Expand Down
69 changes: 68 additions & 1 deletion plugins/dagu-ocr/src/capture/pin.js
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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;
Expand Down Expand Up @@ -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) => {
Expand All @@ -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,
Expand All @@ -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();
});

Expand All @@ -87,6 +153,7 @@ readEditorImage(pinKey, false).then((dataUrl) => {
closeSelf();
return;
}
imageDataUrl = dataUrl;
image.src = dataUrl;
showHint('滚轮缩放 · 拖动移动 · 双击或 Esc 关闭');
});
58 changes: 51 additions & 7 deletions plugins/dagu-ocr/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ let controller;
// ── 覆盖层窗口:截图与图片编辑共用同一套窗口生命周期 ──
let activeOverlay = null;
const pinWindows = new Map();
let pinMainWindowRestored = false;

function editorWindowSize(imageUrl) {
const clamp = (width, height) => ({
Expand Down Expand Up @@ -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',
Expand All @@ -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 }
Expand All @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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;
Expand Down
Loading