21 Commits

Author SHA1 Message Date
6eff1f68fc Reader: nav hover reveals transiently, restores pre-hover state on leave
Previously hover both revealed (on enter) AND hid (on leave) via the
persistent showUI state — so tapping the nav visible and then moving the
mouse away would hide it. Now hover toggles a separate transient
hoveringNav state; the nav is visible when (showUI || hoveringNav). On
mouseleave, hoveringNav clears and the nav returns to whatever showUI
was before — visible if the user tapped to show it, hidden if it was
scroll-auto-hidden.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 22:25:37 +08:00
f3d71a5423 Reader: hover-reveal / hover-hide top nav on desktop
When the nav is hidden (auto-hidden after scrolling past 50px), a
fixed-position hover sensor at the top of the viewport catches mouseenter
and slides the nav back in. Moving the mouse away from the nav slides it
back out.

Gated by `@media (hover: hover)` via JS matchMedia so touch devices are
unaffected — the existing single-tap toggle stays the only way to show
the nav there.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 22:24:31 +08:00
6ea021d284 Inline drawer-header colors to fix iOS invisible text
iOS WebKit intermittently fails to resolve Tailwind's CSS-variable-based
color utilities (text-foreground, text-muted, bg-background) in this
specific stacking context (fixed + nested absolute + rounded-t-3xl +
overflow-hidden). Text was rendering with an unresolved color, making it
invisible but still selectable/copyable.

Inline hex values bypass the variable chain and force the resolved color.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 22:21:13 +08:00
f6fa5d9d6a Remove sticky top-0 from chapter drawer header
iOS Safari fails to paint sticky descendants of overflow-hidden ancestors
until a repaint is triggered, causing the "Chapters / N total" header to
render invisibly. The sticky was unnecessary — the header sits outside
the drawer's scroll container, so it never actually needed to stick.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 22:18:45 +08:00
9681dddc2e Fix iOS Safari zoom-in on lock/unlock
Adds a tiny client component mounted in the root layout that listens for
pageshow with persisted=true (bfcache resume) and nudges scroll + forces
a reflow. Resolves the zoom-desync bug where viewportFit=cover +
maximumScale=1 leaves the visual viewport stale after device lock.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 22:11:59 +08:00
cd1fd6ad64 Reader: make position tracking accurate to sub-200ms
- Throttled write inside the scroll rAF tick (every 200ms during active
  scroll) captures mid-page ratio changes. Previously only page-boundary
  crossings triggered writes, so refreshing mid-page restored to top of
  that page instead of exact position.
- pagehide + visibilitychange + unmount flush captures the final 0-200ms
  before tab close / bfcache / nav.
- hasScrolledRef guards writes so opening a chapter without scrolling
  doesn't clobber a prior deep-progress bookmark for a different chapter.
- getBoundingClientRect replaces offsetTop/offsetHeight for subpixel
  precision and positioning-ancestor independence.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 22:05:35 +08:00
b993de43bc Reader: fix resume bug, add loading skeleton, scraping protection, bounded image cache
- Resume scroll position only when arriving via 继续阅读 (?resume=1).
  Plain chapter-list / drawer clicks now actively scroll to top on mount.
- Progress format extended to {chapter, page, ratio} for within-page
  precision; legacy bare-number and {chapter, page} still read correctly.
- Tappable skeleton logo (sunflower outline, spins) while a page loads;
  tap force-fetches a fresh signed URL.
- Viewport-priority image loading: second IntersectionObserver at margin 0
  marks truly-visible pages, drives <img fetchpriority="high"> and fires
  immediate single-page fetches that cut the batch queue.
- Bounded image cache: unmount previous chapter's <img> elements when
  currentPage > 5 into the new chapter; placeholders stay for layout.
  One AbortController per live chapter; unmount aborts in-flight batches.
