@@ -174,7 +174,7 @@ const OpenAnnotationLanding = () => {
const webReaderHref = buildWebReaderUrl(bookHash, cfi);
return (
-
+
s.setPreviewMode);
const { getViewState, getProgress, getViewSettings, setViewSettings } = useReaderStore();
const { getParallels } = useParallelViewStore();
const { getBookData } = useBookDataStore();
@@ -355,6 +356,11 @@ const FoliateViewer: React.FC<{
const detail = (event as CustomEvent).detail;
if (detail.reason !== 'scroll' && detail.reason !== 'page') return;
+ // First user-initiated navigation after a deep-link landing — promote
+ // the preview into the real reading position. Subsequent progress writes
+ // can flow normally.
+ setPreviewMode(bookKey, false);
+
const parallelViews = getParallels(bookKey);
if (parallelViews && parallelViews.size > 0) {
parallelViews.forEach((key) => {
@@ -602,6 +608,15 @@ const FoliateViewer: React.FC<{
await view.goToFraction(0);
}
setViewInited(bookKey, true);
+
+ // The reader is showing a deep-link target, not the user's actual reading
+ // position. Mark the view as a preview so progress writers (auto-save,
+ // cloud sync, kosync) skip until the user takes a reading action. The
+ // flag clears on the first user-initiated relocate (page / scroll) in
+ // docRelocateHandler below.
+ if (overrideLocation) {
+ setPreviewMode(bookKey, true);
+ }
};
openBook();
diff --git a/apps/readest-app/src/app/reader/hooks/useKOSync.ts b/apps/readest-app/src/app/reader/hooks/useKOSync.ts
index 3fd62264..84233c78 100644
--- a/apps/readest-app/src/app/reader/hooks/useKOSync.ts
+++ b/apps/readest-app/src/app/reader/hooks/useKOSync.ts
@@ -308,12 +308,15 @@ export const useKOSync = (bookKey: string) => {
// Push: auto-push progress when progress changes with a debounce
useEffect(() => {
if (syncState === 'synced' && progress) {
+ // Skip auto-pushes while previewing a deep-link target. Manual pushes
+ // via the 'push-kosync' event are still respected (explicit user intent).
+ if (useReaderStore.getState().getViewState(bookKey)?.previewMode) return;
const { strategy, enabled } = settings.kosync;
if (strategy !== 'receive' && enabled) {
syncRefs.current.pushProgress();
}
}
- }, [progress, syncState, settings.kosync]);
+ }, [progress, syncState, settings.kosync, bookKey]);
useWindowActiveChanged((isActive) => {
const { pushProgress, pullProgress } = syncRefs.current;
diff --git a/apps/readest-app/src/app/reader/hooks/useProgressAutoSave.ts b/apps/readest-app/src/app/reader/hooks/useProgressAutoSave.ts
index 86b90b7f..a81d13ea 100644
--- a/apps/readest-app/src/app/reader/hooks/useProgressAutoSave.ts
+++ b/apps/readest-app/src/app/reader/hooks/useProgressAutoSave.ts
@@ -15,6 +15,9 @@ export const useProgressAutoSave = (bookKey: string) => {
const saveBookConfig = useCallback(
debounce(() => {
setTimeout(async () => {
+ // Skip while previewing a deep-link target — the user's actual
+ // last-read position should not be overwritten by a transient view.
+ if (useReaderStore.getState().getViewState(bookKey)?.previewMode) return;
const config = getConfig(bookKey);
if (!config) return;
const settings = useSettingsStore.getState().settings;
diff --git a/apps/readest-app/src/app/reader/hooks/useProgressSync.ts b/apps/readest-app/src/app/reader/hooks/useProgressSync.ts
index bb41912b..69ebe394 100644
--- a/apps/readest-app/src/app/reader/hooks/useProgressSync.ts
+++ b/apps/readest-app/src/app/reader/hooks/useProgressSync.ts
@@ -50,6 +50,9 @@ export const useProgressSync = (bookKey: string) => {
if (!configPulled.current) {
pullConfig(bookKey);
} else {
+ // Skip pushes while previewing a deep-link target — the position in
+ // memory reflects the annotation, not what the user is actually reading.
+ if (useReaderStore.getState().getViewState(bookKey)?.previewMode) return;
const config = getConfig(bookKey);
const view = getView(bookKey);
const book = getBookData(bookKey)?.book;
@@ -151,7 +154,12 @@ export const useProgressSync = (bookKey: string) => {
}
if (remoteCFILocation && configCFI) {
if (CFI.compare(configCFI, remoteCFILocation) < 0) {
- if (view) {
+ // While previewing a deep-link target, do NOT yank the view to the
+ // remote position — the user came here to look at a specific
+ // annotation. The local config still gets updated above; the next
+ // open will resolve to the synced position normally.
+ const isPreview = useReaderStore.getState().getViewState(bookKey)?.previewMode;
+ if (view && !isPreview) {
view.goTo(remoteCFILocation);
setHoveredBookKey(null);
eventDispatcher.dispatch('hint', {
diff --git a/apps/readest-app/src/app/reader/page.tsx b/apps/readest-app/src/app/reader/page.tsx
index dd7b56dc..014bdb8f 100644
--- a/apps/readest-app/src/app/reader/page.tsx
+++ b/apps/readest-app/src/app/reader/page.tsx
@@ -3,6 +3,7 @@
import { useEffect } from 'react';
import { useEnv } from '@/context/EnvContext';
import { useTranslation } from '@/hooks/useTranslation';
+import { useAppUrlIngress } from '@/hooks/useAppUrlIngress';
import { useOpenWithBooks } from '@/hooks/useOpenWithBooks';
import { useOpenAnnotationLink } from '@/hooks/useOpenAnnotationLink';
import { useSettingsStore } from '@/store/settingsStore';
@@ -16,6 +17,7 @@ export default function Page() {
const { appService } = useEnv();
const { settings } = useSettingsStore();
+ useAppUrlIngress();
useOpenWithBooks();
useOpenAnnotationLink();
diff --git a/apps/readest-app/src/hooks/useAppUrlIngress.ts b/apps/readest-app/src/hooks/useAppUrlIngress.ts
new file mode 100644
index 00000000..cc8c0b33
--- /dev/null
+++ b/apps/readest-app/src/hooks/useAppUrlIngress.ts
@@ -0,0 +1,103 @@
+import { useEffect, useRef } from 'react';
+import { addPluginListener, PluginListener } from '@tauri-apps/api/core';
+import { onOpenUrl } from '@tauri-apps/plugin-deep-link';
+import { getCurrentWindow } from '@tauri-apps/api/window';
+import { useEnv } from '@/context/EnvContext';
+import { isTauriAppPlatform } from '@/services/environment';
+import { eventDispatcher } from '@/utils/event';
+
+interface SingleInstancePayload {
+ args: string[];
+ cwd: string;
+}
+
+interface OpenFilesPayload {
+ files: string[];
+}
+
+interface SharedIntentPayload {
+ urls: string[];
+}
+
+/**
+ * Single ingress point for incoming URLs from the operating system.
+ *
+ * Subscribes to every Tauri channel that can deliver a URL on any platform:
+ * - `single-instance` event — Win/Linux deep link, macOS open-file
+ * - `open-files` event — macOS in-app open-files
+ * - `shared-intent` plugin — Android "Share to Readest" intent
+ * - `onOpenUrl` — iOS / Android / macOS via Tauri v2
+ *
+ * Re-broadcasts every URL list as the `app-incoming-url` event. Consumers
+ * subscribe to the event instead of the underlying channels, which:
+ * - decouples them from platform specifics
+ * - sidesteps a Tauri Android quirk where multiple `onOpenUrl`
+ * subscribers don't all fire
+ * - keeps the channel-subscription code in exactly one place
+ *
+ * Existing consumers:
+ * - `useOpenWithBooks` — file imports
+ * - `useOpenAnnotationLink` — annotation deep links
+ *
+ * Cold-start URLs (`getCurrent()`) are intentionally NOT read here. Cold-
+ * start handling is consumer-specific (a launching file goes through the
+ * library init flow; an annotation jumps the reader), so each consumer
+ * reads `getCurrent()` itself when it needs to.
+ */
+export function useAppUrlIngress() {
+ const { appService } = useEnv();
+ const listened = useRef(false);
+
+ useEffect(() => {
+ if (!isTauriAppPlatform() || !appService) return;
+ if (listened.current) return;
+ listened.current = true;
+
+ const dispatch = (urls: string[]) => {
+ if (!urls.length) return;
+ console.log('App incoming URL:', urls);
+ eventDispatcher.dispatch('app-incoming-url', { urls });
+ };
+
+ const unlistenSingleInstance = getCurrentWindow().listen(
+ 'single-instance',
+ ({ payload }) => {
+ const url = payload.args?.[1];
+ if (url) dispatch([url]);
+ },
+ );
+
+ const unlistenOpenFiles = getCurrentWindow().listen(
+ 'open-files',
+ ({ payload }) => {
+ if (payload.files?.length) dispatch(payload.files);
+ },
+ );
+
+ // FIXME: register/unregister of this plugin listener has caused freezes
+ // on iOS in the past, so it's gated to Android. The Tauri v2 onOpenUrl
+ // listener below covers iOS.
+ let unlistenSharedIntent: Promise | null = null;
+ if (appService?.isAndroidApp) {
+ unlistenSharedIntent = addPluginListener(
+ 'native-bridge',
+ 'shared-intent',
+ (payload) => {
+ if (payload.urls?.length) dispatch(payload.urls);
+ },
+ );
+ }
+
+ const unlistenOpenUrl = onOpenUrl((urls) => {
+ if (urls?.length) dispatch(urls);
+ });
+
+ return () => {
+ unlistenSingleInstance.then((f) => f());
+ unlistenOpenFiles.then((f) => f());
+ unlistenOpenUrl.then((f) => f());
+ unlistenSharedIntent?.then((f) => f.unregister());
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [appService]);
+}
diff --git a/apps/readest-app/src/hooks/useOpenAnnotationLink.ts b/apps/readest-app/src/hooks/useOpenAnnotationLink.ts
index 30aabc67..0fe511df 100644
--- a/apps/readest-app/src/hooks/useOpenAnnotationLink.ts
+++ b/apps/readest-app/src/hooks/useOpenAnnotationLink.ts
@@ -1,52 +1,56 @@
-import { useEffect, useRef } from 'react';
+import { useCallback, useEffect, useRef } from 'react';
import { useRouter } from 'next/navigation';
-import { getCurrent, onOpenUrl } from '@tauri-apps/plugin-deep-link';
-import { getCurrentWindow } from '@tauri-apps/api/window';
+import { getCurrent } from '@tauri-apps/plugin-deep-link';
import { useEnv } from '@/context/EnvContext';
import { useLibraryStore } from '@/store/libraryStore';
+import { useReaderStore } from '@/store/readerStore';
import { isTauriAppPlatform } from '@/services/environment';
import { navigateToReader } from '@/utils/nav';
import { eventDispatcher } from '@/utils/event';
import { parseAnnotationDeepLink, AnnotationDeepLink } from '@/utils/deeplink';
import { useTranslation } from './useTranslation';
-interface SingleInstancePayload {
- args: string[];
- cwd: string;
-}
+// Module-scoped — survives hook remounts (library → reader → library on
+// book close). Tauri's getCurrent() keeps returning the launch URL for the
+// lifetime of the app session, so without this flag every remount would
+// re-process the cold-start URL and navigate back to the deep-link target
+// in a loop.
+let coldStartConsumed = false;
/**
- * Listen for incoming deep links that target an annotation, and navigate the
- * reader to the corresponding book + CFI. Handles both:
+ * Receive annotation deep links and navigate the reader accordingly.
+ *
+ * Architecture:
+ * - useOpenWithBooks owns the Tauri URL channels (onOpenUrl,
+ * single-instance, shared-intent, open-files) and re-broadcasts every
+ * URL as the 'app-incoming-url' event. This hook subscribes to that
+ * event for the warm-start / live path.
+ * - For cold-start (app launched FROM the URL), getCurrent() is read
+ * once at module scope. useOpenWithBooks doesn't do this — its
+ * channels only fire for live deliveries.
+ * - Library-load deferral: on cold-start the URL may arrive before the
+ * library store has hydrated. Stash and replay once libraryLoaded.
+ *
+ * Supported URL shapes (see src/utils/deeplink.ts):
* readest://book/{hash}/annotation/{id}?cfi=...
* https://web.readest.com/o/book/{hash}/annotation/{id}?cfi=...
- * and the legacy flat shape readest://annotation/{hash}/{id} (Readwise sync
- * before the migration).
+ * readest://annotation/{hash}/{id} (legacy Readwise sync)
*
- * Cold-start vs. live: if the app is launched FROM the URL (cold-start), the
- * URL lives in getCurrent() and never fires on onOpenUrl. On warm/running
- * apps, it arrives via onOpenUrl (mobile) or the single-instance event
- * (Windows/Linux/macOS). We cover all three.
- *
- * Library-load deferral: book lookup needs `libraryLoaded` to be true. On
- * cold-start the hook may mount before the library hydrates, so we stash the
- * pending parsed link in a ref and process it once the store reports loaded.
+ * Already-open shortcut: if the target book has a mounted view, jump in
+ * place via view.goTo(cfi). router.push to the same /reader path with a
+ * different cfi query does NOT re-run the reader's init effect, so
+ * navigation alone wouldn't move the view in that case.
*/
export function useOpenAnnotationLink() {
const _ = useTranslation();
- const { appService } = useEnv();
const router = useRouter();
+ const { appService } = useEnv();
const getBookByHash = useLibraryStore((s) => s.getBookByHash);
const libraryLoaded = useLibraryStore((s) => s.libraryLoaded);
-
- const listened = useRef(false);
const pending = useRef(null);
- const handledColdStart = useRef(false);
- useEffect(() => {
- if (!isTauriAppPlatform() || !appService) return;
-
- const resolveAndNavigate = (parsed: AnnotationDeepLink) => {
+ const resolveAndNavigate = useCallback(
+ (parsed: AnnotationDeepLink) => {
const { bookHash, cfi } = parsed;
const book = getBookByHash(bookHash);
if (!book) {
@@ -57,80 +61,64 @@ export function useOpenAnnotationLink() {
});
return;
}
+
+ const { viewStates, setPreviewMode } = useReaderStore.getState();
+ const openEntry = Object.entries(viewStates).find(
+ ([key, state]) => key.startsWith(bookHash) && state.view,
+ );
+ if (openEntry) {
+ const [bookKey, state] = openEntry;
+ if (cfi) {
+ state.view!.goTo(cfi);
+ setPreviewMode(bookKey, true);
+ }
+ return;
+ }
+
const queryParams = cfi ? `cfi=${encodeURIComponent(cfi)}` : undefined;
navigateToReader(router, [bookHash], queryParams);
- };
+ },
+ [_, getBookByHash, router],
+ );
+
+ useEffect(() => {
+ if (!isTauriAppPlatform() || !appService) return;
const handle = (url: string) => {
const parsed = parseAnnotationDeepLink(url);
- if (!parsed) return false;
- // If the library hasn't finished loading yet, stash the link and let
- // the libraryLoaded effect below pick it up. Otherwise navigate now.
+ if (!parsed) return;
if (!useLibraryStore.getState().libraryLoaded) {
pending.current = parsed;
- } else {
- resolveAndNavigate(parsed);
+ return;
}
- return true;
+ resolveAndNavigate(parsed);
};
- if (listened.current) return;
- listened.current = true;
-
- if (!handledColdStart.current) {
- handledColdStart.current = true;
+ if (!coldStartConsumed) {
+ coldStartConsumed = true;
getCurrent()
- .then((urls) => {
- if (!urls) return;
- for (const url of urls) {
- if (handle(url)) break;
- }
- })
+ .then((urls) => urls?.forEach(handle))
.catch(() => {
- // getCurrent() may reject on platforms without the plugin; the
- // listeners below cover live events.
+ // Plugin not available on this platform — live channel still works.
});
}
- const unlistenSingleInstance = getCurrentWindow().listen(
- 'single-instance',
- ({ payload }) => {
- const url = payload.args?.[1];
- if (url) handle(url);
- },
- );
-
- const unlistenOpenUrl = onOpenUrl((urls) => {
- for (const url of urls) {
- if (handle(url)) break;
- }
- });
+ const onIncoming = (event: CustomEvent) => {
+ const { urls } = event.detail as { urls: string[] };
+ urls.forEach(handle);
+ };
+ eventDispatcher.on('app-incoming-url', onIncoming);
return () => {
- unlistenSingleInstance.then((f) => f());
- unlistenOpenUrl.then((f) => f());
+ eventDispatcher.off('app-incoming-url', onIncoming);
};
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [appService]);
+ }, [appService, resolveAndNavigate]);
+ // Replay any deferred deep link once the library hydrates.
useEffect(() => {
- if (!libraryLoaded) return;
+ if (!libraryLoaded || !pending.current) return;
const parsed = pending.current;
- if (!parsed) return;
pending.current = null;
-
- const { bookHash, cfi } = parsed;
- const book = getBookByHash(bookHash);
- if (!book) {
- eventDispatcher.dispatch('toast', {
- type: 'warning',
- message: _('Book not in your library'),
- timeout: 2500,
- });
- return;
- }
- const queryParams = cfi ? `cfi=${encodeURIComponent(cfi)}` : undefined;
- navigateToReader(router, [bookHash], queryParams);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [libraryLoaded]);
+ resolveAndNavigate(parsed);
+ }, [libraryLoaded, resolveAndNavigate]);
}
diff --git a/apps/readest-app/src/hooks/useOpenWithBooks.ts b/apps/readest-app/src/hooks/useOpenWithBooks.ts
index f7aa9c41..2e774738 100644
--- a/apps/readest-app/src/hooks/useOpenWithBooks.ts
+++ b/apps/readest-app/src/hooks/useOpenWithBooks.ts
@@ -1,56 +1,51 @@
-import { useEffect, useRef } from 'react';
+import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
+import { getAllWindows, getCurrentWindow } from '@tauri-apps/api/window';
import { useEnv } from '@/context/EnvContext';
import { useLibraryStore } from '@/store/libraryStore';
import { useSettingsStore } from '@/store/settingsStore';
-import { addPluginListener, PluginListener } from '@tauri-apps/api/core';
-import { onOpenUrl } from '@tauri-apps/plugin-deep-link';
-import { getCurrentWindow, getAllWindows } from '@tauri-apps/api/window';
import { isTauriAppPlatform } from '@/services/environment';
import { navigateToLibrary, showLibraryWindow } from '@/utils/nav';
+import { eventDispatcher } from '@/utils/event';
-interface SingleInstancePayload {
- args: string[];
- cwd: string;
-}
-
-interface OpenFilesPayload {
- files: string[];
-}
-
-interface SharedIntentPayload {
- urls: string[];
-}
-
+/**
+ * Handle "Open with Readest" file imports. Consumes the `app-incoming-url`
+ * event published by `useAppUrlIngress`, filters URLs that look like a file
+ * (file://, content://, or plain path), and routes them to the library.
+ *
+ * Non-file URL shapes (https, readest://, data:, blob:) are skipped here
+ * — other consumers (e.g. `useOpenAnnotationLink`) act on those.
+ *
+ * Mount this hook alongside `useAppUrlIngress` so the ingress dispatcher is
+ * actually running when URLs arrive.
+ */
export function useOpenWithBooks() {
const router = useRouter();
const { appService } = useEnv();
const { setCheckOpenWithBooks } = useLibraryStore();
- const listenedOpenWithBooks = useRef(false);
- const isFirstWindow = async () => {
- const allWindows = await getAllWindows();
- const currentWindow = getCurrentWindow();
- const sortedWindows = allWindows.sort((a, b) => a.label.localeCompare(b.label));
- return sortedWindows[0]?.label === currentWindow.label;
- };
+ useEffect(() => {
+ if (!isTauriAppPlatform() || !appService) return;
- const handleOpenWithFileUrl = async (urls: string[]) => {
- console.log('Handle Open with URL:', urls);
- const filePaths = [];
- for (let url of urls) {
- if (url.startsWith('file://')) {
- if (appService?.isIOSApp) {
- url = decodeURI(url);
- } else {
- url = decodeURI(url.replace('file://', ''));
+ const isFirstWindow = async () => {
+ const allWindows = await getAllWindows();
+ const currentWindow = getCurrentWindow();
+ const sortedWindows = allWindows.sort((a, b) => a.label.localeCompare(b.label));
+ return sortedWindows[0]?.label === currentWindow.label;
+ };
+
+ const handle = async (urls: string[]) => {
+ const filePaths: string[] = [];
+ for (let url of urls) {
+ if (url.startsWith('file://')) {
+ url = appService?.isIOSApp ? decodeURI(url) : decodeURI(url.replace('file://', ''));
+ }
+ if (!/^(https?:|data:|blob:|readest:)/i.test(url)) {
+ filePaths.push(url);
}
}
- if (!/^(https?:|data:|blob:)/i.test(url)) {
- filePaths.push(url);
- }
- }
- if (filePaths.length > 0) {
+ if (filePaths.length === 0) return;
+
const settings = useSettingsStore.getState().settings;
if (appService?.hasWindow && settings.openBookInNewWindow) {
if (await isFirstWindow()) {
@@ -61,70 +56,16 @@ export function useOpenWithBooks() {
setCheckOpenWithBooks(true);
navigateToLibrary(router, `reload=${Date.now()}`);
}
- }
- };
-
- const initializeListeners = async () => {
- return await addPluginListener(
- 'native-bridge',
- 'shared-intent',
- (payload) => {
- console.log('Received shared intent:', payload);
- const { urls } = payload;
- handleOpenWithFileUrl(urls);
- },
- );
- };
-
- useEffect(() => {
- if (!isTauriAppPlatform() || !appService) return;
- if (listenedOpenWithBooks.current) return;
- listenedOpenWithBooks.current = true;
-
- // For Windows/Linux deep link and macOS open-file event
- const unlistenDeeplink = getCurrentWindow().listen(
- 'single-instance',
- ({ payload }) => {
- console.log('Received deep link:', payload);
- const { args } = payload;
- if (args?.[1]) {
- handleOpenWithFileUrl([args[1]]);
- }
- },
- );
-
- // macOS in-app open-files event
- const unlistenOpenFiles = getCurrentWindow().listen(
- 'open-files',
- ({ payload }) => {
- console.log('Received open files:', payload);
- const { files } = payload;
- if (files && files.length > 0) {
- handleOpenWithFileUrl(files);
- }
- },
- );
-
- // For Android "Share to Readest" intent
- let unlistenSharedIntent: Promise | null = null;
- // FIXME: register/unregister plugin listeniner on iOS might cause app freeze for unknown reason
- // so we only register it on Android for now to support "Shared to Readest" feature
- if (appService?.isAndroidApp) {
- unlistenSharedIntent = initializeListeners();
- }
-
- // iOS Open with URL event
- const listenOpenWithFiles = async () => {
- return await onOpenUrl((urls) => {
- handleOpenWithFileUrl(urls);
- });
};
- const unlistenOpenUrl = listenOpenWithFiles();
+
+ const onIncoming = (event: CustomEvent) => {
+ const { urls } = event.detail as { urls: string[] };
+ handle(urls);
+ };
+ eventDispatcher.on('app-incoming-url', onIncoming);
+
return () => {
- unlistenDeeplink.then((f) => f());
- unlistenOpenFiles.then((f) => f());
- unlistenOpenUrl.then((f) => f());
- unlistenSharedIntent?.then((f) => f.unregister());
+ eventDispatcher.off('app-incoming-url', onIncoming);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [appService]);
diff --git a/apps/readest-app/src/store/readerStore.ts b/apps/readest-app/src/store/readerStore.ts
index 3448f991..98c60a52 100644
--- a/apps/readest-app/src/store/readerStore.ts
+++ b/apps/readest-app/src/store/readerStore.ts
@@ -41,7 +41,13 @@ interface ViewState {
ttsEnabled: boolean;
syncing: boolean;
gridInsets: Insets | null;
- /* View settings for the view:
+ /* True while the reader is showing a position requested by an external
+ deep link (e.g. ?cfi=...) that the user hasn't yet confirmed by reading.
+ Progress writers (auto-save, cloud sync, kosync) skip while this is true
+ so the user's actual last-read position isn't overwritten by a preview.
+ Cleared on the first user-initiated relocate (page turn / scroll). */
+ previewMode: boolean;
+ /* View settings for the view:
generally view settings have a hierarchy of global settings < book settings < view settings
view settings for primary view are saved to book config which is persisted to config file
omitting settings that are not changed from global settings */
@@ -87,6 +93,7 @@ interface ReaderStore {
getGridInsets: (key: string) => Insets | null;
setGridInsets: (key: string, insets: Insets | null) => void;
setViewInited: (key: string, inited: boolean) => void;
+ setPreviewMode: (key: string, previewMode: boolean) => void;
recreateViewer: (envConfig: EnvConfigType, key: string) => void;
}
@@ -145,6 +152,7 @@ export const useReaderStore = create((set, get) => ({
ttsEnabled: false,
syncing: false,
gridInsets: null,
+ previewMode: false,
viewSettings: null,
},
},
@@ -274,6 +282,7 @@ export const useReaderStore = create((set, get) => ({
ttsEnabled: false,
syncing: false,
gridInsets: null,
+ previewMode: false,
viewSettings: { ...globalViewSettings, ...configViewSettings },
},
},
@@ -297,6 +306,7 @@ export const useReaderStore = create((set, get) => ({
ttsEnabled: false,
syncing: false,
gridInsets: null,
+ previewMode: false,
viewSettings: null,
},
},
@@ -476,6 +486,17 @@ export const useReaderStore = create((set, get) => ({
},
})),
+ setPreviewMode: (key: string, previewMode: boolean) =>
+ set((state) => ({
+ viewStates: {
+ ...state.viewStates,
+ [key]: {
+ ...state.viewStates[key]!,
+ previewMode,
+ },
+ },
+ })),
+
recreateViewer: (envConfig: EnvConfigType, key: string) => {
const id = key.split('-')[0]!;
get()