Files
readest/apps/readest-app/src/app/reader/components/BrightnessOverlay.tsx
T
Huang Xin 36e11de332 feat(reader): swipe-to-adjust brightness gesture on mobile (#3021) (#4356)
* docs: design spec for gesture-based brightness control (#3021)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: revise brightness-gesture spec per /autoplan review (#3021)

CEO+Design+Eng dual-voice review. Key fixes: capture-phase listener
(bubble-phase could not suppress foliate paginator), opt-out toggle,
18px threshold, selection guard, brightness seed race, rAF teardown,
e-ink stepped overlay, contrast capsule, perceptual curve reuse,
listener-level test harness.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(reader): swipe-to-adjust brightness gesture on mobile (#3021)

Left-edge vertical swipe adjusts screen brightness on iOS/Android, with a
Sun-icon progress overlay. Capture-phase non-passive listener suppresses the
foliate paginator / page-flip / UI-toggle handlers; selection guard, strip
reservation in scrolled mode, eager brightness seed, rAF throttle + teardown.
Opt-out toggle in Settings > Behavior > Device (default on). Perceptual curve
shared with the menu slider. Pure-helper + listener-level tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(reader): detect brightness-swipe edge by screenX; i18n + shorter label (#3021)

On-device fix: paginated mode lays the iframe doc out as wide side-by-side
columns, so clientX/documentElement.clientWidth are document coordinates and a
left-edge touch on a later page never fell inside the strip (armed stayed false).
Detect with screenX against the parent window width, matching usePagination.

Also: translate the two new setting strings across all locales and shorten the
toggle description to 'Slide along the left edge'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 15:28:42 +02:00

62 lines
2.1 KiB
TypeScript

import clsx from 'clsx';
import React from 'react';
import { PiSun } from 'react-icons/pi';
import { valueToPosition } from '@/app/reader/utils/brightnessGesture';
interface BrightnessOverlayProps {
visible: boolean;
/** Brightness value, 0-1. */
level: number;
}
const isEink = () =>
typeof document !== 'undefined' && document.documentElement.getAttribute('data-eink') === 'true';
/**
* Transient brightness indicator shown at the left edge while the swipe gesture
* adjusts brightness. Its own capsule surface keeps it legible over any book
* background; on e-ink it snaps (no continuous animation) and quantizes the fill.
*/
const BrightnessOverlay: React.FC<BrightnessOverlayProps> = ({ visible, level }) => {
const eink = isEink();
const clamped = Math.max(0, Math.min(1, level));
// Fill height tracks the slider's perceptual position; the label shows the value.
let fillPercent = valueToPosition(clamped) * 100;
let valuePercent = Math.round(clamped * 100);
if (eink) {
fillPercent = Math.round(fillPercent / 10) * 10;
valuePercent = Math.round(valuePercent / 10) * 10;
}
return (
<div
aria-hidden
dir='ltr'
className={clsx(
'pointer-events-none absolute left-0 top-1/2 z-[15] -translate-y-1/2',
'not-eink:transition-opacity not-eink:duration-200 motion-reduce:transition-none',
visible ? 'opacity-100' : 'opacity-0',
)}
style={{ marginInlineStart: 'calc(env(safe-area-inset-left) + 12px)' }}
>
<div
className={clsx(
'eink-bordered flex flex-col items-center gap-2 rounded-full px-2 py-3',
'bg-base-100/90 not-eink:shadow-md',
)}
>
<PiSun className='text-base-content h-4 w-4' />
<div className='bg-base-content/20 relative h-40 w-1.5 overflow-hidden rounded-full'>
<div
className='bg-base-content absolute bottom-0 left-0 w-full rounded-full'
style={{ height: `${fillPercent}%` }}
/>
</div>
<span className='text-base-content text-xs tabular-nums'>{valuePercent}</span>
</div>
</div>
);
};
export default BrightnessOverlay;