feat(reader): Auto Scroll reading mode for scrolled flow (#4998) (#4999)

Teleprompter-style continuous scrolling toggled from the View menu
(Shift+A), available only in scrolled mode. A PacedScroller drives
whole-pixel forward steps at a constant, user-adjustable velocity;
the speed (25-500 percent, persisted as autoScrollSpeed) is tuned
from a floating control pill that also offers pause/resume and exit,
and fades away while scrolling to keep the mode immersive.

Tapping the page pauses and resumes instead of turning pages or
toggling the bars; manual wheel or drag input simply composes with
the paced scrolling. Escape or leaving scrolled mode ends the
session. When forward progress stalls the session hops to the next
section (single-section scroll mode) or stops with a toast at the
end of the book. Vertical-writing books scroll along the horizontal
axis with the sign convention foliate uses for scrolled offsets.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Huang Xin
2026-07-08 00:45:02 +09:00
committed by GitHub
parent 17de9357dd
commit f8ad47a418
46 changed files with 926 additions and 33 deletions
+18
View File
@@ -42,6 +42,9 @@ interface ViewState {
`getBookProgress(key)` for one-shot reads. */
ribbonVisible: boolean;
ttsEnabled: boolean;
/* True while an Auto Scroll session (#4998) is engaged for this view;
session-only, never persisted. Drives the View menu checkmark. */
autoScrollEnabled: boolean;
syncing: boolean;
gridInsets: Insets | null;
/* True while the reader is showing a position requested by an external
@@ -65,6 +68,7 @@ interface ReaderStore {
setHoveredBookKey: (key: string | null) => void;
setBookmarkRibbonVisibility: (key: string, visible: boolean) => void;
setTTSEnabled: (key: string, enabled: boolean) => void;
setAutoScrollEnabled: (key: string, enabled: boolean) => void;
setIsLoading: (key: string, loading: boolean) => void;
setIsSyncing: (key: string, syncing: boolean) => void;
setProgress: (
@@ -157,6 +161,7 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
error: null,
ribbonVisible: false,
ttsEnabled: false,
autoScrollEnabled: false,
syncing: false,
gridInsets: null,
previewMode: false,
@@ -296,6 +301,7 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
error: null,
ribbonVisible: false,
ttsEnabled: false,
autoScrollEnabled: false,
syncing: false,
gridInsets: null,
previewMode: false,
@@ -319,6 +325,7 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
error: 'Failed to load book.',
ribbonVisible: false,
ttsEnabled: false,
autoScrollEnabled: false,
syncing: false,
gridInsets: null,
previewMode: false,
@@ -466,6 +473,17 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
},
})),
setAutoScrollEnabled: (key: string, enabled: boolean) =>
set((state) => ({
viewStates: {
...state.viewStates,
[key]: {
...state.viewStates[key]!,
autoScrollEnabled: enabled,
},
},
})),
setIsLoading: (key: string, loading: boolean) =>
set((state) => ({
viewStates: {