Procházet zdrojové kódy

Merge pull request #5088 from logseq/gesture-support-on-block

Gesture support on mobile
Tienson Qin před 3 roky
rodič
revize
c7698ab77e

+ 2 - 0
android/app/capacitor.build.gradle

@@ -11,7 +11,9 @@ apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
 dependencies {
     implementation project(':capacitor-app')
     implementation project(':capacitor-camera')
+    implementation project(':capacitor-clipboard')
     implementation project(':capacitor-filesystem')
+    implementation project(':capacitor-haptics')
     implementation project(':capacitor-keyboard')
     implementation project(':capacitor-share')
     implementation project(':capacitor-splash-screen')

+ 8 - 0
android/app/src/main/assets/capacitor.plugins.json

@@ -7,10 +7,18 @@
 		"pkg": "@capacitor/camera",
 		"classpath": "com.capacitorjs.plugins.camera.CameraPlugin"
 	},
+	{
+		"pkg": "@capacitor/clipboard",
+		"classpath": "com.capacitorjs.plugins.clipboard.ClipboardPlugin"
+	},
 	{
 		"pkg": "@capacitor/filesystem",
 		"classpath": "com.capacitorjs.plugins.filesystem.FilesystemPlugin"
 	},
+	{
+		"pkg": "@capacitor/haptics",
+		"classpath": "com.capacitorjs.plugins.haptics.HapticsPlugin"
+	},
 	{
 		"pkg": "@capacitor/keyboard",
 		"classpath": "com.capacitorjs.plugins.keyboard.KeyboardPlugin"

+ 6 - 0
android/capacitor.settings.gradle

@@ -8,9 +8,15 @@ project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/
 include ':capacitor-camera'
 project(':capacitor-camera').projectDir = new File('../node_modules/@capacitor/camera/android')
 
+include ':capacitor-clipboard'
+project(':capacitor-clipboard').projectDir = new File('../node_modules/@capacitor/clipboard/android')
+
 include ':capacitor-filesystem'
 project(':capacitor-filesystem').projectDir = new File('../node_modules/@capacitor/filesystem/android')
 
+include ':capacitor-haptics'
+project(':capacitor-haptics').projectDir = new File('../node_modules/@capacitor/haptics/android')
+
 include ':capacitor-keyboard'
 project(':capacitor-keyboard').projectDir = new File('../node_modules/@capacitor/keyboard/android')
 

+ 6 - 6
ios/App/App.xcodeproj/project.pbxproj

@@ -474,7 +474,7 @@
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
-				IPHONEOS_DEPLOYMENT_TARGET = 12.0;
+				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				MTL_ENABLE_DEBUG_INFO = YES;
 				ONLY_ACTIVE_ARCH = YES;
 				SDKROOT = iphoneos;
@@ -528,7 +528,7 @@
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
-				IPHONEOS_DEPLOYMENT_TARGET = 12.0;
+				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				MTL_ENABLE_DEBUG_INFO = NO;
 				SDKROOT = iphoneos;
 				SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
@@ -550,7 +550,7 @@
 				INFOPLIST_FILE = App/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
-				MARKETING_VERSION = 0.6.10;
+				MARKETING_VERSION = 0.7.0;
 				OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq;
 				PRODUCT_NAME = "$(TARGET_NAME)";
@@ -576,7 +576,7 @@
 				INFOPLIST_FILE = App/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
-				MARKETING_VERSION = 0.6.10;
+				MARKETING_VERSION = 0.7.0;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
@@ -601,7 +601,7 @@
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
-				MARKETING_VERSION = 0.6.10;
+				MARKETING_VERSION = 0.7.0;
 				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
 				MTL_FAST_MATH = YES;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
@@ -628,7 +628,7 @@
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
-				MARKETING_VERSION = 0.6.10;
+				MARKETING_VERSION = 0.7.0;
 				MTL_FAST_MATH = YES;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
 				PRODUCT_NAME = "$(TARGET_NAME)";

+ 1 - 0
ios/App/Podfile

@@ -13,6 +13,7 @@ def capacitor_pods
   pod 'CapacitorCamera', :path => '../../node_modules/@capacitor/camera'
   pod 'CapacitorClipboard', :path => '../../node_modules/@capacitor/clipboard'
   pod 'CapacitorFilesystem', :path => '../../node_modules/@capacitor/filesystem'
+  pod 'CapacitorHaptics', :path => '../../node_modules/@capacitor/haptics'
   pod 'CapacitorKeyboard', :path => '../../node_modules/@capacitor/keyboard'
   pod 'CapacitorShare', :path => '../../node_modules/@capacitor/share'
   pod 'CapacitorSplashScreen', :path => '../../node_modules/@capacitor/splash-screen'

+ 1 - 0
package.json

@@ -72,6 +72,7 @@
         "@capacitor/clipboard": "^1.0.8",
         "@capacitor/core": "3.2.2",
         "@capacitor/filesystem": "1.0.6",
+        "@capacitor/haptics": "^1.1.4",
         "@capacitor/ios": "3.2.2",
         "@capacitor/keyboard": "^1.2.0",
         "@capacitor/share": "^1.1.2",

+ 47 - 17
src/main/frontend/components/block.cljs

@@ -12,35 +12,37 @@
             [frontend.commands :as commands]
             [frontend.components.datetime :as datetime-comp]
             [frontend.components.lazy-editor :as lazy-editor]
-            [frontend.components.svg :as svg]
             [frontend.components.macro :as macro]
+            [frontend.components.plugins :as plugins]
+            [frontend.components.query-table :as query-table]
+            [frontend.components.svg :as svg]
             [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
             [frontend.date :as date]
             [frontend.db :as db]
-            [frontend.db.utils :as db-utils]
             [frontend.db-mixins :as db-mixins]
             [frontend.db.model :as model]
             [frontend.db.query-dsl :as query-dsl]
+            [frontend.db.utils :as db-utils]
             [frontend.extensions.highlight :as highlight]
             [frontend.extensions.latex :as latex]
-            [frontend.extensions.sci :as sci]
-            [frontend.extensions.pdf.assets :as pdf-assets]
-            [frontend.extensions.zotero :as zotero]
             [frontend.extensions.lightbox :as lightbox]
+            [frontend.extensions.pdf.assets :as pdf-assets]
+            [frontend.extensions.sci :as sci]
             [frontend.extensions.video.youtube :as youtube]
+            [frontend.extensions.zotero :as zotero]
             [frontend.format.block :as block]
             [frontend.format.mldoc :as mldoc]
-            [frontend.components.plugins :as plugins]
-            [frontend.handler.plugin :as plugin-handler]
             [frontend.handler.block :as block-handler]
+            [frontend.handler.common :as common-handler]
             [frontend.handler.dnd :as dnd]
             [frontend.handler.editor :as editor-handler]
+            [frontend.handler.plugin :as plugin-handler]
+            [frontend.handler.query :as query-handler]
             [frontend.handler.repeated :as repeated]
             [frontend.handler.route :as route-handler]
             [frontend.handler.ui :as ui-handler]
-            [frontend.handler.query :as query-handler]
-            [frontend.handler.common :as common-handler]
+            [frontend.mobile.util :as mobile-util]
             [frontend.modules.outliner.tree :as tree]
             [frontend.search :as search]
             [frontend.security :as security]
@@ -50,8 +52,8 @@
             [frontend.ui :as ui]
             [frontend.util :as util]
             [frontend.util.clock :as clock]
-            [frontend.util.property :as property]
             [frontend.util.drawer :as drawer]
+            [frontend.util.property :as property]
             [logseq.graph-parser.config :as gp-config]
             [logseq.graph-parser.util :as gp-util]
             [logseq.graph-parser.mldoc :as gp-mldoc]
@@ -63,9 +65,7 @@
             [promesa.core :as p]
             [reitit.frontend.easy :as rfe]
             [rum.core :as rum]
-            [shadow.loader :as loader]
-            [frontend.components.query-table :as query-table]
-            [frontend.mobile.util :as mobile-util]))
+            [shadow.loader :as loader]))
 
 (defn safe-read-string
   ([s]
@@ -256,7 +256,8 @@
 
 (rum/defc audio-cp [src]
   [:audio {:src src
-           :controls true}])
+           :controls true
+           :on-touch-start #(util/stop %)}])
 
 (rum/defcs asset-link < rum/reactive
   (rum/local nil ::src)
@@ -2022,7 +2023,7 @@
                                (util/clear-selection!)))}
        (not slide?)
        (merge attrs))
