e.stopPropagation()}
+ >
+ {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
+
+ {exitingParagraph && !isChangingSection && (
+
+ )}
+
+ {activeParagraph ? (
+
+ ) : isChangingSection ? (
+
+ ) : null}
+
+
+ );
+};
+
+export default ParagraphOverlay;
diff --git a/apps/readest-app/src/app/reader/components/paragraph/index.ts b/apps/readest-app/src/app/reader/components/paragraph/index.ts
new file mode 100644
index 00000000..c46a922d
--- /dev/null
+++ b/apps/readest-app/src/app/reader/components/paragraph/index.ts
@@ -0,0 +1,3 @@
+export { default as ParagraphControl } from './ParagraphControl';
+export { default as ParagraphBar } from './ParagraphBar';
+export { default as ParagraphOverlay } from './ParagraphOverlay';
diff --git a/apps/readest-app/src/app/reader/hooks/useBookShortcuts.ts b/apps/readest-app/src/app/reader/hooks/useBookShortcuts.ts
index 70a86033..2bd3ad2b 100644
--- a/apps/readest-app/src/app/reader/hooks/useBookShortcuts.ts
+++ b/apps/readest-app/src/app/reader/hooks/useBookShortcuts.ts
@@ -1,4 +1,4 @@
-import { useEffect } from 'react';
+import { useEffect, useRef } from 'react';
import { useReaderStore } from '@/store/readerStore';
import { useNotebookStore } from '@/store/notebookStore';
import { isTauriAppPlatform } from '@/services/environment';
@@ -27,6 +27,7 @@ const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) =
const { toggleNotebook } = useNotebookStore();
const { getNextBookKey } = useBooksManager();
const { open: openCommandPalette } = useCommandPalette();
+ const lastParagraphToggleRef = useRef(0);
const viewSettings = getViewSettings(sideBarBookKey ?? '');
const fontSize = viewSettings?.defaultFontSize ?? 16;
const lineHeight = viewSettings?.lineHeight ?? 1.6;
@@ -48,17 +49,32 @@ const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) =
const goLeft = () => {
const viewSettings = getViewSettings(sideBarBookKey ?? '');
+ // If paragraph mode is enabled, navigate to previous paragraph instead
+ if (viewSettings?.paragraphMode?.enabled && sideBarBookKey) {
+ eventDispatcher.dispatch('paragraph-prev', { bookKey: sideBarBookKey });
+ return;
+ }
viewPagination(getView(sideBarBookKey), viewSettings, 'left', 'pan', distance);
};
const goRight = () => {
const viewSettings = getViewSettings(sideBarBookKey ?? '');
+ // If paragraph mode is enabled, navigate to next paragraph instead
+ if (viewSettings?.paragraphMode?.enabled && sideBarBookKey) {
+ eventDispatcher.dispatch('paragraph-next', { bookKey: sideBarBookKey });
+ return;
+ }
viewPagination(getView(sideBarBookKey), viewSettings, 'right', 'pan', distance);
};
const goUp = (event?: KeyboardEvent | MessageEvent) => {
const view = getView(sideBarBookKey);
const viewSettings = getViewSettings(sideBarBookKey ?? '');
+ // If paragraph mode is enabled, navigate to previous paragraph instead
+ if (viewSettings?.paragraphMode?.enabled && sideBarBookKey) {
+ eventDispatcher.dispatch('paragraph-prev', { bookKey: sideBarBookKey });
+ return;
+ }
if (view?.renderer.scrolled && event instanceof MessageEvent) return;
viewPagination(view, viewSettings, 'up', 'pan', distance);
};
@@ -66,6 +82,11 @@ const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) =
const goDown = (event?: KeyboardEvent | MessageEvent) => {
const view = getView(sideBarBookKey);
const viewSettings = getViewSettings(sideBarBookKey ?? '');
+ // If paragraph mode is enabled, navigate to next paragraph instead
+ if (viewSettings?.paragraphMode?.enabled && sideBarBookKey) {
+ eventDispatcher.dispatch('paragraph-next', { bookKey: sideBarBookKey });
+ return;
+ }
if (view?.renderer.scrolled && event instanceof MessageEvent) return;
viewPagination(view, viewSettings, 'down', 'pan', distance);
};
@@ -213,6 +234,18 @@ const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) =
eventDispatcher.dispatch('toggle-bookmark', { bookKey: sideBarBookKey });
};
+ const toggleParagraphMode = (event?: KeyboardEvent | MessageEvent) => {
+ if (!sideBarBookKey) return false;
+ if (event instanceof KeyboardEvent && event.repeat) return true;
+
+ const now = Date.now();
+ if (now - lastParagraphToggleRef.current < 300) return true;
+ lastParagraphToggleRef.current = now;
+
+ eventDispatcher.dispatch('toggle-paragraph-mode', { bookKey: sideBarBookKey });
+ return true;
+ };
+
useEffect(() => {
eventDispatcher.on('zoom-in', handleZoomIn);
eventDispatcher.on('zoom-out', handleZoomOut);
@@ -232,6 +265,7 @@ const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) =
onToggleNotebook: toggleNotebook,
onToggleScrollMode: toggleScrollMode,
onToggleBookmark: toggleBookmark,
+ onToggleParagraphMode: toggleParagraphMode,
onOpenFontLayoutSettings: () => setSettingsDialogOpen(true),
onShowSearchBar: showSearchBar,
onToggleFullscreen: toggleFullscreen,
diff --git a/apps/readest-app/src/app/reader/hooks/useParagraphMode.ts b/apps/readest-app/src/app/reader/hooks/useParagraphMode.ts
new file mode 100644
index 00000000..177dfbc4
--- /dev/null
+++ b/apps/readest-app/src/app/reader/hooks/useParagraphMode.ts
@@ -0,0 +1,512 @@
+import { useCallback, useEffect, useRef, useState } from 'react';
+import { useReaderStore } from '@/store/readerStore';
+import { useEnv } from '@/context/EnvContext';
+import { FoliateView } from '@/types/view';
+import { eventDispatcher } from '@/utils/event';
+import { saveViewSettings } from '@/helpers/settings';
+import { ParagraphIterator } from '@/utils/paragraph';
+import { DEFAULT_PARAGRAPH_MODE_CONFIG } from '@/services/constants';
+
+interface UseParagraphModeProps {
+ bookKey: string;
+ viewRef: React.RefObject