From baa907e72f4a3341b11977876a612f7ea0f581f3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 8 Nov 2026 09:28:12 -0500 Subject: [PATCH] Mobile: full-text chip (M19) with tutorial and disclaimer Pages with a DOI or PMID (found on the phone with the capture script, nothing sent) show a Full text button. Tapping it looks the DOI up with Unpaywall (the person's email) and OpenAlex (the person's free API key), merges copies (PDFs and published versions first; version, licence, where), and always offers LibKey ("Through your library") and doi.org. Sven's decision (option a): each person adds their own email and key in Settings, kept in the keychain. Settings and the sheet show a set-up tutorial and a "What NODE sends" disclaimer; off by default. Requests go through core/net (cached, rate limited); cache keys leave out email/key. Only mobile/ changes. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01X9wDmS5VvrWmyHhcWA9MoL --- mobile/App.js | 24 +++++ mobile/README.md | 2 + mobile/package.json | 2 +- mobile/src/components/FullTextSheet.js | 139 +++++++++++++++++++++++++ mobile/src/components/SettingsSheet.js | 22 +++- mobile/src/fulltext.js | 106 +++++++++++++++++++ mobile/src/services.js | 19 +++- mobile/test/fulltext.test.js | 104 ++++++++++++++++++ 8 files changed, 415 insertions(+), 3 deletions(-) create mode 100644 mobile/src/components/FullTextSheet.js create mode 100644 mobile/src/fulltext.js create mode 100644 mobile/test/fulltext.test.js diff --git a/mobile/App.js b/mobile/App.js index 4dd3f21..8202d2c 100644 --- a/mobile/App.js +++ b/mobile/App.js @@ -17,6 +17,7 @@ import TabsSheet from "./src/components/TabsSheet"; import AddressSuggestions from "./src/components/AddressSuggestions"; import AskSheet from "./src/components/AskSheet"; import TrailHome from "./src/components/TrailHome"; +import FullTextSheet from "./src/components/FullTextSheet"; import { captureRef } from "react-native-view-shot"; import { Directory, File, Paths } from "expo-file-system"; import SettingsSheet from "./src/components/SettingsSheet"; @@ -155,6 +156,12 @@ function Browser({ services }) { } catch { return; } + if (msg.type === "capinfo" && String(msg.id).startsWith("ft:")) { + // M19: the page's DOI or PMID, found on the phone; nothing is sent. + const ids = Capture.findIds(msg); + updateTab(tabId, { doi: ids.doi || "", pmid: ids.pmid || "" }); + return; + } if (msg.type === "capinfo") { const p = pending.current.get(msg.id); if (!p) return; @@ -206,6 +213,7 @@ function Browser({ services }) { // new load, so onLoadEnd does not run: start the new page's media here. if (before && !nav.loading && before.url && nav.url !== before.url) { if (lastSelection.current.tabId === tabId) lastSelection.current = { tabId: null, url: "", text: "" }; + updateTab(tabId, { doi: "", pmid: "" }); services.history.record(nav.url, nav.title); const tab = withRule({ ...before, url: nav.url }); refs.current.get(tabId)?.injectJavaScript(autoplayCall(tab.rule.autoplay)); @@ -279,6 +287,7 @@ function Browser({ services }) { playback.navigated(tabId); // media of the previous page will not resume view.injectJavaScript(autoplayCall(tab.rule.autoplay)); if (tab.rule.center !== false) view.injectJavaScript(CENTER_ON_TAP); + view.injectJavaScript(Capture.captureInfoCall(`ft:${tabId}`)); // DOI for the Full text button if (tabId !== activeRef.current) { if (!soundAllRef.current) view.injectJavaScript(MUTE_ALL); // only the active tab plays sound return; @@ -545,6 +554,21 @@ function Browser({ services }) { /> setSheet(null)} services={services} /> + {(active?.doi || active?.pmid) && !home && !sheet ? ( + +