-
+     
      [:<>
       [:div.flex.flex-row.justify-between
        [:div.flex-1
@@ -2084,6 +2085,21 @@
                        (swap! *hide-block-refs? not)))}
         block-refs-count]])))
 
+(rum/defc block-left-menu < rum/reactive
+  [_config {:block/keys [uuid] :as _block}]
+  [: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}})]]])
+
+(rum/defc block-right-menu < rum/reactive
+  [_config {:block/keys [uuid] :as _block}]
+  [:div.block-right-menu.flex.bg-base-2.rounded-md.ml-1
+   [:div.commands-button.w-0.flex.flew-col.rounded-md
+    {:id (str "block-right-menu-" uuid)}
+    [:div.more (ui/icon "dots-circle-horizontal" {:style {:fontSize 16}})]
+    [:div.outdent (ui/icon "indent-decrease" {:style {:fontSize 16}})]]])
+
 (rum/defcs block-content-or-editor < rum/reactive
   (rum/local true :hide-block-refs?)
   [state config {:block/keys [uuid format] :as block} edit-input-id block-id heading-level edit?]
@@ -2390,6 +2406,8 @@
                      (model/sub-block-direct-children repo uuid))
                    children)
         breadcrumb-show? (:breadcrumb-show? config)
+        *show-left-menu? (::show-block-left-menu? state)
+        *show-right-menu? (::show-block-right-menu? state)
         slide? (boolean (:slide? config))
         doc-mode? (:document/mode? config)
         embed? (:embed? config)
