瀏覽代碼

Feat: record audio on mobile (#4766)

* feat(audio): render audio link as component

* refactor frontend/component/block/inline

* fix audio component on mobile

* Fix(iOS): allow clock to replay audio

* feat(mobile): audio record

* add permission request code on record

* rename `editor/recording?` to `editor/record-status`

* move `get-asset-path` into handler/editor.cljs

* add android settings

* support sharing audio file from some apps

* enhance(record): insert audio link in a new block if non-editing

* feat(audio): allow cancelling record

* fix lints

* feat(iOS): add a tab bar

* tweak some css on mobile

* fix lints

* fix https://github.com/logseq/logseq/issues/4798

* enable tab bar on Android

* fix landscape height of tab bar

* add :reuse-last-block? option

* dont't show tab bar when editing mirror code

* tweak code-editor css

* increase width for more functional buttons

* fix card preview css on iPad

* add document-mode to tab bar

* remove tabbar when editing code and tweak textarea resize cursor behavior

* reduce SplashScreen launch screen duration

* don't show tabbar when editing page title

* enhance: float timer

* fix lint

* fix tabbar height on iPhone without notch

* remove .embed-page width

* fix lint

Co-authored-by: Andelf <[email protected]>
llcc 3 年之前
父節點
當前提交
f4985fd8ef
共有 35 個文件被更改,包括 1023 次插入627 次删除
  1. 1 0
      android/app/capacitor.build.gradle
  2. 1 1
      android/app/src/main/assets/capacitor.config.json
  3. 4 0
      android/app/src/main/assets/capacitor.plugins.json
  4. 3 0
      android/capacitor.settings.gradle
  5. 1 1
      capacitor.config.ts
  6. 2 0
      ios/App/App/Info.plist
  7. 1 1
      ios/App/App/capacitor.config.json
  8. 1 0
      ios/App/Podfile
  9. 1 0
      package.json
  10. 2 0
      resources/css/common.css
  11. 557 519
      src/main/frontend/components/block.cljs
  12. 7 9
      src/main/frontend/components/block.css
  13. 15 12
      src/main/frontend/components/editor.cljs
  14. 13 0
      src/main/frontend/components/editor.css
  15. 2 1
      src/main/frontend/components/header.cljs
  16. 13 3
      src/main/frontend/components/header.css
  17. 2 0
      src/main/frontend/components/page.cljs
  18. 19 0
      src/main/frontend/components/page.css
  19. 12 4
      src/main/frontend/components/sidebar.cljs
  20. 4 0
      src/main/frontend/config.cljs
  21. 6 4
      src/main/frontend/extensions/code.cljs
  22. 5 0
      src/main/frontend/extensions/code.css
  23. 10 2
      src/main/frontend/handler/editor.cljs
  24. 2 1
      src/main/frontend/handler/events.cljs
  25. 1 11
      src/main/frontend/mobile/camera.cljs
  26. 7 6
      src/main/frontend/mobile/core.cljs
  27. 68 0
      src/main/frontend/mobile/footer.cljs
  28. 50 0
      src/main/frontend/mobile/index.css
  29. 59 43
      src/main/frontend/mobile/intent.cljs
  30. 78 0
      src/main/frontend/mobile/record.cljs
  31. 4 0
      src/main/frontend/state.cljs
  32. 3 3
      src/main/frontend/text.cljs
  33. 34 6
      src/main/frontend/ui.css
  34. 7 0
      src/main/frontend/util.cljc
  35. 28 0
      yarn.lock

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

@@ -15,6 +15,7 @@ dependencies {
     implementation project(':capacitor-keyboard')
     implementation project(':capacitor-splash-screen')
     implementation project(':capacitor-status-bar')
+    implementation project(':capacitor-voice-recorder')
     implementation project(':send-intent')
 
 }

+ 1 - 1
android/app/src/main/assets/capacitor.config.json

@@ -5,7 +5,7 @@
 	"webDir": "public",
 	"plugins": {
 		"SplashScreen": {
-			"launchShowDuration": 3000,
+			"launchShowDuration": 500,
 			"launchAutoHide": false,
 			"androidScaleType": "CENTER_CROP",
 			"splashImmersive": false,

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

@@ -23,6 +23,10 @@
 		"pkg": "@capacitor/status-bar",
 		"classpath": "com.capacitorjs.plugins.statusbar.StatusBarPlugin"
 	},
+	{
+		"pkg": "capacitor-voice-recorder",
+		"classpath": "com.tchvu3.capacitorvoicerecorder.VoiceRecorder"
+	},
 	{
 		"pkg": "send-intent",
 		"classpath": "de.mindlib.sendIntent.SendIntent"

+ 3 - 0
android/capacitor.settings.gradle

@@ -20,5 +20,8 @@ project(':capacitor-splash-screen').projectDir = new File('../node_modules/@capa
 include ':capacitor-status-bar'
 project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacitor/status-bar/android')
 
+include ':capacitor-voice-recorder'
+project(':capacitor-voice-recorder').projectDir = new File('../node_modules/capacitor-voice-recorder/android')
+
 include ':send-intent'
 project(':send-intent').projectDir = new File('../node_modules/send-intent/android')

+ 1 - 1
capacitor.config.ts

@@ -7,7 +7,7 @@ const config: CapacitorConfig = {
     webDir: 'public',
     plugins: {
         SplashScreen: {
-            launchShowDuration: 3000,
+            launchShowDuration: 500,
             launchAutoHide: false,
             androidScaleType: "CENTER_CROP",
             splashImmersive: false,

+ 2 - 0
ios/App/App/Info.plist

@@ -65,6 +65,8 @@
 	<string></string>
 	<key>NSFileProviderPresenceUsageDescription</key>
 	<string></string>
+	<key>NSMicrophoneUsageDescription</key>
+	<string>We will access your microphone to record audio notes</string>
 	<key>NSPhotoLibraryAddUsageDescription</key>
 	<string>We will access your album when you save a photo.</string>
 	<key>NSPhotoLibraryUsageDescription</key>

+ 1 - 1
ios/App/App/capacitor.config.json

@@ -5,7 +5,7 @@
 	"webDir": "public",
 	"plugins": {
 		"SplashScreen": {
-			"launchShowDuration": 3000,
+			"launchShowDuration": 500,
 			"launchAutoHide": false,
 			"androidScaleType": "CENTER_CROP",
 			"splashImmersive": false,

+ 1 - 0
ios/App/Podfile

@@ -15,6 +15,7 @@ def capacitor_pods
   pod 'CapacitorKeyboard', :path => '../../node_modules/@capacitor/keyboard'
   pod 'CapacitorSplashScreen', :path => '../../node_modules/@capacitor/splash-screen'
   pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
+  pod 'CapacitorVoiceRecorder', :path => '../../node_modules/capacitor-voice-recorder'
   pod 'SendIntent', :path => '../../node_modules/send-intent'
 end
 

+ 1 - 0
package.json

@@ -79,6 +79,7 @@
         "@sentry/tracing": "^6.18.2",
         "@tabler/icons": "1.54.0",
         "@tippyjs/react": "4.2.5",
+        "capacitor-voice-recorder": "2.1.0",
         "chokidar": "3.5.1",
         "chrono-node": "2.2.4",
         "codemirror": "5.58.1",

+ 2 - 0
resources/css/common.css

@@ -52,6 +52,7 @@ html[data-theme='dark'] {
   );
   --ls-border-color: #0e5263;
   --ls-secondary-border-color: #126277;
+  --ls-tertiary-border-color: rgba(0, 2, 0, 0.10);
   --ls-guideline-color: #0b4a5a;
   --ls-menu-hover-color: var(--ls-secondary-background-color);
   --ls-primary-text-color: #a4b5b6;
@@ -111,6 +112,7 @@ html[data-theme='light'] {
   --ls-search-background-color: var(--ls-primary-background-color);
   --ls-border-color: #ccc;
   --ls-secondary-border-color: #e2e2e2;
+  --ls-tertiary-border-color: rgba(200, 200, 200, 0.30);
   --ls-guideline-color: rgba(46, 27, 5, 0.08);
   --ls-menu-hover-color: var(--ls-a-chosen-bg);
   --ls-primary-text-color: #433f38;

File diff suppressed because it is too large
+ 557 - 519
src/main/frontend/components/block.cljs


+ 7 - 9
src/main/frontend/components/block.css

@@ -183,14 +183,6 @@
   }
  }
 
-html.is-mobile,
-html.is-native-iphone,
-html.is-native-android {
-    .references .block-control {
-        margin-left: -20px;
-    }
-}
-
 .block-ref {
   border-bottom: 0.5px solid;
   border-bottom-color: var(--ls-block-ref-link-text-color);
@@ -242,7 +234,7 @@ html.is-native-android {
 
 .embed-page {
   @apply py-2 my-2 px-2;
-
+  
   > section {
     margin-bottom: 5px;
   }
@@ -565,3 +557,9 @@ a.cloze-revealed {
 .block-parents a:hover {
   opacity: 1;
 }
+
+html.is-native-ios {
+    audio {
+        width: 300px;
+    }
+}

+ 15 - 12
src/main/frontend/components/editor.cljs

@@ -27,6 +27,7 @@
             [promesa.core :as p]
             [rum.core :as rum]
             [frontend.handler.history :as history]
+            [frontend.mobile.footer :as footer]
             [frontend.handler.config :as config-handler]))
 
 (rum/defc commands < rum/reactive
@@ -294,23 +295,17 @@
 
 (rum/defc mobile-bar < rum/reactive
   [parent-state parent-id]
-  (let [vw-state (state/sub :ui/visual-viewport-state)
-        vw-pending? (state/sub :ui/visual-viewport-pending?)
-        commands (mobile-bar-commands parent-state parent-id)
+  (let [commands (mobile-bar-commands parent-state parent-id)
         sorted-commands (sort-by (comp :counts second) > @mobile-bar-commands-stats)]
     [:div#mobile-editor-toolbar.bg-base-2
-     {:style {:bottom (if vw-state
-                        (- (.-clientHeight js/document.documentElement)
-                           (:height vw-state)
-                           (:offset-top vw-state))
-                        0)}
-      :class (util/classnames [{:is-vw-pending (boolean vw-pending?)}])}
      [:div.toolbar-commands
       (mobile-bar-indent-outdent true "arrow-bar-right")
       (mobile-bar-indent-outdent false "arrow-bar-left")
       (mobile-bar-command (editor-handler/move-up-down true) "arrow-bar-to-up")
       (mobile-bar-command (editor-handler/move-up-down false) "arrow-bar-to-down")
-      (mobile-bar-command #(commands/simple-insert! parent-id "\n" {}) "arrow-back")
+      (mobile-bar-command #(if (state/sub :document/mode?)
+                             (editor-handler/insert-new-block! nil)
+                             (commands/simple-insert! parent-id "\n" {})) "arrow-back")
       (for [command sorted-commands]
         ((first command) commands))]
      [:div.toolbar-hide-keyboard
@@ -608,12 +603,20 @@
   (mixins/event-mixin setup-key-listener!)
   (shortcut/mixin :shortcut.handler/block-editing-only)
   lifecycle/lifecycle
-  [state {:keys [format block]} id _config]
+  [state {:keys [format block]} id config]
   (let [content (state/sub-edit-content)
         heading-class (get-editor-style-class content format)]
     [:div.editor-inner {:class (if block "block-editor" "non-block-editor")}
-     (when (or (mobile-util/is-native-platform?) config/mobile?)
+
+     (when (= (state/sub :editor/record-status) "RECORDING")
+       [:div#audio-record-toolbar
+        (footer/audio-record-cp)])
+
+     (when (and (or (mobile-util/is-native-platform?)
+                    config/mobile?)
+                (not (:review-cards? config)))
        (mobile-bar state id))
+     
      (ui/ls-textarea
       {:id                id
        :cacheMeasurements (editor-row-height-unchanged?) ;; check when content updated (as the content variable is binded)

+ 13 - 0
src/main/frontend/components/editor.css

@@ -28,6 +28,19 @@
   }
 }
 
+#audio-record-toolbar {
+    position: fixed;
+    background-color: var(--ls-secondary-background-color);
+    bottom: 45px;
+    width: 88px;
+    justify-content: left;
+    left: 5px;
+    transition: none;
+    z-index: 9999;
+    padding: 5px 5px 0px 5px;
+    border-radius: 5px;
+}
+
 .editor-wrapper {
   width: 100%;
   margin: 0 auto;

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

@@ -275,7 +275,8 @@
                 (mobile-util/native-ios?))
         (back-and-forward))
 
-      (new-block-mode)
+      (when-not (mobile-util/is-native-platform?)
+        (new-block-mode))
 
       (repo/sync-status current-repo)
 

+ 13 - 3
src/main/frontend/components/header.css

@@ -224,9 +224,19 @@ html.is-native-ipad {
         padding-top: 0px;
     }
 
-    #main-content-container {
-        padding-top: 0px;
-        height: calc(100vh - var(--ls-headbar-inner-top-padding) - var(--ls-headbar-height));
+     #main-content-container {
+         padding-left: 22px;
+         padding-right: 14px;
+         padding-top: 0px;
+         height: calc(100vh - var(--ls-headbar-inner-top-padding) - var(--ls-headbar-height));
+
+         @screen sm {
+             padding-left: 2rem;
+         }
+
+         .page {
+             margin-top: 24px;
+         }
     }
     
     .cp__header > .r {

+ 2 - 0
src/main/frontend/components/page.cljs

@@ -214,6 +214,7 @@
                     (when (util/wrapped-by-quotes? @*title-value)
                       (swap! *title-value util/unquote-string)
                       (gobj/set (rum/deref input-ref) "value" @*title-value))
+                    (state/set-state! :editor/editing-page-title? false)
                     (cond
                       (= old-name @*title-value)
                       (reset! *edit? false)
@@ -248,6 +249,7 @@
                               (reset! *title-value old-name)
                               (reset! *edit? false)))}]]
         [:a.page-title {:on-mouse-down (fn [e]
+                                         (state/set-state! :editor/editing-page-title? true)
                                          (when (util/right-click? e)
                                            (state/set-state! :page-title/context {:page page-name})))
                         :on-click (fn [e]

+ 19 - 0
src/main/frontend/components/page.css

@@ -274,6 +274,17 @@
   }
 }
 
+html.is-native-android,
+html.is-native-ipad,
+html.is-native-iphone,
+html.is-native-iphone-without-notch {
+    
+    .ls-page-title {
+        margin: 0px 0px 24px -15px;
+        padding: 0px;
+    }
+}
+
 /* Change to another cursor style if Shift key is active */
 [data-active-keystroke*="Shift" i]
 :is(.journal-title, .page-title,
@@ -326,6 +337,14 @@ html.is-native-ios {
       }
     }
   }
+
+  .block-content-wrapper {
+      /* 38px is the width of block-control */
+      width: calc(100% - 35px);
+      @screen sm {
+          width: calc(100% - 33px);
+      }
+  }
 }
 
 .page-blocks-collapse-control, .page-blocks-collapse-control:hover {

+ 12 - 4
src/main/frontend/components/sidebar.cljs

@@ -33,7 +33,8 @@
             [frontend.extensions.pdf.assets :as pdf-assets]
             [frontend.mobile.util :as mobile-util]
             [frontend.handler.mobile.swipe :as swipe]
-            [frontend.components.onboarding :as onboarding]))
+            [frontend.components.onboarding :as onboarding]
+            [frontend.mobile.footer :as footer]))
 
 (rum/defc nav-content-item
   [name {:keys [class]} child]
@@ -290,7 +291,8 @@
      (left-sidebar {:left-sidebar-open? left-sidebar-open?
                     :route-match route-match})
 
-     [:div#main-content-container.scrollbar-spacing.w-full.flex.justify-center
+     [:div#main-content-container.scrollbar-spacing.w-full.flex.justify-center.flex-row
+      
       [:div.cp__sidebar-main-content
        {:data-is-global-graph-pages global-graph-pages?
         :data-is-full-width         (or global-graph-pages?
@@ -499,7 +501,13 @@
                :indexeddb-support?  indexeddb-support?
                :light?              light?
                :db-restoring?       db-restoring?
-               :main-content        main-content})]
+               :main-content        main-content})
+
+        (when (and (mobile-util/is-native-platform?)
+                   current-repo
+                   (not (state/sub :modal/show?)))
+          (footer/footer))]
+       
        (right-sidebar/sidebar)
 
        [:div#app-single-container]]
@@ -518,4 +526,4 @@
       (when
           (and (not config/mobile?)
                (not config/publishing?))
-        (help-button))])))
+          (help-button))])))

+ 4 - 0
src/main/frontend/config.cljs

@@ -95,6 +95,10 @@
      config-formats
      #{:gif :svg :jpeg :ico :png :jpg :bmp :webp})))
 
+(def audio-formats #{:mp3 :ogg :mpeg :wav})
+
+(def media-formats (set/union (img-formats) audio-formats))
+
 (def html-render-formats
   #{:adoc :asciidoc})
 

+ 6 - 4
src/main/frontend/extensions/code.cljs

@@ -252,14 +252,16 @@
         (.on editor "blur" (fn [cm e]
                              (when e (util/stop e))
                              (when-not (gobj/get cm "escPressed")
-                               (save-file-or-block-when-blur-or-esc! editor textarea config state))))
+                               (save-file-or-block-when-blur-or-esc! editor textarea config state))
+                             (state/set-block-component-editing-mode! false)))
+        (.on editor "focus" (fn [_e]
+                              (state/set-block-component-editing-mode! true)))
         (.addEventListener element "mousedown"
                            (fn [e]
                              (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))
-                             (util/stop e)
-                             (state/set-block-component-editing-mode! true)))
+                             (util/stop e)))
         (.save editor)
         (.refresh editor)
         (when default-open?
@@ -297,7 +299,7 @@
      (when-not (= mode "calc")
        [:div.extensions__code-lang
         (string/lower-case mode)]))
