9180767ba4
Tapping an EPUB in the system file browser and choosing Readest could
silently fail to open the book in two distinct scenarios on Android:
1. Cold launch — the system delivers the ACTION_VIEW intent to
onCreate / onNewIntent before the JS layer has finished hydrating
and called addPluginListener('native-bridge', 'shared-intent', ...).
The upstream Tauri Plugin.trigger() drops events when the per-event
listener list is empty, so the intent vanishes. Fix this in
NativeBridgePlugin by queueing emits whose event has no listener,
then overriding registerListener so the queue is drained whenever
a listener becomes available.
2. React strict-mode re-mount — useAppUrlIngress had a one-shot
listened.current ref guard meant to avoid double registration. In
strict mode (and any subsequent effect re-run) the cleanup
unregister()'d the underlying native plugin listener but the next
mount short-circuited on the ref and never re-registered. The
shared-intent listener list ended up empty for the rest of the
session, so any subsequent Open-with intent went into the queue and
never came out. Drop the guard and let the effect register on every
mount; cleanup balances each registration.
123 lines
4.8 KiB
TypeScript
123 lines
4.8 KiB
TypeScript
import { useEffect } 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[];
|
|
/**
|
|
* Android-only. Distinguishes "Open with Readest" (`VIEW` — the user
|
|
* tapped a file in their file browser and chose Readest) from "Send to
|
|
* Readest" (`SEND` / `SEND_MULTIPLE` — share-sheet capture). We forward
|
|
* it on the `app-incoming-url` event so consumers can pick the right
|
|
* import strategy: VIEW should open the file directly without writing
|
|
* it to the library, SEND should ingest it like a sync capture.
|
|
*/
|
|
action?: 'VIEW' | 'SEND';
|
|
}
|
|
|
|
/**
|
|
* 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();
|
|
|
|
useEffect(() => {
|
|
if (!isTauriAppPlatform() || !appService) return;
|
|
// Note: removed an old `listened.current` ref guard that tried to
|
|
// make this effect a one-shot. In React strict mode (dev) the effect
|
|
// mounts → cleans up → mounts again. With the guard, the second
|
|
// mount short-circuited (ref still true) and DID NOT re-register
|
|
// any listeners — but the previous cleanup had already
|
|
// `unregister()`ed the underlying native plugin listener
|
|
// (NativeBridgePlugin's listeners["shared-intent"] map). Net result:
|
|
// the app ended up with zero shared-intent listeners on the native
|
|
// side, so any "Open with Readest" intent that arrived AFTER cold
|
|
// start was silently dropped (event got queued by our pending-events
|
|
// workaround but, with no future register call, never replayed).
|
|
// Letting the effect re-run on every mount cycle keeps the JS-side
|
|
// and native-side listener bookkeeping in lockstep.
|
|
|
|
const dispatch = (urls: string[], action?: 'VIEW' | 'SEND') => {
|
|
if (!urls.length) return;
|
|
console.log('App incoming URL:', urls, 'action:', action);
|
|
eventDispatcher.dispatch('app-incoming-url', { urls, action });
|
|
};
|
|
|
|
const unlistenSingleInstance = getCurrentWindow().listen<SingleInstancePayload>(
|
|
'single-instance',
|
|
({ payload }) => {
|
|
const url = payload.args?.[1];
|
|
if (url) dispatch([url]);
|
|
},
|
|
);
|
|
|
|
const unlistenOpenFiles = getCurrentWindow().listen<OpenFilesPayload>(
|
|
'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<PluginListener> | null = null;
|
|
if (appService?.isAndroidApp) {
|
|
unlistenSharedIntent = addPluginListener<SharedIntentPayload>(
|
|
'native-bridge',
|
|
'shared-intent',
|
|
(payload) => {
|
|
if (payload.urls?.length) dispatch(payload.urls, payload.action);
|
|
},
|
|
);
|
|
}
|
|
|
|
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]);
|
|
}
|