From 18a6925d131aa21c4ebd59cc30fe69c8e0dfd892 Mon Sep 17 00:00:00 2001 From: DeskTiles Dev Date: Tue, 9 Jun 2026 00:29:48 +0800 Subject: [PATCH 1/3] feat: refine reader AI selection trigger --- .../2026-06-double-click-near-selection.md | 15 ++++++ .../2026-06-selection-hit-area-spec.md | 15 ++++++ docs/archive/reader-selection-ai/README.md | 35 ++++++++++++++ docs/reader-selection-ai-minimal-plan.md | 47 +++++++++++++++++++ .../app/src/components/layout/AppLayout.tsx | 9 ++++ .../app/src/components/reader/ReaderView.tsx | 1 + .../components/reader/SelectionPopover.tsx | 47 ++++++++++++++++++- 7 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 docs/archive/reader-selection-ai/2026-06-double-click-near-selection.md create mode 100644 docs/archive/reader-selection-ai/2026-06-selection-hit-area-spec.md create mode 100644 docs/archive/reader-selection-ai/README.md create mode 100644 docs/reader-selection-ai-minimal-plan.md diff --git a/docs/archive/reader-selection-ai/2026-06-double-click-near-selection.md b/docs/archive/reader-selection-ai/2026-06-double-click-near-selection.md new file mode 100644 index 000000000..4478cca31 --- /dev/null +++ b/docs/archive/reader-selection-ai/2026-06-double-click-near-selection.md @@ -0,0 +1,15 @@ +# 已归档:选区附近双击加入 AI 方案 + +原文件来源: + +- `D:\Project\ReadAny\ReadAny\docs\选区附近双击加入AI方案.md` + +归档原因: + +- 该方案要求在原链路外新增一套双击命中逻辑 +- 现在已经确认不采用这条实现路线 + +当前状态: + +- 仅保留做历史记录 +- 不再作为当前开发依据 diff --git a/docs/archive/reader-selection-ai/2026-06-selection-hit-area-spec.md b/docs/archive/reader-selection-ai/2026-06-selection-hit-area-spec.md new file mode 100644 index 000000000..151798cef --- /dev/null +++ b/docs/archive/reader-selection-ai/2026-06-selection-hit-area-spec.md @@ -0,0 +1,15 @@ +# 已归档:双击选区命中范围修正规范 + +原文件来源: + +- `D:\Project\ReadAny\ReadAny\docs\双击选区命中范围修正规范.md` + +归档原因: + +- 该规范基于“范围内双击切换加入 / 删除”的旧方向 +- 现在已经确认不再继续做删除切换,也不再单独扩展底层命中系统 + +当前状态: + +- 仅保留做历史记录 +- 不再作为当前开发依据 diff --git a/docs/archive/reader-selection-ai/README.md b/docs/archive/reader-selection-ai/README.md new file mode 100644 index 000000000..8423711de --- /dev/null +++ b/docs/archive/reader-selection-ai/README.md @@ -0,0 +1,35 @@ +# 阅读选区加入 AI 方案归档 + +## 目的 + +统一存放已经过时的“阅读选区加入 AI”改造方案,避免后续继续按旧思路开发。 + +## 归档范围 + +- `2026-06-double-click-near-selection.md` +- `2026-06-selection-hit-area-spec.md` + +## 为什么归档 + +这两份方案都基于一条已经确认不再采用的思路: + +- 在原有“小菜单加入对话”之外,再新造一套双击命中与切换逻辑 + +这个方向的问题已经确认: + +- 改动面过大 +- 容易和原有选区状态分叉 +- 容易丢失原本按钮链路已有的能力 + +## 当前采用的方向 + +后续统一采用“最小改造”原则: + +- 不新造第二套选区状态 +- 不改阅读器底层选区事件链 +- 只复用原本“小菜单加入对话”的现成动作 +- 只把触发方式尽量小范围地改到现有 UI 层 + +## 说明 + +这些归档文档保留,只用于追溯,不作为当前开发依据。 diff --git a/docs/reader-selection-ai-minimal-plan.md b/docs/reader-selection-ai-minimal-plan.md new file mode 100644 index 000000000..9831ddda4 --- /dev/null +++ b/docs/reader-selection-ai-minimal-plan.md @@ -0,0 +1,47 @@ +# 阅读选区加入 AI 最小改造方案 + +## 结论 + +本次只做最小改造: + +- 继续使用现有“小菜单加入对话”动作 +- 不再新造第二套双击选区系统 +- 不做“再次双击删除” + +## 改造原则 + +1. `FoliateViewer` 不动 + +- 不改 `pointerup` +- 不改选区清空规则 +- 不新增底层双击监听 + +2. `handleAskAI` 不复制 + +- 直接复用 `ReaderView` 里现有的 `handleAskAI` +- 双击只是换一种触发方式 + +3. 命中范围直接使用当前 `selection.rects` 的最大外接矩形 + +- 由当前已经存在的选区状态来判断 +- 不再额外维护第二份选区缓存 +- 取最左、最右、最上、最下四条边,组成一个完整矩形 +- 矩形内命中,矩形外不命中 + +## 落点 + +- `packages/app/src/components/reader/ReaderView.tsx` +- `packages/app/src/components/reader/SelectionPopover.tsx` + +## 预期效果 + +- 用户先正常框选文字 +- 小菜单仍然照常出现 +- 在当前蓝色选区最大外接矩形内双击 +- 直接触发和小菜单星星按钮同一条逻辑 + +## 不做的事 + +- 不做删除切换 +- 不做底层命中系统重写 +- 不做新的快捷键方案 diff --git a/packages/app/src/components/layout/AppLayout.tsx b/packages/app/src/components/layout/AppLayout.tsx index e995265a7..f2c8e6da9 100644 --- a/packages/app/src/components/layout/AppLayout.tsx +++ b/packages/app/src/components/layout/AppLayout.tsx @@ -150,6 +150,15 @@ export function AppLayout() { return () => window.removeEventListener("keydown", handleKeyDown, { capture: true }); }, [toggleCommandPalette]); + useEffect(() => { + const handleContextMenu = (event: MouseEvent) => { + event.preventDefault(); + }; + + window.addEventListener("contextmenu", handleContextMenu, { capture: true }); + return () => window.removeEventListener("contextmenu", handleContextMenu, { capture: true }); + }, []); + useEffect(() => { if (!isReaderActive) return; diff --git a/packages/app/src/components/reader/ReaderView.tsx b/packages/app/src/components/reader/ReaderView.tsx index 754f7cee5..74ab474db 100644 --- a/packages/app/src/components/reader/ReaderView.tsx +++ b/packages/app/src/components/reader/ReaderView.tsx @@ -2833,6 +2833,7 @@ export function ReaderView({ bookId, tabId }: ReaderViewProps) { (currentColor || defaultColor); const overlayRef = useRef(null); const popoverRef = useRef(null); + const closeTimerRef = useRef | null>(null); const [clampedPosition, setClampedPosition] = useState(position); + const clearPendingClose = () => { + if (!closeTimerRef.current) return; + clearTimeout(closeTimerRef.current); + closeTimerRef.current = null; + }; + + const selectionBounds = selectionRects.reduce((bounds, rect) => { + if (!bounds) { + return new DOMRect(rect.left, rect.top, rect.width, rect.height); + } + const left = Math.min(bounds.left, rect.left); + const top = Math.min(bounds.top, rect.top); + const right = Math.max(bounds.right, rect.right); + const bottom = Math.max(bounds.bottom, rect.bottom); + return new DOMRect(left, top, right - left, bottom - top); + }, null); + + const isPointInsideSelection = (x: number, y: number) => + !!selectionBounds && + x >= selectionBounds.left && + x <= selectionBounds.right && + y >= selectionBounds.top && + y <= selectionBounds.bottom; + const handleHighlightClick = () => { // PDF doesn't support highlighting if (isPdf) return; @@ -121,13 +148,31 @@ export function SelectionPopover({ ); }); + const handleBackdropClick = () => { + clearPendingClose(); + closeTimerRef.current = setTimeout(() => { + closeTimerRef.current = null; + onClose(); + }, 320); + }; + + const handleBackdropDoubleClick = (event: React.MouseEvent) => { + clearPendingClose(); + if (!isPointInsideSelection(event.clientX, event.clientY)) { + onClose(); + return; + } + onAskAI(); + }; + return (