@@ -2443,20 +2461,32 @@
 
      [:div.flex.flex-row.pr-2
       {:class (if (and heading? (seq (:block/title block))) "items-baseline" "")
+       :on-touch-start block-handler/on-touch-start
+       :on-touch-move (fn [event]
+                        (block-handler/on-touch-move event block uuid *show-left-menu? *show-right-menu?))
+       :on-touch-end (fn [event]
+                       (block-handler/on-touch-end event block uuid *show-left-menu? *show-right-menu?))
+       :on-touch-cancel block-handler/on-touch-cancel 
        :on-mouse-over (fn [e]
                         (block-mouse-over uuid e *control-show? block-id doc-mode?))
        :on-mouse-leave (fn [e]
                          (block-mouse-leave e *control-show? block-id doc-mode?))}
       (when (not slide?)
         (block-control config block uuid block-id collapsed? *control-show? edit?))
-
-      (block-content-or-editor config block edit-input-id block-id heading-level edit?)]
+      
+      (when @*show-left-menu?
+        (block-left-menu config block))
+      (block-content-or-editor config block edit-input-id block-id heading-level edit?)
+      (when @*show-right-menu?
+        (block-right-menu config block))]
 
      (block-children config children collapsed?)
 
      (dnd-separator-wrapper block block-id slide? false false)]))
 
 (rum/defcs block-container < rum/reactive
+  (rum/local false ::show-block-left-menu?) 
+  (rum/local false ::show-block-right-menu?)
   {:init (fn [state]
            (let [[config block] (:rum/args state)
                  block-id (:block/uuid block)]

+ 38 - 0
src/main/frontend/components/block.css

@@ -185,6 +185,44 @@
   }
 }
 
+.block-left-menu {
+    background-color: var(--ls-secondary-background-color);
+    background: linear-gradient(90deg, var(--ls-primary-background-color) 0%, var(--ls-secondary-background-color) 100%);
+    
+    .commands-button {
+        overflow: hidden;
+        max-width: 50px;
+        text-align: center;
+        margin: auto 0;
+
+        .indent {
+            opacity: 30%;
+        }
+    }
+}
+
+.block-right-menu {
+    background-color: var(--ls-secondary-background-color);
+    /* background: linear-gradient(-90deg, var(--ls-primary-background-color) 0%, var(--ls-secondary-background-color) 100%); */
+
+    .commands-button {
+        overflow: hidden;
+        max-width: 80px;
+        text-align: center;
+        margin: auto 0;
+
+        .outdent {
+            margin: 0 12px;
+            opacity: 30%;
+        }
+
+        .more {
+            margin: 0 12px;
+            opacity: 30%;
+        }
+    }
+}
+
 .block-ref {
   border-bottom: 0.5px solid;
   border-bottom-color: var(--ls-block-ref-link-text-color);

+ 2 - 1
src/main/frontend/components/reference.cljs

@@ -123,7 +123,8 @@
                                   references (->> (concat ref-pages references)
                                                   (remove nil?)
                                                   (distinct))]
-                              (state/set-modal! (filter-dialog filters-atom references page-name))))}
+                              (state/set-modal! (filter-dialog filters-atom references page-name)
+                                                {:center? true})))}
                (ui/icon "filter" {:class (cond
                                            (empty? filter-state)
                                            ""

+ 26 - 20
src/main/frontend/components/sidebar.cljs

@@ -26,6 +26,7 @@
             [frontend.handler.user :as user-handler]
             [frontend.handler.common :as common-handler]
             [frontend.mixins :as mixins]
+            [frontend.mobile.action-bar :as action-bar]
             [frontend.mobile.footer :as footer]
             [frontend.mobile.util :as mobile-util]
             [frontend.mobile.mobile-bar :refer [mobile-bar]]
@@ -35,8 +36,8 @@
             [frontend.util :as util]
             [goog.dom :as gdom]
             [goog.object :as gobj]
-            [reitit.frontend.easy :as rfe]
-            [rum.core :as rum]))
+            [rum.core :as rum]
+            [reitit.frontend.easy :as rfe]))
 
 (rum/defc nav-content-item
   [name {:keys [class]} child]
@@ -249,16 +250,17 @@
 
       (when (and left-sidebar-open? (not config/publishing?)) (recent-pages t))
 
-      [:nav.px-2 {:aria-label "Sidebar"
-                  :class      "new-page"}
-       (when-not config/publishing?
-         [:a.item.group.flex.items-center.px-2.py-2.text-sm.font-medium.rounded-md
-          {:on-click (fn []
-                       (and (util/sm-breakpoint?)
-                            (state/toggle-left-sidebar!))
-                       (state/pub-event! [:go/search]))}
-          (ui/icon "circle-plus mr-3" {:style {:font-size 20}})
-          [:span.flex-1 (t :right-side-bar/new-page)]])]]]))
+      (when-not (mobile-util/native-platform?)
+       [:nav.px-2 {:aria-label "Sidebar"
+                   :class      "new-page"}
+        (when-not config/publishing?
+          [:a.item.group.flex.items-center.px-2.py-2.text-sm.font-medium.rounded-md
+           {:on-click (fn []
+                        (and (util/sm-breakpoint?)
+                             (state/toggle-left-sidebar!))
+                        (state/pub-event! [:go/search]))}
+           (ui/icon "circle-plus mr-3" {:style {:font-size 20}})
+           [:span.flex-1 (t :right-side-bar/new-page)]])])]]))
 
 (rum/defc left-sidebar < rum/reactive
   [{:keys [left-sidebar-open? route-match]}]
@@ -282,7 +284,7 @@
                                 (editor-handler/upload-asset id files format editor-handler/*asset-uploading? true))))})
                   (common-handler/listen-to-scroll! element))
                 state)}
-  [{:keys [route-match global-graph-pages? route-name indexeddb-support? db-restoring? main-content]}]
+  [{:keys [route-match global-graph-pages? route-name indexeddb-support? db-restoring? main-content show-action-bar?]}]
   (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?)
@@ -296,6 +298,9 @@
 
      [:div#main-content-container.scrollbar-spacing.w-full.flex.justify-center.flex-row
 
+      (when show-action-bar?
+        (action-bar/action-bar))
+      
       [:div.cp__sidebar-main-content
        {:data-is-global-graph-pages global-graph-pages?
         :data-is-full-width         (or global-graph-pages?
@@ -307,7 +312,7 @@
        (when (and (not (mobile-util/native-platform?))
                   (contains? #{:page :home} route-name))
          (widgets/demo-graph-alert))
-
+       
        (cond
          (not indexeddb-support?)
          nil
@@ -377,10 +382,9 @@
                                               [page :page])]
                      (state/sidebar-add-block! current-repo db-id block-type)))
                  (reset! sidebar-inited? true))))
-           state)
-   :did-mount (fn [state]
-                (state/set-state! :mobile/show-tabbar? true)
-                state)}
+           (when (state/mobile?)
+                  (state/set-state! :mobile/show-tabbar? true))
+           state)}
   []
   (let [default-home (get-default-home-if-valid)
         current-repo (state/sub :git/current-repo)
@@ -511,7 +515,8 @@
         home? (= :home route-name)
         edit? (:editor/editing? @state/state)
         default-home (get-default-home-if-valid)
-        logged? (user-handler/logged-in?)]
+        logged? (user-handler/logged-in?)
+        show-action-bar? (state/sub :mobile/show-action-bar?)]
     (theme/container
      {:t             t
       :theme         theme
@@ -553,7 +558,8 @@
                :indexeddb-support?  indexeddb-support?
                :light?              light?
                :db-restoring?       db-restoring?
-               :main-content        main-content})]
+               :main-content        main-content
+               :show-action-bar?    show-action-bar?})]
 
        (right-sidebar/sidebar)
 

+ 3 - 0
src/main/frontend/extensions/code.cljs

@@ -265,6 +265,9 @@
                              (state/clear-selection!)
                              (when-let [block (and (:block/uuid config) (into {} (db/get-block-by-uuid (:block/uuid config))))]
                                (state/set-editing! id (.getValue editor) block nil false))))
+        (.addEventListener element "touchstart"
+                           (fn [e]
+                             (.stopPropagation e)))
         (.save editor)
         (.refresh editor)
         (when default-open?

+ 170 - 9
src/main/frontend/handler/block.cljs

@@ -1,13 +1,18 @@
 (ns frontend.handler.block
-  (:require [clojure.set :as set]
-            [clojure.walk :as walk]
-            [frontend.db :as db]
-            [frontend.db.model :as db-model]
-            [frontend.db.react :as react]
-            [frontend.state :as state]
-            [logseq.graph-parser.block :as gp-block]
-            [frontend.util :as util]))
-
+  (:require
+   [clojure.set :as set]
+   [clojure.walk :as walk]
+   [frontend.db :as db]
+   [frontend.db.model :as db-model]
+   [frontend.db.react :as react]
+   [frontend.db.utils :as db-utils]
+   [frontend.mobile.haptics :as haptics]
+   [frontend.modules.outliner.core :as outliner-core]
+   [frontend.modules.outliner.transaction :as outliner-tx]
+   [frontend.state :as state]
+   [frontend.util :as util]
+   [goog.dom :as gdom]
+   [logseq.graph-parser.block :as gp-block]))
 
 ;;  Fns
 
@@ -114,3 +119,159 @@
                             (fn [result]
                               (->> (concat result more-data)
                                    (util/distinct-by :db/id))))))
+
+(defn indentable?
+  [{:block/keys [parent] :as block}]
+  (when parent
+    (let [parent-block (db-utils/pull (:db/id parent))
+          first-child (first
+                       (db-model/get-block-immediate-children
+                        (state/get-current-repo)
+                        (:block/uuid parent-block)))]
+      (not= (:db/id block) (:db/id first-child)))))
+
+(defn outdentable?
+  [{:block/keys [level] :as _block}]
+  (not= level 1))
+
+(defn indent-outdent-block!
+  [block direction]
+  (outliner-tx/transact!
+   {:outliner-op :move-blocks}
+   (outliner-core/indent-outdent-blocks! [block] (= direction :right))))
+
+(defn select-block!
+  [block-uuid]
+  (let [blocks (js/document.getElementsByClassName (str block-uuid))]
+    (when (seq blocks)
+      (state/exit-editing-and-set-selected-blocks! blocks))))
+
+(def *swipe (atom nil))
+
+(defn on-touch-start
+  [event]
+  (when-let [touches (.-targetTouches event)]
+    (when (= (.-length touches) 1)
+      (let [touch (aget touches 0)
+            x (.-clientX touch)
+            y (.-clientY touch)]
+        (reset! *swipe {:x0 x :y0 y :xi x :yi y :tx x :ty y :direction nil})))))
+
+(defn on-touch-move
+  [event block uuid *show-left-menu? *show-right-menu?]
+  (when-let [touches (.-targetTouches event)]
+    (when (and (= (.-length touches) 1) @*swipe)
+      (let [{:keys [x0 xi direction]} @*swipe
+            touch (aget touches 0)
+            tx (.-clientX touch)
+            ty (.-clientY touch)
+            direction (if (nil? direction)
+                        (if (> tx x0)
+                          :right
+                          :left)
+                        direction)]
+        (swap! *swipe #(-> %
+                           (assoc :tx tx)
+                           (assoc :ty ty)
+                           (assoc :xi tx)
+                           (assoc :yi ty)
+                           (assoc :direction direction)))
+        (when (< (* (- xi x0) (- tx xi)) 0)
+          (swap! *swipe #(-> %
+                             (assoc :x0 tx)
+                             (assoc :y0 ty))))
+        (let [{:keys [x0 y0]} @*swipe
+              dx (- tx x0)
+              dy (- ty y0)]
+          (when (and (< (. js/Math abs dy) 20)
+                     (> (. js/Math abs dx) 10))
+            (let [left (gdom/getElement (str "block-left-menu-" uuid))
+                  right (gdom/getElement (str "block-right-menu-" uuid))]
+
+              (cond
+                (= direction :right)
+                (do
+                  (reset! *show-left-menu? true)
+                  (when left
+                    (when (>= dx 0)
+                      (set! (.. left -style -width) (str dx "px")))
+                    (when (< dx 0)
+                      (set! (.. left -style -width) (str (max (+ 50 dx) 0) "px")))
+
+                    (let [indent (gdom/getFirstElementChild left)]
+                      (when (indentable? block)
+                        (if (>= (.-clientWidth left) 50)
+                          (set! (.. indent -style -opacity) "100%")
+                          (set! (.. indent -style -opacity) "30%"))))))
+
+                (= direction :left)
+                (do
+                  (reset! *show-right-menu? true)
+                  (when right
+                    (when (<= dx 0)
+                      (set! (.. right -style -width) (str (- dx) "px")))
+                    (when (> dx 0)
+                      (set! (.. right -style -width) (str (max (- 80 dx) 0) "px")))
+
+                    (let [outdent (gdom/getFirstElementChild right)
+                          more (gdom/getLastElementChild right)]
+                      (if (and (>= (.-clientWidth right) 40)
+                               (< (.-clientWidth right) 80))
+                        (set! (.. outdent -style -opacity) "100%")
+                        (set! (.. outdent -style -opacity) "30%"))
+
+                      (when (outdentable? block)
+                        (if (>= (.-clientWidth right) 80)
+                          (set! (.. more -style -opacity) "100%")
+                          (set! (.. more -style -opacity) "30%") 
+                        ;; (set! (.. outdent -style -opacity) "100%")
+                          ;; (set! (.. outdent -style -opacity) "30%")
+                        )))))
+                :else
+                nil))))))))
+
+(defn on-touch-end
+  [_event block uuid *show-left-menu? *show-right-menu?]
+  (when @*swipe
+    (let [left-menu (gdom/getElement (str "block-left-menu-" uuid))
+          right-menu (gdom/getElement (str "block-right-menu-" uuid))
+          {:keys [x0 tx]} @*swipe
+          dx (- tx x0)]
+      (try
+        (when (> (. js/Math abs dx) 10)
+          (cond
+            (and left-menu (>= (.-clientWidth left-menu) 50))
+            (when (indentable? block)
+              (haptics/with-haptics-impact
+                (indent-outdent-block! block :right)
+                :light))
+
+            (and right-menu (< 40 (.-clientWidth right-menu) 80))
+            (haptics/with-haptics-impact
+              (do (state/set-state! :mobile/show-action-bar? true)
+                  (state/set-state! :mobile/actioned-block block)
+                  (select-block! uuid))
+              :light)
+
+            (and right-menu (>= (.-clientWidth right-menu) 80))
+            (when (outdentable? block)
+              (haptics/with-haptics-impact
+                (indent-outdent-block! block :left)
+                :light))
+
+            :else
+            nil))
+        (catch js/Error e
+          (js/console.error e))
+        (finally
+          (reset! *show-left-menu? false)
+          (reset! *show-right-menu? false)
+          (reset! *swipe nil))))))
+
+(defn on-touch-cancel
+  [_event *show-left-menu? *show-right-menu?]
+  (reset! *show-left-menu? false)
+  (reset! *show-right-menu? false)
+  (reset! *swipe nil))
+
+

+ 1 - 3
src/main/frontend/handler/editor.cljs

@@ -962,9 +962,7 @@
 
 (defn select-block!
   [block-uuid]
-  (let [blocks (js/document.getElementsByClassName (str block-uuid))]
-    (when (seq blocks)
-      (state/exit-editing-and-set-selected-blocks! blocks))))
+  (block-handler/select-block! block-uuid))
 
 (defn- compose-copied-blocks-contents
   [repo block-ids]

+ 1 - 0
src/main/frontend/handler/events.cljs

@@ -292,6 +292,7 @@
   (let [main-node (util/app-scroll-container-node)]
     (state/set-state! :mobile/show-tabbar? false)
     (state/set-state! :mobile/show-toolbar? true)
+    (state/set-state! :mobile/show-action-bar? false)
     (when (mobile-util/native-ios?)
       (reset! util/keyboard-height keyboard-height)
       (set! (.. main-node -style -marginBottom) (str keyboard-height "px"))

+ 78 - 0
src/main/frontend/mobile/action_bar.cljs

@@ -0,0 +1,78 @@
+(ns frontend.mobile.action-bar
+  (:require
+   [frontend.db :as db]
+   [frontend.extensions.srs :as srs]
+   [frontend.handler.editor :as editor-handler]
+   [frontend.mixins :as mixins]
+   [frontend.state :as state]
+   [frontend.ui :as ui]
+   [frontend.util :as util]
+   [frontend.util.url :as url-util]
+   [goog.dom :as gdom]
+   [goog.object :as gobj]
+   [rum.core :as rum]
+   [frontend.mobile.util :as mobile-util]))
+
+(defn- action-command
+  [icon description command-handler]
+  (let [callback
+        (fn []
+          (state/set-state! :mobile/show-action-bar? false)
+          (editor-handler/clear-selection!))]
+    [:button.bottom-action.flex-row
+     {:on-click (fn [_event]
+                  (command-handler)
+                  (callback))}
+     (ui/icon icon {:style {:fontSize 23}})
+     [:div.description description]]))
+
+(rum/defcs action-bar < rum/reactive
+  (mixins/event-mixin
+   (fn [state]
+     (mixins/hide-when-esc-or-outside
+      state
+      :on-hide (fn []
+                 (editor-handler/clear-selection!)
+                 (state/set-state! :mobile/show-action-bar? false)))))
+  [state]
+  (when-let [block (state/sub :mobile/actioned-block)]
+    (let [{:block/keys [uuid children]} block
+          last-child-block-id (when-not (empty? children)
+                                (-> (db/get-block-children (state/get-current-repo) uuid)
+                                    last
+                                    :block/uuid))]
+      (let [tag-id (or last-child-block-id uuid)
+            bottom-el (gdom/getElement (str "block-content-" tag-id))
+            bottom (gobj/get (.getBoundingClientRect bottom-el) "bottom")
+            vw-height (or (.-height js/window.visualViewport)
+                          (.-clientHeight js/document.documentElement))
+            delta (- vw-height bottom 170)]
+        (when (< delta 0)
+          (.scrollBy (util/app-scroll-container-node) #js {:top (- 10 delta)})))
+      [:div.action-bar
+       [:div.action-bar-commands
+        (when-not (= (:block/format block) :org)
+          (action-command "heading" "Heading"
+                          #(let [properties (:block/properties block)
+                                 heading?   (true? (:heading properties))]
+                             (if heading?
+                               (editor-handler/remove-block-property! uuid :heading)
+                               (editor-handler/set-block-property! uuid :heading true)))))
+        (action-command "infinity" "Card" #(srs/make-block-a-card! (:block/uuid block)))
+        (action-command "copy" "Copy" #(editor-handler/copy-selection-blocks))
+        (action-command "cut" "Cut" #(editor-handler/cut-selection-blocks true))
+        (action-command "trash" "Delete" #(editor-handler/delete-block-aux! block true))
+        (action-command "registered" "Copy ref"
+                        (fn [_event] (editor-handler/copy-block-ref! uuid #(str "((" % "))"))))
+        (action-command "link" "Copy url"
+                        (fn [_event] (let [current-repo (state/get-current-repo)
+                                           tap-f (fn [block-id]
+                                                   (url-util/get-logseq-graph-uuid-url nil current-repo block-id))]
+                                       (editor-handler/copy-block-ref! uuid tap-f))))
+        (when (mobile-util/native-ipad?)
+          (action-command "text-direction-ltr" "Right sidebar"
+                          (fn [_event]
+                            (let [current-repo (state/get-current-repo)]
+                              (state/sidebar-add-block! current-repo uuid :block-ref)))))]])))
+
+

+ 2 - 4
src/main/frontend/mobile/footer.cljs

@@ -50,10 +50,8 @@
 
 (rum/defc footer < rum/reactive
   []
-  (when (and
-         (state/mobile?)
-         (state/sub :mobile/show-tabbar?)
-         (state/get-current-repo))
+  (when (and (state/sub :mobile/show-tabbar?)
+             (state/get-current-repo))
     [:div.cp__footer.w-full.bottom-0.justify-between
      (audio-record-cp)
      (mobile-bar-command

+ 15 - 0
src/main/frontend/mobile/haptics.cljs

@@ -0,0 +1,15 @@
+(ns frontend.mobile.haptics
+  (:require
+   ["@capacitor/haptics" :refer [Haptics ImpactStyle]]
+   [promesa.core :as p]))
+
+(defn with-haptics-impact
+  [fn impact-style]
+  (let [style (cond
+                (= impact-style :light)
+                {:style (.-Light ImpactStyle)}
+
+                (= impact-style :medium)
+                {:style (.-Medium ImpactStyle)})]
+    (p/do! (.impact Haptics (clj->js style))
+           fn)))

+ 87 - 2
src/main/frontend/mobile/index.css

@@ -27,6 +27,42 @@
     }
 }
 
+.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 {
+            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
+        }
+    }
+}
+
 #mobile-editor-toolbar {
   position: fixed;
   bottom: 0;
@@ -80,19 +116,49 @@
 html.is-native-ipad {
     .cp__footer {
         height: 55px;
-        /* width: calc(100vw - var(--ls-left-sidebar-width)); */
         right: 0;
         box-shadow: none;
         flex: 1;
         index: 0;
     }
+    
+    .action-bar {
+        width: 70%;
+        min-width:550px;
+        
+        .action-bar-commands {
+            width: 100%;
+        }
+
+        @media (orientation: landscape) {
+            width: 50%;
+        }
+    }
 }
 
 html.is-native-iphone {
+
+    .action-bar {
+        left: 3%;
+        right: 3%;
+    }
+    
     @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%;
+            }
+        }
     }
 }
 
