Switch nav toggle to tap-only and harden image protection

Replace scroll-direction nav reappear with a one-shot scroll-down hide
plus tap-on-image toggle. Distinguish tap from scroll on touch via
touchstart/touchmove tracking so swipes don't re-show the nav.
Discourage casual image saving with contextmenu prevent, draggable=false,
select-none, and -webkit-touch-callout:none. Add 10.8.0.2 to
allowedDevOrigins for VPN dev access.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
yiekheng 2026-04-12 07:56:39 +08:00
parent 57255e2624
commit c099673f6b
2 changed files with 29 additions and 10 deletions

View File

@ -42,7 +42,7 @@ export function PageReader({
const [showUI, setShowUI] = useState(true); const [showUI, setShowUI] = useState(true);
const [showDrawer, setShowDrawer] = useState(false); const [showDrawer, setShowDrawer] = useState(false);
const [pages, setPages] = useState<PageData[]>([]); const [pages, setPages] = useState<PageData[]>([]);
const lastScrollY = useRef(0); const hiddenByScrollRef = useRef(false);
const offsetRef = useRef(0); const offsetRef = useRef(0);
const doneRef = useRef(false); const doneRef = useRef(false);
const loadingRef = useRef(false); const loadingRef = useRef(false);
@ -127,17 +127,31 @@ export function PageReader({
[] []
); );
// Distinguish tap from scroll on touch devices
const touchMovedRef = useRef(false);
const onTouchStart = useCallback(() => {
touchMovedRef.current = false;
}, []);
const onTouchMove = useCallback(() => {
touchMovedRef.current = true;
}, []);
const onTap = useCallback(() => {
if (touchMovedRef.current) return;
setShowUI((v) => !v);
}, []);
// Hide nav on first scroll down; after that, only tap toggles
useEffect(() => { useEffect(() => {
const handleScroll = () => { const handleScroll = () => {
const currentY = window.scrollY; if (!hiddenByScrollRef.current && window.scrollY > 50) {
if (currentY > lastScrollY.current && currentY > 50) { hiddenByScrollRef.current = true;
setShowUI(false); setShowUI(false);
} else if (currentY < lastScrollY.current) { window.removeEventListener("scroll", handleScroll);
setShowUI(true);
} }
lastScrollY.current = currentY;
}; };
window.addEventListener("scroll", handleScroll, { passive: true }); window.addEventListener("scroll", handleScroll, { passive: true });
return () => window.removeEventListener("scroll", handleScroll); return () => window.removeEventListener("scroll", handleScroll);
}, []); }, []);
@ -178,8 +192,11 @@ export function PageReader({
{/* Pages - vertical scroll (webtoon style, best for mobile) */} {/* Pages - vertical scroll (webtoon style, best for mobile) */}
<div <div
className="max-w-4xl mx-auto leading-[0]" className="max-w-4xl mx-auto leading-[0] select-none"
onClick={() => setShowUI(!showUI)} onClick={onTap}
onTouchStart={onTouchStart}
onTouchMove={onTouchMove}
onContextMenu={(e) => e.preventDefault()}
> >
{pages.map((page, i) => ( {pages.map((page, i) => (
<div <div
@ -191,7 +208,8 @@ export function PageReader({
<img <img
src={page.imageUrl} src={page.imageUrl}
alt={`Page ${page.number}`} alt={`Page ${page.number}`}
className="w-full h-auto block align-bottom -mb-px" className="w-full h-auto block align-bottom -mb-px [-webkit-touch-callout:none]"
draggable={false}
/> />
</div> </div>
))} ))}

View File

@ -1,6 +1,7 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
allowedDevOrigins: ["10.8.0.2"],
images: { images: {
remotePatterns: [ remotePatterns: [
{ {