-   [:div.flex.flex-1.flex-row.w-full.mt-6
+   [:div.code-editor.flex.flex-1.flex-row.w-full
     [:textarea (merge {:id id
                        ;; Expose the textarea associated with the CodeMirror instance via
                        ;; ref so that we can autofocus into the CodeMirror instance later.

+ 5 - 0
src/main/frontend/extensions/code.css

@@ -5,6 +5,7 @@
   flex-direction: row;
   flex-wrap: nowrap;
   justify-content: space-between;
+
   &-lang {
     @apply p-1 text-sm;
     background: var(--ls-secondary-background-color);
@@ -20,6 +21,10 @@
     z-index: 9999;
   }
 
+  .code-editor {
+      margin-top: 28px;
+  }
+
   &-calc {
     @apply text-sm;
     padding: 0.25em;

+ 10 - 2
src/main/frontend/handler/editor.cljs

@@ -54,7 +54,8 @@
             [lambdaisland.glogi :as log]
             [medley.core :as medley]
             [promesa.core :as p]
-            [frontend.util.keycode :as keycode]))
+            [frontend.util.keycode :as keycode]
+            ["path" :as path]))
 
 ;; FIXME: should support multiple images concurrently uploading
 
@@ -1525,6 +1526,13 @@
      (fs/mkdir-if-not-exists (str repo-dir "/" assets-dir))
      (fn [] [repo-dir assets-dir]))))
 
