From 52dae0feed4296739b1b3000d6493fc5ab788083 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Tue, 16 Dec 2025 14:36:33 +0800 Subject: [PATCH] fix(iOS): failed to share screenshot related to https://github.com/logseq/db-test/issues/594 --- .../ShareViewController.swift | 60 +++++++++++++++++++ src/main/frontend/mobile/intent.cljs | 44 ++++++++++++-- 2 files changed, 99 insertions(+), 5 deletions(-) diff --git a/ios/App/ShareViewController/ShareViewController.swift b/ios/App/ShareViewController/ShareViewController.swift index 9645da0e58..ec8414c04b 100644 --- a/ios/App/ShareViewController/ShareViewController.swift +++ b/ios/App/ShareViewController/ShareViewController.swift @@ -50,6 +50,21 @@ class ShareViewController: UIViewController { return copyFileUrl } + private func saveData(_ data: Data, fileExtension: String) -> URL? { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd-HH-mm-ss" + let filename = dateFormatter.string(from: Date()) + "." + fileExtension + + let copyFileUrl = groupContainerUrl!.appendingPathComponent(filename) + do { + try data.write(to: copyFileUrl) + return copyFileUrl + } catch { + print(error.localizedDescription) + return nil + } + } + // Screenshots, shared images from some system App are passed as UIImage func saveUIImage(_ image: UIImage) -> URL? { let dateFormatter = DateFormatter() @@ -89,6 +104,28 @@ class ShareViewController: UIViewController { return res } + // Some shares (notably system screenshots) can come through as a file URL item. + fileprivate func handleTypeFileUrl(_ attachment: NSItemProvider) + async throws -> SharedResource + { + let results = try await attachment.loadItem(forTypeIdentifier: kUTTypeFileURL as String, options: nil) + let url = results as! URL? + + var res = SharedResource() + + if let url, url.isFileURL { + res.name = url.lastPathComponent + res.ext = url.pathExtension.lowercased() + res.type = url.pathExtensionAsMimeType() + res.url = createSharedFileUrl(url) + } else if let url { + res.name = url.absoluteString + res.type = "text/plain" + } + + return res + } + fileprivate func handleTypeText(_ attachment: NSItemProvider) async throws -> SharedResource? { @@ -127,6 +164,22 @@ class ShareViewController: UIViewController { res.ext = "png" res.name = res.url?.lastPathComponent res.type = res.url?.pathExtensionAsMimeType() + case let data as Data: + let ext: String + if attachment.hasItemConformingToTypeIdentifier(UTType.png.identifier) { + ext = "png" + } else if attachment.hasItemConformingToTypeIdentifier(UTType.jpeg.identifier) { + ext = "jpg" + } else if attachment.hasItemConformingToTypeIdentifier(UTType.heic.identifier) { + ext = "heic" + } else { + ext = "png" + } + + res.url = self.saveData(data, fileExtension: ext) + res.ext = ext + res.name = res.url?.lastPathComponent + res.type = res.url?.pathExtensionAsMimeType() case let url as URL: res.name = url.lastPathComponent res.ext = url.pathExtension.lowercased() @@ -155,6 +208,10 @@ class ShareViewController: UIViewController { taskGroup.addTask { return try await self.handleTypeUrl(attachment) } + } else if attachment.hasItemConformingToTypeIdentifier(kUTTypeFileURL as String) { + taskGroup.addTask { + return try await self.handleTypeFileUrl(attachment) + } } else if attachment.hasItemConformingToTypeIdentifier(kUTTypeText as String) { taskGroup.addTask { return try await self.handleTypeText(attachment) @@ -167,6 +224,9 @@ class ShareViewController: UIViewController { taskGroup.addTask { return try await self.handleTypeImage(attachment) } + } else { + // Useful for diagnosing shares that don't match the legacy kUTType checks. + print("Unhandled attachment types:", attachment.registeredTypeIdentifiers) } } } diff --git a/src/main/frontend/mobile/intent.cljs b/src/main/frontend/mobile/intent.cljs index a837e636e7..42a74bf6f4 100644 --- a/src/main/frontend/mobile/intent.cljs +++ b/src/main/frontend/mobile/intent.cljs @@ -24,6 +24,35 @@ [logseq.common.util :as common-util] [promesa.core :as p])) +(defn- normalize-native-file-path + "Normalize iOS shared file URLs to paths that Capacitor Filesystem can read. + iOS share extensions commonly provide `file://` URLs." + [url] + (let [url (some-> url common-util/safe-decode-uri-component)] + (cond + (string/blank? url) + url + + (string/starts-with? url "file://") + (subs url (count "file://")) + + ;; Some Capacitor APIs may provide `_capacitor_file_` URLs. + (string/starts-with? url "capacitor://localhost/_capacitor_file_") + (string/replace url "capacitor://localhost/_capacitor_file_" "") + + :else + url))) + +(defn- (.readFile Filesystem #js {:path path}) + (p/catch (fn [error] + ;; Fallback to the original string for older plugin versions. + (if (= path url) + (p/rejected error) + (.readFile Filesystem #js {:path url}))))))) + (defn open-or-share-file "Share file to mobile platform" [uri] @@ -85,7 +114,7 @@ (defn- embed-asset-file [url _format] (p/let [basename (node-path/basename url) - file (.readFile Filesystem #js {:path url}) + file ( file (.-data)) file (some-> file-base64-str (util/base64string-to-unit8array) (vector) (clj->js) (js/File. basename #js {})) @@ -107,7 +136,7 @@ (config/get-pages-directory) (str (js/encodeURI (fs-util/file-name-sanity title :markdown)) (node-path/extname url))) _ (p/catch - (.copy Filesystem (clj->js {:from url :to path})) + (.copy Filesystem (clj->js {:from (normalize-native-file-path url) :to path})) (fn [error] (log/error :copy-file-error {:error error}))) url (ref/->page-ref title) @@ -124,7 +153,10 @@ (p/let [{:keys [url]} result page (or (state/get-current-page) (string/lower-case (date/journal-name))) format (db/get-page-format page)] - (embed-asset-file url format))) + (-> (embed-asset-file url format) + (p/catch (fn [error] + (log/error :share-import-media-failed {:error error :url url}) + (notification/show! "Failed to import the shared media. Please try again." :error false)))))) (defn- handle-received-application [result] (p/let [{:keys [title url type]} result @@ -173,14 +205,16 @@ (-> (p/let [basename (node-path/basename url) _label (-> basename util/node-path.name) _path (assets-handler/get-asset-path basename) - file (.readFile Filesystem #js {:path url}) + file ( file (.-data)) file (some-> file-base64-str (util/base64string-to-unit8array) (vector) (clj->js) (js/File. basename #js {})) result (editor-handler/db-based-save-assets! (state/get-current-repo) [file] {})] result) - (p/catch #(log/error :handle-asset-file %)))) + (p/catch (fn [error] + (log/error :handle-asset-file {:error error :url url}) + (notification/show! "Failed to import the shared file. Please try again." :error false))))) (defn- handle-payload-resource [{:keys [type name ext url] :as resource} format]