Ver Fonte

Merge pull request #7184 from logseq/enhance/ios-folder-picker

enhance: graph select and create on iOS
Tienson Qin há 3 anos atrás
pai
commit
f90e8ee7d5
48 ficheiros alterados com 802 adições e 661 exclusões
  1. 1 1
      deps/graph-parser/src/logseq/graph_parser/util.cljs
  2. 2 2
      ios/App/App/FileContainer.m
  3. 39 37
      ios/App/App/FileContainer.swift
  4. 49 34
      ios/App/App/FolderPicker.swift
  5. 3 2
      ios/App/App/Utils.m
  6. 17 5
      ios/App/App/Utils.swift
  7. 2 1
      package.json
  8. 1 1
      resources/index.html
  9. 4 4
      src/main/frontend/components/block.cljs
  10. 6 16
      src/main/frontend/components/block.css
  11. 1 1
      src/main/frontend/components/encryption.cljs
  12. 1 1
      src/main/frontend/components/file_sync.cljs
  13. 5 1
      src/main/frontend/components/file_sync.css
  14. 2 2
      src/main/frontend/components/header.css
  15. 2 2
      src/main/frontend/components/onboarding.cljs
  16. 10 5
      src/main/frontend/components/onboarding/index.css
  17. 39 35
      src/main/frontend/components/onboarding/setups.cljs
  18. 4 6
      src/main/frontend/components/repo.cljs
  19. 8 9
      src/main/frontend/components/sidebar.cljs
  20. 7 5
      src/main/frontend/components/sidebar.css
  21. 9 27
      src/main/frontend/components/widgets.cljs
  22. 2 19
      src/main/frontend/config.cljs
  23. 2 2
      src/main/frontend/fs.cljs
  24. 1 1
      src/main/frontend/fs/bfs.cljs
  25. 70 50
      src/main/frontend/fs/capacitor_fs.cljs
  26. 1 1
      src/main/frontend/fs/nfs.cljs
  27. 4 4
      src/main/frontend/fs/node.cljs
  28. 1 1
      src/main/frontend/fs/protocol.cljs
  29. 13 9
      src/main/frontend/fs/sync.cljs
  30. 7 19
      src/main/frontend/fs/watcher_handler.cljs
  31. 0 2
      src/main/frontend/handler.cljs
  32. 6 7
      src/main/frontend/handler/common/file.cljs
  33. 65 5
      src/main/frontend/handler/events.cljs
  34. 0 22
      src/main/frontend/handler/file.cljs
  35. 0 86
      src/main/frontend/handler/metadata.cljs
  36. 2 0
      src/main/frontend/handler/notification.cljs
  37. 5 4
      src/main/frontend/handler/page.cljs
  38. 8 55
      src/main/frontend/handler/repo.cljs
  39. 15 5
      src/main/frontend/handler/web/nfs.cljs
  40. 9 3
      src/main/frontend/mobile/core.cljs
  41. 149 0
      src/main/frontend/mobile/graph_picker.cljs
  42. 177 132
      src/main/frontend/mobile/index.css
  43. 11 0
      src/main/frontend/state.cljs
  44. 12 29
      src/main/frontend/ui.cljs
  45. 1 1
      src/main/frontend/ui.css
  46. 4 1
      src/main/frontend/util.cljc
  47. 6 6
      src/test/frontend/fs/capacitor_fs_test.cljs
  48. 19 0
      yarn.lock

+ 1 - 1
deps/graph-parser/src/logseq/graph_parser/util.cljs

@@ -22,7 +22,7 @@
     string))
 
 (defn path-normalize
-  "Normalize file path (for reading paths from FS, not required by writting)
+  "Normalize file path (for reading paths from FS, not required by writing)
    Keep capitalization senstivity"
   [s]
   (.normalize s "NFC"))

+ 2 - 2
ios/App/App/FileContainer.m

@@ -8,5 +8,5 @@
 #import <Capacitor/Capacitor.h>
 
 CAP_PLUGIN(FileContainer, "FileContainer",
-           CAP_PLUGIN_METHOD(ensureDocuments, CAPPluginReturnPromise);
-           )
+    CAP_PLUGIN_METHOD(ensureDocuments, CAPPluginReturnPromise);
+)

+ 39 - 37
ios/App/App/FileContainer.swift

@@ -11,50 +11,52 @@ import MobileCoreServices
 @objc(FileContainer)
 public class FileContainer: CAPPlugin, UIDocumentPickerDelegate {
 
-    var iCloudContainerUrl: URL? {
-        return FileManager.default.url(forUbiquityContainerIdentifier: nil)?.appendingPathComponent("Documents")
-    }
+  var iCloudContainerUrl: URL? {
+    return FileManager.default.url(forUbiquityContainerIdentifier: nil)?.appendingPathComponent("Documents")
+  }
+
+  var localContainerUrl: URL? {
+    return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
+  }
+
+  @objc func ensureDocuments(_ call: CAPPluginCall) {
+    var data: [String: String] = [:]
 
-    var localContainerUrl: URL? {
-        return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
+    if self.iCloudContainerUrl != nil {
+      validateDocuments(at: self.iCloudContainerUrl!)
+      data["iCloudContainerUrl"] = self.iCloudContainerUrl?.absoluteString
     }
 
-    @objc func ensureDocuments(_ call: CAPPluginCall) {
+    if self.localContainerUrl != nil {
+      validateDocuments(at: self.localContainerUrl!)
+      data["localContainerUrl"] = self.localContainerUrl?.absoluteString
+    }
 
-        if self.iCloudContainerUrl != nil {
-            validateDocuments(at: self.iCloudContainerUrl!)
-        }
+    call.resolve(data)
+  }
 
-        if self.localContainerUrl != nil {
-            validateDocuments(at: self.localContainerUrl!)
-        }
+  func validateDocuments(at url: URL) {
 
-        call.resolve(["path": [self.iCloudContainerUrl?.absoluteString as Any,
-                               self.localContainerUrl?.absoluteString as Any]])
+    if !FileManager.default.fileExists(atPath: url.path, isDirectory: nil) {
+      do {
+        print("the url = " + url.path)
+        try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
+      } catch {
+        print("container doesn't exist")
+        print(error.localizedDescription)
+      }
     }
 
-    func validateDocuments(at url: URL) {
-
-        if !FileManager.default.fileExists(atPath: url.path, isDirectory: nil) {
-            do {
-                print("the url = " + url.path)
-                try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
-            } catch {
-                print("container doesn't exist")
-                print(error.localizedDescription)
-            }
-        }
-
-        let str = ""
-        let filename = url.appendingPathComponent(".logseq", isDirectory: false)
-
-        if !FileManager.default.fileExists(atPath: filename.path) {
-            do {
-                try str.write(to: filename, atomically: true, encoding: String.Encoding.utf8)
-            } catch {
-                print("write .logseq failed")
-                print(error.localizedDescription)
-            }
-        }
+    let str = ""
+    let filename = url.appendingPathComponent(".logseq", isDirectory: false)
+
+    if !FileManager.default.fileExists(atPath: filename.path) {
+      do {
+        try str.write(to: filename, atomically: true, encoding: String.Encoding.utf8)
+      } catch {
+        print("write .logseq failed")
+        print(error.localizedDescription)
+      }
     }
+  }
 }

+ 49 - 34
ios/App/App/FolderPicker.swift

@@ -12,43 +12,58 @@ import MobileCoreServices
 @objc(FolderPicker)
 public class FolderPicker: CAPPlugin, UIDocumentPickerDelegate {
 
-    public var _call: CAPPluginCall?
-
-    @objc func pickFolder(_ call: CAPPluginCall) {
-        self._call = call
-
-        DispatchQueue.main.async { [weak self] in
-            let documentPicker = UIDocumentPickerViewController(
-                documentTypes: [String(kUTTypeFolder)],
-                in: UIDocumentPickerMode.open
-            )
-
-            documentPicker.allowsMultipleSelection = false
-            documentPicker.delegate = self
-            documentPicker.modalPresentationStyle = UIModalPresentationStyle.fullScreen
-
-            self?.bridge?.viewController?.present(
-                documentPicker,
-                animated: true,
-                completion: nil
-            )
-        }
-    }
+  public var _call: CAPPluginCall?
+
+  @objc func pickFolder(_ call: CAPPluginCall) {
+    self._call = call
+
+    DispatchQueue.main.async { [weak self] in
 
-    public func documentPicker(
-        _ controller: UIDocumentPickerViewController,
-        didPickDocumentsAt urls: [URL]
-    ) {
-        var items: [String] = []
-        let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
+      let documentPicker = UIDocumentPickerViewController(
+        documentTypes: [String(kUTTypeFolder)],
+        in: UIDocumentPickerMode.open
+      )
 
-        for url in urls {
-            items.append(url.absoluteString)
+      // Set the initial directory.
+
+      if let path = call.getString("path") {
+        guard let url = URL(string: path) else {
+             call.reject("can not parse url")
+             return
         }
 
-        self._call?.resolve([
-            "path": items.first as Any,
-            "localDocumentsPath": documentsPath[0] as Any
-        ])
+        print("picked folder url = " + url.path)
+
+        documentPicker.directoryURL = url
+      }
+
+      documentPicker.allowsMultipleSelection = false
+      documentPicker.delegate = self
+
+      documentPicker.modalPresentationStyle = UIModalPresentationStyle.fullScreen
+
+      self?.bridge?.viewController?.present(
+        documentPicker,
+        animated: true,
+        completion: nil
+      )
+    }
+  }
+
+  public func documentPicker(
+    _ controller: UIDocumentPickerViewController,
+    didPickDocumentsAt urls: [URL]
+  ) {
+    var items: [String] = []
+    let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
+
+    for url in urls {
+      items.append(url.absoluteString)
     }
+
+    self._call?.resolve([
+      "path": items.first as Any,
+      "localDocumentsPath": documentsPath[0] as Any
+    ])
+  }
 }

+ 3 - 2
ios/App/App/Utils.m

@@ -9,5 +9,6 @@
 #import <Capacitor/Capacitor.h>
 
 CAP_PLUGIN(Utils, "Utils",
-           CAP_PLUGIN_METHOD(isZoomed, CAPPluginReturnPromise);
-           )
+    CAP_PLUGIN_METHOD(isZoomed, CAPPluginReturnPromise);
+    CAP_PLUGIN_METHOD(getDocumentRoot, CAPPluginReturnPromise);
+)

+ 17 - 5
ios/App/App/Utils.swift

@@ -11,12 +11,24 @@ import Capacitor
 @objc(Utils)
 public class Utils: CAPPlugin {
 
-    @objc func isZoomed(_ call: CAPPluginCall) {
+  @objc func isZoomed(_ call: CAPPluginCall) {
 
-        var isZoomed: Bool {
-            return UIScreen.main.scale < UIScreen.main.nativeScale
-        }
+    var isZoomed: Bool {
+      UIScreen.main.scale < UIScreen.main.nativeScale
+    }
+
+    call.resolve(["isZoomed": isZoomed])
+  }
+
+  @objc func getDocumentRoot(_ call: CAPPluginCall) {
+    let doc = FileManager.default.urls(
+        for: .documentDirectory,
+        in: .userDomainMask).first
 
-        call.resolve(["isZoomed": isZoomed])
+    if doc != nil {
+      call.resolve(["documentRoot": doc!.path])
+    } else {
+      call.resolve(["documentRoot": ""])
     }
+  }
 }

+ 2 - 1
package.json

@@ -138,7 +138,8 @@
         "threads": "1.6.5",
         "url": "^0.11.0",
         "yargs-parser": "20.2.4",
-        "path-complete-extname": "1.0.0"
+        "path-complete-extname": "1.0.0",
+        "sanitize-filename": "1.6.3"
     },
     "resolutions": {
         "pixi-graph-fork/@pixi/app": "6.2.0",

+ 1 - 1
resources/index.html

@@ -2,7 +2,7 @@
 <html>
 <head>
   <meta charset="utf-8">
-  <meta content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no" name="viewport">
+  <meta content="minimum-scale=1, initial-scale=1, maximum-scale=1, width=device-width, shrink-to-fit=no" name="viewport">
   <link rel="stylesheet" type="text/css" href="./css/tabler-icons.min.css">
   <link href="./css/style.css" rel="stylesheet" type="text/css">
   <link href="./img/logo.png" rel="shortcut icon" type="image/png">

+ 4 - 4
src/main/frontend/components/block.cljs

@@ -2346,17 +2346,17 @@
   [:div.block-left-menu.flex.bg-base-2.rounded-r-md.mr-1
    [:div.commands-button.w-0.rounded-r-md
     {:id (str "block-left-menu-" uuid)}
-    [:div.indent (ui/icon "indent-increase" {:style {:fontSize 16}})]]])
+    [:div.indent (ui/icon "indent-increase" {:size 18})]]])
 
 (rum/defc block-right-menu < rum/reactive
   [_config {:block/keys [uuid] :as _block} edit?]
   [:div.block-right-menu.flex.bg-base-2.rounded-md.ml-1
-   [:div.commands-button.w-0.flex.flew-col.rounded-md
+   [:div.commands-button.w-0.rounded-md
     {:id (str "block-right-menu-" uuid)
      :style {:max-width (if edit? 40 80)}}
-    [:div.outdent (ui/icon "indent-decrease" {:style {:fontSize 16}})]
+    [:div.outdent (ui/icon "indent-decrease" {:size 18})]
     (when-not edit?
-      [:div.more (ui/icon "dots-circle-horizontal" {:style {:fontSize 16}})])]])
+      [:div.more (ui/icon "dots-circle-horizontal" {:size 18})])]])
 
 (rum/defcs block-content-or-editor < rum/reactive
   (rum/local true ::hide-block-refs?)

+ 6 - 16
src/main/frontend/components/block.css

@@ -192,13 +192,10 @@
   background: linear-gradient(90deg, var(--ls-primary-background-color) 0%, var(--ls-secondary-background-color) 100%);
 
   .commands-button {
-    overflow: hidden;
-    max-width: 40px;
-    text-align: center;
-    margin: auto 0;
+    @apply overflow-hidden flex max-w-[40px];
 
     .indent {
-      opacity: 30%;
+      @apply flex items-center w-full justify-center opacity-30;
     }
   }
 }