+(defn get-asset-path [filename]
+  (p/let [[repo-dir assets-dir] (ensure-assets-dir! (state/get-current-repo))
+          path (path/join repo-dir assets-dir filename)]
+    (if (mobile-util/native-android?)
+      path
+      (js/encodeURI (js/decodeURI path)))))
+
 (defn save-assets!
   ([_ repo files]
    (p/let [[repo-dir assets-dir] (ensure-assets-dir! repo)]
@@ -3042,7 +3050,7 @@
       (when (<  vw-height (+ cursor-y mobile-toolbar-height))
         (let [main-node (gdom/getElement "main-content-container")
               scroll-top (.-scrollTop main-node)]
-          (set! (.-scrollTop main-node) (+ scroll-top (/ vw-height 2))))))))
+          (set! (.-scrollTop main-node) (+ scroll-top row-height)))))))
 
 (defn editor-on-change!
   [block id search-timeout]

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

@@ -230,7 +230,8 @@
     (state/set-modal! (query-properties-settings block shown-properties all-properties))))
 
 (defmethod handle :modal/show-cards [_]
-  (state/set-modal! srs/global-cards {:id :srs}))
+  (state/set-modal! srs/global-cards {:id :srs
+                                      :label "flashcards__cp"}))
 
 (defmethod handle :modal/show-themes-modal [_]
   (plugin/open-select-theme!))

