A recent change (#4407) made Android's "Open with Readest" (VIEW intent, used by Telegram and similar apps) always open the file as a transient book: into the reader but never written to the library, with its filePath pointing at the original content:// URI. Once that temporary URI grant dies the book can no longer be reopened, so the user has to re-share it from the source app every time. Make the transient behavior an opt-out gated by the existing autoImportBooksOnOpen setting, and surface it on mobile: - The VIEW handler now consults the setting via a new shouldOpenTransient predicate. When auto-import is on it falls through to the same library ingest path as a share-sheet SEND (full ingest plus cloud upload); when off it keeps the transient open. - Read the setting from disk in the handler rather than the settings store, since on a cold-start "Open with" the store is not hydrated yet and would wrongly fall back to a transient open. - Show the "Auto Import on File Open" toggle on mobile (was desktop only). - Default autoImportBooksOnOpen to true on mobile so shared files persist and sync by default; the desktop default is unchanged. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,7 +19,7 @@ vi.mock('@tauri-apps/plugin-cli', () => ({
|
||||
getMatches: () => mockGetMatches(),
|
||||
}));
|
||||
|
||||
import { parseOpenWithFiles } from '@/helpers/openWith';
|
||||
import { parseOpenWithFiles, shouldOpenTransient } from '@/helpers/openWith';
|
||||
|
||||
// Helper type matching the AppService subset used in openWith
|
||||
interface MockAppService {
|
||||
@@ -260,3 +260,26 @@ describe('parseOpenWithFiles', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldOpenTransient', () => {
|
||||
// Android "Open with" (VIEW) is the only intent that can open a file as a
|
||||
// transient book, and only when the user has turned auto-import off.
|
||||
test('VIEW with auto-import off opens transiently', () => {
|
||||
expect(shouldOpenTransient('VIEW', false)).toBe(true);
|
||||
});
|
||||
|
||||
test('VIEW with auto-import on imports into the library', () => {
|
||||
expect(shouldOpenTransient('VIEW', true)).toBe(false);
|
||||
});
|
||||
|
||||
// Share-sheet SEND captures always import to the library regardless.
|
||||
test('SEND always imports into the library', () => {
|
||||
expect(shouldOpenTransient('SEND', false)).toBe(false);
|
||||
expect(shouldOpenTransient('SEND', true)).toBe(false);
|
||||
});
|
||||
|
||||
test('undefined action always imports into the library', () => {
|
||||
expect(shouldOpenTransient(undefined, false)).toBe(false);
|
||||
expect(shouldOpenTransient(undefined, true)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -376,7 +376,7 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ onPullLibrary, setIsDropdow
|
||||
onClick={toggleAutoUploadBooks}
|
||||
/>
|
||||
|
||||
{isTauriAppPlatform() && !appService?.isMobile && (
|
||||
{isTauriAppPlatform() && (
|
||||
<MenuItem
|
||||
label={_('Auto Import on File Open')}
|
||||
toggled={isAutoImportBooksOnOpen}
|
||||
|
||||
@@ -60,6 +60,23 @@ const parseIntentOpenWithFiles = async (appService: AppService | null) => {
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Decide whether an "Open with" file intent should open as a transient book
|
||||
* (straight to the reader, no library write) or be imported into the library.
|
||||
*
|
||||
* Only Android's `VIEW` intent (the system "Open with Readest" chooser) can be
|
||||
* transient, and only when the user has turned off "Auto Import on File Open".
|
||||
* Every other case — a share-sheet `SEND` capture, or `VIEW` with auto-import
|
||||
* on — imports the file so it persists in the library (and syncs to the cloud
|
||||
* on mobile).
|
||||
*/
|
||||
export const shouldOpenTransient = (
|
||||
action: 'VIEW' | 'SEND' | undefined,
|
||||
autoImportBooksOnOpen: boolean,
|
||||
): boolean => {
|
||||
return action === 'VIEW' && !autoImportBooksOnOpen;
|
||||
};
|
||||
|
||||
export const parseOpenWithFiles = async (appService: AppService | null) => {
|
||||
if (isWebAppPlatform()) return [];
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@ 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 { isTauriAppPlatform } from '@/services/environment';
|
||||
import { navigateToLibrary, navigateToReader, showLibraryWindow } from '@/utils/nav';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { partialMD5 } from '@/utils/md5';
|
||||
import { shouldOpenTransient } from '@/helpers/openWith';
|
||||
|
||||
/**
|
||||
* Handle "Open with Readest" file imports. Consumes the `app-incoming-url`
|
||||
@@ -20,11 +20,13 @@ import { partialMD5 } from '@/utils/md5';
|
||||
* Mount this hook alongside `useAppUrlIngress` so the ingress dispatcher is
|
||||
* actually running when URLs arrive.
|
||||
*
|
||||
* Two routing modes by `action`:
|
||||
* Two routing modes by `action`, gated by the `autoImportBooksOnOpen` setting
|
||||
* (see `shouldOpenTransient`):
|
||||
*
|
||||
* `'VIEW'` (Android only — user picked Readest from the system "Open with"
|
||||
* chooser for an epub/pdf): we MUST NOT silently import the file into the
|
||||
* library. We first hash the file ourselves and check the in-memory library:
|
||||
* `'VIEW'` with auto-import OFF (Android only — user picked Readest from the
|
||||
* system "Open with" chooser for an epub/pdf): open the file as a transient
|
||||
* book without importing it. We first hash the file ourselves and check the
|
||||
* in-memory library:
|
||||
* - if a non-deleted entry with the same hash already exists, jump
|
||||
* straight into the reader on that entry — no `importBook` call, so the
|
||||
* managed `Books/<hash>/` filePath, createdAt and reading progress are
|
||||
@@ -35,10 +37,11 @@ import { partialMD5 } from '@/utils/md5';
|
||||
* uploading to the cloud, then navigate to the reader on that hash.
|
||||
*
|
||||
* `'SEND'` / undefined (iOS / macOS / desktop / Android share-sheet
|
||||
* capture): keep the existing flow that pushes the URLs through
|
||||
* capture), and `'VIEW'` with auto-import ON: push the URLs through
|
||||
* `window.OPEN_WITH_FILES` so `library/page.tsx::processOpenWithFiles`
|
||||
* does a full ingest + cloud upload — that's the contract a "Send to
|
||||
* Readest" share is meant to honour.
|
||||
* does a full ingest + cloud upload — the file lands in the library and
|
||||
* syncs, which is what a "Send to Readest" share (and an opt-in "Open
|
||||
* with" import) is meant to honour.
|
||||
*/
|
||||
export function useOpenWithBooks() {
|
||||
const router = useRouter();
|
||||
@@ -155,8 +158,18 @@ export function useOpenWithBooks() {
|
||||
const filePaths = normalizeUrls(urls);
|
||||
if (filePaths.length === 0) return;
|
||||
|
||||
// Android "Open with" → straight to reader, no library write.
|
||||
if (action === 'VIEW') {
|
||||
// Read the persisted setting from disk rather than the settings store:
|
||||
// on a cold-start "Open with" the store may not be hydrated yet (it's
|
||||
// seeded by the library page's init effect, which races the queued
|
||||
// intent replay), and an unhydrated store would wrongly fall back to a
|
||||
// transient open for a user who has auto-import on.
|
||||
const settings = await appService.loadSettings();
|
||||
|
||||
// Android "Open with" with auto-import off → straight to reader, no
|
||||
// library write. When auto-import is on, fall through to the library
|
||||
// ingest path below so the file is copied into the managed library
|
||||
// and synced (the default on mobile).
|
||||
if (shouldOpenTransient(action, settings.autoImportBooksOnOpen)) {
|
||||
// If a reader is already mounted, ignore the second tap rather
|
||||
// than try to swap books underneath it. The in-place URL swap
|
||||
// would otherwise leave ReaderContent's init effect (gated by
|
||||
@@ -172,7 +185,6 @@ export function useOpenWithBooks() {
|
||||
return;
|
||||
}
|
||||
|
||||
const settings = useSettingsStore.getState().settings;
|
||||
if (appService?.hasWindow && settings.openBookInNewWindow) {
|
||||
if (await isFirstWindow()) {
|
||||
showLibraryWindow(appService, filePaths);
|
||||
|
||||
@@ -172,6 +172,9 @@ export const DEFAULT_SYSTEM_SETTINGS: Partial<SystemSettings> = {
|
||||
|
||||
export const DEFAULT_MOBILE_SYSTEM_SETTINGS: Partial<SystemSettings> = {
|
||||
libraryColumns: 3,
|
||||
// Import files opened via the system "Open with" chooser into the library by
|
||||
// default so they persist and sync, instead of opening them transiently.
|
||||
autoImportBooksOnOpen: true,
|
||||
};
|
||||
|
||||
export const HIGHLIGHT_COLOR_HEX: Record<HighlightColor, string> = {
|
||||
|
||||
Reference in New Issue
Block a user