- Hashed chapter IDs on the wire via hashids; DB PKs unchanged.
- Origin/Referer allowlist + rate limiting on all /api/* routes via a
  withGuards(opts, handler) wrapper (eliminates 6-line boilerplate x5).
- robots.txt allows Googlebot/Bingbot/Slurp/DuckDuckBot/Baiduspider/
  YandexBot only; disallows /api/ for all UAs.
- Extract pure helpers for future TDD: lib/scroll-ratio.ts (calcScrollRatio,
  scrollOffsetFromRatio), lib/progress.ts (parseProgress + injectable
  StorageLike), lib/rate-limit.ts (optional { now, store, ipOf } deps),
  lib/api-guards.ts.
- New env keys: HASHIDS_SALT, ALLOWED_ORIGINS (wired into docker-compose).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:48:15 +08:00
61c73c9e2d Add scroll={false} to the last two reader-entry Links
The back-to-manga header Link and the end-of-manga "Back to Home" Link
were missing scroll={false}, so navigating away and returning (or going
home then re-entering via Continue Reading) could lose the resume scroll.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 18:24:56 +08:00
3e4b87329a Added logo and changed theme color from light blue to green 2026-04-12 15:45:46 +08:00
1c74348fae Sync URL and prev/next nav with the chapter being viewed
Once continuous scroll crosses a chapter boundary, the URL was stuck at
the originally-opened chapter so browser back / reload would jump the
user back there. Double-tap left/right also walked off the wrong chapter
since prevChapter/nextChapter were frozen at mount time.

- replaceState the URL as currentChapterNum changes (no server refetch).
- Derive prevChapter/nextChapter dynamically via useMemo on
  currentChapterNum, dropping the now-redundant server-computed props.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 14:40:13 +08:00
cad59143a3 Center current chapter in drawer on open
Chapter drawer now opens with the active row pre-scrolled to the center
of the list instead of always starting at chapter #1. useLayoutEffect
measures via getBoundingClientRect so the scroll lands before paint —
no visible jump.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 13:04:35 +08:00
43a2a6d3f8 Rewrite reader around known-dimension page placeholders
Replaces the prepend/flushSync/scrollBy gymnastics with placeholder divs
sized by each page's width/height. Document height is correct from the
first paint, so resume + backward scroll just work — no scroll
compensation, no gesture fights, no forced aspect ratio distorting images.

- New /api/chapters/[id]/meta returns the dim skeleton for any chapter.
- Chapter page pre-fetches the starting chapter's meta server-side and
  parallelizes the two Prisma queries via Promise.all.
- Reader renders placeholders with aspectRatio: w/h, lazy-loads image
  URLs in batches via IntersectionObserver, and prefetches the next
  chapter's meta ~3 pages from the end.
- Scroll tracker walks only the intersecting-pages set (~3–5 elements)
  instead of every loaded page per rAF.
- scroll={false} on all Links into the reader + { scroll: false } on
  double-tap router.push, plus a belt-and-suspenders rAF re-scroll, so
  resume survives soft navigation and browser scroll-restoration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 13:01:41 +08:00
3745f1f316 Add backward prefetch for resumed mid-chapter reads
When the user resumes at a mid-chapter page, the reader previously
skipped the earlier pages entirely. This adds a scroll-up prefetch so
those earlier pages appear smoothly as the user scrolls toward the top.

- prependBatch() mirrors fetchBatch but decrements the offset cursor
  and prepends the new pages. prependExhaustedRef fires when the
  cursor hits 0 (start of chapter).
- Trigger: scrollY < 2500px fires prepend — well before the user
  reaches the top, so the DOM + images spawn ahead of the scroll
  position.
- Scroll preservation: aspect-[3/4] on <img> reserves vertical space
  before the image bytes arrive, so scrollHeight is accurate
  immediately after React commits. A single scrollBy(delta) keeps the
  previously-visible page visually anchored — no per-image jitter.
- Forward-fetch trigger indices + loadedCountRef are shifted by
  batch.length on each prepend so next-batch prefetch still fires at
  the correct page.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 11:00:01 +08:00
0c6425f0ff Track reading progress at the page level and resume there
Upgrades the reader's last-read tracking from {chapter} to {chapter, page}.

- ReadingProgressButton: storage is now JSON {chapter, page}; legacy
  bare-number values are read back as {chapter, page: 1}. Button label
  is unchanged ("继续阅读 · #N title") — the extra precision lives in
  the reader's first-fetch offset, not the label.
- PageReader: on mount with saved progress, seed offsetRef to
  (page - 1) so the first /api/pages call starts AT the resumed page
  instead of the beginning of the chapter. currentPageNum state is
  initialized from storage too, so the first persist write is a no-op
  that matches the saved value.
- Scroll tracker now also tracks currentPageNum (last page whose top
  has crossed above viewport top+80), and persistence writes the
  {chapter, page} pair on each change.

Known limitation: earlier pages of the resumed chapter aren't loaded
yet — a follow-up commit adds scroll-up prefetch for those.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 10:59:40 +08:00
26b620de2f Add reading-progress resume + multi-genre support
Reading progress
- New ReadingProgressButton client component that reads/writes
  localStorage key "sunnymh:last-read:{slug}"
- PageReader writes the currently-visible chapter as the user scrolls
  through continuous chapters
- Manga detail CTA now reads: "开始阅读" on first visit, or
  "继续阅读 · #{N} {title}" when a prior chapter is stored (with
  spacing and truncation for long titles)

Multiple genres
- lib/genres.ts with parseGenres() and collectGenres() helpers to
  split "冒险, 恋爱, 魔幻" into a list and aggregate across a collection
- Manga detail renders one pill per genre
- GenreTabs filters via parseGenres(...).includes(activeGenre) so a
  multi-genre manga appears under each of its genres
- Home / Genre pages compute the tab list from collectGenres(signedManga)
- Card captions (GenreTabs + TrendingCarousel) show "冒险 · 恋爱 · 魔幻"
  with truncation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 10:27:28 +08:00
06dcf0a649 Rewrite reader for iOS Safari with continuous multi-chapter flow
Replaces the fixed-position reader with a sticky layout that works
correctly on iPhone Safari and Edge, while also auto-appending the
next chapter's pages when the current one finishes.

Layout
- Swap all position:fixed for sticky (Header, BottomNav, reader top nav)
  — fixed-positioning quirks broke the bottom nav in Edge and
  prevented Safari's URL bar from collapsing on scroll
- Viewport: viewport-fit=cover + interactiveWidget=overlays-content
  so manga extends edge-to-edge and the URL bar overlays content
  without resizing the viewport
- Add pt-safe / pb-safe utilities; apply on nav bars so chrome
  respects the notch and home-indicator
- Drop fixed-positioning bottom padding now that BottomNav is in flow

Continuous reading
- PageReader now receives the full chapter manifest (id + totalPages)
  and auto-fetches the next chapter when the current one is done
- Subtle chapter divider strip appears between chapters in the scroll
- Top nav chapter title updates as the user scrolls into a new chapter
  (rAF-throttled scroll listener, cached offsetTop)
- Double-tap on left/right viewport half navigates prev/next chapter
- End-of-manga footer fills the viewport with a Back-to-Manga action

Theme polish
- Light theme: white body/background, blue accent preserved for
  chapter numbers, badges, active states
- Modern chapter drawer: white sheet, rounded-t-3xl, two-column
  rows with chapter-number badge, blue highlight for current chapter
- Suppress hydration warnings for extension-injected attributes on
  <html> and the search input
- Manga detail CTA localized to 开始阅读

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 10:17:54 +08:00
c099673f6b 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>
2026-04-12 07:56:39 +08:00
57255e2624 Add signed R2 URLs, batched page fetching, and 3D chapter badges
- Sign all image URLs server-side with 60s expiry presigned URLs
- Add /api/pages endpoint for batched page fetching (7 per batch)
- PageReader prefetches next batch when user scrolls to 3rd page
- Move chapter count badge outside overflow-hidden for 3D effect
- Fix missing URL signing on search and genre pages
- Extract signCoverUrls helper to reduce duplication
- Clamp API limit param to prevent abuse

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 21:15:01 +08:00
e7dc39738c Add auto-hide nav bars on scroll and chapter selector bottom sheet
Nav bars hide when scrolling down and reappear on scroll up.
Replace static page count with a chapter picker that opens as a
bottom sheet drawer for quick chapter navigation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 18:38:54 +08:00
09fb507e05 Second Commit 2026-04-11 16:59:44 +08:00
f1c649429e First Commit 2026-03-25 22:20:37 +08:00