+ 1 - 11
src/main/frontend/mobile/camera.cljs

@@ -8,7 +8,6 @@
             [frontend.date :as date]
             [frontend.util :as util]
             [frontend.commands :as commands]
-            [frontend.mobile.util :as mobile-util]
             [goog.object :as gobj]
             [frontend.util.cursor :as cursor]))
 
@@ -22,17 +21,8 @@
                                         :resultType (.-Base64 CameraResultType)}))
                     (fn [error]
                       (log/error :photo/get-failed {:error error})))
-          [repo-dir assets-dir] (editor-handler/ensure-assets-dir! (state/get-current-repo))
           filename (str (date/get-date-time-string-2) ".jpeg")
-          path (cond
-                 (mobile-util/native-android?)
-                 (str "file://" repo-dir "/" assets-dir "/" filename)
-
-                 (mobile-util/native-ios?)
-                 (str repo-dir assets-dir "/" filename)
-
-                 :else
-                 (str repo-dir assets-dir "/" filename))
+          path (editor-handler/get-asset-path filename)
           _file (p/catch
                     (.writeFile Filesystem (clj->js {:data (.-base64String photo)
                                                      :path path

+ 7 - 6
src/main/frontend/mobile/core.cljs

@@ -2,7 +2,7 @@
   (:require [frontend.mobile.util :as mobile-util]
             [frontend.state :as state]
             ["@capacitor/app" :refer [^js App]]
-            ["@capacitor/keyboard" :refer [^js Keyboard]]
+            ;; ["@capacitor/keyboard" :refer [^js Keyboard]]
             #_:clj-kondo/ignore
             ["@capacitor/status-bar" :refer [^js StatusBar]]
             [frontend.mobile.intent :as intent]
@@ -19,8 +19,9 @@
   ;; Keyboard watcher
   ;; (.addListener Keyboard "keyboardWillShow"
   ;;               #(state/pub-event! [:mobile/keyboard-will-show]))
-  (.addListener Keyboard "keyboardDidShow"
-                #(state/pub-event! [:mobile/keyboard-did-show])))
+  ;; (.addListener Keyboard "keyboardDidShow"
+  ;;               #(state/pub-event! [:mobile/keyboard-did-show]))
+  )
 
 (defn init!
   []
@@ -62,7 +63,7 @@
                     (when (state/get-current-repo)
                       (let [is-active? (.-isActive state)]
                         (when is-active?
-                          (editor-handler/save-current-block!))))))))
+                          (editor-handler/save-current-block!))))))
 