@@ -101,11 +167,30 @@ html.is-native-android {
     .cp__footer {
         height: 45px;
     }
+    
+    .action-bar {
+        left: 5%;
+        right: 5%;
+    }
+
+    @media (orientation: landscape) {
+        
+        .action-bar {
+            bottom: 50px;
+            left: 15%;
+            right: 15%;
+            width: 70%;
+
+            .action-bar-commands {
+                width: 100%;
+            }
+        }
+    }
 }
 
 html.is-zoomed-native-ios {
     .cp__footer {
-        height: 60px;
+        height: 70px;
     }
 
     @media (orientation: landscape) {

+ 1 - 0
src/main/frontend/mobile/intent.cljs

@@ -80,6 +80,7 @@
                         js/decodeURIComponent
                         util/node-path.name
                         util/file-name-sanity
+                        js/decodeURIComponent
                         (string/replace "." ""))
           path (path/join (config/get-repo-dir (state/get-current-repo))
                           (config/get-pages-directory)

+ 2 - 2
src/main/frontend/mobile/mobile_bar.cljs

@@ -130,8 +130,8 @@
                  (state/sub :editor/editing?))
         [:div#mobile-editor-toolbar.bg-base-2
          [:div.toolbar-commands
-          (indent-outdent false "arrow-bar-left")
-          (indent-outdent true "arrow-bar-right")
+          (indent-outdent false "indent-decrease")
+          (indent-outdent true "indent-increase")
           (command (editor-handler/move-up-down true) "arrow-bar-to-up")
           (command (editor-handler/move-up-down false) "arrow-bar-to-down")
           (command #(if (state/sub :document/mode?)

+ 7 - 2
src/main/frontend/state.cljs

@@ -145,6 +145,8 @@
      :electron/user-cfgs                    nil
 
      ;; mobile
+     :mobile/show-action-bar?               false
+     :mobile/actioned-block                 nil
      :mobile/show-toolbar?                  false
      ;;; toolbar icon doesn't update correctly when clicking after separate it from box,
      ;;; add a random in (<= 1000000) to observer its update
@@ -174,7 +176,7 @@
      :plugin/updates-downloading?           false
      :plugin/updates-unchecked              #{}
      :plugin/navs-settings?                 true
-     :plugin/focused-settings               nil            ;; plugin id
+     :plugin/focused-settings               nil ;; plugin id
 
      ;; pdf
      :pdf/current                           nil
@@ -856,7 +858,10 @@
              (util/set-change-value input content))
 
            (when move-cursor?
-             (cursor/move-cursor-to input pos))))))))
+             (cursor/move-cursor-to input pos))
+
+           (when (or (util/mobile?) (mobile-util/native-platform?))
+             (set-state! :mobile/show-action-bar? false))))))))
 
 (defn clear-edit!
   []

+ 4 - 0
yarn.lock

@@ -559,6 +559,10 @@
   resolved "https://registry.yarnpkg.com/@capacitor/filesystem/-/filesystem-1.0.6.tgz#b837585e6b5d48dc705ee89e49cc7c6aeb8874ec"
   integrity sha512-8xqUbDZFGBMhgqoBSn9wEd9OBPdHIRegQ9zCCZcpHNf3FFAIby1ck+aDFnoq+Da49xhD6ks1SKCBSxz/26qWTw==
 
+"@capacitor/haptics@^1.1.4":
+  version "1.1.4"
+  integrity sha512-+pJIb5X7xAcbrWj6rJaV+cwBlv8aFwB1/Ob6EV4atydThuuVSSsAL4hI4ZYlPNOxM6H5s+ZDLj7Pa2os4eFmtg==
+
 "@capacitor/[email protected]":
   version "3.2.2"
   resolved "https://registry.yarnpkg.com/@capacitor/ios/-/ios-3.2.2.tgz#0417cf89df23f651c6a1e20a8fb98294a918ce1f"