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/