-(.addEventListener js/window "sendIntentReceived"
-                   #(intent/handle-received))
+    (.addEventListener js/window "sendIntentReceived"
+                       #(intent/handle-received))))

+ 68 - 0
src/main/frontend/mobile/footer.cljs

@@ -0,0 +1,68 @@
+(ns frontend.mobile.footer
+  (:require [frontend.ui :as ui]
+            [rum.core :as rum]
+            [frontend.state :as state]
+            [frontend.mobile.record :as record]
+            [frontend.util :as util]
+            [frontend.handler.editor :as editor-handler]
+            [clojure.string :as string]
+            [frontend.date :as date]))
+
+(rum/defc mobile-bar-command [command-handler icon]
+  [:div
+   [:button.bottom-action
+    {:on-mouse-down (fn [e]
+                      (util/stop e)
+                      (command-handler))}
+    (ui/icon icon {:style {:fontSize ui/icon-size}})]])
+
+(defn seconds->minutes:seconds
+  [seconds]
+  (let [minutes (quot seconds 60)
+        seconds (mod seconds 60)]
+    (util/format "%02d:%02d" minutes seconds)))
+
+(def *record-start (atom -1))
+(rum/defcs audio-record-cp < rum/reactive
+  {:did-mount (fn [state]
+                (let [comp (:rum/react-component state)
+                      callback #(rum/request-render comp)
+                      interval (js/setInterval callback 1000)]
+                  (assoc state ::interval interval)))
+   :will-mount (fn [state]
+                 (js/clearInterval (::interval state))
+                 (dissoc state ::interval))}
+  [state]
+  (when (= (state/sub :editor/record-status) "RECORDING")
+    (swap! *record-start inc))
+  [:div.flex.flex-row
+   (if (= (state/sub :editor/record-status) "NONE")
+     (do
+       (reset! *record-start -1)
+       (mobile-bar-command #(record/start-recording) "microphone"))
+     [:div.flex.flex-row
+      (mobile-bar-command #(record/stop-recording) "player-stop")
+      [:div.timer.pl-2 (seconds->minutes:seconds @*record-start)]])])
+
+(rum/defc footer < rum/reactive
+  []
+  (when-not (or (state/sub :editor/editing?)
+                (state/sub :block/component-editing-mode?)
+                (state/sub :editor/editing-page-title?))
+    [:div.cp__footer.w-full.bottom-0.justify-between
+     (audio-record-cp)
+     (mobile-bar-command #(state/toggle-document-mode!) "notes")
+     (mobile-bar-command
+      #(let [page (or (state/get-current-page)
+                      (string/lower-case (date/journal-name)))
+             block (editor-handler/api-insert-new-block!
+                    ""
+                    {:page page
+                     :reuse-last-block? true})]
+         (js/setTimeout
+          (fn [] (editor-handler/edit-block!
+                  block
+                  :max
+                  (:block/uuid block))) 100))
+      "edit")]))
+

+ 50 - 0
src/main/frontend/mobile/index.css

@@ -0,0 +1,50 @@
+.cp__footer {
+    position: absolute;
+    bottom: 0px;
+    padding: 10px 20px;
+    background-color: var(--ls-primary-background-color);
+    z-index: 1000;
+    display: flex;
+    flex: 0 0 auto;
+    white-space: nowrap;
+    height: 80px;
+    /* border-top: 1.5px solid var(--ls-tertiary-border-color); */
+    box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.10);
+    
+    .ti, .timer {
+        color: var(--ls-primary-text-color);
+        opacity: 0.5;
+    }
+
+    .timer {
+        position: absolute;
+        left: 40px;
+    }
+}
+
+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;
+    }
+}
+
+html.is-native-iphone {
+    @media (orientation: landscape) {
+        .cp__footer {
+            height: 45px;
+        }
+    }
+}
+
+html.is-native-iphone-without-notch,
+html.is-native-android {
+    .cp__footer {
+        height: 45px;
+    }
+}
+

