Bladeren bron

fix(iOS): failed to share screenshot

related to https://github.com/logseq/db-test/issues/594
Tienson Qin 3 dagen geleden
bovenliggende
commit
52dae0feed
2 gewijzigde bestanden met toevoegingen van 99 en 5 verwijderingen
  1. 60 0
      ios/App/ShareViewController/ShareViewController.swift
  2. 39 5
      src/main/frontend/mobile/intent.cljs

+ 60 - 0
ios/App/ShareViewController/ShareViewController.swift

@@ -50,6 +50,21 @@ class ShareViewController: UIViewController {
         return copyFileUrl
         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
     // Screenshots, shared images from some system App are passed as UIImage
     func saveUIImage(_ image: UIImage) -> URL? {
     func saveUIImage(_ image: UIImage) -> URL? {
         let dateFormatter = DateFormatter()
         let dateFormatter = DateFormatter()
@@ -89,6 +104,28 @@ class ShareViewController: UIViewController {
         return res
         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)
     fileprivate func handleTypeText(_ attachment: NSItemProvider)
     async throws -> SharedResource?
     async throws -> SharedResource?
     {
     {
@@ -127,6 +164,22 @@ class ShareViewController: UIViewController {
             res.ext = "png"
             res.ext = "png"
             res.name = res.url?.lastPathComponent
             res.name = res.url?.lastPathComponent
             res.type = res.url?.pathExtensionAsMimeType()
             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:
         case let url as URL:
             res.name = url.lastPathComponent
             res.name = url.lastPathComponent
             res.ext = url.pathExtension.lowercased()
             res.ext = url.pathExtension.lowercased()
@@ -155,6 +208,10 @@ class ShareViewController: UIViewController {
                                 taskGroup.addTask {
                                 taskGroup.addTask {
                                     return try await self.handleTypeUrl(attachment)
                                     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) {
                             } else if attachment.hasItemConformingToTypeIdentifier(kUTTypeText as String) {
                                 taskGroup.addTask {
                                 taskGroup.addTask {
                                     return try await self.handleTypeText(attachment)
                                     return try await self.handleTypeText(attachment)
@@ -167,6 +224,9 @@ class ShareViewController: UIViewController {
                                 taskGroup.addTask {
                                 taskGroup.addTask {
                                     return try await self.handleTypeImage(attachment)
                                     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)
                             }
                             }
                         }
                         }
                     }
                     }

+ 39 - 5
src/main/frontend/mobile/intent.cljs

@@ -24,6 +24,35 @@
             [logseq.common.util :as common-util]
             [logseq.common.util :as common-util]
             [promesa.core :as p]))
             [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- <filesystem-read-file
+  [url]
+  (let [path (normalize-native-file-path url)]
+    (-> (.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
 (defn open-or-share-file
   "Share file to mobile platform"
   "Share file to mobile platform"
   [uri]
   [uri]
@@ -85,7 +114,7 @@
 
 
 (defn- embed-asset-file [url _format]
 (defn- embed-asset-file [url _format]
   (p/let [basename (node-path/basename url)
   (p/let [basename (node-path/basename url)
-          file (.readFile Filesystem #js {:path url})
+          file (<filesystem-read-file url)
           file-base64-str (some-> file (.-data))
           file-base64-str (some-> file (.-data))
           file (some-> file-base64-str (util/base64string-to-unit8array)
           file (some-> file-base64-str (util/base64string-to-unit8array)
                        (vector) (clj->js) (js/File. basename #js {}))
                        (vector) (clj->js) (js/File. basename #js {}))
@@ -107,7 +136,7 @@
                                (config/get-pages-directory)
                                (config/get-pages-directory)
                                (str (js/encodeURI (fs-util/file-name-sanity title :markdown)) (node-path/extname url)))
                                (str (js/encodeURI (fs-util/file-name-sanity title :markdown)) (node-path/extname url)))
           _ (p/catch
           _ (p/catch
-             (.copy Filesystem (clj->js {:from url :to path}))
+             (.copy Filesystem (clj->js {:from (normalize-native-file-path url) :to path}))
              (fn [error]
              (fn [error]
                (log/error :copy-file-error {:error error})))
                (log/error :copy-file-error {:error error})))
           url (ref/->page-ref title)
           url (ref/->page-ref title)
@@ -124,7 +153,10 @@
   (p/let [{:keys [url]} result
   (p/let [{:keys [url]} result
           page (or (state/get-current-page) (string/lower-case (date/journal-name)))
           page (or (state/get-current-page) (string/lower-case (date/journal-name)))
           format (db/get-page-format page)]
           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]
 (defn- handle-received-application [result]
   (p/let [{:keys [title url type]} result
   (p/let [{:keys [title url type]} result
@@ -173,14 +205,16 @@
   (-> (p/let [basename (node-path/basename url)
   (-> (p/let [basename (node-path/basename url)
               _label (-> basename util/node-path.name)
               _label (-> basename util/node-path.name)
               _path (assets-handler/get-asset-path basename)
               _path (assets-handler/get-asset-path basename)
-              file (.readFile Filesystem #js {:path url})
+              file (<filesystem-read-file url)
               file-base64-str (some-> file (.-data))
               file-base64-str (some-> file (.-data))
               file (some-> file-base64-str (util/base64string-to-unit8array)
               file (some-> file-base64-str (util/base64string-to-unit8array)
                            (vector) (clj->js) (js/File. basename #js {}))
                            (vector) (clj->js) (js/File. basename #js {}))
               result (editor-handler/db-based-save-assets!
               result (editor-handler/db-based-save-assets!
                       (state/get-current-repo) [file] {})]
                       (state/get-current-repo) [file] {})]
         result)
         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
 (defn- handle-payload-resource
   [{:keys [type name ext url] :as resource} format]
   [{:keys [type name ext url] :as resource} format]