@@ -208,18 +205,11 @@
   /* background: linear-gradient(-90deg, var(--ls-primary-background-color) 0%, var(--ls-secondary-background-color) 100%); */
 
   .commands-button {
-    overflow: hidden;
-    text-align: center;
-    margin: auto 0;
+    @apply overflow-hidden flex flex-nowrap;
 
-    .outdent {
-      margin: 0 12px;
-      opacity: 30%;
-    }
-
-    .more {
-      margin: 0 12px;
-      opacity: 30%;
+    .outdent, .more {
+      @apply flex items-center justify-center
+      overflow-hidden opacity-30 m-0 w-[40px];
     }
   }
 }

+ 1 - 1
src/main/frontend/components/encryption.cljs

@@ -158,7 +158,7 @@
            ;; password strength checker
            (when-not (string/blank? @*password)
              [:<>
-              [:div.input-hints.text-sm.py-2.px-3.rounded.mb-2.-mt-1.5.flex.items-center.space-x-3
+              [:div.input-hints.text-sm.py-2.px-3.rounded.mb-2.-mt-1.5.flex.items-center.sm:space-x-3.strength-wrap
                (let [included-set (set (:contains pw-strength))]
                  (for [i ["lowercase" "uppercase" "number" "symbol"]
                        :let [included? (contains? included-set i)]]

+ 1 - 1
src/main/frontend/components/file_sync.cljs

@@ -14,9 +14,9 @@
             [frontend.fs.sync :as fs-sync]
             [frontend.handler.file-sync :refer [*beta-unavailable?] :as file-sync-handler]
             [frontend.handler.notification :as notification]
-            [frontend.handler.page :as page-handler]
             [frontend.handler.repo :as repo-handler]
             [frontend.handler.user :as user-handler]
+            [frontend.handler.page :as page-handler]
             [frontend.handler.web.nfs :as web-nfs]
             [frontend.mobile.util :as mobile-util]
             [frontend.state :as state]

+ 5 - 1
src/main/frontend/components/file_sync.css

@@ -461,8 +461,12 @@
       @apply flex-nowrap md:flex-wrap;
       background-color: var(--ls-primary-background-color);
 
+      &.strength-wrap {
+        @apply flex-wrap;
+      }
+      
       .strength-item {
-        @apply flex items-center leading-none opacity-60 mr-2;
+        @apply whitespace-nowrap flex items-center leading-none opacity-60;
 
         .ti {
           @apply scale-75;

+ 2 - 2
src/main/frontend/components/header.css

@@ -229,7 +229,7 @@ html.is-native-iphone-without-notch,
 html.is-native-ipad {
 
   #main-container {
-    padding-top: 0px;
+    padding-top: 0;
     display: flex;
     flex-direction: column;
   }
@@ -237,7 +237,7 @@ html.is-native-ipad {
   #main-content-container {
     padding-left: 22px;
     padding-right: 14px;
-    padding-top: 0px;
+    padding-top: 0;
     height: calc(100vh - var(--ls-headbar-inner-top-padding) - var(--ls-headbar-height));
 
     @screen sm {

+ 2 - 2
src/main/frontend/components/onboarding.cljs

@@ -6,8 +6,8 @@
             [frontend.components.onboarding.setups :as setups]))
 
 (rum/defc intro
-  []
-  (setups/picker))
+  [onboarding-and-home?]
+  (setups/picker onboarding-and-home?))
 
 (defn help
   []

+ 10 - 5
src/main/frontend/components/onboarding/index.css

@@ -568,16 +568,13 @@ html.is-native-iphone,
 html.is-native-iphone-without-notch {
   .cp__onboarding {
     &-setups {
-      position: absolute;
       width: 100%;
-      top: 0;
-      left: 0;
-      height: 100vh;
+      height: 100%;
       overflow-y: auto;
 
       .inner-card {
         padding-top: 30px;
-        min-height: 100vh;
+        min-height: 100%;
         width: 100%;
 
         > h2 {
@@ -617,3 +614,11 @@ html.is-native-iphone-without-notch {
     }
   }
 }
+
+html.is-native-android {
+  .cp__onboarding-setups {
+    position: absolute;
+    top: 0;
+    left: 0;
+  }
+}

+ 39 - 35
src/main/frontend/components/onboarding/setups.cljs

@@ -10,9 +10,11 @@
             [frontend.util :as util]
             [frontend.handler.web.nfs :as nfs]
             [frontend.mobile.util :as mobile-util]
+            [frontend.mobile.graph-picker :as graph-picker]
             [frontend.handler.notification :as notification]
             [frontend.handler.external :as external-handler]
             [frontend.modules.shortcut.core :as shortcut]
+            [frontend.handler.user :as user-handler]
             [clojure.string :as string]
             [goog.object :as gobj]))
 
@@ -41,15 +43,9 @@
   []
   [:div.mobile-intro
    (cond
-     (mobile-util/native-ios?)
-     [:div
-      [:ul
-       [:li "Save them in " [:span.font-bold "iCloud Drive's Logseq directory"] ", and sync them across devices using iCloud."]
-       [:li "Save them in Logseq's directory of your device's local storage."]]]
-
      (mobile-util/native-android?)
      [:div
-      "You can save them in your local storage, and use any third-party sync service to keep your notes sync with other devices. "
+      "You can save them in your local storage, and use Logseq Sync or any third-party sync service to keep your notes sync with other devices. "
       "If you prefer to use Dropbox to sync your notes, you can use "
       [:a {:href "https://play.google.com/store/apps/details?id=com.ttxapps.dropsync"
            :target "_blank"}
@@ -64,39 +60,47 @@
      nil)])
 
 (rum/defcs picker < rum/reactive
-  [_state]
-  (let [parsing? (state/sub :repo/parsing-files?)]
+  [_state onboarding-and-home?]
+  (let [parsing?       (state/sub :repo/parsing-files?)
+        _              (state/sub :auth/id-token)
+        native-ios?    (mobile-util/native-ios?)
+        native-icloud? (not (string/blank? (state/sub [:mobile/container-urls :iCloudContainerUrl])))
+        logged?        (user-handler/logged-in?)]
 
     (setups-container
      :picker
-     [:article.flex
-      [:section.a
-       [:strong "Let’s get you set up."]
-       [:small (str "Where on your " DEVICE " do you want to save your work?")
-        (when (mobile-util/native-platform?)
-          (mobile-intro))]
+     [:article.flex.w-full
+      [:section.a.
+       (when (and (mobile-util/native-platform?) (not native-ios?))
+         (mobile-intro))
+
+       (if native-ios?
+         ;; TODO: open for all native mobile platforms
+         (graph-picker/graph-picker-cp {:onboarding-and-home? onboarding-and-home?
+                                        :logged? logged?
+                                        :native-icloud? native-icloud?})
 
-       (if (or (nfs/supported?) (mobile-util/native-platform?))
-         [:div.choose.flex.flex-col.items-center
-          {:on-click #(page-handler/ls-dir-files!
-                       (fn []
-                         (shortcut/refresh!)))}
-          [:i]
-          [:div.control
-           [:label.action-input.flex.items-center.justify-center.flex-col
-            {:disabled parsing?}
+         (if (or (nfs/supported?) (mobile-util/native-platform?))
+           [:div.choose.flex.flex-col.items-center
+            {:on-click #(page-handler/ls-dir-files!
+                         (fn []
+                           (shortcut/refresh!)))}
+            [:i]
+            [:div.control
+             [:label.action-input.flex.items-center.justify-center.flex-col
+              {:disabled parsing?}
 
-            (if parsing?
-              (ui/loading "")
-              [[:strong "Choose a folder"]
-               [:small "Open existing directory or Create a new one"]])]]]
-         [:div.px-5
-          (ui/admonition :warning
-                         [:p "It seems that your browser doesn't support the "
-                          [:a {:href   "https://web.dev/file-system-access/"
-                               :target "_blank"}
-                           "new native filesystem API"]
-                          [:span ", please use any Chromium 86+ based browser like Chrome, Vivaldi, Edge, etc. Notice that the API doesn't support mobile browsers at the moment."]])])]
+              (if parsing?
+                (ui/loading "")
+                [[:strong "Choose a folder"]
+                 [:small "Open existing directory or Create a new one"]])]]]
+           [:div.px-5
+            (ui/admonition :warning
+                           [:p "It seems that your browser doesn't support the "
+                            [:a {:href   "https://web.dev/file-system-access/"
+                                 :target "_blank"}
+                             "new native filesystem API"]
+                            [:span ", please use any Chromium 86+ based browser like Chrome, Vivaldi, Edge, etc. Notice that the API doesn't support mobile browsers at the moment."]])]))]
       [:section.b.flex.items-center.flex-col
        [:p.flex
         [:i.as-flex-center (ui/icon "zoom-question" {:style {:fontSize "22px"}})]

+ 4 - 6
src/main/frontend/components/repo.cljs

@@ -4,10 +4,8 @@
             [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
             [frontend.db :as db]
-            [frontend.handler.page :as page-handler]
             [frontend.handler.repo :as repo-handler]
             [frontend.handler.web.nfs :as nfs-handler]
-            [frontend.modules.shortcut.core :as shortcut]
             [frontend.state :as state]
             [frontend.ui :as ui]
             [frontend.util :as util]
@@ -54,7 +52,7 @@
         :let [only-cloud? (and remote? (nil? url))]]
     [:div.flex.justify-between.mb-4.items-center {:key (or url GraphUUID)}
      (normalized-graph-label repo #(if only-cloud?
-                                     (state/pub-event! [:graph/pick-dest-to-sync repo])
+                                     (state/pub-event! [:graph/pull-down-remote-graph repo])
                                      (state/pub-event! [:graph/switch url])))
 
      [:div.controls
@@ -118,7 +116,7 @@
             [:div.mr-8
              (ui/button
                (t :open-a-directory)
-               :on-click #(page-handler/ls-dir-files! shortcut/refresh!))])]]
+               :on-click #(state/pub-event! [:graph/setup-a-repo]))])]]
 
         (when (seq remote-graphs)
           [:div
@@ -159,7 +157,7 @@
                                                       (if (gobj/get e "shiftKey")
                                                         (state/pub-event! [:graph/open-new-window url])
                                                         (if-not local?
-                                                          (state/pub-event! [:graph/pick-dest-to-sync graph])
+                                                          (state/pub-event! [:graph/pull-down-remote-graph graph])
                                                           (state/pub-event! [:graph/switch url]))))}})))
                     switch-repos)
         refresh-link (let [nfs-repo? (config/local-db? current-repo)]
@@ -184,7 +182,7 @@
     (->>
      (concat repo-links
              [(when (seq repo-links) {:hr true})
-              {:title (t :new-graph) :options {:on-click #(page-handler/ls-dir-files! shortcut/refresh!)}}
+              {:title (t :new-graph) :options {:on-click #(state/pub-event! [:graph/setup-a-repo])}}
               {:title (t :all-graphs) :options {:href (rfe/href :repos)}}
               refresh-link
               reindex-link

+ 8 - 9
src/main/frontend/components/sidebar.cljs

@@ -397,7 +397,8 @@
   (let [left-sidebar-open? (state/sub :ui/left-sidebar-open?)
         onboarding-and-home? (and (or (nil? (state/get-current-repo)) (config/demo-graph?))
                                   (not config/publishing?)
-                                  (= :home route-name))]
+                                  (= :home route-name))
+        margin-less-pages? (or onboarding-and-home? margin-less-pages?)]
     [:div#main-container.cp__sidebar-main-layout.flex-1.flex
      {:class (util/classnames [{:is-left-sidebar-open left-sidebar-open?}])}
 
@@ -442,17 +443,15 @@
 
          :else
          [:div
-          {:class (if margin-less-pages? "" (util/hiccup->class "mx-auto.pb-24"))
-           :style {:margin-bottom (cond
-                                    margin-less-pages? 0
-                                    onboarding-and-home? -48
-                                    :else 120)
-                   :padding-bottom (when (mobile-util/native-iphone?) "7rem")}}
+          {:class (if (or onboarding-and-home? margin-less-pages?) "" (util/hiccup->class "mx-auto.pb-24"))
+           :style {:margin-bottom  (cond
+                                     margin-less-pages? 0
+                                     onboarding-and-home? 0
+                                     :else 120)}}
           main-content])
 
        (when onboarding-and-home?
-         [:div {:style {:padding-bottom 200}}
-          (onboarding/intro)])]]]))
+         (onboarding/intro onboarding-and-home?))]]]))
 
 (defonce sidebar-inited? (atom false))
 ;; TODO: simplify logic

+ 7 - 5
src/main/frontend/components/sidebar.css

@@ -65,11 +65,12 @@
 
 #main-content-container {
   @apply p-4 sm:px-8;
+
   font-size: 1em;
 }
 
 #main-content-container[data-is-margin-less-pages=true] {
-  padding: 0;
+  padding: 0 !important;
   position: relative;
 }
 
@@ -614,13 +615,14 @@ html[data-theme='dark'] {
 }
 
 a.ui__modal-close svg, a.close svg, span.rotating-arrow svg {
-    color: var(--ls-primary-text-color);
+  color: var(--ls-primary-text-color);
 }
+
 a.ui__modal-close, a.close {
-    color: var(--ls-primary-text-color);
-    opacity: 0.6;
+  color: var(--ls-primary-text-color);
+  opacity: 0.6;
 }
 
 a.ui__modal-close:hover, a.close:hover {
-    opacity: 1;
+  opacity: 1;
 }

+ 9 - 27
src/main/frontend/components/widgets.cljs

@@ -6,7 +6,8 @@
             [frontend.ui :as ui]
             [rum.core :as rum]
             [frontend.config :as config]
-            [frontend.mobile.util :as mobile-util]))
+            [frontend.mobile.util :as mobile-util]
+            [frontend.state :as state]))
 
 (rum/defc add-local-directory
   []
@@ -16,33 +17,14 @@
      (if (mobile-util/native-platform?)
        [:div.text-sm
         (ui/button "Open a local directory"
-          :on-click #(page-handler/ls-dir-files! shortcut/refresh!))
+          :on-click #(state/pub-event! [:graph/setup-a-repo]))
         [:hr]
-        [:ol
-         [:li
-          [:div.font-bold.mb-2 "How to sync my notes?"]
-          (if (mobile-util/native-android?)
-            [:div
-             [:p "We're developing our built-in paid Logseq Sync, but you can use any third-party sync service to keep your notes sync with other devices."]
-             [:p "If you prefer to use Dropbox to sync your notes, you can use "
-              [:a {:href "https://play.google.com/store/apps/details?id=com.ttxapps.dropsync"
-                   :target "_blank"}
-               "Dropsync"]
-              ". Or you can use "
-              [:a {:href "https://play.google.com/store/apps/details?id=dk.tacit.android.foldersync.lite"
-                   :target "_blank"}
-               "FolderSync"]
-              "."]]
-            [:div
-             [:p "You can sync your graphs by using iCloud. Please choose an existing graph or create a new graph in your iCloud Drive's Logseq directory."]
-             [:p "We're developing our built-in paid Logseq Sync. Stay tuned."]])]
-
-         [:li.mt-8
-          [:div.font-bold.mb-2 "I need some help"]
-          [:p "👋 Join our Forum to chat with the makers and our helpful community members."]
-          (ui/button "Join the community"
-            :href "https://discuss.logseq.com"
-            :target "_blank")]]]
+        [:div
+         [:div.font-bold.mb-2 "I need some help"]
+         [:p "👋 Join our Forum to chat with the makers and our helpful community members."]
+         (ui/button "Join the community"
+           :href "https://discuss.logseq.com"
+           :target "_blank")]]
        [:div.cp__widgets-open-local-directory
         [:div.select-file-wrap.cursor
          (when nfs-supported?

+ 2 - 19
src/main/frontend/config.cljs

@@ -306,9 +306,6 @@
 (def custom-css-file "custom.css")
 (def export-css-file "export.css")
 (def custom-js-file "custom.js")
-(def metadata-file "metadata.edn")
-(def pages-metadata-file "pages-metadata.edn")
-
 (def config-default-content (rc/inline "config.edn"))
 
 (defonce idb-db-prefix "logseq-db/")
@@ -376,7 +373,7 @@
     path
     (util/node-path.join (get-repo-dir repo-url) path)))
 
-;; FIXME: There is another get-file-path at src/main/frontend/fs/capacitor_fs.cljs
+;; FIXME: There is another normalize-file-protocol-path at src/main/frontend/fs/capacitor_fs.cljs
 (defn get-file-path
   "Normalization happens here"
   [repo-url relative-path]
@@ -394,7 +391,7 @@
 
                  (and (mobile-util/native-ios?) (local-db? repo-url))
                  (let [dir (get-repo-dir repo-url)]
-                   (str dir relative-path))
+                   (util/safe-path-join dir relative-path))
 
                  (and (mobile-util/native-android?) (local-db? repo-url))
                  (let [dir (get-repo-dir repo-url)
@@ -427,20 +424,6 @@
    (when repo
      (get-file-path repo (str app-name "/" config-file)))))
 
-(defn get-metadata-path
-  ([]
-   (get-metadata-path (state/get-current-repo)))
-  ([repo]
-   (when repo
-     (get-file-path repo (str app-name "/" metadata-file)))))
-
-(defn get-pages-metadata-path
-  ([]
-   (get-pages-metadata-path (state/get-current-repo)))
-  ([repo]
-   (when repo
-     (get-file-path repo (str app-name "/" pages-metadata-file)))))
-
 (defn get-custom-css-path
   ([]
    (get-custom-css-path (state/get-current-repo)))

+ 2 - 2
src/main/frontend/fs.cljs

@@ -154,9 +154,9 @@
     nfs-record))
 
 (defn open-dir
-  [ok-handler]
+  [dir ok-handler]
   (let [record (get-record)]
-    (p/let [result (protocol/open-dir record ok-handler)]
+    (p/let [result (protocol/open-dir record dir ok-handler)]
       (if (or (util/electron?)
               (mobile-util/native-platform?))
         (let [[dir & paths] (bean/->clj result)]

+ 1 - 1
src/main/frontend/fs/bfs.cljs

@@ -30,7 +30,7 @@
     (js/window.pfs.rename old-path new-path))
   (stat [_this dir path]
     (js/window.pfs.stat (str dir path)))
-  (open-dir [_this _ok-handler]
+  (open-dir [_this _dir _ok-handler]
     nil)
   (get-files [_this _path-or-handle _ok-handler]
     nil)

+ 70 - 50
src/main/frontend/fs/capacitor_fs.cljs

@@ -168,7 +168,7 @@
 
 (defn- write-file-impl!
   [_this repo _dir path content {:keys [ok-handler error-handler old-content skip-compare?]} stat]
-  (if skip-compare?
+  (if (or (string/blank? repo) skip-compare?)
     (p/catch
      (p/let [result (<write-file-with-utf8 path content)]
        (when ok-handler
@@ -212,31 +212,44 @@
                       (error-handler error)
                       (log/error :write-file-failed error)))))))))
 
-(defn get-file-path [dir path]
-  (let [dir        (some-> dir (string/replace #"/+$" ""))
-        dir        (if (and (not-empty dir) (string/starts-with? dir "/"))
-                     (do
-                       (js/console.trace "WARN: detect absolute path, use URL instead")
-                       (str "file://" (js/encodeURI dir)))
-                     dir)
-        path       (some-> path (string/replace #"^/+" ""))
-        encode-url #(let [encoded-chars?
-                          (and (string? %) (boolean (re-find #"(?i)%[0-9a-f]{2}" %)))]
-                      (cond-> %
-                        (not encoded-chars?)
-                        (js/encodeURI path)))]
-    (cond (string/blank? path)
-          (encode-url dir)
+(defn remove-private-part
+  "iOS sometimes return the private part."
+  [path]
+  (string/replace path "///private/" "///"))
+
+(defn normalize-file-protocol-path [dir path]
+  (let [dir             (some-> dir (string/replace #"/+$" ""))
+        dir             (if (and (not-empty dir) (string/starts-with? dir "/"))
+                          (do
+                            (js/console.trace "WARN: detect absolute path, use URL instead")
+                            (str "file://" (js/encodeURI dir)))
+                          dir)
+        path            (some-> path (string/replace #"^/+" ""))
+        safe-encode-url #(let [encoded-chars?
+                               (and (string? %) (boolean (re-find #"(?i)%[0-9a-f]{2}" %)))]
+                           (cond
+                             (not encoded-chars?)
+                             (js/encodeURI %)
+
+                             :else
+                             (js/encodeURI (js/decodeURI %))))
+        path' (cond
+                (and path (string/starts-with? path "file:/"))
+                (safe-encode-url path)
+
+                (string/blank? path)
+                (safe-encode-url dir)
 
-          (string/blank? dir)
-          (encode-url path)
+                (string/blank? dir)
+                (safe-encode-url path)
 
-          (string/starts-with? path dir)
-          (encode-url path)
+                (string/starts-with? path dir)
+                (safe-encode-url path)
 
-          :else
-          (let [path' (encode-url path)]
-            (str dir "/" path')))))
+                :else
+                (let [path' (safe-encode-url path)]
+                  (str dir "/" path')))]
+    (remove-private-part path')))
 
 (defn- local-container-path?
   "Check whether `path' is logseq's container `localDocumentsPath' on iOS"
@@ -260,15 +273,35 @@
      :webkit-allow-full-screen "webkitallowfullscreen"
      :height "100%"}]])
 
+(defn- open-dir
+  [dir]
+  (p/let [_ (when (mobile-util/native-android?) (android-check-permission))
+          {:keys [path localDocumentsPath]} (-> (.pickFolder mobile-util/folder-picker
+                                                             (clj->js (when (and dir (mobile-util/native-ios?))
+                                                                        {:path dir})))
+                                                (p/then #(js->clj % :keywordize-keys true))
+                                                (p/catch (fn [e]
+                                                           (js/alert (str e))
+                                                           nil))) ;; NOTE: If pick folder fails, let it crash
+          _ (when (and (mobile-util/native-ios?)
+                       (not (or (local-container-path? path localDocumentsPath)
+                                (mobile-util/iCloud-container-path? path))))
+              (state/pub-event! [:modal/show-instruction]))
+          _ (js/console.log "Opening or Creating graph at directory: " path)
+          files (readdir path)
+          files (js->clj files :keywordize-keys true)]
+    (into [] (concat [{:path path}] files))))
+
 (defrecord ^:large-vars/cleanup-todo Capacitorfs []
   protocol/Fs
   (mkdir! [_this dir]
-    (-> (.mkdir Filesystem
-                (clj->js
-                 {:path dir}))
-        (p/catch (fn [error]
-                   (log/error :mkdir! {:path dir
-                                       :error error})))))
+    (let [dir' (normalize-file-protocol-path "" dir)]
+      (-> (.mkdir Filesystem
+                  (clj->js
+                   {:path dir'}))
+          (p/catch (fn [error]
+                     (log/error :mkdir! {:path  dir'
+                                         :error error}))))))
   (mkdir-recur! [_this dir]
     (p/let
      [_ (-> (.mkdir Filesystem
@@ -288,7 +321,7 @@
                 dir)]
       (readdir dir)))
   (unlink! [this repo path _opts]
-    (p/let [path (get-file-path nil path)
+    (p/let [path (normalize-file-protocol-path nil path)
             repo-url (config/get-local-dir repo)
             recycle-dir (util/safe-path-join repo-url config/app-name ".recycle") ;; logseq/.recycle
             ;; convert url to pure path
@@ -302,20 +335,20 @@
     ;; Too dangerous!!! We'll never implement this.
     nil)
   (read-file [_this dir path _options]
-    (let [path (get-file-path dir path)]
+    (let [path (normalize-file-protocol-path dir path)]
       (->
        (<read-file-with-utf8 path)
        (p/catch (fn [error]
                   (log/error :read-file-failed error))))))
   (write-file! [this repo dir path content opts]
-    (let [path (get-file-path dir path)]
+    (let [path (normalize-file-protocol-path dir path)]
       (p/let [stat (p/catch
                     (.stat Filesystem (clj->js {:path path}))
                     (fn [_e] :not-found))]
         ;; `path` is full-path
         (write-file-impl! this repo dir path content opts stat))))
   (rename! [_this _repo old-path new-path]
-    (let [[old-path new-path] (map #(get-file-path "" %) [old-path new-path])]
+    (let [[old-path new-path] (map #(normalize-file-protocol-path "" %) [old-path new-path])]
       (p/catch
        (p/let [_ (.rename Filesystem
                           (clj->js
@@ -324,7 +357,7 @@
        (fn [error]
          (log/error :rename-file-failed error)))))
   (copy! [_this _repo old-path new-path]
-    (let [[old-path new-path] (map #(get-file-path "" %) [old-path new-path])]
+    (let [[old-path new-path] (map #(normalize-file-protocol-path "" %) [old-path new-path])]
       (p/catch
        (p/let [_ (.copy Filesystem
                         (clj->js
@@ -333,24 +366,11 @@
        (fn [error]
          (log/error :copy-file-failed error)))))
   (stat [_this dir path]
-    (let [path (get-file-path dir path)]
+    (let [path (normalize-file-protocol-path dir path)]
       (p/chain (.stat Filesystem (clj->js {:path path}))
                #(js->clj % :keywordize-keys true))))
-  (open-dir [_this _ok-handler]
-    (p/let [_ (when (mobile-util/native-android?) (android-check-permission))
-            {:keys [path localDocumentsPath]} (-> (.pickFolder mobile-util/folder-picker)
-                                                  (p/then #(js->clj % :keywordize-keys true))
-                                                  (p/catch (fn [e]
-                                                             (js/alert (str e))
-                                                             nil))) ;; NOTE: If pick folder fails, let it crash
-            _ (when (and (mobile-util/native-ios?)
-                         (not (or (local-container-path? path localDocumentsPath)
-                                  (mobile-util/iCloud-container-path? path))))
-                (state/pub-event! [:modal/show-instruction]))
-            _ (js/console.log "Opening or Creating graph at directory: " path)
-            files (readdir path)
-            files (js->clj files :keywordize-keys true)]
-      (into [] (concat [{:path path}] files))))
+  (open-dir [_this dir _ok-handler]
+    (open-dir dir))
   (get-files [_this path-or-handle _ok-handler]
     (readdir path-or-handle))
   (watch-dir! [_this dir _options]

+ 1 - 1
src/main/frontend/fs/nfs.cljs

@@ -224,7 +224,7 @@
            :file/size (get-attr "size")
            :file/type (get-attr "type")}))
       (p/rejected "File not exists")))
-  (open-dir [_this ok-handler]
+  (open-dir [_this _dir ok-handler]
     (utils/openDirectory #js {:recursive true}
                          ok-handler))
   (get-files [_this path-or-handle ok-handler]

+ 4 - 4
src/main/frontend/fs/node.cljs

@@ -75,8 +75,8 @@
                       (error-handler error)
                       (log/error :write-file-failed error)))))))))
 
-(defn- open-dir []
-  (p/let [dir-path (util/mocked-open-dir-path)
+(defn- open-dir [dir]
+  (p/let [dir-path (or dir (util/mocked-open-dir-path))
           result (if dir-path
                    (ipc/ipc "getFiles" dir-path)
                    (ipc/ipc "openDir" {}))]
@@ -113,8 +113,8 @@
   (stat [_this dir path]
     (let [path (concat-path dir path)]
       (ipc/ipc "stat" path)))
-  (open-dir [_this _ok-handler]
-    (open-dir))
+  (open-dir [_this dir _ok-handler]
+    (open-dir dir))
   (get-files [_this path-or-handle _ok-handler]
     (ipc/ipc "getFiles" path-or-handle))
   (watch-dir! [_this dir options]

+ 1 - 1
src/main/frontend/fs/protocol.cljs

@@ -13,7 +13,7 @@
   (rename! [this repo old-path new-path])
   (copy! [this repo old-path new-path])
   (stat [this dir path])
-  (open-dir [this ok-handler])
+  (open-dir [this dir ok-handler])
   (get-files [this path-or-handle ok-handler])
   (watch-dir! [this dir options])
   (unwatch-dir! [this dir])

+ 13 - 9
src/main/frontend/fs/sync.cljs

@@ -322,15 +322,20 @@
   (-relative-path [this]))
 
 (defn relative-path [o]
-  (cond
-    (implements? IRelativePath o)
-    (-relative-path o)
+  (let [repo-dir (config/get-repo-dir (state/get-current-repo))]
+    (cond
+     (implements? IRelativePath o)
+     (-relative-path o)
 
-    (string? o)
-    (remove-user-graph-uuid-prefix o)
+     ;; full path
+     (and (string? o) (string/starts-with? o repo-dir))
+     (string/replace o (str repo-dir "/") "")
 
-    :else
-    (throw (js/Error. (str "unsupport type " (str o))))))
+     (string? o)
+     (remove-user-graph-uuid-prefix o)
+
+     :else
+     (throw (js/Error. (str "unsupport type " (str o)))))))
 
 (defprotocol IChecksum
   (-checksum [this]))
@@ -472,8 +477,7 @@
       (state/pub-event! [:ui/notify-skipped-downloading-files
                          (map -relative-path reserved-files)])
       (prn "Skipped downloading those file paths with reserved chars: "
-           (map -relative-path reserved-files))
-      )
+           (map -relative-path reserved-files)))
     (remove
      #(fs-util/include-reserved-chars? (-relative-path %))
      files)))

+ 7 - 19
src/main/frontend/fs/watcher_handler.cljs

@@ -7,15 +7,16 @@
             [frontend.handler.editor :as editor]
             [frontend.handler.file :as file-handler]
             [frontend.handler.page :as page-handler]
-            [frontend.handler.repo :as repo-handler]
             [frontend.handler.ui :as ui-handler]
             [logseq.graph-parser.util :as gp-util]
             [logseq.graph-parser.config :as gp-config]
             [logseq.graph-parser.util.block-ref :as block-ref]
+            [frontend.mobile.util :as mobile-util]
             [lambdaisland.glogi :as log]
             [promesa.core :as p]
             [frontend.state :as state]
-            [frontend.fs :as fs]))
+            [frontend.fs :as fs]
+            [frontend.fs.capacitor-fs :as capacitor-fs]))
 
 ;; all IPC paths must be normalized! (via gp-util/path-normalize)
 
@@ -49,10 +50,12 @@
   [type {:keys [dir path content stat global-dir] :as payload}]
   (when dir
     (let [path (gp-util/path-normalize path)
+          path (if (mobile-util/native-platform?)
+                 (capacitor-fs/normalize-file-protocol-path nil path)
+                 path)
           ;; Global directory events don't know their originating repo so we rely
           ;; on the client to correctly identify it
           repo (if global-dir (state/get-current-repo) (config/get-local-repo dir))
-          pages-metadata-path (config/get-pages-metadata-path)
           {:keys [mtime]} stat
           db-content (or (db/get-file repo path) "")]
       (when (or content (contains? #{"unlink" "unlinkDir" "addDir"} type))
@@ -67,8 +70,7 @@
           nil
 
           (and (= "add" type)
-               (not= (string/trim content) (string/trim db-content))
-               (not= path pages-metadata-path))
+               (not= (string/trim content) (string/trim db-content)))
           (let [backup? (not (string/blank? db-content))]
             (handle-add-and-change! repo path content db-content mtime backup?))
 
@@ -78,7 +80,6 @@
 
           (and (= "change" type)
                (not= (string/trim content) (string/trim db-content))
-               (not= path pages-metadata-path)
                (not (gp-config/local-asset? (string/replace-first path dir ""))))
           (when-not (and
                      (string/includes? path (str "/" (config/get-journals-directory) "/"))
@@ -103,19 +104,6 @@
             (println "reloading custom.css")
             (ui-handler/add-style-if-exists!))
 
-          ;; When metadata is added to watcher, update timestamps in db accordingly
-          ;; This event is not triggered on re-index
-          ;; Persistent metadata is gold standard when db is offline, so it's forced
-          (and (contains? #{"add"} type)
-               (= path pages-metadata-path))
-          (p/do! (repo-handler/update-pages-metadata! repo content true))
-
-          ;; Change is triggered by external changes, so update to the db
-          ;; Don't forced update when db is online, but resolving conflicts
-          (and (contains? #{"change"} type)
-               (= path pages-metadata-path))
-          (p/do! (repo-handler/update-pages-metadata! repo content false))
-
           (contains? #{"add" "change" "unlink"} type)
           nil
 

+ 0 - 2
src/main/frontend/handler.cljs

@@ -29,7 +29,6 @@
             [frontend.handler.repo-config :as repo-config-handler]
             [frontend.handler.global-config :as global-config-handler]
             [frontend.handler.plugin-config :as plugin-config-handler]
-            [frontend.handler.metadata :as metadata-handler]
             [frontend.idb :as idb]
             [frontend.mobile.util :as mobile-util]
             [frontend.modules.instrumentation.core :as instrument]
@@ -238,7 +237,6 @@
   (persist-var/load-vars)
   (user-handler/restore-tokens-from-localstorage)
   (user-handler/refresh-tokens-loop)
-  (metadata-handler/run-set-page-metadata-job!)
   (js/setTimeout instrument! (* 60 1000)))
 
 (defn stop! []

+ 6 - 7
src/main/frontend/handler/common/file.cljs

@@ -8,7 +8,8 @@
             [frontend.mobile.util :as mobile-util]
             [logseq.graph-parser :as graph-parser]
             [logseq.graph-parser.util :as gp-util]
-            [logseq.graph-parser.config :as gp-config]))
+            [logseq.graph-parser.config :as gp-config]
+            [frontend.fs.capacitor-fs :as capacitor-fs]))
 
 (defn- page-exists-in-another-file
   "Conflict of files towards same page"
@@ -40,6 +41,7 @@
   ([repo-url file content {:keys [verbose] :as options}]
    (let [electron-local-repo? (and (util/electron?)
                                    (config/local-db? repo-url))
+         repo-dir (config/get-repo-dir repo-url)
          file (cond
                 (and electron-local-repo?
                      util/win32?
@@ -49,13 +51,10 @@
                 (and electron-local-repo? (or
                                            util/win32?
                                            (not= "/" (first file))))
-                (str (config/get-repo-dir repo-url) "/" file)
+                (str repo-dir "/" file)
 
-                (and (mobile-util/native-android?) (not= "/" (first file)))
-                file
-
-                (and (mobile-util/native-ios?) (not= "/" (first file)))
-                file
+                (mobile-util/native-platform?)
+                (capacitor-fs/normalize-file-protocol-path repo-dir file)
 
                 :else
                 file)

+ 65 - 5
src/main/frontend/handler/events.cljs

@@ -42,6 +42,7 @@
             [frontend.handler.web.nfs :as nfs-handler]
             [frontend.mobile.core :as mobile]
             [frontend.mobile.util :as mobile-util]
+            [frontend.mobile.graph-picker :as graph-picker]
             [frontend.modules.instrumentation.posthog :as posthog]
             [frontend.modules.outliner.file :as outliner-file]
             [frontend.modules.shortcut.core :as st]
@@ -173,10 +174,39 @@
       "Please wait seconds until all changes are saved for the current graph."
       :warning))))
 
-(defmethod handle :graph/pick-dest-to-sync [[_ graph]]
-  (state/set-modal!
-   (file-sync/pick-dest-to-sync-panel graph)
-   {:center? true}))
+(defmethod handle :graph/pull-down-remote-graph [[_ graph dir-name]]
+  (if (mobile-util/native-ios?)
+    (when-let [graph-name (or dir-name (:GraphName graph))]
+      (let [graph-name (util/safe-sanitize-file-name graph-name)]
+        (if (string/blank? graph-name)
+          (notification/show! "Illegal graph folder name.")
+
+          ;; Create graph directory under Logseq document folder (local)
+          (when-let [root (state/get-local-container-root-url)]
+            (let [graph-path (graph-picker/validate-graph-dirname root graph-name)]
+              (->
+               (p/let [exists? (fs/dir-exists? graph-path)]
+                 (let [overwrite? (if exists?
+                                    (js/confirm (str "There's already a directory with the name \"" graph-name "\", do you want to overwrite it? Make sure to backup it first if you're not sure about it."))
+                                    true)]
+                   (if overwrite?
+                     (p/let [_ (fs/mkdir-if-not-exists graph-path)]
+                       (nfs-handler/ls-dir-files-with-path!
+                        graph-path
+                        {:ok-handler (fn []
+                                       (file-sync-handler/init-remote-graph graph-path graph)
+                                       (js/setTimeout (fn [] (repo-handler/refresh-repos!)) 200))}))
+                     (let [graph-name (-> (js/prompt "Please specify a new directory name to download the graph:")
+                                          str
+                                          string/trim)]
+                       (when-not (string/blank? graph-name)
+                         (state/pub-event! [:graph/pull-down-remote-graph graph graph-name]))))))
+               (p/catch (fn [^js e]
+                          (notification/show! (str e) :error)
+                          (js/console.error e)))))))))
+    (state/set-modal!
+     (file-sync/pick-dest-to-sync-panel graph)
+     {:center? true})))
 
 (defmethod handle :graph/pick-page-histories [[_ graph-uuid page-name]]
   (state/set-modal!
@@ -538,7 +568,9 @@
 (defmethod handle :file-watcher/changed [[_ ^js event]]
   (let [type (.-event event)
         payload (-> event
-                    (js->clj :keywordize-keys true))]
+                    (js->clj :keywordize-keys true)
+                    (update :path (fn [path]
+                                    (when (string? path) (capacitor-fs/remove-private-part path)))))]
     (fs-watcher/handle-changed! type payload)
     (when (file-sync-handler/enable-sync?)
      (sync/file-watch-handler type payload))))
@@ -567,6 +599,24 @@
                   (state/close-modal!)
                   (nfs-handler/refresh! (state/get-current-repo) refresh-cb)))]]))
 
+(defmethod handle :sync/create-remote-graph [[_ current-repo]]
+  (let [graph-name (js/decodeURI (util/node-path.basename current-repo))]
+    (async/go
+      (async/<! (sync/<sync-stop))
+      (state/set-state! [:ui/loading? :graph/create-remote?] true)
+      (when-let [GraphUUID (get (async/<! (file-sync-handler/create-graph graph-name)) 2)]
+        (async/<! (sync/<sync-start))
+        (state/set-state! [:ui/loading? :graph/create-remote?] false)
+        ;; update existing repo
+        (state/set-repos! (map (fn [r]
+                                 (if (= (:url r) current-repo)
+                                   (assoc r
+                                          :GraphUUID GraphUUID
+                                          :GraphName graph-name
+                                          :remote? true)
+                                   r))
+                            (state/get-repos)))))))
+
 (defmethod handle :graph/re-index [[_]]
   ;; Ensure the graph only has ONE window instance
   (repo-handler/re-index!
@@ -738,6 +788,16 @@
    :warning
    false))
 
+(defmethod handle :graph/setup-a-repo [[_ opts]]
+  (let [opts' (merge {:picked-root-fn #(state/close-modal!)
+                      :native-icloud? (not (string/blank? (state/get-icloud-container-root-url)))
+                      :logged?        (user-handler/logged-in?)} opts)]
+    (if (mobile-util/native-ios?)
+      (state/set-modal!
+       #(graph-picker/graph-picker-cp opts')
+       {:label "graph-setup"})
+      (page-handler/ls-dir-files! st/refresh! opts'))))
+
 (defmethod handle :file/alter [[_ repo path content]]
   (p/let [_ (file-handler/alter-file repo path content {:from-disk? true})]
     (ui-handler/re-render-root!)))

+ 0 - 22
src/main/frontend/handler/file.cljs

@@ -219,25 +219,3 @@
       ;; after an app refresh can cause stale page data to load
       (fs/unwatch-dir! dir)
       (fs/watch-dir! dir))))
-
-(defn create-metadata-file
-  [repo-url]
-  (let [repo-dir (config/get-repo-dir repo-url)
-        path (str config/app-name "/" config/metadata-file)
-        file-path (str "/" path)
-        default-content "{}"]
-    (p/let [_ (fs/mkdir-if-not-exists (util/safe-path-join repo-dir config/app-name))
-            file-exists? (fs/create-if-not-exists repo-url repo-dir file-path default-content)]
-      (when-not file-exists?
-        (file-common-handler/reset-file! repo-url path default-content)))))
-
-(defn create-pages-metadata-file
-  [repo-url]
-  (let [repo-dir (config/get-repo-dir repo-url)
-        path (str config/app-name "/" config/pages-metadata-file)
-        file-path (str "/" path)
-        default-content "{}"]
-    (p/let [_ (fs/mkdir-if-not-exists (util/safe-path-join repo-dir config/app-name))
-            file-exists? (fs/create-if-not-exists repo-url repo-dir file-path default-content)]
-      (when-not file-exists?
-        (file-common-handler/reset-file! repo-url path default-content)))))

+ 0 - 86
src/main/frontend/handler/metadata.cljs

@@ -1,86 +0,0 @@
-(ns frontend.handler.metadata
-  "System-component-like ns that manages writing to pages-metadata.edn"
-  (:require [cljs.reader :as reader]
-            [cljs.pprint]
-            [datascript.db :as ddb]
-            [frontend.config :as config]
-            [frontend.db :as db]
-            [frontend.fs :as fs]
-            [frontend.handler.common :as common-handler]
-            [frontend.handler.file :as file-handler]
-            [frontend.state :as state]
-            [promesa.core :as p]))
-
-(def default-metadata-str "{}")
-
-(defn set-metadata!
-  [k v]
-  (when-let [repo (state/get-current-repo)]
-    (let [path (config/get-metadata-path)
-          file-content (db/get-file path)]
-      (p/let [_ (file-handler/create-metadata-file repo)]
-        (let [metadata-str (or file-content default-metadata-str)
-              metadata (try
-                         (reader/read-string metadata-str)
-                         (catch :default e
-                           (println "Parsing metadata.edn failed: ")
-                           (js/console.dir e)
-                           {}))
-              new-metadata (cond
-                             (= k :block/properties)
-                             (update metadata :block/properties v) ; v should be a function
-                             :else
-                             (let [ks (if (vector? k) k [k])]
-                               (assoc-in metadata ks v)))
-              new-content (pr-str new-metadata)]
-          (file-handler/set-file-content! repo path new-content))))))
-
-(defn set-pages-metadata!
-  [repo]
-  (when-not (state/unlinked-dir? (config/get-repo-dir repo))
-    (let [path (config/get-pages-metadata-path repo)
-          all-pages (->> (db/get-all-pages repo)
-                         (common-handler/fix-pages-timestamps)
-                         (map #(select-keys % [:block/name :block/created-at :block/updated-at]))
-                         (sort-by :block/name)
-                         (vec))]
-      (p/let [_ (-> (file-handler/create-pages-metadata-file repo)
-                    (p/catch (fn [] nil)))]
-        (let [new-content (with-out-str (cljs.pprint/pprint all-pages))]
-          (fs/write-file! repo
-                          (config/get-repo-dir repo)
-                          path
-                          new-content
-                          {}))))))
-
-(defn- handler-properties!
-  [all-properties properties-tx]
-  (reduce
-   (fn [acc datom]
-     (let [v (:v datom)
-           id (or (get v :id)
-                  (get v :title))]
-       (if id
-         (let [added? (ddb/datom-added datom)
-               remove-all-properties? (and (not added?)
-                                           ;; only id
-                                           (= 1 (count v)))]
-           (if remove-all-properties?
-             (dissoc acc id)
-             (assoc acc id v)))
-         acc)))
-   all-properties
-   properties-tx))
-
-(defn update-properties!
-  [properties-tx]
-  (set-metadata! :block/properties #(handler-properties! % properties-tx)))
-
-(defn run-set-page-metadata-job!
-  []
-  (js/setInterval
-   (fn []
-     (when-let [repo (state/get-current-repo)]
-       (when (state/input-idle? repo :diff 3000)
-         (set-pages-metadata! repo))))
-   (* 1000 60 10)))

+ 2 - 0
src/main/frontend/handler/notification.cljs

@@ -13,6 +13,8 @@
   (state/set-state! :notification/contents nil))
 
 (defn show!
+  ([content]
+   (show! content :info true nil 2000))
   ([content status]
    (show! content status true nil 1500))
   ([content status clear?]

+ 5 - 4
src/main/frontend/handler/page.cljs

@@ -692,10 +692,11 @@
   ([ok-handler] (ls-dir-files! ok-handler nil))
   ([ok-handler opts]
    (web-nfs/ls-dir-files-with-handler!
-     (fn [e]
-       (init-commands!)
-       (when ok-handler (ok-handler e)))
-     opts)))
+    (fn [e]
+      (init-commands!)
+      (when ok-handler
+        (ok-handler e)))
+    opts)))
 
 (defn get-all-pages
   [repo]

+ 8 - 55
src/main/frontend/handler/repo.cljs

@@ -14,7 +14,6 @@
             [frontend.handler.common.file :as file-common-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.ui :as ui-handler]
-            [frontend.handler.metadata :as metadata-handler]
             [frontend.handler.global-config :as global-config-handler]
             [frontend.idb :as idb]
             [frontend.search :as search]
@@ -22,7 +21,6 @@
             [frontend.state :as state]
             [frontend.util :as util]
             [frontend.util.fs :as util-fs]
-            [lambdaisland.glogi :as log]
             [promesa.core :as p]
             [shadow.resource :as rc]
             [frontend.db.persist :as db-persist]
@@ -128,52 +126,11 @@
     (p/let [_ (fs/mkdir-if-not-exists (util/safe-path-join repo-dir config/app-name))
             _ (fs/mkdir-if-not-exists (util/safe-path-join repo-dir (str config/app-name "/" config/recycle-dir)))
             _ (fs/mkdir-if-not-exists (util/safe-path-join repo-dir (config/get-journals-directory)))
-            _ (file-handler/create-metadata-file repo-url)
             _ (repo-config-handler/create-config-file-if-not-exists repo-url)
             _ (create-contents-file repo-url)
             _ (create-custom-theme repo-url)]
       (state/pub-event! [:page/create-today-journal repo-url]))))
 
-(defn- load-pages-metadata!
-  "force?: if set true, skip the metadata timestamp range check"
-  ([repo file-paths files]
-   (load-pages-metadata! repo file-paths files false))
-  ([repo file-paths files force?]
-   (try
-     (let [file (config/get-pages-metadata-path)]
-       (when (contains? (set file-paths) file)
-         (when-let [content (some #(when (= (:file/path %) file) (:file/content %)) files)]
-           (let [metadata (common-handler/safe-read-string content "Parsing pages metadata file failed: ")
-                 pages (db/get-all-pages repo)
-                 pages (zipmap (map :block/name pages) pages)
-                 metadata (->>
-                           (filter (fn [{:block/keys [name created-at updated-at]}]
-                                     (when-let [page (get pages name)]
-                                       (and
-                                        (>= updated-at created-at) ;; metadata validation
-                                        (or force? ;; when force is true, shortcut timestamp range check
-                                            (and (or (nil? (:block/created-at page))
-                                                     (>= created-at (:block/created-at page)))
-                                                 (or (nil? (:block/updated-at page))
-                                                     (>= updated-at (:block/created-at page)))))
-                                        (or ;; persistent metadata is the gold standard
-                                         (not= created-at (:block/created-at page))
-                                         (not= updated-at (:block/created-at page)))))) metadata)
-                           (remove nil?))]
-             (when (seq metadata)
-               (db/transact! repo metadata {:new-graph? true}))))))
-     (catch :default e
-       (log/error :exception e)))))
-
-(defn update-pages-metadata!
-  "update pages meta content -> db. Only accept non-encrypted content!"
-  [repo content force?]
-  (let [path (config/get-pages-metadata-path)
-        files [{:file/path path
-                :file/content content}]
-        file-paths [path]]
-    (load-pages-metadata! repo file-paths files force?)))
-
 (defonce *file-tx (atom nil))
 
 (defn- parse-and-load-file!
@@ -202,8 +159,7 @@
       nil)))
 
 (defn- after-parse
-  [repo-url files file-paths re-render? re-render-opts opts graph-added-chan]
-  (load-pages-metadata! repo-url file-paths files true)
+  [repo-url re-render? re-render-opts opts graph-added-chan]
   (when (or (:new-graph? opts) (not (:refresh? opts)))
     (create-default-files! repo-url))
   (when re-render?
@@ -217,7 +173,7 @@
   (async/offer! graph-added-chan true))
 
 (defn- parse-files-and-create-default-files-inner!
-  [repo-url files delete-files delete-blocks file-paths re-render? re-render-opts opts]
+  [repo-url files delete-files delete-blocks re-render? re-render-opts opts]
   (let [supported-files (graph-parser/filter-files files)
         delete-data (->> (concat delete-files delete-blocks)
                          (remove nil?))
@@ -239,7 +195,7 @@
           (parse-and-load-file! repo-url file (assoc
                                                (select-keys opts [:new-graph? :verbose])
                                                :skip-db-transact? false)))
-        (after-parse repo-url files file-paths re-render? re-render-opts opts graph-added-chan))
+        (after-parse repo-url re-render? re-render-opts opts graph-added-chan))
       (async/go-loop [tx []]
         (if-let [item (async/<! chan)]
           (let [[idx file] item
@@ -267,18 +223,17 @@
               (recur tx')))
           (do
             (when (seq tx) (db/transact! repo-url tx {:from-disk? true}))
-            (after-parse repo-url files file-paths re-render? re-render-opts opts graph-added-chan)))))
+            (after-parse repo-url re-render? re-render-opts opts graph-added-chan)))))
     graph-added-chan))
 
 (defn- parse-files-and-create-default-files!
-  [repo-url files delete-files delete-blocks file-paths re-render? re-render-opts opts]
-  (parse-files-and-create-default-files-inner! repo-url files delete-files delete-blocks file-paths re-render? re-render-opts opts))
+  [repo-url files delete-files delete-blocks re-render? re-render-opts opts]
+  (parse-files-and-create-default-files-inner! repo-url files delete-files delete-blocks re-render? re-render-opts opts))
 
 (defn parse-files-and-load-to-db!
   [repo-url files {:keys [delete-files delete-blocks re-render? re-render-opts _refresh?] :as opts
                    :or {re-render? true}}]
-  (let [file-paths (map :file/path files)]
-    (parse-files-and-create-default-files! repo-url files delete-files delete-blocks file-paths re-render? re-render-opts opts)))
+  (parse-files-and-create-default-files! repo-url files delete-files delete-blocks re-render? re-render-opts opts))
 
 (defn load-repo-to-db!
   [repo-url {:keys [diffs nfs-files refresh? new-graph? empty-graph?]}]
@@ -428,8 +383,7 @@
        (route-handler/redirect-to-home!)
        (let [local? (config/local-db? repo)]
          (if local?
-           (p/let [_ (metadata-handler/set-pages-metadata! repo)]
-             (nfs-rebuild-index! repo ok-handler))
+           (nfs-rebuild-index! repo ok-handler)
            (rebuild-index! repo))
          (js/setTimeout
           (route-handler/redirect-to-home!)
@@ -445,7 +399,6 @@
     (p/do!
      (when before
        (before))
-     (metadata-handler/set-pages-metadata! repo)
      (db/persist! repo)
      (when on-success
        (on-success)))

+ 15 - 5
src/main/frontend/handler/web/nfs.cljs

@@ -23,7 +23,8 @@
             [lambdaisland.glogi :as log]
             [logseq.graph-parser.config :as gp-config]
             [logseq.graph-parser.util :as gp-util]
-            [promesa.core :as p]))
+            [promesa.core :as p]
+            [frontend.fs.capacitor-fs :as capacitor-fs]))
 
 (defn remove-ignore-files
   [files dir-name nfs?]
@@ -121,18 +122,23 @@
 (defn ^:large-vars/cleanup-todo ls-dir-files-with-handler!
   "Read files from directory and setup repo (for the first time setup a repo)"
   ([ok-handler] (ls-dir-files-with-handler! ok-handler nil))
-  ([ok-handler {:keys [empty-dir?-or-pred dir-result-fn]}]
+  ([ok-handler {:keys [empty-dir?-or-pred dir-result-fn picked-root-fn dir]}]
    (let [path-handles (atom {})
          electron? (util/electron?)
          mobile-native? (mobile-util/native-platform?)
          nfs? (and (not electron?)
                    (not mobile-native?))
-         *repo (atom nil)]
-    ;; TODO: add ext filter to avoid loading .git or other ignored file handlers
+         *repo (atom nil)
+         dir (or dir nil)
+         dir (some-> dir
+                     (string/replace "file:///" "file://")
+                     (string/replace " " "%20"))]
+     ;; TODO: add ext filter to avoid loading .git or other ignored file handlers
      (->
       (p/let [result (if (fn? dir-result-fn)
                        (dir-result-fn {:path-handles path-handles :nfs? nfs?})
-                       (fs/open-dir (fn [path handle]
+                       (fs/open-dir dir
+                                    (fn [path handle]
                                       (when nfs?
                                         (swap! path-handles assoc path handle)))))
               _ (when-not (nil? empty-dir?-or-pred)
@@ -144,9 +150,13 @@
                     (fn? empty-dir?-or-pred)
                     (empty-dir?-or-pred result)))
               root-handle (first result)
+              _ (when (fn? picked-root-fn) (picked-root-fn root-handle))
               dir-name (if nfs?
                          (gobj/get root-handle "name")
                          root-handle)
+              dir-name (if (mobile-util/native-platform?)
+                         (capacitor-fs/normalize-file-protocol-path "" dir-name)
+                         dir-name)
               repo (str config/local-db-prefix dir-name)
               _ (state/set-loading-files! repo true)
               _ (when-not (or (state/home?) (state/setups-picker?))

+ 9 - 3
src/main/frontend/mobile/core.cljs

@@ -10,7 +10,8 @@
             [frontend.mobile.intent :as intent]
             [frontend.mobile.util :as mobile-util]
             [frontend.state :as state]
-            [frontend.util :as util]))
+            [frontend.util :as util]
+            [cljs-bean.core :as bean]))
 
 
 (def *url (atom nil))
@@ -23,8 +24,13 @@
 (defn- ios-init
   "Initialize iOS-specified event listeners"
   []
-  (p/let [path (capacitor-fs/ios-ensure-documents!)]
-    (println "iOS container path: " (js->clj path)))
+  (p/let [^js path (capacitor-fs/ios-ensure-documents!)]
+    (when-let [path' (bean/->clj path)]
+      (state/set-state! :mobile/container-urls
+                        (update-vals path' #(cond-> %
+                                              string?
+                                              (js/decodeURIComponent))))
+      (println "iOS container path: " path')))
 
   (state/pub-event! [:validate-appId])
 

+ 149 - 0
src/main/frontend/mobile/graph_picker.cljs

@@ -0,0 +1,149 @@
+(ns frontend.mobile.graph-picker
+  (:require
+   [clojure.string :as string]
+   [rum.core :as rum]
+   [frontend.ui :as ui]
+   [frontend.handler.notification :as notification]
+   [frontend.handler.web.nfs :as web-nfs]
+   [frontend.handler.page :as page-handler]
+   [frontend.util :as util]
+   [frontend.modules.shortcut.core :as shortcut]
+   [frontend.state :as state]
+   [frontend.mobile.util :as mobile-util]
+   [frontend.fs :as fs]
+   [frontend.components.svg :as svg]
+   [promesa.core :as p]))
+
+(defn validate-graph-dirname
+  [root dirname]
+  (util/node-path.join root dirname))
+
+(rum/defc toggle-item
+  [{:keys [on? title on-toggle]}]
+  (ui/button
+    [:span.flex.items-center.justify-between.w-full.py-1
+     [:strong title]
+     (ui/toggle on? (fn []) true)]
+    :class (str "toggle-item " (when on? "is-on"))
+    :intent "logseq"
+    :on-mouse-down #(util/stop %)
+    :on-click #(when (fn? on-toggle)
+                 (on-toggle (not on?)))))
+
+(rum/defc ^:large-vars/cleanup-todo graph-picker-cp
+  [{:keys [onboarding-and-home? logged? native-icloud?] :as opts}]
+  (let [can-logseq-sync? (and logged? (state/enable-sync?))
+        [step set-step!] (rum/use-state :init)
+        [sync-mode set-sync-mode!] (rum/use-state
+                                    (cond
+                                      can-logseq-sync? :logseq-sync
+                                      native-icloud? :icloud-sync))
+        icloud-sync-on?  (= sync-mode :icloud-sync)
+        logseq-sync-on?  (= sync-mode :logseq-sync)
+        *input-ref       (rum/create-ref)
+        native-ios?      (mobile-util/native-ios?)
+        open-picker      #(page-handler/ls-dir-files! shortcut/refresh! opts)
+        on-create        (fn [input-val]
+                           (let [graph-name (util/safe-sanitize-file-name input-val)]
+                             (if (string/blank? graph-name)
+                               (notification/show! "Illegal graph folder name.")
+
+                               ;; create graph directory under Logseq document folder (local/icloud)
+                               (when-let [root (if icloud-sync-on?
+                                                 (state/get-icloud-container-root-url)
+                                                 (state/get-local-container-root-url))]
+                                 (-> (let [graph-path (validate-graph-dirname root graph-name)]
+                                       (-> (fs/mkdir-if-not-exists graph-path)
+                                           (p/then
+                                            (fn []
+                                              (web-nfs/ls-dir-files-with-path!
+                                               graph-path (merge
+                                                           {:ok-handler
+                                                            (fn []
+                                                              (when logseq-sync-on?
+                                                                (state/pub-event! [:sync/create-remote-graph (state/get-current-repo)])))}
+                                                           opts))
+                                              (notification/show! (str "Create graph: " graph-name) :success)))
+                                           (p/catch (fn [^js e]
+                                                      (notification/show! (str e) :error)
+                                                      (js/console.error e))))))))))]
+
+    (rum/use-effect!
+     (fn []
+       (when-let [^js input (and onboarding-and-home?
+                                 (rum/deref *input-ref))]
+         (let [handle (fn [] (js/setTimeout
+                              #(.scrollIntoView
+                                input #js {:behavior "smooth", :block "center", :inline "nearest"}) 100))]
+           (.addEventListener input "focus" handle)
+           (handle))))
+     [step])
+
+    [:div.cp__graph-picker.w-full
+     {:class (when onboarding-and-home? (util/hiccup->class "px-10.py-10"))}
+
+     (when-not onboarding-and-home?
+       [:h1.flex.items-center
+        [:span.scale-75 (svg/logo false)]
+        [:span.pl-1 "Set up a graph"]])
+
+     (case step
+       ;; step 0
+       :init
+       [:div.flex.flex-col.w-full.space-y-6
+        (ui/button
+          [:span.flex.items-center.justify-between.w-full.py-1
+           [:strong "Create a new graph"]
+           (ui/icon "chevron-right")]
+
+          :on-click #(if (and native-ios?
+                              (some (fn [s] (not (string/blank? s)))
+                                    (vals (:mobile/container-urls @state/state))))
+                       (set-step! :new-graph)
+                       (open-picker)))
+
+        (ui/button
+          [:span.flex.items-center.justify-between.w-full.py-1
+           [:strong "Select an existing graph"]
+           (ui/icon "folder-plus")]
+
+          :intent "logseq"
+          :on-click (fn []
+                      (state/close-modal!)
+                      (page-handler/ls-dir-files! shortcut/refresh!
+                                                  {:dir (when native-ios?
+                                                          (or
+                                                           (state/get-icloud-container-root-url)
+                                                           (state/get-local-container-root-url)))})))]
+
+       ;; step 1
+       :new-graph
+       [:div.flex.flex-col.w-full.space-y-3.faster-fade-in
+        [:input.form-input.block
+         {:auto-focus  true
+          :ref         *input-ref
+          :placeholder "What's the graph name?"}]
+
+        [:div.flex.flex-col
+         (when can-logseq-sync?
+           (toggle-item {:title     "Logseq sync"
+                         :on?       logseq-sync-on?
+                         :on-toggle #(set-sync-mode! (if % :logseq-sync (if native-icloud? :icloud-sync nil)))}))
+
+         (when (and native-icloud? (not logseq-sync-on?))
+           (toggle-item {:title     "iCloud sync"
+                         :on?       icloud-sync-on?
+                         :on-toggle #(set-sync-mode! (if % :icloud-sync nil))}))]
+
+        [:div.flex.justify-between.items-center.pt-2
+         (ui/button [:span.flex.items-center
+                     (ui/icon "chevron-left" {:size 18}) "Back"]
+           :intent "logseq"
+           :on-click #(set-step! :init))
+
+         (ui/button "Create"
+                    :on-click
+                    #(let [val (util/trim-safe (.-value (rum/deref *input-ref)))]
+                       (if (string/blank? val)
+                         (.focus (rum/deref *input-ref))
+                         (on-create val))))]])]))

+ 177 - 132
src/main/frontend/mobile/index.css

@@ -1,66 +1,66 @@
 .cp__footer {
+  position: absolute;
+  bottom: 0px;
+  left: 0px;
+  padding: 10px 20px;
+  background-color: var(--ls-primary-background-color);
+  z-index: 10;
+  display: flex;
+  flex: 0 0 auto;
+  white-space: nowrap;
+  height: 80px;
+  align-items: start;
+  box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.05);
+
+  .bottom-action {
+    width: 23px;
+    height: 23px;
+  }
+
+  .ti, .timer {
+    color: var(--ls-primary-text-color);
+  }
+
+  .timer {
     position: absolute;
-    bottom: 0px;
-    left: 0px;
-    padding: 10px 20px;
-    background-color: var(--ls-primary-background-color);
-    z-index: 10;
+    left: 40px;
+  }
+}
+
+.action-bar {
+  position: absolute;
+  bottom: 100px;
+  height: 70px;
+  padding: 6px;
+  border-radius: 10px;
+  background-color: var(--ls-secondary-background-color);
+  overflow-x: overlay;
+  box-shadow: rgba(0, 0, 0, 0.02) 0px 1px 1px 0px, rgba(27, 31, 35, 0.10) 0px 0px 0px 1px;
+  z-index: 100;
+
+  .action-bar-commands {
+    position: relative;
     display: flex;
-    flex: 0 0 auto;
-    white-space: nowrap;
-    height: 80px;
-    align-items: start;
-    box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.05);
+    justify-content: space-around;
+    width: 120%;
 
-    .bottom-action {
-        width: 23px;
-        height: 23px;
-    }
-                                                             
-    .ti, .timer {
-        color: var(--ls-primary-text-color);
+
+    .ti, .tie {
+      color: var(--ls-primary-text-color);
+      font-size: 23px;
+      opacity: 50%;
     }
 
-    .timer {
-        position: absolute;
-        left: 40px;
+    .description {
+      color: var(--ls-primary-text-color);
+      font-size: 13px;
+      opacity: 60%;
     }
-}
 
-.action-bar {
-    position: absolute;
-    bottom: 100px;
-    height: 70px;
-    padding: 6px;
-    border-radius: 10px;
-    background-color: var(--ls-secondary-background-color);
-    overflow-x: overlay;
-    box-shadow: rgba(0, 0, 0, 0.02) 0px 1px 1px 0px, rgba(27, 31, 35, 0.10) 0px 0px 0px 1px;
-    z-index: 100;
-        
-    .action-bar-commands {
-        position: relative;
-        display: flex;
-        justify-content: space-around;
-        width: 120%;
-        
-        
-        .ti, .tie {
-            color: var(--ls-primary-text-color);
-            font-size: 23px;
-            opacity: 50%;
-        }
-        
-        .description {
-            color: var(--ls-primary-text-color);
-            font-size: 13px;
-            opacity: 60%;
-        }
-        
-        button {
-            padding: 5px 10px
-        }
+    button {
+      padding: 5px 10px
     }
+  }
 }
 
 #mobile-editor-toolbar {
@@ -78,23 +78,23 @@
   justify-content: space-between;
 
   button {
-      padding: 7px 10px;
-
-      .submenu {
-          background-color: red;
-          z-index: 100;
-          background-color: var(--ls-secondary-background-color);
-          border-radius: 5px;
-          box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.02);
-          overflow-x: overlay;
-          overflow-y: hidden;
-          left: 0px;
-          height: 40px;
-      }
+    padding: 7px 10px;
+
+    .submenu {
+      background-color: red;
+      z-index: 100;
+      background-color: var(--ls-secondary-background-color);
+      border-radius: 5px;
+      box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.02);
+      overflow-x: overlay;
+      overflow-y: hidden;
+      left: 0px;
+      height: 40px;
+    }
 
-      .show-submenu {
-          display: block;
-      }
+    .show-submenu {
+      display: block;
+    }
   }
 
   .toolbar-commands {
@@ -112,90 +112,135 @@
   }
 }
 
-
 html.is-native-ipad {
-    .cp__footer {
-        height: 55px;
-        right: 0;
-        box-shadow: none;
-        flex: 1;
-        index: 0;
+  .cp__footer {
+    height: 55px;
+    right: 0;
+    box-shadow: none;
+    flex: 1;
+    index: 0;
+  }
+
+  .action-bar {
+    width: 70%;
+    min-width: 550px;
+
+    .action-bar-commands {
+      width: 100%;
     }
-    
-    .action-bar {
-        width: 70%;
-        min-width:550px;
-        
-        .action-bar-commands {
-            width: 100%;
-        }
 
-        @media (orientation: landscape) {
-            width: 50%;
-        }
+    @media (orientation: landscape) {
+      width: 50%;
     }
+  }
 }
 
 html.is-native-iphone {
 
-    .action-bar {
-        left: 3%;
-        right: 3%;
+  .action-bar {
+    left: 3%;
+    right: 3%;
+  }
+
+  @media (orientation: landscape) {
+    .cp__footer {
+      height: 45px;
     }
-    
-    @media (orientation: landscape) {
-        .cp__footer {
-            height: 45px;
-        }
-
-        .action-bar {
-            bottom: 50px;
-            left: 15%;
-            right: 15%;
-            width: 70%;
-            min-width: 450px;
-
-            .action-bar-commands {
-                width: 100%;
-            }
-        }
+
+    .action-bar {
+      bottom: 50px;
+      left: 15%;
+      right: 15%;
+      width: 70%;
+      min-width: 450px;
+
+      .action-bar-commands {
+        width: 100%;
+      }
     }
+  }
 }
 
 html.is-native-iphone-without-notch,
 html.is-native-android {
-    .cp__footer {
-        height: 45px;
-    }
-    
+  .cp__footer {
+    height: 45px;
+  }
+
+  .action-bar {
+    left: 5%;
+    right: 5%;
+  }
+
+  @media (orientation: landscape) {
+
     .action-bar {
-        left: 5%;
-        right: 5%;
-    }
+      bottom: 50px;
+      left: 15%;
+      right: 15%;
+      width: 70%;
 
-    @media (orientation: landscape) {
-        
-        .action-bar {
-            bottom: 50px;
-            left: 15%;
-            right: 15%;
-            width: 70%;
-
-            .action-bar-commands {
-                width: 100%;
-            }
-        }
+      .action-bar-commands {
+        width: 100%;
+      }
     }
+  }
 }
 
 html.is-zoomed-native-ios {
+  .cp__footer {
+    height: 70px;
+  }
+
+  @media (orientation: landscape) {
     .cp__footer {
-        height: 70px;
+      height: 50px;
     }
+  }
+}
 
-    @media (orientation: landscape) {
-        .cp__footer {
-            height: 50px;
-        }
+
+.cp__graph-picker {
+  button.toggle-item {
+    opacity: .5;
+    background: transparent !important;
+    -webkit-tap-highlight-color: transparent;
+
+    &:hover {
+      color: inherit;
+      opacity: .5;
+    }
+
+    &.is-on {
+      @apply opacity-100;
     }
+  }
 }
+
+.ui__modal {
+  &[label=graph-setup] {
+    align-items: center;
+
+    .ui__modal-panel {
+      transform: translate3d(0, -78px, 0);
+    }
+
+    .panel-content {
+      padding: 0;
+    }
+
+    .cp__graph-picker {
+      padding: 58px 20px 20px 20px;
+      background: var(--ls-search-background-color);
+
+      > h1 {
+        position: absolute;
+        font-size: 18px;
+        font-weight: 500;
+        top: 12px;
+        left: 20px;
+        opacity: .9;
+      }
+    }
+  }
+}

+ 11 - 0
src/main/frontend/state.cljs

@@ -158,6 +158,7 @@
      :assets/alias-dirs                     (or (storage/get :assets/alias-dirs) [])
 
      ;; mobile
+     :mobile/container-urls                 nil
      :mobile/show-action-bar?               false
      :mobile/actioned-block                 nil
      :mobile/show-toolbar?                  false
@@ -1948,3 +1949,13 @@ Similar to re-frame subscriptions"
 (defn get-onboarding-whiteboard?
   []
   (get-in @state [:whiteboard/onboarding-whiteboard?]))
+
+(defn get-local-container-root-url
+  []
+  (when (mobile-util/native-ios?)
+    (get-in @state [:mobile/container-urls :localContainerUrl])))
+
+(defn get-icloud-container-root-url
+  []
+  (when (mobile-util/native-ios?)
+    (get-in @state [:mobile/container-urls :iCloudContainerUrl])))

+ 12 - 29
src/main/frontend/ui.cljs

@@ -36,6 +36,8 @@
             [promesa.core :as p]
             [rum.core :as rum]))
 
+(declare icon)
+
 (defonce transition-group (r/adapt-class TransitionGroup))
 (defonce css-transition (r/adapt-class CSSTransition))
 (defonce textarea (r/adapt-class (gobj/get TextareaAutosize "default")))
@@ -216,29 +218,15 @@
     (let [svg
           (case status
             :success
-            [:svg.h-6.w-6.text-green-400
-             {:stroke "var(--ls-success-color)", :viewBox "0 0 24 24", :fill "none"}
-             [:path
-              {:d               "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
-               :stroke-width    "2"
-               :stroke-linejoin "round"
-               :stroke-linecap  "round"}]]
+            (icon "circle-check" {:class "text-green-500" :size "22"})
+
             :warning
-            [:svg.h-6.w-6.text-yellow-500
-             {:stroke "var(--ls-warning-color)", :viewBox "0 0 24 24", :fill "none"}
-             [:path
-              {:d               "M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
-               :stroke-width    "2"
-               :stroke-linejoin "round"
-               :stroke-linecap  "round"}]]
-
-            [:svg.h-6.w-6.text-red-500
-             {:view-box "0 0 20 20", :fill "var(--ls-error-color)"}
-             [:path
-              {:clip-rule "evenodd"
-               :d
-               "M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
-               :fill-rule "evenodd"}]])]
+            (icon "alert-circle" {:class "text-yellow-500" :size "22"})
+
+            :error
+            (icon "circle-x" {:class "text-red-500" :size "22"})
+
+            (icon "info-circle" {:class "text-indigo-500" :size "22"}))]
       [:div.ui__notifications-content
        {:style
         (when (or (= state "exiting")
@@ -264,13 +252,8 @@
             [:button.inline-flex.text-gray-400.focus:outline-none.focus:text-gray-500.transition.ease-in-out.duration-150.notification-close-button
              {:on-click (fn []
                           (notification/clear! uid))}
-             [:svg.h-5.w-5
-              {:fill "currentColor", :view-Box "0 0 20 20"}
-              [:path
-               {:clip-rule "evenodd"
-                :d
-                "M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
-                :fill-rule "evenodd"}]]]]]]]]])))
+
+            (icon "x" {:fill "currentColor"})]]]]]]])))
 
 (rum/defc notification < rum/reactive
   []

+ 1 - 1
src/main/frontend/ui.css

@@ -106,7 +106,7 @@
     .panel-content {
       overflow-y: auto;
       overflow-x: hidden;
-      width: 94vw;
+      width: calc(94vw - 2rem);
       max-height: 89vh;
       padding: 2rem 1.5rem;
 

+ 4 - 1
src/main/frontend/util.cljc

@@ -8,6 +8,7 @@
             ["@capacitor/status-bar" :refer [^js StatusBar Style]]
             ["grapheme-splitter" :as GraphemeSplitter]
             ["remove-accents" :as removeAccents]
+            ["sanitize-filename" :as sanitizeFilename]
             ["check-password-strength" :refer [passwordStrength]]
             [frontend.loader :refer [load]]
             [cljs-bean.core :as bean]
@@ -72,7 +73,9 @@
        (when-let [^js ret (and (string? input)
                                (not (string/blank? input))
                                (passwordStrength input))]
-         (bean/->clj ret)))))
+         (bean/->clj ret)))
+     (defn safe-sanitize-file-name [s]
+       (sanitizeFilename (str s)))))
 
 #?(:cljs
    (defn ios?

+ 6 - 6
src/test/frontend/fs/capacitor_fs_test.cljs

@@ -8,38 +8,38 @@
     (let [dir "file:///private/var/mobile/Library/Mobile%20Documents/iCloud~com~logseq~logseq/Documents/"
           url-decoded-dir "file:///private/var/mobile/Library/Mobile Documents/iCloud~com~logseq~logseq/Documents/"]
       (is (= (str url-decoded-dir "pages/pages-metadata.edn")
-             (capacitor-fs/get-file-path
+             (capacitor-fs/normalize-file-protocol-path
               dir
               "file:///private/var/mobile/Library/Mobile Documents/iCloud~com~logseq~logseq/Documents/pages/pages-metadata.edn"))
           "full path returns as url decoded full path")
 
       (is (= (str url-decoded-dir "journals/2002_01_28.md")
-             (capacitor-fs/get-file-path
+             (capacitor-fs/normalize-file-protocol-path
               dir
               "/journals/2002_01_28.md"))
           "relative path returns as url decoded full path")
 
       (is (= dir
-             (capacitor-fs/get-file-path
+             (capacitor-fs/normalize-file-protocol-path
               dir
               nil))
           "nil path returns url encoded dir"))
     
     (let [dir "file:///storage/emulated/0/Graphs/Test"]
       (is (= (str dir "/pages/pages-metadata.edn")
-             (capacitor-fs/get-file-path
+             (capacitor-fs/normalize-file-protocol-path
               dir
               "file:///storage/emulated/0/Graphs/Test/pages/pages-metadata.edn"))
           "full path returns as url decoded full path")
 
       (is (= (str dir "/journals/2002_01_28.md")
-             (capacitor-fs/get-file-path
+             (capacitor-fs/normalize-file-protocol-path
               dir
               "/journals/2002_01_28.md"))
           "relative path returns as url decoded full path")
 
       (is (= dir
-             (capacitor-fs/get-file-path
+             (capacitor-fs/normalize-file-protocol-path
               dir
               nil))
           "nil path returns url encoded dir"))))

+ 19 - 0
yarn.lock

@@ -6447,6 +6447,13 @@ safer-buffer@^2.1.0:
   resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
   integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
 
[email protected]:
+  version "1.6.3"
+  resolved "https://registry.yarnpkg.com/sanitize-filename/-/sanitize-filename-1.6.3.tgz#755ebd752045931977e30b2025d340d7c9090378"
+  integrity sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==
+  dependencies:
+    truncate-utf8-bytes "^1.0.0"
+
 [email protected]:
   version "1.1.4"
   resolved "https://registry.yarnpkg.com/sax/-/sax-1.1.4.tgz#74b6d33c9ae1e001510f179a91168588f1aedaa9"
@@ -7280,6 +7287,13 @@ trough@^1.0.0:
   resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406"
   integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==
 
+truncate-utf8-bytes@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz#405923909592d56f78a5818434b0b78489ca5f2b"
+  integrity sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==
+  dependencies:
+    utf8-byte-length "^1.0.1"
+
 tslib@^1.9.3:
   version "1.14.1"
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
@@ -7525,6 +7539,11 @@ use@^3.1.0:
   resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
   integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
 
+utf8-byte-length@^1.0.1:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz#f45f150c4c66eee968186505ab93fcbb8ad6bf61"
+  integrity sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA==
+
 util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"