+ 59 - 43
src/main/frontend/mobile/intent.cljs

@@ -14,7 +14,8 @@
             ["path" :as path]
             [frontend.mobile.util :as mobile-util]
             [frontend.handler.notification :as notification]
-            [clojure.pprint :as pprint]))
+            [clojure.pprint :as pprint]
+            [clojure.set :as set]))
 
 (defn- handle-received-text [result]
   (let [{:keys [title url]} result
@@ -51,64 +52,79 @@
                    (string/replace "{url}" (or url "")))]
     (if (state/get-edit-block)
       (state/append-current-edit-content! values)
-      (editor-handler/api-insert-new-block! values {:page page}))))
+      (editor-handler/api-insert-new-block! values {:page page
+                                                    :reuse-last-block? true}))))
 
-(defn get-asset-path
-  [filename]
-  (p/let [[repo-dir assets-dir]
-          (editor-handler/ensure-assets-dir! (state/get-current-repo))
-          path (path/join repo-dir assets-dir filename)]
-    (if (mobile-util/native-android?)
-      path
-      (js/encodeURI (js/decodeURI path)))))
-
-(defn- handle-received-media [result]
-  (p/let [{:keys [title url]} result
-          page (or (state/get-current-page)
-                   (string/lower-case (date/journal-name)))
-          format (db/get-page-format page)
-          time (date/get-current-time)
-          basename (path/basename url)
+(defn- embed-asset-file [url format]
+  (p/let [basename (path/basename url)
           label (-> basename util/node-path.name)
-          path (get-asset-path (or (path/basename url) title))
-          _ (p/catch
-                (.copy Filesystem (clj->js {:from url :to path}))
-                (fn [error]
-                  (log/error :copy-file-error {:error error})))
+          time (date/get-current-time)
+          path (editor-handler/get-asset-path basename)
+          _file (p/catch
+                    (.copy Filesystem (clj->js {:from url :to path}))
+                    (fn [error]
+                      (log/error :copy-file-error {:error error})))
           url (util/format "../assets/%s" basename)
           url (editor-handler/get-asset-file-link format url label true)
           template (get-in (state/get-config)
-                           [:quick-capture-template :image]
-                           "**{time}** [[quick capture]]: {url}")
-          values (-> (string/replace template "{time}" time)
-                     (string/replace "{url}" (or url "")))]
-    (if (state/get-edit-block)
-      (state/append-current-edit-content! values)
-      (editor-handler/api-insert-new-block! values {:page page}))))
+                           [:quick-capture-templates :media]
+                           "**{time}** [[quick capture]]: {url}")]
+    (-> (string/replace template "{time}" time)
+        (string/replace "{url}" (or url "")))))
 
