From 62b5ed81380186d77508e5ca5902de7f3ec3e296 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Fri, 22 May 2026 00:50:33 +0800 Subject: [PATCH] feat(send): handle shared URLs from system share sheets (iOS + Android) (#4256) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Users can now tap "Share → Readest" in Safari, Chrome, or any other browser on iOS / Android and the article URL flows through the same clip-and-import pipeline the in-app "From Web URL" entry uses. Android `MainActivity.handleIncomingIntent` already routed file shares via `ACTION_SEND` + `EXTRA_STREAM`. Extend it to also pick up URL shares via `ACTION_SEND` + `EXTRA_TEXT`: parse the first http(s) token out of the text payload and dispatch it on the existing `shared-intent` event channel. No new event channel needed — `useAppUrlIngress` already listens and re-broadcasts as `app-incoming-url`. The existing `` for `ACTION_SEND` with `*/*` MIME type already accepts `text/plain` from browsers — no manifest change required. iOS `gen/apple/` gains a new ShareExtension target. The extension's `ShareViewController` extracts a URL from `NSExtensionContext.inputItems` (prefers `public.url`, falls back to first http(s) token in `public.plain-text`) and forwards it to the main app as `readest://clip?url=` via the responder-chain `openURL:` selector — the standard share-extension trick used by Pocket, Instapaper, Matter, etc. `project.yml` adds the ShareExtension target and switches the main app's Info.plist / entitlements references to `INFOPLIST_FILE` / `CODE_SIGN_ENTITLEMENTS` build settings instead of xcodegen's `info:` / `entitlements:` blocks. That way the hand-tuned `Readest_iOS/Info.plist` (CFBundleDocumentTypes, UTExportedTypeDeclarations, locales, CFBundleURLTypes for readest://, applesignin, associated-domains for Universal Links) is treated as an opaque input — xcodegen won't regenerate it. JS New `useClipUrlIngress` hook subscribes to `app-incoming-url`, unwraps `readest://clip?url=` into the inner URL (the iOS forwarding path), filters out file URIs and annotation deep links, and runs each remaining http(s) URL through `clip_url` → `convertToEpubWithWorker` → `ingestFile` — the same path `/send` uses. Mounted alongside `useOpenWithBooks` and `useOpenAnnotationLink` in both `app/library/page.tsx` and `app/reader/page.tsx` so shares arriving while the user is reading still process. Notes - The PR targets `feat/send-clip-mobile` (PR #4252) since the share pipeline depends on `clip_url` being available on mobile. - iOS Share Extension built locally via xcodegen; the regenerated pbxproj is tracked because gen/apple is gitignored. Co-authored-by: Claude Opus 4.7 (1M context) --- apps/readest-app/package.json | 2 +- .../.well-known/apple-app-site-association | 4 + .../com/bilingify/readest/MainActivity.kt | 34 +- .../apple/Readest.xcodeproj/project.pbxproj | 679 ++++++++++++++++++ .../Readest_iOS/Readest_iOS.entitlements | 18 + .../gen/apple/ShareExtension/Info.plist | 41 ++ .../ShareExtension.entitlements | 10 + .../ShareExtension/ShareViewController.swift | 215 ++++++ .../src-tauri/gen/apple/project.yml | 164 +++++ apps/readest-app/src/app/library/page.tsx | 2 + apps/readest-app/src/app/reader/page.tsx | 2 + .../src/hooks/useClipUrlIngress.ts | 141 ++++ 12 files changed, 1310 insertions(+), 2 deletions(-) create mode 100644 apps/readest-app/src-tauri/gen/apple/Readest.xcodeproj/project.pbxproj create mode 100644 apps/readest-app/src-tauri/gen/apple/Readest_iOS/Readest_iOS.entitlements create mode 100644 apps/readest-app/src-tauri/gen/apple/ShareExtension/Info.plist create mode 100644 apps/readest-app/src-tauri/gen/apple/ShareExtension/ShareExtension.entitlements create mode 100644 apps/readest-app/src-tauri/gen/apple/ShareExtension/ShareViewController.swift create mode 100644 apps/readest-app/src-tauri/gen/apple/project.yml create mode 100644 apps/readest-app/src/hooks/useClipUrlIngress.ts diff --git a/apps/readest-app/package.json b/apps/readest-app/package.json index b160e312..53eed086 100644 --- a/apps/readest-app/package.json +++ b/apps/readest-app/package.json @@ -15,7 +15,7 @@ "start-web:vinext": "dotenv -e .env.web -- vinext start", "build-tauri": "dotenv -e .env.tauri -- next build", "dev-android": "tauri android build -t aarch64 -- --features devtools && adb install -r src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release.apk", - "dev-ios": "tauri ios build -- --features devtools && ideviceinstaller -i src-tauri/gen/apple/build/arm64/Readest.ipa", + "dev-ios": "tauri ios build -- --features devtools && ideviceinstaller install src-tauri/gen/apple/build/arm64/Readest.ipa", "i18n:extract": "i18next-scanner --config i18next-scanner.config.cjs", "lint": "tsgo --noEmit && biome lint . && pnpm lint:lua", "lint:lua": "node ../readest.koplugin/scripts/lint-koplugin.mjs", diff --git a/apps/readest-app/public/.well-known/apple-app-site-association b/apps/readest-app/public/.well-known/apple-app-site-association index e58baabd..253e5699 100644 --- a/apps/readest-app/public/.well-known/apple-app-site-association +++ b/apps/readest-app/public/.well-known/apple-app-site-association @@ -17,6 +17,10 @@ { "/": "/s/*", "comment": "Matches book share links" + }, + { + "/": "/clip*", + "comment": "Matches Share-Extension URL handoff: /clip?url=..." } ] } diff --git a/apps/readest-app/src-tauri/gen/android/app/src/main/java/com/bilingify/readest/MainActivity.kt b/apps/readest-app/src-tauri/gen/android/app/src/main/java/com/bilingify/readest/MainActivity.kt index bbc93836..9f8d73a6 100644 --- a/apps/readest-app/src-tauri/gen/android/app/src/main/java/com/bilingify/readest/MainActivity.kt +++ b/apps/readest-app/src-tauri/gen/android/app/src/main/java/com/bilingify/readest/MainActivity.kt @@ -261,7 +261,15 @@ class MainActivity : TauriActivity(), KeyDownInterceptor { when (intent.action) { Intent.ACTION_SEND -> { if (intent.type != null) { - handleSingleFile(intent) + // Browsers share article URLs as ACTION_SEND with + // type "text/plain" and the URL in EXTRA_TEXT. File + // shares (epub, pdf, etc.) put a content:// URI in + // EXTRA_STREAM. Try text first; on a miss fall back + // to the file path so the existing behaviour is + // preserved. + if (!handleSharedText(intent)) { + handleSingleFile(intent) + } } } Intent.ACTION_SEND_MULTIPLE -> { @@ -272,6 +280,30 @@ class MainActivity : TauriActivity(), KeyDownInterceptor { } } + /** + * Read the first http(s) URL out of `EXTRA_TEXT` and emit it on the + * existing `shared-intent` event so the JS layer can clip it through + * the same path that handles file shares. + * + * Returns true when a URL was found and dispatched; false when there + * was no usable URL so the caller can try the file-share path. + */ + private fun handleSharedText(intent: Intent): Boolean { + val text = intent.getStringExtra(Intent.EXTRA_TEXT)?.trim() ?: return false + // Browsers usually share just the URL, but some prepend a page + // title or a tracker preamble. Pick the first http(s) token. + val url = text.split(Regex("\\s+")) + .firstOrNull { it.startsWith("http://") || it.startsWith("https://") } + ?: return false + val payload = JSObject().apply { + val urls = JSArray() + urls.put(url) + put("urls", urls) + } + NativeBridgePlugin.getInstance()?.triggerEvent("shared-intent", payload) + return true + } + private fun handleSingleFile(intent: Intent) { val uri = intent.getParcelableExtra(Intent.EXTRA_STREAM) uri?.let { fileUri -> diff --git a/apps/readest-app/src-tauri/gen/apple/Readest.xcodeproj/project.pbxproj b/apps/readest-app/src-tauri/gen/apple/Readest.xcodeproj/project.pbxproj new file mode 100644 index 00000000..c9e2eb58 --- /dev/null +++ b/apps/readest-app/src-tauri/gen/apple/Readest.xcodeproj/project.pbxproj @@ -0,0 +1,679 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 1792D73C771B1E81C35E6437 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 13DB566659AC0A0F741EBBBF /* Security.framework */; }; + 1F65F6BB7F9A5D5B84F5AC1E /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FB09AA381EE87AED8151864C /* WebKit.framework */; }; + 283677BE23AD394BECB28EBE /* MetalKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5D765B7634BFC9B2D619B82E /* MetalKit.framework */; }; + 4F33997845572B54DBBE431F /* libapp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 576303E7E1596790E0DEF8DC /* libapp.a */; }; + 6C2F2F58E6E25C554C47E527 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2F0000D58F392BC2F808417D /* Assets.xcassets */; }; + 6CD28C6C81C7C367F51FCB70 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A77628B94260724841CB0C1F /* QuartzCore.framework */; }; + 7B3153C771891CB24D874FB8 /* Metal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1C29EFE86C85351F81C99F2A /* Metal.framework */; }; + A3B9B497F2EC434EEEBA2235 /* main.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1CF94262BF7499D2AD8B627D /* main.mm */; }; + A870FB123FF2CAFB98EDCD36 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 57535F4C21FEA405C0A45945 /* CoreGraphics.framework */; }; + BA0A7C5B168460D96FA21D6B /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB595336F6275E2B879AF397 /* ShareViewController.swift */; }; + D608DE39BD2F9FA892508F37 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B62E5ED5797D06981F30B6F /* UIKit.framework */; }; + E6D9510F89AD18B18D0FCBC9 /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = E5F4A6E57CB4895FFFBC2DE8 /* ShareExtension.appex */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + F6003A5A70DA4C33BDBD838B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 30D8E7F6023C1DF41F8BD47C /* LaunchScreen.storyboard */; }; + FB2C2290EAFB8BA38C973E4B /* assets in Resources */ = {isa = PBXBuildFile; fileRef = 40409827389F82307433878B /* assets */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 03BA6A76BE384252922805BF /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 4DC84354F14AFCCC05EC3197 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 44B76E0EA5B592540156EFE5; + remoteInfo = ShareExtension; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + A279EE551117F7AC6F6762E7 /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + E6D9510F89AD18B18D0FCBC9 /* ShareExtension.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 092369AA44558E8949D2757A /* mod.rs */ = {isa = PBXFileReference; path = mod.rs; sourceTree = ""; }; + 0EB90B3B3C88D7C6ABEDF11E /* menu.rs */ = {isa = PBXFileReference; path = menu.rs; sourceTree = ""; }; + 0F0D4492F8DDE94F4DC4854F /* safari_auth.rs */ = {isa = PBXFileReference; path = safari_auth.rs; sourceTree = ""; }; + 13DB566659AC0A0F741EBBBF /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; + 1A5492F8B4749D670A93DE66 /* clip_url.rs */ = {isa = PBXFileReference; path = clip_url.rs; sourceTree = ""; }; + 1C29EFE86C85351F81C99F2A /* Metal.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Metal.framework; path = System/Library/Frameworks/Metal.framework; sourceTree = SDKROOT; }; + 1CF94262BF7499D2AD8B627D /* main.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = main.mm; sourceTree = ""; }; + 1F45B5B458F63D98B6113A19 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 27B7FEAE989ABFF6C8FF0F77 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 282EE616A1F2B81C6121C9B6 /* transfer_file.rs */ = {isa = PBXFileReference; path = transfer_file.rs; sourceTree = ""; }; + 2E226C87AD0606FFF9DDEE11 /* dir_scanner.rs */ = {isa = PBXFileReference; path = dir_scanner.rs; sourceTree = ""; }; + 2F0000D58F392BC2F808417D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 30D8E7F6023C1DF41F8BD47C /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; + 32229C1C4618F3DE22D7DA83 /* bindings.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = bindings.h; sourceTree = ""; }; + 40409827389F82307433878B /* assets */ = {isa = PBXFileReference; lastKnownFileType = folder; path = assets; sourceTree = SOURCE_ROOT; }; + 4924DDD171577C52C0C5809E /* ShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ShareExtension.entitlements; sourceTree = ""; }; + 4B62E5ED5797D06981F30B6F /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; + 4C4A7AE08BDD49657A0C749E /* mod.rs */ = {isa = PBXFileReference; path = mod.rs; sourceTree = ""; }; + 57535F4C21FEA405C0A45945 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; + 576303E7E1596790E0DEF8DC /* libapp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libapp.a; sourceTree = ""; }; + 5D765B7634BFC9B2D619B82E /* MetalKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MetalKit.framework; path = System/Library/Frameworks/MetalKit.framework; sourceTree = SDKROOT; }; + 661E6D88481643D5928A8853 /* discord_rpc.rs */ = {isa = PBXFileReference; path = discord_rpc.rs; sourceTree = ""; }; + 7DADD61846736B0E638BF012 /* Readest_iOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Readest_iOS.entitlements; sourceTree = ""; }; + 983AD7B7F8D8B9BF527EF05D /* traffic_light.rs */ = {isa = PBXFileReference; path = traffic_light.rs; sourceTree = ""; }; + 9B22460180952B7C3F11DE9D /* main.rs */ = {isa = PBXFileReference; path = main.rs; sourceTree = ""; }; + A02C074BB1C537B8020DB593 /* system_dictionary.rs */ = {isa = PBXFileReference; path = system_dictionary.rs; sourceTree = ""; }; + A7318D4877A991A1DAB79861 /* lib.rs */ = {isa = PBXFileReference; path = lib.rs; sourceTree = ""; }; + A77628B94260724841CB0C1F /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; + B16A12C78C47C4990EB4A028 /* apple_auth.rs */ = {isa = PBXFileReference; path = apple_auth.rs; sourceTree = ""; }; + CB595336F6275E2B879AF397 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; }; + D3F4A4DF56E899C9DB0B7D60 /* eink.rs */ = {isa = PBXFileReference; path = eink.rs; sourceTree = ""; }; + DAC68A4107C30410409E5506 /* Readest_iOS.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = Readest_iOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; + E5F4A6E57CB4895FFFBC2DE8 /* ShareExtension.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + FB09AA381EE87AED8151864C /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; + FF84C225AF4E94849713D59C /* mod.rs */ = {isa = PBXFileReference; path = mod.rs; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 13234A71E754A795C45E44EC /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4F33997845572B54DBBE431F /* libapp.a in Frameworks */, + A870FB123FF2CAFB98EDCD36 /* CoreGraphics.framework in Frameworks */, + 7B3153C771891CB24D874FB8 /* Metal.framework in Frameworks */, + 283677BE23AD394BECB28EBE /* MetalKit.framework in Frameworks */, + 6CD28C6C81C7C367F51FCB70 /* QuartzCore.framework in Frameworks */, + 1792D73C771B1E81C35E6437 /* Security.framework in Frameworks */, + D608DE39BD2F9FA892508F37 /* UIKit.framework in Frameworks */, + 1F65F6BB7F9A5D5B84F5AC1E /* WebKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 14DF549AD83CD73CFFB27C6D /* Readest */ = { + isa = PBXGroup; + children = ( + 1CF94262BF7499D2AD8B627D /* main.mm */, + 9C2B108362B7772D6E89122F /* bindings */, + ); + path = Readest; + sourceTree = ""; + }; + 241834D53E673450176D63E0 /* Sources */ = { + isa = PBXGroup; + children = ( + 14DF549AD83CD73CFFB27C6D /* Readest */, + ); + path = Sources; + sourceTree = ""; + }; + 2D6F041DFA6B062224902B98 /* Products */ = { + isa = PBXGroup; + children = ( + DAC68A4107C30410409E5506 /* Readest_iOS.app */, + E5F4A6E57CB4895FFFBC2DE8 /* ShareExtension.appex */, + ); + name = Products; + sourceTree = ""; + }; + 2D86805A2877EA3ADDDC28F8 /* android */ = { + isa = PBXGroup; + children = ( + D3F4A4DF56E899C9DB0B7D60 /* eink.rs */, + 092369AA44558E8949D2757A /* mod.rs */, + ); + path = android; + sourceTree = ""; + }; + 3ABE44D21683D1A3A9053FF4 /* macos */ = { + isa = PBXGroup; + children = ( + B16A12C78C47C4990EB4A028 /* apple_auth.rs */, + 0EB90B3B3C88D7C6ABEDF11E /* menu.rs */, + 4C4A7AE08BDD49657A0C749E /* mod.rs */, + 0F0D4492F8DDE94F4DC4854F /* safari_auth.rs */, + A02C074BB1C537B8020DB593 /* system_dictionary.rs */, + 983AD7B7F8D8B9BF527EF05D /* traffic_light.rs */, + ); + path = macos; + sourceTree = ""; + }; + 60924896DC53876DC5E00FC4 /* Externals */ = { + isa = PBXGroup; + children = ( + ); + path = Externals; + sourceTree = ""; + }; + 9C2B108362B7772D6E89122F /* bindings */ = { + isa = PBXGroup; + children = ( + 32229C1C4618F3DE22D7DA83 /* bindings.h */, + ); + path = bindings; + sourceTree = ""; + }; + 9D615C0A1CC543EF4AE91D05 = { + isa = PBXGroup; + children = ( + 40409827389F82307433878B /* assets */, + 2F0000D58F392BC2F808417D /* Assets.xcassets */, + 30D8E7F6023C1DF41F8BD47C /* LaunchScreen.storyboard */, + 60924896DC53876DC5E00FC4 /* Externals */, + ADF310BD2F14DA462EF79E53 /* Readest_iOS */, + A350AB745ED478D99A088984 /* ShareExtension */, + 241834D53E673450176D63E0 /* Sources */, + C86DD901A804E29C9D5A85A0 /* src */, + C5040CC68EE16BB47F46BFEB /* Frameworks */, + 2D6F041DFA6B062224902B98 /* Products */, + ); + sourceTree = ""; + }; + A350AB745ED478D99A088984 /* ShareExtension */ = { + isa = PBXGroup; + children = ( + 27B7FEAE989ABFF6C8FF0F77 /* Info.plist */, + 4924DDD171577C52C0C5809E /* ShareExtension.entitlements */, + CB595336F6275E2B879AF397 /* ShareViewController.swift */, + ); + path = ShareExtension; + sourceTree = ""; + }; + ADF310BD2F14DA462EF79E53 /* Readest_iOS */ = { + isa = PBXGroup; + children = ( + 1F45B5B458F63D98B6113A19 /* Info.plist */, + 7DADD61846736B0E638BF012 /* Readest_iOS.entitlements */, + ); + path = Readest_iOS; + sourceTree = ""; + }; + C41C744CD45746D97A7E1DC3 /* windows */ = { + isa = PBXGroup; + children = ( + FF84C225AF4E94849713D59C /* mod.rs */, + ); + path = windows; + sourceTree = ""; + }; + C5040CC68EE16BB47F46BFEB /* Frameworks */ = { + isa = PBXGroup; + children = ( + 57535F4C21FEA405C0A45945 /* CoreGraphics.framework */, + 576303E7E1596790E0DEF8DC /* libapp.a */, + 1C29EFE86C85351F81C99F2A /* Metal.framework */, + 5D765B7634BFC9B2D619B82E /* MetalKit.framework */, + A77628B94260724841CB0C1F /* QuartzCore.framework */, + 13DB566659AC0A0F741EBBBF /* Security.framework */, + 4B62E5ED5797D06981F30B6F /* UIKit.framework */, + FB09AA381EE87AED8151864C /* WebKit.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + C86DD901A804E29C9D5A85A0 /* src */ = { + isa = PBXGroup; + children = ( + 1A5492F8B4749D670A93DE66 /* clip_url.rs */, + 2E226C87AD0606FFF9DDEE11 /* dir_scanner.rs */, + 661E6D88481643D5928A8853 /* discord_rpc.rs */, + A7318D4877A991A1DAB79861 /* lib.rs */, + 9B22460180952B7C3F11DE9D /* main.rs */, + 282EE616A1F2B81C6121C9B6 /* transfer_file.rs */, + 2D86805A2877EA3ADDDC28F8 /* android */, + 3ABE44D21683D1A3A9053FF4 /* macos */, + C41C744CD45746D97A7E1DC3 /* windows */, + ); + name = src; + path = ../../src; + sourceTree = ""; + }; + "TEMP_9CD1A90B-7A94-4ADB-B0C8-40427CCB2DCF" /* arm64 */ = { + isa = PBXGroup; + children = ( + ); + path = arm64; + sourceTree = ""; + }; + "TEMP_BBED8FED-FDB6-46D2-A460-69DB6036C32D" /* x86_64 */ = { + isa = PBXGroup; + children = ( + ); + path = x86_64; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 004FF73D61EDB0623C953EBF /* Readest_iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8181C35DF0A6425D50B70F74 /* Build configuration list for PBXNativeTarget "Readest_iOS" */; + buildPhases = ( + 1C7AC10FC5ACD89D8003DF53 /* Build Rust Code */, + 30C7D79143BD14DFDA5E1062 /* Sources */, + D688C250839BE2493EC2C660 /* Resources */, + 13234A71E754A795C45E44EC /* Frameworks */, + A279EE551117F7AC6F6762E7 /* Embed Foundation Extensions */, + ); + buildRules = ( + ); + dependencies = ( + 36709209B4F223291AF5B857 /* PBXTargetDependency */, + ); + name = Readest_iOS; + packageProductDependencies = ( + ); + productName = Readest_iOS; + productReference = DAC68A4107C30410409E5506 /* Readest_iOS.app */; + productType = "com.apple.product-type.application"; + }; + 44B76E0EA5B592540156EFE5 /* ShareExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 34F2EE036FDE625F6654A1DF /* Build configuration list for PBXNativeTarget "ShareExtension" */; + buildPhases = ( + 4474D64BA33AEDC957661F2E /* Sources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ShareExtension; + packageProductDependencies = ( + ); + productName = ShareExtension; + productReference = E5F4A6E57CB4895FFFBC2DE8 /* ShareExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 4DC84354F14AFCCC05EC3197 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1430; + TargetAttributes = { + 004FF73D61EDB0623C953EBF = { + DevelopmentTeam = J5W48D69VR; + }; + 44B76E0EA5B592540156EFE5 = { + DevelopmentTeam = J5W48D69VR; + }; + }; + }; + buildConfigurationList = B1D240E8E88B6626B5836EE7 /* Build configuration list for PBXProject "Readest" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + Base, + en, + ); + mainGroup = 9D615C0A1CC543EF4AE91D05; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = 2D6F041DFA6B062224902B98 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 004FF73D61EDB0623C953EBF /* Readest_iOS */, + 44B76E0EA5B592540156EFE5 /* ShareExtension */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + D688C250839BE2493EC2C660 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6C2F2F58E6E25C554C47E527 /* Assets.xcassets in Resources */, + F6003A5A70DA4C33BDBD838B /* LaunchScreen.storyboard in Resources */, + FB2C2290EAFB8BA38C973E4B /* assets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 1C7AC10FC5ACD89D8003DF53 /* Build Rust Code */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Build Rust Code"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(SRCROOT)/Externals/x86_64/${CONFIGURATION}/libapp.a", + "$(SRCROOT)/Externals/arm64/${CONFIGURATION}/libapp.a", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "pnpm tauri ios xcode-script -v --platform ${PLATFORM_DISPLAY_NAME:?} --sdk-root ${SDKROOT:?} --framework-search-paths \"${FRAMEWORK_SEARCH_PATHS:?}\" --header-search-paths \"${HEADER_SEARCH_PATHS:?}\" --gcc-preprocessor-definitions \"${GCC_PREPROCESSOR_DEFINITIONS:-}\" --configuration ${CONFIGURATION:?} ${FORCE_COLOR} ${ARCHS:?}"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 30C7D79143BD14DFDA5E1062 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A3B9B497F2EC434EEEBA2235 /* main.mm in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4474D64BA33AEDC957661F2E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BA0A7C5B168460D96FA21D6B /* ShareViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 36709209B4F223291AF5B857 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 44B76E0EA5B592540156EFE5 /* ShareExtension */; + targetProxy = 03BA6A76BE384252922805BF /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 04ED2C8C8903E9E56E98A711 /* debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + ARCHS = ( + arm64, + ); + CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION = YES; + CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; + DEVELOPMENT_TEAM = J5W48D69VR; + ENABLE_BITCODE = NO; + "EXCLUDED_ARCHS[sdk=iphoneos*]" = x86_64; + INFOPLIST_FILE = ShareExtension/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.bilingify.readest.ShareExtension; + PRODUCT_NAME = ShareExtension; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALID_ARCHS = arm64; + }; + name = debug; + }; + 2A8D4045800079AA24F37354 /* debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ARCHS = ( + arm64, + ); + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = Readest_iOS/Readest_iOS.entitlements; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = J5W48D69VR; + EMBEDDED_CONTENT_CONTAINS_SWIFT_CODE = YES; + ENABLE_BITCODE = NO; + "EXCLUDED_ARCHS[sdk=iphoneos*]" = x86_64; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "\".\"", + ); + INFOPLIST_FILE = Readest_iOS/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + "LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)"; + "LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)"; + PRODUCT_BUNDLE_IDENTIFIER = com.bilingify.readest; + PRODUCT_NAME = Readest; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALID_ARCHS = arm64; + }; + name = debug; + }; + 356947E2563DA47502A8C4EF /* debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "DEBUG=1", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = debug; + }; + 3F6A0A6F9FE76F726D0186B1 /* release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ARCHS = ( + arm64, + ); + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = Readest_iOS/Readest_iOS.entitlements; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = J5W48D69VR; + EMBEDDED_CONTENT_CONTAINS_SWIFT_CODE = YES; + ENABLE_BITCODE = NO; + "EXCLUDED_ARCHS[sdk=iphoneos*]" = x86_64; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "\".\"", + ); + INFOPLIST_FILE = Readest_iOS/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + "LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)"; + "LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)"; + PRODUCT_BUNDLE_IDENTIFIER = com.bilingify.readest; + PRODUCT_NAME = Readest; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALID_ARCHS = arm64; + }; + name = release; + }; + 9B92BAA323FCAA6E4D43A905 /* release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + }; + name = release; + }; + C8133B6719701AA0FCBFB505 /* release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + ARCHS = ( + arm64, + ); + CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION = YES; + CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; + DEVELOPMENT_TEAM = J5W48D69VR; + ENABLE_BITCODE = NO; + "EXCLUDED_ARCHS[sdk=iphoneos*]" = x86_64; + INFOPLIST_FILE = ShareExtension/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.bilingify.readest.ShareExtension; + PRODUCT_NAME = ShareExtension; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALID_ARCHS = arm64; + }; + name = release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 34F2EE036FDE625F6654A1DF /* Build configuration list for PBXNativeTarget "ShareExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 04ED2C8C8903E9E56E98A711 /* debug */, + C8133B6719701AA0FCBFB505 /* release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = debug; + }; + 8181C35DF0A6425D50B70F74 /* Build configuration list for PBXNativeTarget "Readest_iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2A8D4045800079AA24F37354 /* debug */, + 3F6A0A6F9FE76F726D0186B1 /* release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = debug; + }; + B1D240E8E88B6626B5836EE7 /* Build configuration list for PBXProject "Readest" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 356947E2563DA47502A8C4EF /* debug */, + 9B92BAA323FCAA6E4D43A905 /* release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = debug; + }; +/* End XCConfigurationList section */ + }; + rootObject = 4DC84354F14AFCCC05EC3197 /* Project object */; +} diff --git a/apps/readest-app/src-tauri/gen/apple/Readest_iOS/Readest_iOS.entitlements b/apps/readest-app/src-tauri/gen/apple/Readest_iOS/Readest_iOS.entitlements new file mode 100644 index 00000000..59462039 --- /dev/null +++ b/apps/readest-app/src-tauri/gen/apple/Readest_iOS/Readest_iOS.entitlements @@ -0,0 +1,18 @@ + + + + + com.apple.developer.applesignin + + Default + + com.apple.developer.associated-domains + + applinks:web.readest.com + + com.apple.security.application-groups + + group.com.bilingify.readest + + + \ No newline at end of file diff --git a/apps/readest-app/src-tauri/gen/apple/ShareExtension/Info.plist b/apps/readest-app/src-tauri/gen/apple/ShareExtension/Info.plist new file mode 100644 index 00000000..05269d2b --- /dev/null +++ b/apps/readest-app/src-tauri/gen/apple/ShareExtension/Info.plist @@ -0,0 +1,41 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Readest + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + NSExtension + + NSExtensionAttributes + + NSExtensionActivationRule + + NSExtensionActivationSupportsText + + NSExtensionActivationSupportsWebURLWithMaxCount + 1 + + + NSExtensionPointIdentifier + com.apple.share-services + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).ShareViewController + + + diff --git a/apps/readest-app/src-tauri/gen/apple/ShareExtension/ShareExtension.entitlements b/apps/readest-app/src-tauri/gen/apple/ShareExtension/ShareExtension.entitlements new file mode 100644 index 00000000..1f712d42 --- /dev/null +++ b/apps/readest-app/src-tauri/gen/apple/ShareExtension/ShareExtension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.com.bilingify.readest + + + diff --git a/apps/readest-app/src-tauri/gen/apple/ShareExtension/ShareViewController.swift b/apps/readest-app/src-tauri/gen/apple/ShareExtension/ShareViewController.swift new file mode 100644 index 00000000..50461395 --- /dev/null +++ b/apps/readest-app/src-tauri/gen/apple/ShareExtension/ShareViewController.swift @@ -0,0 +1,215 @@ +// Share Extension for Readest: catches an article URL from any iOS share +// sheet (Safari, Chrome, third-party browsers) and forwards it to the +// main app via the existing `readest://` URL scheme. The main app's +// `tauri-plugin-deep-link` integration emits an `onOpenUrl` event, +// `useAppUrlIngress` re-broadcasts it as `app-incoming-url`, and +// `useClipUrlIngress` clips + ingests the article through the same +// pipeline the in-app "From Web URL" entry uses. + +import UIKit +import UniformTypeIdentifiers + +final class ShareViewController: UIViewController { + + // Single-shot: avoid double-firing if iOS re-presents the extension. + private var didCompleteOnce = false + + override func viewDidLoad() { + super.viewDidLoad() + NSLog("[ReadestShare] viewDidLoad") + // Kick the work as early as possible — iOS 26 sometimes dismisses + // the extension before `viewDidAppear` fires when the activation + // rule matches a single URL exactly. Running from `viewDidLoad` + // gives us the longest possible window. + Task { await processInput() } + } + + /// Walk the inputItems for a URL or text payload and forward it. + /// Cancels the extension if no URL was found. + private func processInput() async { + NSLog("[ReadestShare] processInput started") + guard let context = extensionContext else { + NSLog("[ReadestShare] no extensionContext, bailing") + return + } + let items = (context.inputItems.compactMap { $0 as? NSExtensionItem }) + NSLog("[ReadestShare] inputItems count=\(items.count)") + + let url = await firstShareableURL(from: items) + if let url = url { + NSLog("[ReadestShare] found URL: \(url.absoluteString)") + await openInMainApp(url: url) + } else { + NSLog("[ReadestShare] no URL found in any inputItem") + } + await MainActor.run { + if !self.didCompleteOnce { + self.didCompleteOnce = true + NSLog("[ReadestShare] completing extension request") + context.completeRequest(returningItems: [], completionHandler: nil) + } + } + } + + /// Probe attachments for the first usable URL. Prefers a real + /// `public.url` attachment; falls back to scanning a `public.plain-text` + /// payload for an http(s) substring (some apps share "Title\nURL"). + private func firstShareableURL(from items: [NSExtensionItem]) async -> URL? { + for (itemIdx, item) in items.enumerated() { + guard let attachments = item.attachments else { continue } + NSLog("[ReadestShare] item[\(itemIdx)] has \(attachments.count) attachments") + for (attIdx, attachment) in attachments.enumerated() { + NSLog( + "[ReadestShare] attachment[\(attIdx)] types: \(attachment.registeredTypeIdentifiers)") + if attachment.hasItemConformingToTypeIdentifier(UTType.url.identifier) { + if let url = try? await loadURL(from: attachment), Self.isHttp(url) { + return url + } + } + if attachment.hasItemConformingToTypeIdentifier(UTType.plainText.identifier) { + if let text = try? await loadText(from: attachment), + let url = Self.extractHTTPURL(from: text) + { + return url + } + } + } + } + return nil + } + + private func loadURL(from provider: NSItemProvider) async throws -> URL? { + let item = try await provider.loadItem(forTypeIdentifier: UTType.url.identifier, options: nil) + if let url = item as? URL { return url } + if let str = item as? String { return URL(string: str) } + if let data = item as? Data, + let str = String(data: data, encoding: .utf8) + { + return URL(string: str) + } + return nil + } + + private func loadText(from provider: NSItemProvider) async throws -> String? { + let item = try await provider.loadItem( + forTypeIdentifier: UTType.plainText.identifier, options: nil) + if let text = item as? String { return text } + if let data = item as? Data { return String(data: data, encoding: .utf8) } + return nil + } + + private static func isHttp(_ url: URL) -> Bool { + let scheme = url.scheme?.lowercased() ?? "" + return scheme == "http" || scheme == "https" + } + + private static func extractHTTPURL(from text: String) -> URL? { + for token in text.split(whereSeparator: { $0.isWhitespace }) { + let s = String(token) + if s.hasPrefix("http://") || s.hasPrefix("https://") { + if let url = URL(string: s), isHttp(url) { return url } + } + } + return nil + } + + /// Open Readest with the article URL. Tries three paths in order: + /// + /// 1. `extensionContext.open(_:)` against `readest://clip?url=...`. + /// Apple's sanctioned share-extension → host-app handoff for + /// custom URL schemes. The main app's `tauri-plugin-deep-link` + /// catches the `readest://` scheme. + /// 2. `extensionContext.open(_:)` against the Universal Link + /// `https://web.readest.com/clip?url=...`. Only works when the + /// web.readest.com AASA file claims `/clip` for the app — + /// currently it likely doesn't, but tried as a defensive + /// fallback in case (1) fails on some iOS version. + /// 3. Responder-chain `openURL:` trick. iOS 26 silently blocks + /// this even when `responds(to: selector)` returns true and + /// the responder is `UIApplication`, so it's purely a + /// last-ditch attempt for older iOS. + /// + /// Inner URL gets RFC-3986 percent-encoded so the outer URL's + /// query parser sees exactly one `?` (separating outer query from + /// outer path) and exactly one `=` per param. URLComponents alone + /// is NOT enough — its `.urlQueryAllowed` set permits `=`, `?`, + /// `&` and leaves them unescaped, which silently breaks the deep- + /// link parse (the inner URL's first `?s=...` gets promoted to an + /// outer query param). All log messages use the `%@` format + /// specifier with the URL passed as an argument so `printf`'s + /// percent-spec parser doesn't try to interpret the percent-encoded + /// characters in the URL. + @MainActor + private func openInMainApp(url: URL) async { + // 1. Custom URL scheme via extensionContext.open — the modern + // sanctioned path. + if let target = buildTargetURL(scheme: "readest", host: "clip", inner: url) { + NSLog("[ReadestShare] trying custom scheme via extensionContext: %@", target.absoluteString) + if await openViaExtensionContext(target) { + NSLog("[ReadestShare] custom scheme open succeeded") + return + } + NSLog("[ReadestShare] custom scheme open failed") + } + + // 2. Universal Link via extensionContext.open. + if let target = buildTargetURL( + scheme: "https", host: "web.readest.com", path: "/clip", inner: url) + { + NSLog("[ReadestShare] trying universal link: %@", target.absoluteString) + if await openViaExtensionContext(target) { + NSLog("[ReadestShare] universal link open succeeded") + return + } + NSLog("[ReadestShare] universal link open failed") + } + + // 3. Responder-chain — usually blocked on iOS 26 but tried for + // completeness so older devices still get the handoff. + if let target = buildTargetURL(scheme: "readest", host: "clip", inner: url) { + NSLog("[ReadestShare] trying responder-chain: %@", target.absoluteString) + openViaResponderChain(target) + } + } + + /// Build a target URL like `://?url=`. + /// Hand-encodes the inner URL against the RFC 3986 "unreserved" set + /// (alnum + `-._~`) so every URL-significant character — including + /// `?`, `&`, `=`, `:`, `/`, `#` — gets percent-encoded. See + /// `openInMainApp` for why URLComponents alone is insufficient. + private func buildTargetURL(scheme: String, host: String, path: String = "", inner: URL) -> URL? + { + let unreserved = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-._~")) + guard + let encoded = inner.absoluteString.addingPercentEncoding(withAllowedCharacters: unreserved) + else { return nil } + return URL(string: "\(scheme)://\(host)\(path)?url=\(encoded)") + } + + @MainActor + private func openViaExtensionContext(_ url: URL) async -> Bool { + await withCheckedContinuation { continuation in + guard let ctx = extensionContext else { + continuation.resume(returning: false) + return + } + ctx.open(url, completionHandler: { success in + continuation.resume(returning: success) + }) + } + } + + private func openViaResponderChain(_ url: URL) { + var responder: UIResponder? = self + let selector = sel_registerName("openURL:") + while let r = responder { + if r.responds(to: selector) { + _ = r.perform(selector, with: url) + NSLog("[ReadestShare] responder-chain openURL: invoked on \(type(of: r))") + return + } + responder = r.next + } + NSLog("[ReadestShare] responder-chain found no responder for openURL:") + } +} diff --git a/apps/readest-app/src-tauri/gen/apple/project.yml b/apps/readest-app/src-tauri/gen/apple/project.yml new file mode 100644 index 00000000..b9040a37 --- /dev/null +++ b/apps/readest-app/src-tauri/gen/apple/project.yml @@ -0,0 +1,164 @@ +name: Readest +options: + bundleIdPrefix: com.bilingify.readest + deploymentTarget: + iOS: 15.0 +fileGroups: [../../src] +configs: + debug: debug + release: release +settingGroups: + app: + base: + PRODUCT_NAME: Readest + PRODUCT_BUNDLE_IDENTIFIER: com.bilingify.readest + DEVELOPMENT_TEAM: J5W48D69VR +targetTemplates: + app: + type: application + sources: + - path: Sources + scheme: + environmentVariables: + RUST_BACKTRACE: full + RUST_LOG: info + settings: + groups: [app] +targets: + Readest_iOS: + type: application + platform: iOS + # NOTE on In-App Purchase capability + # ---------------------------------- + # IAP is enabled at three layers: + # 1. App ID `com.bilingify.readest` services (Apple Developer + # portal) — must have "In-App Purchase" service ON. + # 2. Provisioning profile — pulled automatically by Xcode when + # DEVELOPMENT_TEAM is set; inherits IAP from the App ID. + # 3. StoreKit framework — linked transitively by the + # tauri-plugin-native-bridge plugin's iap_* commands. + # The `SystemCapabilities` pbxproj entry that Xcode writes when you + # add IAP via Signing & Capabilities is informational only; it + # doesn't gate the actual capability. xcodegen 2.45 can't + # round-trip nested dictionaries into pbxproj attributes (it + # serializes them as quoted strings), so we omit the entry rather + # than ship a malformed one. IAP works without it. + sources: + - path: Sources + - path: Assets.xcassets + # Only reference Externals as a file group (no compile/resource + # phases) — the actual libapp.a lookup happens via the + # `framework: libapp.a` dependency + LIBRARY_SEARCH_PATHS pointing + # at `Externals/arm64/$(CONFIGURATION)`. Listing the whole folder + # as a source caused xcodegen to add copy-resource phases for + # both `debug/libapp.a` and `release/libapp.a`, conflicting at + # build time: + # "Multiple commands produce Readest.app/libapp.a" + - path: Externals + buildPhase: none + - path: Readest_iOS + - path: assets + buildPhase: resources + type: folder + - path: LaunchScreen.storyboard + # Info.plist and entitlements live on disk with hand-tuned content + # (CFBundleDocumentTypes, UTExportedTypeDeclarations, locales, + # CFBundleURLTypes for readest://, com.apple.developer.applesignin, + # associated-domains for Universal Links). Reference them via + # INFOPLIST_FILE / CODE_SIGN_ENTITLEMENTS instead of `info:` / + # `entitlements:` so xcodegen treats them as opaque inputs and + # doesn't rewrite them on regeneration. + scheme: + environmentVariables: + RUST_BACKTRACE: full + RUST_LOG: info + settings: + base: + INFOPLIST_FILE: Readest_iOS/Info.plist + CODE_SIGN_ENTITLEMENTS: Readest_iOS/Readest_iOS.entitlements + ENABLE_BITCODE: false + ARCHS: [arm64] + VALID_ARCHS: arm64 + LIBRARY_SEARCH_PATHS[arch=x86_64]: $(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME) + LIBRARY_SEARCH_PATHS[arch=arm64]: $(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME) + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES: true + # Tell Xcode the app embeds Swift code (the new ShareExtension + # is a Swift app-extension target). Without this the + # swift-stdlib-tool phase that copies libswiftCore.dylib + + # friends into Readest.app/Frameworks runs incorrectly under + # Xcode 26 — the app launches and immediately dyld-fails with + # "libswiftCore.dylib: no such file". + EMBEDDED_CONTENT_CONTAINS_SWIFT_CODE: YES + EXCLUDED_ARCHS[sdk=iphoneos*]: x86_64 + groups: [app] + dependencies: + - framework: libapp.a + embed: false + - target: ShareExtension + embed: true + codeSign: true + - sdk: CoreGraphics.framework + - sdk: Metal.framework + - sdk: MetalKit.framework + - sdk: QuartzCore.framework + - sdk: Security.framework + - sdk: UIKit.framework + - sdk: WebKit.framework + preBuildScripts: + - script: pnpm tauri ios xcode-script -v --platform ${PLATFORM_DISPLAY_NAME:?} --sdk-root ${SDKROOT:?} --framework-search-paths "${FRAMEWORK_SEARCH_PATHS:?}" --header-search-paths "${HEADER_SEARCH_PATHS:?}" --gcc-preprocessor-definitions "${GCC_PREPROCESSOR_DEFINITIONS:-}" --configuration ${CONFIGURATION:?} ${FORCE_COLOR} ${ARCHS:?} + name: Build Rust Code + basedOnDependencyAnalysis: false + outputFiles: + - $(SRCROOT)/Externals/x86_64/${CONFIGURATION}/libapp.a + - $(SRCROOT)/Externals/arm64/${CONFIGURATION}/libapp.a + + # Share-sheet extension that lets users send article URLs from Safari / + # Chrome / etc. directly to Readest. The extension grabs the URL via + # NSExtensionContext, encodes it into `readest://share?url=...`, and + # opens the main app — which catches the deep-link via + # `tauri-plugin-deep-link` and runs the clip-and-import pipeline. + ShareExtension: + type: app-extension + platform: iOS + sources: + - path: ShareExtension + info: + path: ShareExtension/Info.plist + properties: + CFBundleDisplayName: Readest + NSExtension: + NSExtensionPointIdentifier: com.apple.share-services + NSExtensionPrincipalClass: $(PRODUCT_MODULE_NAME).ShareViewController + NSExtensionAttributes: + NSExtensionActivationRule: + NSExtensionActivationSupportsWebURLWithMaxCount: 1 + NSExtensionActivationSupportsText: true + # Entitlements referenced via CODE_SIGN_ENTITLEMENTS build setting + # rather than xcodegen's `entitlements:` block — otherwise xcodegen + # may overwrite the file (even with no properties declared) during + # regeneration, and Xcode rejects the build with + # "Entitlements file ... was modified during the build" + settings: + base: + PRODUCT_NAME: ShareExtension + PRODUCT_BUNDLE_IDENTIFIER: com.bilingify.readest.ShareExtension + DEVELOPMENT_TEAM: J5W48D69VR + CODE_SIGN_ENTITLEMENTS: ShareExtension/ShareExtension.entitlements + # Permit Xcode's productPackagingUtility to write the auto-derived + # signing entitlements (application-identifier, team-identifier, + # get-task-allow) back to the source entitlements file at build + # time. Without this, Xcode 16+ refuses the modification it + # itself just performed and the build fails with: + # "Entitlements file ... was modified during the build" + # Safe for this target because the entitlements file is empty — + # the only modifications Xcode performs are the team / app-id + # auto-population every iOS extension target needs anyway. + CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION: YES + SWIFT_VERSION: "5.0" + IPHONEOS_DEPLOYMENT_TARGET: 15.0 + ARCHS: [arm64] + VALID_ARCHS: arm64 + EXCLUDED_ARCHS[sdk=iphoneos*]: x86_64 + ENABLE_BITCODE: false + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES: false + SKIP_INSTALL: true diff --git a/apps/readest-app/src/app/library/page.tsx b/apps/readest-app/src/app/library/page.tsx index 451e43d3..fd44a71e 100644 --- a/apps/readest-app/src/app/library/page.tsx +++ b/apps/readest-app/src/app/library/page.tsx @@ -45,6 +45,7 @@ import { useAppUrlIngress } from '@/hooks/useAppUrlIngress'; import { useOpenWithBooks } from '@/hooks/useOpenWithBooks'; import { useOpenAnnotationLink } from '@/hooks/useOpenAnnotationLink'; import { useOpenShareLink } from '@/hooks/useOpenShareLink'; +import { useClipUrlIngress } from '@/hooks/useClipUrlIngress'; import { useKeyDownActions } from '@/hooks/useKeyDownActions'; import { SelectedFile, useFileSelector } from '@/hooks/useFileSelector'; import { lockScreenOrientation, selectDirectory } from '@/utils/bridge'; @@ -227,6 +228,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP useOpenWithBooks(); useOpenAnnotationLink(); useOpenShareLink(); + useClipUrlIngress(); useTransferQueue(libraryLoaded); const { pullLibrary, pushLibrary } = useBooksSync(); diff --git a/apps/readest-app/src/app/reader/page.tsx b/apps/readest-app/src/app/reader/page.tsx index 500ca740..b437ec60 100644 --- a/apps/readest-app/src/app/reader/page.tsx +++ b/apps/readest-app/src/app/reader/page.tsx @@ -7,6 +7,7 @@ import { useAppUrlIngress } from '@/hooks/useAppUrlIngress'; import { useOpenWithBooks } from '@/hooks/useOpenWithBooks'; import { useOpenAnnotationLink } from '@/hooks/useOpenAnnotationLink'; import { useOpenShareLink } from '@/hooks/useOpenShareLink'; +import { useClipUrlIngress } from '@/hooks/useClipUrlIngress'; import { useSettingsStore } from '@/store/settingsStore'; import { checkForAppUpdates, checkAppReleaseNotes } from '@/helpers/updater'; import { tauriHandleSetAlwaysOnTop } from '@/utils/window'; @@ -22,6 +23,7 @@ export default function Page() { useOpenWithBooks(); useOpenAnnotationLink(); useOpenShareLink(); + useClipUrlIngress(); useEffect(() => { const doCheckAppUpdates = async () => { diff --git a/apps/readest-app/src/hooks/useClipUrlIngress.ts b/apps/readest-app/src/hooks/useClipUrlIngress.ts new file mode 100644 index 00000000..7716ffe0 --- /dev/null +++ b/apps/readest-app/src/hooks/useClipUrlIngress.ts @@ -0,0 +1,141 @@ +import { useCallback, useEffect, useRef } from 'react'; +import { invoke } from '@tauri-apps/api/core'; +import { useAuth } from '@/context/AuthContext'; +import { useEnv } from '@/context/EnvContext'; +import { useLibraryStore } from '@/store/libraryStore'; +import { useSettingsStore } from '@/store/settingsStore'; +import { isTauriAppPlatform } from '@/services/environment'; +import { ingestFile } from '@/services/ingestService'; +import { convertToEpubWithWorker } from '@/services/send/conversion/conversionWorker'; +import { getClipOptions } from '@/services/send/clipOptions'; +import { eventDispatcher } from '@/utils/event'; +import { parseAnnotationDeepLink } from '@/utils/deeplink'; +import { useTranslation } from './useTranslation'; + +/** + * Handle "Share to Readest" article URLs from the OS share sheet + * (Safari, Chrome, etc.). Consumes the `app-incoming-url` event published + * by `useAppUrlIngress`, filters URLs that look like article links, and + * runs them through the same clip → EPUB → import pipeline the `/send` + * page uses. + * + * Filter rules — only act on URLs that are: + * - http(s) (not file://, content://, readest://, blob:, data:) + * - NOT an annotation deep link (those go to useOpenAnnotationLink and + * would otherwise be double-handled) + * + * Failures surface as toasts. Successful clips show "Saving article…" + * then "Saved to your library." once `ingestFile` completes. + * + * Mount this hook alongside `useAppUrlIngress` (same places as + * `useOpenWithBooks` and `useOpenAnnotationLink`) so the ingress + * dispatcher is running when URLs arrive. + */ +export function useClipUrlIngress() { + const _ = useTranslation(); + const { envConfig, appService } = useEnv(); + const { user } = useAuth(); + const inflight = useRef>(new Set()); + + const clipAndImport = useCallback( + async (url: string) => { + if (!appService) return; + if (inflight.current.has(url)) return; + inflight.current.add(url); + + eventDispatcher.dispatch('toast', { + type: 'info', + message: _('Saving article from share…'), + timeout: 2500, + }); + + try { + const html = await invoke('clip_url', { + url, + options: getClipOptions(_), + }); + const book = await convertToEpubWithWorker({ kind: 'page', html, url }); + const { library } = useLibraryStore.getState(); + const { settings } = useSettingsStore.getState(); + const ingested = await ingestFile( + { file: book.file, books: library, forceUpload: true }, + { appService, settings, isLoggedIn: !!user }, + ); + if (!ingested) { + throw new Error('Import produced no book'); + } + await useLibraryStore.getState().updateBooks(envConfig, [ingested]); + eventDispatcher.dispatch('toast', { + type: 'success', + message: _('Saved “{{title}}” to your library.', { + title: ingested.title || book.title || url, + }), + timeout: 3000, + }); + } catch (err) { + const detail = + err instanceof Error + ? err.message + : typeof err === 'string' + ? err + : _('Could not fetch this page'); + eventDispatcher.dispatch('toast', { + type: 'error', + message: detail, + timeout: 3500, + }); + } finally { + inflight.current.delete(url); + } + }, + [_, appService, envConfig, user], + ); + + useEffect(() => { + if (!isTauriAppPlatform() || !appService) return; + + const handle = (url: string) => { + // iOS Share Extension forwards URLs to the main app in one of + // two shapes — both are unwrapped to the inner article URL so + // we can share the http(s) clip path with the Android side: + // + // - Universal Link (primary): + // https://web.readest.com/clip?url= + // - Custom URL scheme (fallback): + // readest://clip?url= + const isClipUrl = + url.startsWith('readest://clip?') || + url.startsWith('readest://clip/') || + /^https:\/\/web\.readest\.com\/clip(?:[/?].*)?$/i.test(url); + if (isClipUrl) { + try { + const inner = new URL(url).searchParams.get('url'); + if (inner) { + url = inner; + } else { + return; + } + } catch { + return; + } + } + // Only act on http(s). file://, content://, blob: and data: belong + // to other consumers (or aren't shareable URLs). + if (!/^https?:\/\//i.test(url)) return; + // Annotation deep links can come over https (web.readest.com). + // Skip them — useOpenAnnotationLink owns that path. + if (parseAnnotationDeepLink(url)) return; + void clipAndImport(url); + }; + + const onIncoming = (event: CustomEvent) => { + const { urls } = event.detail as { urls: string[] }; + urls.forEach(handle); + }; + eventDispatcher.on('app-incoming-url', onIncoming); + + return () => { + eventDispatcher.off('app-incoming-url', onIncoming); + }; + }, [appService, clipAndImport]); +}