diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle
index 2f38dd5812..e862e31b65 100644
--- a/android/app/capacitor.build.gradle
+++ b/android/app/capacitor.build.gradle
@@ -21,7 +21,6 @@ dependencies {
implementation project(':capacitor-share')
implementation project(':capacitor-splash-screen')
implementation project(':capacitor-status-bar')
- implementation project(':capacitor-voice-recorder')
implementation project(':send-intent')
implementation project(':jcesarmobile-ssl-skip')
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 7e02f310ed..476c0fb551 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -9,6 +9,7 @@
+
NSAppTransportSecurity
-
- NSAllowsArbitraryLoads
-
-
- APFiles
-
- APFileDescriptionKey
-
- APFileDestinationPath
-
- APFileName
-
- APFileSourcePath
-
-
- CFBundleDevelopmentRegion
- en
- CFBundleDisplayName
- Logseq
- CFBundleExecutable
- $(EXECUTABLE_NAME)
- CFBundleIdentifier
- $(PRODUCT_BUNDLE_IDENTIFIER)
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleName
- $(PRODUCT_NAME)
- CFBundlePackageType
- APPL
- CFBundleShortVersionString
- $(MARKETING_VERSION)
- CFBundleURLTypes
-
-
- CFBundleTypeRole
- Viewer
- CFBundleURLName
- com.logseq.logseq
- CFBundleURLSchemes
-
- logseq
-
-
-
- CFBundleVersion
- $(CURRENT_PROJECT_VERSION)
- LSApplicationCategoryType
-
- LSRequiresIPhoneOS
-
- LSSupportsOpeningDocumentsInPlace
-
- UIFileSharingEnabled
-
- NSCameraUsageDescription
- We will access your camera when you take a photo, and embed it in your note.
- NSDocumentsFolderUsageDescription
-
- NSDownloadsFolderUsageDescription
-
- NSFileProviderDomainUsageDescription
-
- NSFileProviderPresenceUsageDescription
-
- NSMicrophoneUsageDescription
- We will access your microphone to record audio notes
- NSPhotoLibraryAddUsageDescription
- We will access your album when you save a photo.
- NSPhotoLibraryUsageDescription
- We will access your album when you choose a photo, and embed it in your note.
- NSUbiquitousContainers
-
- iCloud.com.logseq.logseq
-
- NSUbiquitousContainerIsDocumentScopePublic
-
- NSUbiquitousContainerName
- Logseq
- NSUbiquitousContainerSupportedFolderLevels
- ANY
-
-
- UIBackgroundModes
-
- audio
-
- UILaunchStoryboardName
- LaunchScreen
- UIMainStoryboardFile
- Main
- UIRequiredDeviceCapabilities
-
- armv7
-
- UISupportedInterfaceOrientations
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- UISupportedInterfaceOrientations~ipad
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationPortraitUpsideDown
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- UISupportsDocumentBrowser
-
- UIViewControllerBasedStatusBarAppearance
-
- CFBundleGetInfoString
-
- ITSAppUsesNonExemptEncryption
-
+
+ NSAllowsArbitraryLoads
+
+
+ APFiles
+
+ APFileDescriptionKey
+
+ APFileDestinationPath
+
+ APFileName
+
+ APFileSourcePath
+
+
+ CFBundleDevelopmentRegion
+ en
+ CFBundleDisplayName
+ Logseq
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(MARKETING_VERSION)
+ CFBundleURLTypes
+
+
+ CFBundleTypeRole
+ Viewer
+ CFBundleURLName
+ com.logseq.logseq
+ CFBundleURLSchemes
+
+ logseq
+
+
+
+ CFBundleVersion
+ $(CURRENT_PROJECT_VERSION)
+ LSApplicationCategoryType
+
+ LSRequiresIPhoneOS
+
+ LSSupportsOpeningDocumentsInPlace
+
+ UIFileSharingEnabled
+
+ NSCameraUsageDescription
+ We will access your camera when you take a photo, and embed it in your note.
+ NSDocumentsFolderUsageDescription
+
+ NSDownloadsFolderUsageDescription
+
+ NSFileProviderDomainUsageDescription
+
+ NSFileProviderPresenceUsageDescription
+
+ NSSpeechRecognitionUsageDescription
+ We need access to speech recognition to convert your voice to text.
+ NSMicrophoneUsageDescription
+ We will access your microphone to record audio notes
+ NSPhotoLibraryAddUsageDescription
+ We will access your album when you save a photo.
+ NSPhotoLibraryUsageDescription
+ We will access your album when you choose a photo, and embed it in your note.
+ NSUbiquitousContainers
+
+ iCloud.com.logseq.logseq
+
+ NSUbiquitousContainerIsDocumentScopePublic
+
+ NSUbiquitousContainerName
+ Logseq
+ NSUbiquitousContainerSupportedFolderLevels
+ ANY
+
+
+ UIBackgroundModes
+
+ audio
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIMainStoryboardFile
+ Main
+ UIRequiredDeviceCapabilities
+
+ armv7
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportsDocumentBrowser
+
+ UIViewControllerBasedStatusBarAppearance
+
+ CFBundleGetInfoString
+
+ ITSAppUsesNonExemptEncryption
+
diff --git a/ios/App/App/UILocalPlugin.swift b/ios/App/App/UILocalPlugin.swift
index c683cc4393..1888a575d5 100644
--- a/ios/App/App/UILocalPlugin.swift
+++ b/ios/App/App/UILocalPlugin.swift
@@ -7,6 +7,7 @@
import Capacitor
import Foundation
+import Speech
func isDarkMode() -> Bool {
if #available(iOS 12.0, *) {
@@ -204,9 +205,82 @@ public class UILocalPlugin: CAPPlugin, CAPBridgedPlugin {
private var datepickerDialogView: UIView?
public let pluginMethods: [CAPPluginMethod] = [
- CAPPluginMethod(name: "showDatePicker", returnType: CAPPluginReturnPromise)
+ CAPPluginMethod(name: "showDatePicker", returnType: CAPPluginReturnPromise),
+ CAPPluginMethod(name: "transcribeAudio2Text", returnType: CAPPluginReturnPromise)
]
+ // TODO: switch to use https://developer.apple.com/documentation/speech/speechanalyzer for iOS 26+
+ // 语音识别方法
+ private func recognizeSpeech(from url: URL, completion: @escaping (String?, Error?) -> Void) {
+ SFSpeechRecognizer.requestAuthorization { authStatus in
+ guard authStatus == .authorized else {
+ completion(nil, NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "语音识别权限未授权"]))
+ return
+ }
+
+ let recognizer = SFSpeechRecognizer(locale: Locale(identifier: "en-US"))
+ let request = SFSpeechURLRecognitionRequest(url: url)
+
+ // Setting up offline speech recognition
+ recognizer?.supportsOnDeviceRecognition = true
+ request.shouldReportPartialResults = false
+ request.requiresOnDeviceRecognition = true
+ request.taskHint = .dictation
+ if #available(iOS 16, *) {
+ request.addsPunctuation = true
+ }
+
+ recognizer?.recognitionTask(with: request) { result, error in
+ if let result = result {
+ let transcription = result.bestTranscription.formattedString
+ completion(transcription, nil)
+ } else if let error = error {
+ completion(nil, error)
+ }
+ }
+ }
+ }
+
+ @objc func transcribeAudio2Text(_ call: CAPPluginCall) {
+ self.call = call
+
+ // 接收音频数据 arrayBuffer
+ guard let audioArray = call.getArray("audioData", NSNumber.self) as? [UInt8] else {
+ call.reject("无效的音频数据")
+ return
+ }
+
+ // 将数组转换为 Data
+ let audioData = Data(audioArray)
+
+ // 保存为本地文件
+ let fileURL = FileManager.default.temporaryDirectory.appendingPathComponent("recordedAudio.m4a")
+
+ do {
+ try audioData.write(to: fileURL)
+
+ let fileExists = FileManager.default.fileExists(atPath: fileURL.path)
+
+ print("文件是否存在: \(fileExists), 路径: \(fileURL.path)")
+ if !fileExists {
+ call.reject("文件保存失败,文件不存在")
+ return
+ }
+
+
+ // 调用语音识别
+ self.recognizeSpeech(from: fileURL) { result, error in
+ if let result = result {
+ call.resolve(["transcription": result])
+ } else if let error = error {
+ call.reject("语音识别失败: \(error.localizedDescription)")
+ }
+ }
+ } catch {
+ call.reject("保存文件失败: \(error.localizedDescription)")
+ }
+ }
+
@objc func showDatePicker(_ call: CAPPluginCall) {
self.call = call
diff --git a/ios/App/Podfile b/ios/App/Podfile
index d82d1096b9..ca72e21924 100644
--- a/ios/App/Podfile
+++ b/ios/App/Podfile
@@ -23,7 +23,6 @@ def capacitor_pods
pod 'CapacitorShare', :path => '../../node_modules/@capacitor/share'
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'
pod 'JcesarmobileSslSkip', :path => '../../node_modules/@jcesarmobile/ssl-skip'
end
diff --git a/ios/App/Podfile.lock b/ios/App/Podfile.lock
index f0cf79ce66..a4bb94499c 100644
--- a/ios/App/Podfile.lock
+++ b/ios/App/Podfile.lock
@@ -26,8 +26,6 @@ PODS:
- Capacitor
- CapacitorStatusBar (7.0.1):
- Capacitor
- - CapacitorVoiceRecorder (5.0.0):
- - Capacitor
- JcesarmobileSslSkip (0.4.0):
- Capacitor
- SendIntent (7.0.0):
@@ -48,7 +46,6 @@ DEPENDENCIES:
- "CapacitorShare (from `../../node_modules/@capacitor/share`)"
- "CapacitorSplashScreen (from `../../node_modules/@capacitor/splash-screen`)"
- "CapacitorStatusBar (from `../../node_modules/@capacitor/status-bar`)"
- - CapacitorVoiceRecorder (from `../../node_modules/capacitor-voice-recorder`)
- "JcesarmobileSslSkip (from `../../node_modules/@jcesarmobile/ssl-skip`)"
- SendIntent (from `../../node_modules/send-intent`)
@@ -81,8 +78,6 @@ EXTERNAL SOURCES:
:path: "../../node_modules/@capacitor/splash-screen"
CapacitorStatusBar:
:path: "../../node_modules/@capacitor/status-bar"
- CapacitorVoiceRecorder:
- :path: "../../node_modules/capacitor-voice-recorder"
JcesarmobileSslSkip:
:path: "../../node_modules/@jcesarmobile/ssl-skip"
SendIntent:
@@ -103,10 +98,9 @@ SPEC CHECKSUMS:
CapacitorShare: 58d6c2da63b093e8693287b2d36db92435538435
CapacitorSplashScreen: 19cd3573e57507e02d6f34597a8c421e00931487
CapacitorStatusBar: 275cbf2f4dfc00388f519ef80c7ec22edda342c9
- CapacitorVoiceRecorder: 872ea857b497ce2c71afe3e4eb5de0a74290c0db
JcesarmobileSslSkip: b0f921e9d397a57f7983731209ca1ee244119c1f
SendIntent: 1f4f65c7103eb423067c566682dfcda973b5fb29
-PODFILE CHECKSUM: c36fe2977577d9ee26e6a71a903c924657c49bbb
+PODFILE CHECKSUM: d1ad773ee5fbd3415c2d78d69f4396a1dc68bed9
COCOAPODS: 1.16.2
diff --git a/package.json b/package.json
index db346ff661..e8f96a69a9 100644
--- a/package.json
+++ b/package.json
@@ -145,7 +145,6 @@
"@tabler/icons-webfont": "^2.47.0",
"@tippyjs/react": "4.2.5",
"bignumber.js": "^9.0.2",
- "capacitor-voice-recorder": "^5.0.0",
"check-password-strength": "2.0.7",
"chokidar": "3.5.1",
"chrono-node": "2.2.4",
@@ -192,6 +191,7 @@
"threads": "1.6.5",
"url": "^0.11.0",
"util": "^0.12.5",
+ "wavesurfer.js": "7.10.1",
"yargs-parser": "20.2.4"
},
"resolutions": {
diff --git a/resources/mobile/index.html b/resources/mobile/index.html
index fa78e34469..cc9378babe 100644
--- a/resources/mobile/index.html
+++ b/resources/mobile/index.html
@@ -15,6 +15,8 @@
+
+
diff --git a/src/main/frontend/components/block.cljs b/src/main/frontend/components/block.cljs
index 635772b55b..581e50f141 100644
--- a/src/main/frontend/components/block.cljs
+++ b/src/main/frontend/components/block.cljs
@@ -459,11 +459,16 @@
(editor-handler/resize-image! config block-id metadata full-text {:width width'})))
(reset! *resizing-image? false))))))])))
-(rum/defc audio-cp [src]
- ;; Change protocol to allow media fragment uris to play
- [:audio {:src (string/replace-first src common-config/asset-protocol "file://")
- :controls true
- :on-touch-start #(util/stop %)}])
+(rum/defc audio-cp
+ ([src] (audio-cp src nil))
+ ([src ext]
+ ;; Change protocol to allow media fragment uris to play
+ (let [src (string/replace-first src common-config/asset-protocol "file://")
+ opts {:controls true
+ :on-touch-start #(util/stop %)}]
+ (case ext
+ :m4a [:audio opts [:source {:src src :type "audio/mp4"}]]
+ [:audio (assoc opts :src src)]))))
(defn- open-pdf-file
[e block href]
@@ -524,9 +529,10 @@
(mobile-intent/open-or-share-file asset-url))))]
(cond
- (contains? config/audio-formats ext)
+ (or (contains? config/audio-formats ext)
+ (and (= ext :webm) (string/starts-with? title "record-")))
(if db-based?
- (audio-cp @src)
+ (audio-cp @src ext)
(file-based-asset-loader @src #(audio-cp @src)))
(contains? config/video-formats ext)
@@ -537,7 +543,7 @@
(if db-based?
(resizable-image config title @src metadata full_text true)
(file-based-asset-loader @src
- #(resizable-image config title @src metadata full_text true)))
+ #(resizable-image config title @src metadata full_text true)))
(and (not db-based?) (contains? (common-config/text-formats) ext))
[:a.asset-ref.is-plaintext {:href (rfe/href :file {:path path})
diff --git a/src/main/frontend/components/container.cljs b/src/main/frontend/components/container.cljs
index 6f961f55b6..dd3730f4d1 100644
--- a/src/main/frontend/components/container.cljs
+++ b/src/main/frontend/components/container.cljs
@@ -29,7 +29,6 @@
[frontend.state :as state]
[frontend.ui :as ui]
[frontend.util :as util]
- [frontend.util.cursor :as cursor]
[frontend.version :refer [version]]
[goog.dom :as gdom]
[goog.object :as gobj]
@@ -45,22 +44,6 @@
[reitit.frontend.easy :as rfe]
[rum.core :as rum]))
-(rum/defc recording-bar
- []
- [:> react-draggable
- {:onStart (fn [_event]
- (when-let [pos (some-> (state/get-input) cursor/pos)]
- (state/set-editor-last-pos! pos)))
- :onStop (fn [_event]
- (when-let [block (get @(get @state/state :editor/block) :block/uuid)]
- (editor-handler/edit-block! block :max)
- (when-let [input (state/get-input)]
- (when-let [saved-cursor (state/get-editor-last-pos)]
- (cursor/move-cursor-to input saved-cursor)))))}
- [:div#audio-record-toolbar
- {:style {:bottom (+ @util/keyboard-height 45)}}
- (footer/audio-record-cp)]])
-
(rum/defc main <
{:did-mount (fn [state]
(when-let [element (gdom/getElement "main-content-container")]
@@ -79,7 +62,7 @@
(when-let [el (gdom/getElement "main-content-container")]
(dnd/unsubscribe! el :upload-files))
state)}
- [{:keys [route-match margin-less-pages? route-name indexeddb-support? db-restoring? main-content show-recording-bar?]}]
+ [{:keys [route-match margin-less-pages? route-name indexeddb-support? db-restoring? main-content]}]
(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?)
@@ -103,9 +86,6 @@
:data-is-full-width (or margin-less-pages?
(contains? #{:all-files :all-pages :my-publishing} route-name))}
- (when show-recording-bar?
- (recording-bar))
-
(footer/footer)
(cond
@@ -453,7 +433,6 @@
logged? (user-handler/logged-in?)
fold-button-on-right? (state/enable-fold-button-right?)
show-action-bar? (state/sub :mobile/show-action-bar?)
- show-recording-bar? (state/sub :mobile/show-recording-bar?)
preferred-language (state/sub [:preferred-language])]
(theme/container
{:t t
@@ -522,8 +501,7 @@
:light? light?
:db-restoring? db-restoring?
:main-content main-content'
- :show-action-bar? show-action-bar?
- :show-recording-bar? show-recording-bar?}))]
+ :show-action-bar? show-action-bar?}))]
(when window-controls?
(window-controls/container))
diff --git a/src/main/frontend/components/editor.css b/src/main/frontend/components/editor.css
index 1d37de7bc2..8f3b812342 100644
--- a/src/main/frontend/components/editor.css
+++ b/src/main/frontend/components/editor.css
@@ -1,15 +1,3 @@
-#audio-record-toolbar {
- position: fixed;
- background-color: var(--ls-secondary-background-color);
- width: 90px;
- justify-content: left;
- left: 5px;
- transition: none;
- z-index: 9999;
- padding: 5px 5px 5px 8px;
- border-radius: 5px;
-}
-
.editor-inner {
@apply relative flex;
@@ -120,4 +108,4 @@ pre {
@apply opacity-100;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/main/frontend/components/svg.cljs b/src/main/frontend/components/svg.cljs
index 9f84857465..0c24c5fd15 100644
--- a/src/main/frontend/components/svg.cljs
+++ b/src/main/frontend/components/svg.cljs
@@ -349,15 +349,16 @@
[:svg.icon {:width size :height size :viewBox "0 0 24 24" :stroke "none" :fill "currentColor"}
[:path {:d "M11.14.028C7.315.36 4.072 2.263 1.98 5.411.487 7.646-.232 10.589.067 13.211c.32 2.772 1.4 5.124 3.242 7.049 4.643 4.852 12.252 5.001 17.038.343 1.085-1.057 1.738-1.959 2.407-3.303a11.943 11.943 0 0 0-2.429-13.925C18.372 1.495 16.015.388 13.27.078c-.68-.083-1.56-.1-2.13-.05zm4.814 2.567c1.112.437 2.086 1.068 3.032 1.986.62.598 1.323 1.46 1.3 1.599-.016.072-1.626.725-1.792.725-.056 0-.078-.072-.078-.25 0-.138-.011-.248-.028-.248-.01 0-.758.459-1.654 1.023-.897.565-1.666 1.024-1.71 1.024-.05 0-.133-.061-.194-.139-.127-.16-.216-.171-.354-.044-.066.056-.1.166-.1.316v.226l-.824.46c-.46.249-.89.453-.968.453h-.144V8.161c0-.863.016-2.025.038-2.573.034-.99.04-1.007.155-1.007.117 0 .128-.028.155-.514.067-1.107.25-1.284 1.362-1.323l.514-.016.16-.233c.156-.226.167-.226.366-.171.116.028.46.15.764.271zm-7.05.011l.122.183.641-.006c.604 0 .659.011.902.15.355.21.482.497.526 1.145l.033.498.172.016.171.017.017 2.716.011 2.722-.232.138a3.024 3.024 0 0 0-.936.875l-.177.27h-5.24v-.325l-.592-.017-.598-.017-.398-.586c-.332-.493-.454-.626-.758-.825-.415-.265-.404-.193-.139-1.023.659-2.025 2.203-3.945 4.1-5.107.67-.409 1.932-.995 2.159-1.001.055-.005.155.078.216.177zm12.163 4.902c.354.686.725 1.588.725 1.765 0 .071-.1.149-.327.26-.326.154-.393.237-.393.503 0 .155-.166.36-.564.692l-.327.27h-.99v.333h-2.767v-.886l-.332-.42c-.183-.227-.332-.432-.332-.454 0-.022 1.073-.68 2.39-1.46 2.17-1.29 2.402-1.417 2.485-1.34.05.045.244.377.432.737zm-5.556 3.087c.243.354.454.664.46.686.01.027-.394.05-.892.05h-.918l-.2-.332c-.11-.183-.193-.36-.182-.388.028-.083 1.167-.708 1.234-.68.033.011.254.31.498.664zm-7.282 2.567c.254.398.442.741.415.769-.111.1-5.163 3.32-5.213 3.32-.155 0-.813-1.317-1.024-2.048-.249-.863-.265-.769.188-1.045.178-.111.371-.321.637-.703l.387-.548.603-.027.609-.028.017-.21.016-.205H7.77l.459.725zm1.815-.476c.066.122.127.249.127.288 0 .077-.996.686-1.057.647-.05-.028-.714-1.1-.714-1.15 0-.023.343-.028.758-.023l.758.017.128.221zm9.158-.044l.016.21.554.028c.597.027.525 0 1.184.481.011.006.06.194.11.41.095.425.128.459.493.547.288.072.293.133.072.78-.57 1.682-1.787 3.425-3.287 4.686-.642.542-.603.542-.559-.055.045-.614-.027-.935-.254-1.162-.26-.255-.526-.221-1.3.177-.51.26-.698.332-.897.332-.327 0-.631-.094-.825-.255l-.16-.127.393-.36c.42-.381.62-.73.525-.907-.16-.298-.453-.37-1.045-.26-.498.1-.864.105-1.013.028-.188-.105-.288-.376-.26-.741.028-.332.022-.343-.216-.62l-.238-.282v-1.765l.393-.271c.216-.144.559-.448.758-.675l.37-.404h5.17l.017.205zm-7.814 2.157v.758l-.276.282-.277.283.083.238c.1.282.105.52.022.674-.1.194-.293.222-.896.133a8.212 8.212 0 0 0-.764-.083c-.68 0-.703.482-.06 1.256.31.37.31.365-.084.564-.553.277-.902.25-1.389-.116-.41-.304-.647-.393-.968-.36-.21.017-.31.061-.443.2l-.177.177.006.686c0 .382-.011.691-.023.691-.06 0-1.023-.846-1.45-1.272-.442-.448-.995-1.123-.995-1.217 0-.044 1.516-.72 1.615-.72.034 0 .045.084.034.194-.011.105-.006.194.01.194.017 0 1.362-.747 2.989-1.66a204.276 204.276 0 0 1 3.005-1.66c.022 0 .038.343.038.758z"}]]))
-(def circle-stop
- [:svg
- {:width "20px"
- :height "20px"
- :viewBox "0 0 512 512"
- :fill "currentColor"}
- [:path
- {:d
- "M256 0C114.6 0 0 114.6 0 256c0 141.4 114.6 256 256 256s256-114.6 256-256C512 114.6 397.4 0 256 0zM352 328c0 13.2-10.8 24-24 24h-144C170.8 352 160 341.2 160 328v-144C160 170.8 170.8 160 184 160h144C341.2 160 352 170.8 352 184V328z"}]])
+(comment
+ (def circle-stop
+ [:svg
+ {:width "20px"
+ :height "20px"
+ :viewBox "0 0 512 512"
+ :fill "currentColor"}
+ [:path
+ {:d
+ "M256 0C114.6 0 0 114.6 0 256c0 141.4 114.6 256 256 256s256-114.6 256-256C512 114.6 397.4 0 256 0zM352 328c0 13.2-10.8 24-24 24h-144C170.8 352 160 341.2 160 328v-144C160 170.8 170.8 160 184 160h144C341.2 160 352 170.8 352 184V328z"}]]))
;; Titlebar icons from https://github.com/microsoft/vscode-codicons
(defn window-minimize
@@ -384,3 +385,22 @@
([size]
[:svg.icon {:width size :height size :viewBox "0 0 16 16" :fill "currentColor"}
[:path {:fill-rule "evenodd" :clip-rule "evenodd" :d "M7.116 8l-4.558 4.558.884.884L8 8.884l4.558 4.558.884-.884L8.884 8l4.558-4.558-.884-.884L8 7.116 3.442 2.558l-.884.884L7.116 8z"}]]))
+
+(defn audio-lines
+ ([] (audio-lines 16))
+ ([size]
+ [:svg.icon
+ {:stroke "currentColor",
+ :fill "none",
+ :stroke-linejoin "round",
+ :width size,
+ :height "24"
+ :stroke-linecap "round",
+ :stroke-width "2.5",
+ :viewBox "0 0 24 24"}
+ [:path {:d "M2 10v3"}]
+ [:path {:d "M6 6v11"}]
+ [:path {:d "M10 3v18"}]
+ [:path {:d "M14 8v7"}]
+ [:path {:d "M18 5v13"}]
+ [:path {:d "M22 10v3"}]]))
diff --git a/src/main/frontend/date.cljs b/src/main/frontend/date.cljs
index 24cd9d115c..b38d6cc94d 100644
--- a/src/main/frontend/date.cljs
+++ b/src/main/frontend/date.cljs
@@ -30,8 +30,10 @@
(defn get-date-time-string
([]
(get-date-time-string (t/now)))
- ([date-time]
- (tf/unparse custom-formatter date-time)))
+ ([date-time & {:keys [formatter-str]}]
+ (tf/unparse (if formatter-str
+ (tf/formatter formatter-str)
+ custom-formatter) date-time)))
(defn get-locale-string
"Accepts a :date-time-no-ms string representation, or a cljs-time date object"
diff --git a/src/main/frontend/handler/editor.cljs b/src/main/frontend/handler/editor.cljs
index b60b5a1fc1..01ec0d7140 100644
--- a/src/main/frontend/handler/editor.cljs
+++ b/src/main/frontend/handler/editor.cljs
@@ -912,17 +912,25 @@
block (first blocks)
block-parent (get uuid->dom-block (:block/uuid block))
sibling-block (when block-parent (util/get-prev-block-non-collapsed-non-embed block-parent))
- blocks' (block-handler/get-top-level-blocks blocks)]
+ blocks' (block-handler/get-top-level-blocks blocks)
+ mobile? (util/capacitor-new?)]
(p/do!
- (when (and sibling-block (not (util/capacitor-new?)))
+ (when (and sibling-block (not mobile?))
(let [{:keys [edit-block-f]} (move-to-prev-block repo sibling-block
(get block :block/format :markdown)
"")]
(state/set-state! :editor/edit-block-fn edit-block-f)))
- (ui-outliner-tx/transact!
- {:outliner-op :delete-blocks
- :mobile-action-bar? mobile-action-bar?}
- (outliner-op/delete-blocks! blocks' nil))))))
+ (let [journals (and mobile? (filter ldb/journal? blocks'))
+ blocks (remove (fn [b] (contains? (set (map :db/id journals)) (:db/id b))) blocks)]
+ (when (or (seq journals) (seq blocks))
+ (ui-outliner-tx/transact!
+ {:outliner-op :delete-blocks
+ :mobile-action-bar? mobile-action-bar?}
+ (when (seq blocks)
+ (outliner-op/delete-blocks! blocks nil))
+ (when (seq journals)
+ (doseq [journal journals]
+ (outliner-op/delete-page! (:block/uuid journal)))))))))))
(defn set-block-timestamp!
[block-id key value]
@@ -1493,7 +1501,7 @@
"Save incoming(pasted) assets to assets directory.
Returns: asset entity"
- [repo files & {:keys [pdf-area?]}]
+ [repo files & {:keys [pdf-area? last-edit-block]}]
(p/let [[repo-dir asset-dir-rpath] (assets-handler/ensure-assets-dir! repo)]
(p/all
(for [[_index ^js file] (map-indexed vector files)]
@@ -1524,19 +1532,23 @@
:edit-block? false
:properties properties}
_ (db-based-save-asset! repo dir file file-rpath)
- edit-block (state/get-edit-block)
+ edit-block (or (state/get-edit-block) last-edit-block)
+ today-page-name (date/today)
+ today-page-e (db-model/get-journal-page today-page-name)
+ today-page (if (nil? today-page-e)
+ (state/pub-event! [:page/create today-page-name])
+ today-page-e)
insert-to-current-block-page? (and (:block/uuid edit-block) (string/blank? (state/get-edit-content)) (not pdf-area?))
insert-opts' (if insert-to-current-block-page?
(assoc insert-opts
:block-uuid (:block/uuid edit-block)
:replace-empty-target? true
:sibling? true)
- (assoc insert-opts :page (:block/uuid asset)))
- result (api-insert-new-block! file-name-without-ext insert-opts')
- new-entity (db/entity [:block/uuid (:block/uuid result)])]
+ (assoc insert-opts :page (:block/uuid today-page)))
+ new-block (api-insert-new-block! file-name-without-ext insert-opts')]
(when insert-to-current-block-page?
(state/clear-edit!))
- (or new-entity
+ (or new-block
(throw (ex-info "Can't save asset" {:files files}))))))))))
(def insert-command! editor-common-handler/insert-command!)
diff --git a/src/main/frontend/handler/events.cljs b/src/main/frontend/handler/events.cljs
index 959a5ea24e..f0e07987f3 100644
--- a/src/main/frontend/handler/events.cljs
+++ b/src/main/frontend/handler/events.cljs
@@ -219,9 +219,6 @@
(defmethod handle :mobile/keyboard-will-show [[_ keyboard-height]]
(let [_main-node (util/app-scroll-container-node)]
- (state/set-state! :mobile/show-action-bar? false)
- (when (= (state/sub :editor/record-status) "RECORDING")
- (state/set-state! :mobile/show-recording-bar? true))
(when-let [^js html (js/document.querySelector ":root")]
(.setProperty (.-style html) "--ls-native-kb-height" (str keyboard-height "px"))
(.add (.-classList html) "has-mobile-keyboard")
@@ -234,8 +231,6 @@
(defmethod handle :mobile/keyboard-will-hide [[_]]
(let [main-node (util/app-scroll-container-node)]
- (when (= (state/sub :editor/record-status) "RECORDING")
- (state/set-state! :mobile/show-recording-bar? false))
(when-let [^js html (js/document.querySelector ":root")]
(.removeProperty (.-style html) "--ls-native-kb-height")
(.setProperty (.-style html) "--ls-native-toolbar-opacity" 0)
diff --git a/src/main/frontend/handler/page.cljs b/src/main/frontend/handler/page.cljs
index 0d9f60adce..83d19e9658 100644
--- a/src/main/frontend/handler/page.cljs
+++ b/src/main/frontend/handler/page.cljs
@@ -342,13 +342,13 @@
format (state/get-preferred-format repo)
db-based? (config/db-based-graph? repo)
create-f (fn []
- (p/do!
- (minutes:seconds
- [seconds]
- (let [minutes (quot seconds 60)
- seconds (mod seconds 60)]
- (util/format "%02d:%02d" minutes seconds)))
-
-(def *record-start (atom nil))
-(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]
- (if (= (state/sub :editor/record-status) "NONE")
- (mobile-bar-command #(do (record/start-recording)
- (reset! *record-start (js/Date.now))) "microphone")
- [:div.flex.flex-row.items-center
- (mobile-bar-command #(do (reset! *record-start nil)
- (state/set-state! :mobile/show-recording-bar? false)
- (record/stop-recording))
- "player-stop")
- [:div.timer.ml-2
- {:on-click record/stop-recording}
- (seconds->minutes:seconds (/ (- (js/Date.now) @*record-start) 1000))]]))
+ (util/stop e)
+ (command-handler))}
+ (ui/icon icon {:size 24})])
(rum/defc footer < rum/reactive
[]
@@ -55,7 +22,6 @@
(state/sub :mobile/show-tabbar?)
(state/get-current-repo))
[:div.cp__footer.w-full.bottom-0.justify-between
- (audio-record-cp)
(mobile-bar-command
#(do (when-not (mobile-util/native-ipad?)
(state/set-left-sidebar-open! false))
diff --git a/src/main/frontend/mobile/intent.cljs b/src/main/frontend/mobile/intent.cljs
index 7792220a3f..163e06c6aa 100644
--- a/src/main/frontend/mobile/intent.cljs
+++ b/src/main/frontend/mobile/intent.cljs
@@ -85,26 +85,13 @@
(defn- embed-asset-file [url _format]
(p/let [basename (node-path/basename url)
- _label (-> basename util/node-path.name)
- _path (assets-handler/get-asset-path basename)
- time (date/get-current-time)
- date-ref-name (date/today)
file (.readFile Filesystem #js {:path url})
file-base64-str (some-> file (.-data))
file (some-> file-base64-str (util/base64string-to-unit8array)
- (vector) (clj->js) (js/File. basename #js {}))
- asset-entity (editor-handler/db-based-save-assets!
- (state/get-current-repo) [file] {})
- asset-entity (some-> asset-entity (first))
- url (util/format "[[%s]]" (:block/uuid asset-entity))
- template (get-in (state/get-config)
- [:quick-capture-templates :media]
- "**{time}** [[quick capture]]: {url}")]
- (-> template
- (string/replace "{time}" time)
- (string/replace "{date}" date-ref-name)
- (string/replace "{text}" "")
- (string/replace "{url}" (or url "")))))
+ (vector) (clj->js) (js/File. basename #js {}))
+ result (editor-handler/db-based-save-assets!
+ (state/get-current-repo) [file] {})]
+ (first result)))
(defn- embed-text-file
"Store external content with url into Logseq repo"
@@ -136,13 +123,8 @@
(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)
- (editor-handler/insert content)
- (editor-handler/api-insert-new-block! content {:page page
- :edit-block? false
- :replace-empty-target? true}))))
+ format (db/get-page-format page)]
+ (embed-asset-file url format)))
(defn- handle-received-application [result]
(p/let [{:keys [title url type]} result
@@ -155,7 +137,9 @@
(contains? (set/union config/doc-formats config/media-formats)
(keyword application-type))
- (embed-asset-file url format)
+ (do
+ (embed-asset-file url format)
+ nil)
:else
(notification/show!
@@ -165,11 +149,12 @@
:target "_blank"} "Github"]
". We will look into it soon."]
:warning false))]
- (if (state/get-edit-block)
- (editor-handler/insert content)
- (editor-handler/api-insert-new-block! content {:page page
- :edit-block? false
- :replace-empty-target? true}))))
+ (when content
+ (if (state/get-edit-block)
+ (editor-handler/insert content)
+ (editor-handler/api-insert-new-block! content {:page page
+ :edit-block? false
+ :replace-empty-target? true})))))
(defn decode-received-result [m]
(into {} (for [[k v] m]
@@ -191,13 +176,13 @@
file (.readFile Filesystem #js {:path url})
file-base64-str (some-> file (.-data))
file (some-> file-base64-str (util/base64string-to-unit8array)
- (vector) (clj->js) (js/File. basename #js {}))
- asset-entity (editor-handler/db-based-save-assets!
- (state/get-current-repo) [file] {})
- asset-entity (some-> asset-entity (first))
+ (vector) (clj->js) (js/File. basename #js {}))
+ result (editor-handler/db-based-save-assets!
+ (state/get-current-repo) [file] {})
+ asset-entity (first result)
url-link (util/format "[[%s]]" (:block/uuid asset-entity))]
url-link)
- (p/catch #(js/console.error "Error(handle asset file):" %))))
+ (p/catch #(js/console.error "Error(handle asset file):" %))))
(defn- handle-payload-resource
[{:keys [type name ext url] :as resource} format]
@@ -245,7 +230,7 @@
(handle-payload-resource resource format))
resources))
(p/then (partial string/join "\n")))]
- (when (or (not-empty text) (not-empty rich-content))
+ (when (not-empty text)
(let [time (date/get-current-time)
date-ref-name (date/today)
content (-> template
diff --git a/src/main/frontend/mobile/record.cljs b/src/main/frontend/mobile/record.cljs
deleted file mode 100644
index 397d044036..0000000000
--- a/src/main/frontend/mobile/record.cljs
+++ /dev/null
@@ -1,82 +0,0 @@
-(ns frontend.mobile.record
- (:require ["@capacitor/filesystem" :refer [Filesystem]]
- ["capacitor-voice-recorder" :refer [VoiceRecorder]]
- [clojure.string :as string]
- [frontend.date :as date]
- [frontend.handler.assets :as assets-handler]
- [frontend.handler.editor :as editor-handler]
- [frontend.state :as state]
- [frontend.util :as util]
- [lambdaisland.glogi :as log]
- [promesa.core :as p]))
-
-(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) ".aac")
- edit-block (state/get-edit-block)
- format (get edit-block :block/format :markdown)
- path (assets-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 (assets-handler/get-asset-file-link format url filename true)
- args (merge (if (parse-uuid page)
- {:block-uuid (uuid page)}
- {:page page})
- {:edit-block? false
- :replace-empty-target? true})]
- (if edit-block
- (editor-handler/insert file-link)
- (editor-handler/api-insert-new-block! file-link args))))
-
-(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))))
diff --git a/src/main/frontend/state.cljs b/src/main/frontend/state.cljs
index a73ddcd553..5b8cd780c4 100644
--- a/src/main/frontend/state.cljs
+++ b/src/main/frontend/state.cljs
@@ -177,9 +177,6 @@
;; Stores deleted refed blocks, indexed by repo
:editor/last-replace-ref-content-tx nil
- ;; for audio record
- :editor/record-status "NONE"
-
:editor/code-block-context nil
:editor/latest-shortcut (atom nil)
@@ -225,7 +222,6 @@
;; mobile
:mobile/container-urls nil
:mobile/show-action-bar? false
- :mobile/show-recording-bar? false
;; plugin
:plugin/enabled (and util/plugin-platform?
@@ -733,7 +729,9 @@ Similar to re-frame subscriptions"
([]
(enable-journals? (get-current-repo)))
([repo]
- (not (false? (:feature/enable-journals? (sub-config repo))))))
+ (if (sqlite-util/db-based-graph? repo) ; db graphs rely on journals for quick capture/sharing/assets, etc.
+ true
+ (not (false? (:feature/enable-journals? (sub-config repo)))))))
(defn enable-flashcards?
([]
diff --git a/src/main/mobile/components/app.css b/src/main/mobile/components/app.css
index 08260cb431..7fc7cd8d01 100644
--- a/src/main/mobile/components/app.css
+++ b/src/main/mobile/components/app.css
@@ -87,6 +87,10 @@ html {
background: var(--ls-secondary-background-color);
}
+ .Card-content {
+ background: var(--ls-primary-background-color);
+ }
+
.BottomSheet-handle {
@apply bg-gray-03;
}
@@ -430,25 +434,23 @@ html[data-silk-native-page-scroll-replaced=false] .app-silk-index-scroll-view {
@apply flex border-t overflow-hidden select-none
bg-gray-02 absolute left-0 -bottom-0 w-full z-[1] dark:bg-gray-01;
- padding-top: 6px;
+ padding-top: 4px;
padding-bottom: calc(env(safe-area-inset-bottom) + var(--silk-tabbar-bottom-paddding));
> .as-item {
- @apply flex flex-1 flex-col items-center pb-1 transition-opacity;
- @apply opacity-60 active:opacity-90;
-
- &.active {
- @apply text-accent-10 opacity-100;
-
- > small {
- @apply font-semibold;
- }
- }
+ @apply flex flex-1 flex-col items-center pb-1 transition-opacity opacity-60;
> small {
@apply text-[9px] -mt-2;
}
}
+
+ .as-item.active {
+ @apply opacity-90 text-accent-10;
+ small {
+ @apply font-semibold;
+ }
+ }
}
.app-silk-search-page {
@@ -534,6 +536,47 @@ html[data-silk-native-page-scroll-replaced=false] .app-silk-index-scroll-view {
overflow: hidden;
}
+.app-audio-recorder-inner {
+ @apply relative pb-1;
+
+ h1 {
+ @apply pl-6 flex flex-col;
+
+ > small {
+ @apply opacity-40 text-sm;
+ }
+
+ &:after {
+ @apply content-[''] absolute top-[35px] left-[70px] bg-red-700
+ w-1.5 h-1.5 overflow-hidden rounded-full;
+ }
+ }
+
+ select {
+ @apply bg-transparent;
+ }
+
+ .record-ctrl-btn {
+ @apply w-12 h-12 text-green-800 bg-green-200 border-none;
+
+ &.recording {
+ @apply bg-red-200 border-red-500 text-red-700;
+ }
+ }
+
+ .timer-wrap {
+ @apply select-none;
+
+ > .timer {
+ @apply text-[28px] font-[500] font-mono opacity-90;
+ }
+
+ > small {
+ @apply opacity-50 -mt-1;
+ }
+ }
+}
+
.left-sidebar-inner {
@apply -mx-4;
diff --git a/src/main/mobile/components/editor_toolbar.cljs b/src/main/mobile/components/editor_toolbar.cljs
index 1ba55a858a..cc14c0b697 100644
--- a/src/main/mobile/components/editor_toolbar.cljs
+++ b/src/main/mobile/components/editor_toolbar.cljs
@@ -1,6 +1,7 @@
(ns mobile.components.editor-toolbar
"Mobile editor toolbar"
(:require [frontend.commands :as commands]
+ [frontend.components.svg :as svg]
[frontend.handler.editor :as editor-handler]
[frontend.mobile.camera :as mobile-camera]
[frontend.mobile.haptics :as haptics]
@@ -10,7 +11,9 @@
[frontend.util.cursor :as cursor]
[goog.dom :as gdom]
[logseq.common.util.page-ref :as page-ref]
- [mobile.init :as init]
+ [mobile.components.recorder :as recorder]
+ [mobile.init :as mobile-init]
+ [mobile.state :as mobile-state]
[promesa.core :as p]
[rum.core :as rum]))
@@ -42,7 +45,8 @@
(if event?
(command-handler e)
(command-handler)))}
- (ui/icon icon {:size ui/icon-size :class class})]])
+ (if (string? icon)
+ (ui/icon icon {:size ui/icon-size :class class}) icon)]])
(defn- insert-text
[text opts]
@@ -76,7 +80,8 @@
(not (state/sub :editor/code-block-context))
(or (state/sub :editor/editing?)
(= "app-keep-keyboard-open-input" (some-> js/document.activeElement (.-id)))))
- (let [commands' (commands)]
+ (let [commands' (commands)
+ quick-add? (mobile-state/quick-add-open?)]
[:div#mobile-editor-toolbar
{:on-click #(util/stop %)}
[:div.toolbar-commands
@@ -94,9 +99,14 @@
(for [command' commands']
command')
(command #(let [parent-id (state/get-edit-input-id)]
- (mobile-camera/embed-photo parent-id)) {:icon "camera"} true)]
+ (mobile-camera/embed-photo parent-id)) {:icon "camera"} true)
+ (when-not quick-add?
+ (command (fn [] (recorder/record!)) {:icon (svg/audio-lines 20)}))]
[:div.toolbar-hide-keyboard
- (command #(p/do!
- (editor-handler/save-current-block!)
- (state/clear-edit!)
- (init/keyboard-hide)) {:icon "keyboard-show"})]])))
+ (if quick-add?
+ (command (fn [] (recorder/record!))
+ {:icon (svg/audio-lines 20)})
+ (command #(p/do!
+ (editor-handler/save-current-block!)
+ (state/clear-edit!)
+ (mobile-init/keyboard-hide)) {:icon "keyboard-show"}))]])))
diff --git a/src/main/mobile/components/popup.cljs b/src/main/mobile/components/popup.cljs
index ee7fb643d2..63899ab104 100644
--- a/src/main/mobile/components/popup.cljs
+++ b/src/main/mobile/components/popup.cljs
@@ -70,6 +70,7 @@
[]
(let [{:keys [open? content-fn opts]} (rum/react mobile-state/*popup-data)
quick-add? (= :ls-quick-add (:id opts))
+ audio-record? (= :ls-audio-record (:id opts))
action-sheet? (= :action-sheet (:type opts))
default-height (:default-height opts)]
@@ -94,7 +95,7 @@
(editor-handler/quick-add-open-last-block!)))
:onPresentAutoFocus #js {:focus false}}
(silkhq/bottom-sheet-backdrop
- (when quick-add?
+ (when (or quick-add? audio-record?)
{:travelAnimation {:opacity (fn [data]
(let [progress (gobj/get data "progress")]
(js/Math.min (* progress 0.9) 0.9)))}}))
diff --git a/src/main/mobile/components/recorder.cljs b/src/main/mobile/components/recorder.cljs
new file mode 100644
index 0000000000..e8138ff688
--- /dev/null
+++ b/src/main/mobile/components/recorder.cljs
@@ -0,0 +1,208 @@
+(ns mobile.components.recorder
+ "Audio record"
+ (:require [cljs-time.core :as t]
+ [clojure.string :as string]
+ [frontend.date :as date]
+ [frontend.db.model :as db-model]
+ [frontend.handler.editor :as editor-handler]
+ [frontend.handler.notification :as notification]
+ [frontend.mobile.util :as mobile-util]
+ [frontend.state :as state]
+ [goog.functions :as gfun]
+ [logseq.shui.hooks :as hooks]
+ [logseq.shui.ui :as shui] ;; [mobile.speech :as speech]
+ [mobile.init :as init]
+ [mobile.state :as mobile-state]
+ [promesa.core :as p]
+ [rum.core :as rum]))
+
+(defonce audio-file-format "MM-dd HH:mm")
+
+(def *last-edit-block (atom nil))
+(defn set-last-edit-block! [block] (reset! *last-edit-block block))
+
+(defn ms-to-time-format [ms]
+ (let [total-seconds (quot ms 1000)
+ minutes (quot total-seconds 60)
+ seconds (mod total-seconds 60)]
+ (str (.padStart (str minutes) 2 "0") ":"
+ (.padStart (str seconds) 2 "0"))))
+
+(defn save-asset-audio!
+ [blob]
+ (let [ext (some-> blob
+ (.-type)
+ (string/split ";")
+ (first)
+ (string/split "/")
+ (last))
+ ext (case ext
+ "mp4" "m4a"
+ ext)]
+
+ ;; save local
+ (when-let [filename (some->> ext (str "Audio-"
+ (date/get-date-time-string (t/now)
+ {:formatter-str audio-file-format})
+ "."))]
+ (p/let [file (js/File. [blob] filename #js {:type (.-type blob)})
+ result (editor-handler/db-based-save-assets! (state/get-current-repo)
+ [file]
+ {:last-edit-block @*last-edit-block})
+ asset-entity (first result)]
+ (when asset-entity
+ (p/let [buffer-data (.arrayBuffer blob)
+ unit8-data (js/Uint8Array. buffer-data)]
+ (-> (.transcribeAudio2Text mobile-util/ui-local #js {:audioData (js/Array.from unit8-data)})
+ (p/then (fn [^js r]
+ (let [content (.-transcription r)]
+ (when-not (string/blank? content)
+ (editor-handler/api-insert-new-block! content
+ {:block-uuid (:block/uuid asset-entity)
+ :sibling? false
+ :replace-empty-target? true
+ :edit-block? false})))))
+ (p/catch #(js/console.error "Error(transcribeAudio2Text):" %)))))))))
+
+(rum/defc ^:large-vars/cleanup-todo audio-recorder-aux
+ []
+ (let [*wave-ref (rum/use-ref nil)
+ *micid-ref (rum/use-ref nil)
+ *timer-ref (rum/use-ref nil)
+ *save-ref (rum/use-ref false)
+ [^js wavesurfer set-wavesurfer!] (rum/use-state nil)
+ [^js recorder set-recorder!] (rum/use-state nil)
+ [mic-devices set-mic-devices!] (rum/use-state nil)
+ [_ set-status-pulse!] (rum/use-state 0)
+ recording? (some-> recorder (.isRecording))]
+
+ (hooks/use-effect!
+ (fn [] #(some-> wavesurfer (.destroy)))
+ [])
+
+ ;; load mic devices
+ (hooks/use-effect!
+ (fn []
+ (when recorder
+ (-> js/window.WaveSurfer.Record
+ (.getAvailableAudioDevices)
+ (.then (fn [^js devices]
+ (let [*vs (volatile! [])]
+ (.forEach devices
+ (fn [^js device]
+ (vswap! *vs conj {:text (or (.-label device) (.-deviceId device))
+ :value (.-deviceId device)})))
+ (set-mic-devices! @*vs))))
+ (.catch (fn [^js err]
+ (js/console.error "ERR: load mic devices" err)))))
+ #())
+ [recorder])
+
+ (hooks/use-effect!
+ (fn []
+ (let [dark? (= "dark" (state/sub :ui/theme))
+ ^js w (.create js/window.WaveSurfer
+ #js {:container (rum/deref *wave-ref)
+ :waveColor "rgb(167, 167, 167)"
+ :progressColor (if dark? "rgb(219, 216, 216)" "rgb(10, 10, 10)")
+ :barWidth 2
+ :barRadius 6})
+ ^js r (.registerPlugin w
+ (.create js/window.WaveSurfer.Record
+ #js {:renderRecordedAudio false
+ :scrollingWaveform false
+ :continuousWaveform true
+ :mimeType "audio/mp4" ;; m4a
+ :audioBitsPerSecond 128000 ;; 128kbps,适合 AAC-LC
+ :continuousWaveformDuration 30 ;; optional
+ }))]
+ (set-wavesurfer! w)
+ (set-recorder! r)
+
+ ;; events
+ (let [handle-status-changed! (fn []
+ (set-status-pulse! (js/Date.now)))]
+ (doto r
+ (.on "record-end" (fn [^js blob]
+ (when (true? (rum/deref *save-ref))
+ (save-asset-audio! blob)
+ (rum/set-ref! *save-ref false)
+ (mobile-state/close-popup!))
+ (handle-status-changed!)))
+ (.on "record-progress" (gfun/throttle
+ (fn [time]
+ (try
+ (let [t (ms-to-time-format time)]
+ (set! (. (rum/deref *timer-ref) -textContent) t))
+ (catch js/Error e
+ (js/console.warn "WARN: bad progress time:" e))))
+ 50))
+ (.on "record-start" handle-status-changed!)
+ (.on "record-pause" handle-status-changed!)
+ (.on "record-resume" handle-status-changed!))
+ ;; auto start
+ (.startRecording r))
+ #()))
+ [])
+
+ [:div.app-audio-recorder-inner
+ [:h1.text-xl.p-6.relative
+ [:span.font-bold "REC"]
+ [:small (date/get-date-time-string (t/now) {:formatter-str audio-file-format})]]
+
+ [:div.px-6
+ [:div.flex.justify-between.items-center.hidden
+ [:span " "]
+ [:select.opacity-60
+ {:name "mic-select"
+ :style {:max-width "220px" :border "none"}
+ :ref *micid-ref}
+ (for [d mic-devices]
+ [:option {:value (:value d)}
+ (str "Mic: " (if (string/blank? (:text d)) "Default" (:text d)))])]]
+ [:div.wave.border.rounded {:ref *wave-ref}]]
+
+ [:div.p-6.flex.justify-between
+ (let [handle-record!
+ (fn []
+ (let [micid (some-> (rum/deref *micid-ref) (.-value))]
+ (-> (.startRecording recorder #js {:deviceId micid})
+ (.catch #(notification/show! (.-message %) :error)))))]
+
+ [:div.flex.justify-between.items-center.w-full
+ [:span.flex.flex-col.timer-wrap
+ [:strong.timer {:ref *timer-ref} "00:00"]
+ [:small "05:00"]]
+ (shui/button {:variant :outline
+ :class "record-ctrl-btn rounded-full recording"
+ :on-click (fn []
+ (if recording? ;; save audio
+ (do
+ (rum/set-ref! *save-ref true)
+ (.stopRecording recorder))
+ (handle-record!)))}
+ (shui/tabler-icon "player-stop" {:size 22}))])]]))
+
+(defn- show-recorder
+ []
+ (mobile-state/set-popup! {:open? true
+ :content-fn (fn [] (audio-recorder-aux))
+ :opts {:id :ls-audio-record}}))
+
+(defn record!
+ []
+ (let [editing-id (state/get-edit-input-id)
+ quick-add? (mobile-state/quick-add-open?)]
+ (set-last-edit-block! nil)
+ (if-not (string/blank? editing-id)
+ (p/do!
+ (editor-handler/save-current-block!)
+ (let [block (db-model/query-block-by-uuid (:block/uuid (state/get-edit-block)))]
+ (if quick-add?
+ (p/do!
+ (state/clear-edit!)
+ (init/keyboard-hide)
+ (show-recorder))
+ (do (set-last-edit-block! block)
+ (show-recorder)))))
+ (show-recorder))))
diff --git a/src/main/mobile/components/search.cljs b/src/main/mobile/components/search.cljs
index ee5502a271..fd19c4b754 100644
--- a/src/main/mobile/components/search.cljs
+++ b/src/main/mobile/components/search.cljs
@@ -72,10 +72,10 @@
[focused?])
(hooks/use-effect!
- (fn []
- (js/setTimeout #(some-> (rum/deref *ref) (.focus)) 32)
- #())
- [])
+ (fn []
+ (js/setTimeout #(some-> (rum/deref *ref) (.focus)) 32)
+ #())
+ [])
[:div.app-silk-search-page
[:div.hd
@@ -131,25 +131,29 @@
{:on-click #(set-input! item)}
item)])])
- [:ul.px-3
- {:class (when (and (not (string/blank? input))
- (seq search-result))
- "as-results")}
- (for [{:keys [page? icon text header source-block]} result]
- (let [block source-block]
- [:li.flex.gap-1
- {:on-click (fn []
- (when-let [id (:block/uuid block)]
- (p/let [block (db-async/