Skip to content

Commit e557f83

Browse files
committed
feat:优化·体验·
1 parent cd2fd27 commit e557f83

2 files changed

Lines changed: 39 additions & 36 deletions

File tree

packages/app-mobile/src/components/reader/MobileReaderView.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ export function MobileReaderView() {
128128
const updateReadSettings = useSettingsStore((s) => s.updateReadSettings);
129129

130130
const foliateRef = useRef<MobileFoliateViewerHandle>(null);
131+
const readerContainerRef = useRef<HTMLDivElement>(null);
131132

132133
// Book state
133134
const [bookDoc, setBookDoc] = useState<BookDoc | null>(null);
@@ -671,6 +672,7 @@ export function MobileReaderView() {
671672

672673
return (
673674
<div
675+
ref={readerContainerRef}
674676
className="relative flex h-full flex-col bg-background"
675677
style={{
676678
paddingTop: "env(safe-area-inset-top, 0px)",
@@ -762,6 +764,7 @@ export function MobileReaderView() {
762764
{selection && (
763765
<MobileSelectionPopover
764766
selection={selection}
767+
containerRef={readerContainerRef}
765768
isPdf={bookFormat === "PDF"}
766769
onHighlight={handleHighlight}
767770
onNote={handleNote}

packages/app-mobile/src/components/reader/MobileSelectionPopover.tsx

Lines changed: 36 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
/**
22
* MobileSelectionPopover — floating menu when text is selected in the reader.
33
* Supports: highlight (6 colors + underline), note, copy, translate, ask AI, TTS, edit/delete.
4+
*
5+
* Uses position: absolute within the reader container (like Readest) so the
6+
* popover shares the same stacking context as the iframe content and reliably
7+
* renders above the selection highlight.
48
*/
5-
import { useCallback, useMemo, useState } from "react";
9+
import { useCallback, useState, type RefObject } from "react";
610
import { useTranslation } from "react-i18next";
711
import {
812
Copy,
@@ -27,6 +31,7 @@ export interface BookSelection {
2731

2832
interface MobileSelectionPopoverProps {
2933
selection: BookSelection;
34+
containerRef: RefObject<HTMLDivElement | null>;
3035
isPdf?: boolean;
3136
onHighlight: (color: string) => void;
3237
onNote: () => void;
@@ -49,6 +54,7 @@ const HIGHLIGHT_COLORS = [
4954

5055
export function MobileSelectionPopover({
5156
selection,
57+
containerRef,
5258
isPdf,
5359
onHighlight,
5460
onNote,
@@ -71,72 +77,66 @@ export function MobileSelectionPopover({
7177
}
7278
}, [isPdf, showColors]);
7379

74-
// Position: smart above/below placement like desktop
80+
// Popover dimensions
7581
const popoverWidth = 220;
7682
const popoverH = 44; // single row height
7783
const colorRowH = 40; // color picker row height
7884
const gap = 8;
7985
const padding = 8;
8086
const totalH = showColors ? popoverH + colorRowH + 6 : popoverH; // 6 for mb-1.5
8187

82-
// Read safe-area-inset-top for top boundary clamping
83-
const safeAreaTop = useMemo(() => {
84-
const el = document.documentElement;
85-
const val = getComputedStyle(el).getPropertyValue("--safe-area-top")?.trim();
86-
if (val) return parseInt(val, 10) || 0;
87-
// Fallback: read env(safe-area-inset-top) via a temp element
88-
const tmp = document.createElement("div");
89-
tmp.style.position = "fixed";
90-
tmp.style.top = "env(safe-area-inset-top, 0px)";
91-
tmp.style.visibility = "hidden";
92-
document.body.appendChild(tmp);
93-
const top = tmp.getBoundingClientRect().top;
94-
document.body.removeChild(tmp);
95-
return top;
96-
}, []);
88+
// Get container rect for coordinate conversion (viewport → container-relative)
89+
const containerRect = containerRef.current?.getBoundingClientRect();
90+
const containerTop = containerRect?.top ?? 0;
91+
const containerLeft = containerRect?.left ?? 0;
92+
const containerW = containerRect?.width ?? window.innerWidth;
93+
const containerH = containerRect?.height ?? window.innerHeight;
9794

98-
const topPadding = padding + safeAreaTop;
99-
const { selectionTop, selectionBottom, direction } = selection.position;
95+
// Convert viewport coordinates to container-relative coordinates
96+
const { selectionTop: vpSelTop, selectionBottom: vpSelBot, direction } = selection.position;
97+
const selTop = vpSelTop - containerTop;
98+
const selBot = vpSelBot - containerTop;
99+
const selCenterX = selection.position.x - containerLeft;
100100

101-
// X: centered on selection, clamped to viewport
101+
// X: centered on selection, clamped within container
102102
const x = Math.max(padding, Math.min(
103-
selection.position.x - popoverWidth / 2,
104-
window.innerWidth - popoverWidth - padding,
103+
selCenterX - popoverWidth / 2,
104+
containerW - popoverWidth - padding,
105105
));
106106

107-
// Y positioning: direction-aware with viewport clamp
108-
const yAbove = selectionTop - totalH - gap;
109-
const yBelow = selectionBottom + gap;
110-
const aboveValid = yAbove >= topPadding;
111-
const belowValid = yBelow + totalH + padding <= window.innerHeight;
107+
// Y positioning: direction-aware, coordinates relative to container
108+
const yAbove = selTop - totalH - gap;
109+
const yBelow = selBot + gap;
110+
const aboveValid = yAbove >= padding;
111+
const belowValid = yBelow + totalH + padding <= containerH;
112112

113113
let y: number;
114114
if (direction === "backward") {
115115
// User selected upward — prefer placing above the selection top
116116
if (aboveValid) {
117117
y = yAbove;
118118
} else {
119-
// Can't fit above — place at the top of the visible area
120-
y = topPadding;
119+
// Can't fit above — place at the top of the container
120+
y = padding;
121121
}
122122
} else {
123-
// User selected downward (or unknown) — prefer placing below the selection bottom
123+
// User selected downward (or default) — prefer placing below the selection bottom
124124
if (belowValid) {
125125
y = yBelow;
126126
} else {
127-
// Can't fit below — place at the bottom of the visible area
128-
y = window.innerHeight - totalH - padding;
127+
// Can't fit below — place at the bottom of the container
128+
y = containerH - totalH - padding;
129129
}
130130
}
131131

132-
// Final clamp: always keep fully visible within the safe viewport
133-
y = Math.max(topPadding, Math.min(y, window.innerHeight - totalH - padding));
132+
// Final clamp: always keep fully visible within the container
133+
y = Math.max(padding, Math.min(y, containerH - totalH - padding));
134134

135135
const style: React.CSSProperties = {
136-
position: "fixed",
136+
position: "absolute",
137137
left: x,
138138
top: y,
139-
zIndex: 9999,
139+
zIndex: 50,
140140
};
141141

142142
return (

0 commit comments

Comments
 (0)