-(defn- handle-received-application [result]
-  (p/let [{:keys [title url]} result
-          page (or (state/get-current-page) (string/lower-case (date/journal-name)))
-          time (date/get-current-time)
+(defn- embed-text-file [url title]
+  (p/let [time (date/get-current-time)
           title (some-> (or title (path/basename url))
                         js/decodeURIComponent
                         util/node-path.name)
-          path (and url (path/join (config/get-repo-dir (state/get-current-repo))
-                                   (config/get-pages-directory)
-                                   (path/basename url)))
+          path (path/join (config/get-repo-dir (state/get-current-repo))
+                          (config/get-pages-directory)
+                          (path/basename url))
           _ (p/catch
                 (.copy Filesystem (clj->js {:from url :to path}))
                 (fn [error]
                   (log/error :copy-file-error {:error error})))
           url (util/format "[[%s]]" title)
           template (get-in (state/get-config)
-                           [:quick-capture-template :image]
-                           "**{time}** [[quick capture]]: {url}")
-          values (-> (string/replace template "{time}" time)
-                     (string/replace "{url}" (or url "")))]
+                           [:quick-capture-template :text]
+                           "**{time}** [[quick capture]]: {url}")]
+    (-> (string/replace template "{time}" time)
+        (string/replace "{url}" (or url "")))))
+
+(defn- handle-received-media [result]
+  (p/let [{:keys [url]} result
+          page (or (state/get-current-page) (string/lower-case (date/journal-name)))
+          format (db/get-page-format page)
+          content (embed-asset-file url format)]
     (if (state/get-edit-block)
-      (state/append-current-edit-content! values)
-      (editor-handler/api-insert-new-block! values {:page page}))))
+      (state/append-current-edit-content! content)
+      (editor-handler/api-insert-new-block! content {:page page
+                                                     :reuse-last-block? true}))))
+
+(defn- handle-received-application [result]
+  (p/let [{:keys [title url type]} result
+          page (or (state/get-current-page) (string/lower-case (date/journal-name)))
+          format (db/get-page-format page)
+          application-type (last (string/split type "/"))
+          content (cond
+                    (config/mldoc-support? application-type)
+                    (embed-text-file url title)
+
+                    (contains? (set/union #{:pdf} config/media-formats) (keyword application-type))
+                    (embed-asset-file url format)
+
+                    :else
+                    (notification/show!
+                     [:div
+                      (str "Import " application-type " file has not been supported. You can report it on ")
+                      [:a {:href "https://github.com/logseq/logseq/issues"
+                           :target "_blank"} "Github"]
+                      ". We will look into it soon."]
+                     :warning false))]
+    (if (state/get-edit-block)
+      (state/append-current-edit-content! content)
+      (editor-handler/api-insert-new-block! content {:page page
+                                                     :reuse-last-block? true}))))
 
 (defn decode-received-result [m]
   (into {} (for [[k v] m]

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

@@ -0,0 +1,78 @@
+(ns frontend.mobile.record
+  (:require ["@capacitor/filesystem" :refer [Filesystem]]
+            ["capacitor-voice-recorder" :refer [VoiceRecorder]]
+            [promesa.core :as p]
+            [frontend.handler.editor :as editor-handler]
+            [frontend.state :as state]
+            [frontend.date :as date]
+            [lambdaisland.glogi :as log]
+            [frontend.util :as util]
+            [clojure.string :as string]
+            [frontend.db :as db]))
+
+(defn request-audio-recording-permission []
+  (p/then
+   (.requestAudioRecordingPermission VoiceRecorder)
+   (fn [^js result] (.-value result))))
+
+(defn- has-audio-recording-permission? []
+  (p/then
+   (.hasAudioRecordingPermission VoiceRecorder)
+   (fn [^js result] (.-value result))))
+
+(defn- set-recording-state []
+  (p/catch
+   (p/then (.getCurrentStatus VoiceRecorder)
+           (fn [^js result]
+             (let [{:keys [status]} (js->clj result :keywordize-keys true)]
+               (state/set-state! :editor/record-status status))))
+   (fn [error]
+     (js/console.error error))))
+
+(defn start-recording []
+  (p/let [permission-granted? (has-audio-recording-permission?)
+          permission-granted? (or permission-granted?
+                                  (request-audio-recording-permission))]
+    (when permission-granted?
+      (p/catch
+       (p/then (.startRecording VoiceRecorder)
+               (fn [^js _result]
+                 (set-recording-state)
+                 (js/console.log "Start recording...")))
+       (fn [error]
+         (log/error :start-recording-error error))))))
+
+(defn- embed-audio [database64]
+  (p/let [page (or (state/get-current-page) (string/lower-case (date/journal-name)))
+          filename (str (date/get-date-time-string-2) ".mp3")
+          edit-block (state/get-edit-block)
+          format (or (:block/format edit-block) (db/get-page-format page))
+          path (editor-handler/get-asset-path filename)
+          _file (p/catch
+                 (.writeFile Filesystem (clj->js {:data database64
+                                                  :path path
+                                                  :recursive true}))
+                 (fn [error]
+                   (log/error :file/write-failed {:path path
+                                                  :error error})))
+          url (util/format "../assets/%s" filename)
+          file-link (editor-handler/get-asset-file-link format url filename true)]
+    (if edit-block
+      (state/append-current-edit-content! file-link)
+      (editor-handler/api-insert-new-block! file-link {:page page
+                                                       :reuse-last-block? true}))))
+
+(defn stop-recording []
+  (p/catch
+   (p/then
+    (.stopRecording VoiceRecorder)
+    (fn [^js result]
+      (let [value (.-value result)
+            {:keys [_msDuration recordDataBase64 _mimeType]}
+            (js->clj value :keywordize-keys true)]
+        (set-recording-state)
+        (when (string? recordDataBase64)
+          (embed-audio recordDataBase64)
+          (js/console.log "Stop recording...")))))
+   (fn [error]
+     (js/console.error error))))

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

@@ -122,7 +122,11 @@
      :editor/args                           nil
      :editor/on-paste?                      false
      :editor/last-key-code                  nil
+     :editor/editing-page-title?            false
 
+     ;; for audio record
+     :editor/record-status                  "NONE"
+     
      :db/last-transact-time                 {}
      ;; whether database is persisted
      :db/persisted?                         {}

+ 3 - 3
src/main/frontend/text.cljs

@@ -229,9 +229,9 @@
     (util/format "[%s]"
                  (string/join ", " items))))
 
-(defn image-link?
-  [img-formats s]
-  (some (fn [fmt] (util/safe-re-find (re-pattern (str "(?i)\\." fmt "(?:\\?([^#]*))?(?:#(.*))?$")) s)) img-formats))
+(defn media-link?
+  [media-formats s]
+  (some (fn [fmt] (util/safe-re-find (re-pattern (str "(?i)\\." fmt "(?:\\?([^#]*))?(?:#(.*))?$")) s)) media-formats))
 
 (defn namespace-page?
   [p]

+ 34 - 6
src/main/frontend/ui.css

@@ -146,22 +146,50 @@
           }
       }
   }
+
+  &[label="flashcards__cp"] {
+      .panel-content {
+          padding: 2rem 0rem;
+          
+          @screen sm {
+              padding: 2rem 2rem;
+          }
+      }
+  }
 }
 
 html.is-native-andorid,
 html.is-native-iphone,
 html.is-native-iphone-without-notch
 {
-    .references .foldable-title {
-        margin-left: 0px
+    .references  {
+        .blocks-container {
+            transform: translateX(-8px);
+            width: 104%;
+        }
     }
 
-    .cards-review .block-control {
-        margin-left: -24px;
+    .ls-card {
+        min-height: 65vh;
     }
 
-    .ls-card {
-        min-height: 75vh;
+    .ui__modal {
+        &[label="flashcards__cp"] {
+            .panel-content {
+                padding: 2rem 1rem 1rem;
+                width: 90vw;
+
+                .cards-review {
+                    padding-left: 0px;
+                    padding-right: 0px;
+                }
+
+                .blocks-container {
+                    transform: translateX(-7px);
+                    width: 103%;
+                }
+            }
+        }
     }
 }
 

+ 7 - 0
src/main/frontend/util.cljc

@@ -461,6 +461,13 @@
       #{"TIME"}
       (gobj/get node "tagName"))))
 
+#?(:cljs
+   (defn audio?
+     [node]
+     (contains?
+      #{"AUDIO"}
+      (gobj/get node "tagName"))))
+
 #?(:cljs
    (defn sup?
      [node]

+ 28 - 0
yarn.lock

@@ -429,6 +429,13 @@
     "@babel/helper-validator-option" "^7.16.7"
     "@babel/plugin-transform-typescript" "^7.16.7"
 
+"@babel/[email protected]":
+  version "7.11.2"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736"
+  integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==
+  dependencies:
+    regenerator-runtime "^0.13.4"
+
 "@babel/runtime@^7.10.2", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7":
   version "7.17.7"
   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.7.tgz#a5f3328dc41ff39d803f311cfe17703418cf9825"
@@ -1937,6 +1944,13 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001317:
   resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001317.tgz#0548fb28fd5bc259a70b8c1ffdbe598037666a1b"
   integrity sha512-xIZLh8gBm4dqNX0gkzrBeyI86J2eCjWzYAs40q88smG844YIrN4tVQl/RhquHvKEKImWWFIVh1Lxe5n1G/N+GQ==
 
[email protected]:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/capacitor-voice-recorder/-/capacitor-voice-recorder-2.1.0.tgz#142e7bfa62e88530279f478b79735a0dc68a7d1a"
+  integrity sha512-H0c/sUVD7cduVS5VqutKk00whyqXZUFi56ChRMl9Ke/LBU71HhHwzonPmheT8i9gQmgOaplc3TOpaKqckXb+3A==
+  dependencies:
+    get-blob-duration "^1.2.0"
+
 chalk@^2.0.0, chalk@^2.4.1:
   version "2.4.2"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
@@ -3535,6 +3549,13 @@ gensync@^1.0.0-beta.2:
   resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
   integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
 
+get-blob-duration@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/get-blob-duration/-/get-blob-duration-1.2.0.tgz#73cf7dac2fbaa219a5a03d5e5093e06e43814d49"
+  integrity sha512-2xNJa+oKznR21eC2ThMzw4a1931a3ogA8aHoY92xruZufc/02G7pl/P793GJZytkyI8xMJ2DepEQ7MWvg/tn/Q==
+  dependencies:
+    "@babel/runtime" "7.11.2"
+
 get-caller-file@^1.0.1:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a"
@@ -6757,6 +6778,11 @@ [email protected]:
     react-draggable "3.x"
     react-resizable "1.x"
 
[email protected]:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/react-icon-base/-/react-icon-base-2.1.0.tgz#a196e33fdf1e7aaa1fda3aefbb68bdad9e82a79d"
+  integrity sha1-oZbjP98eeqof2jrvu2i9rZ6Cp50=
+
 react-icon-base@^2.1.2:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/react-icon-base/-/react-icon-base-2.1.2.tgz#a17101dad9c1192652356096860a9ab43a0766c7"
@@ -6766,6 +6792,8 @@ [email protected]:
   version "2.2.7"
   resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-2.2.7.tgz#d7860826b258557510dac10680abea5ca23cf650"
   integrity sha512-0n4lcGqzJFcIQLoQytLdJCE0DKSA9dkwEZRYoGrIDJZFvIT6Hbajx5mv9geqhqFiNjUgtxg8kPyDfjlhymbGFg==
+  dependencies:
+    react-icon-base "2.1.0"
 
 react-is@^16.13.1, react-is@^16.3.1, react-is@^16.7.0:
   version "16.13.1"

Some files were not shown because too many files changed in this diff