mirror of
https://github.com/logseq/logseq.git
synced 2026-02-01 22:47:36 +00:00
feat(mobile): audio record && transcribe (#12105)
* feat: audio transcribe * enhance(mobile): auto start recording on initialization * fix(mobile): can't delete journal from selection bar * fix: duplicated audio record buttons in quick add * fix(mobile): inactive bottom tab color * enhance(mobile): display no results when there's no matched items * enhance(mobile): add audio transcription feature and enhance audio component * fix: store assets directly instead in today page instead of node ref * save transcribed text to audio's child block * enhance: transcribe supports punctuations and being offline only * fix(mobile): save assets to current editing page --------- Co-authored-by: Tienson Qin <tiensonqin@gmail.com>
This commit is contained in:
@@ -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')
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
|
||||
@@ -47,10 +47,6 @@
|
||||
"pkg": "@capacitor/status-bar",
|
||||
"classpath": "com.capacitorjs.plugins.statusbar.StatusBarPlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "capacitor-voice-recorder",
|
||||
"classpath": "com.tchvu3.capacitorvoicerecorder.VoiceRecorder"
|
||||
},
|
||||
{
|
||||
"pkg": "send-intent",
|
||||
"classpath": "de.mindlib.sendIntent.SendIntent"
|
||||
|
||||
@@ -38,9 +38,6 @@ project(':capacitor-splash-screen').projectDir = new File('../node_modules/@capa
|
||||
include ':capacitor-status-bar'
|
||||
project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacitor/status-bar/android')
|
||||
|
||||
include ':capacitor-voice-recorder'
|
||||
project(':capacitor-voice-recorder').projectDir = new File('../node_modules/capacitor-voice-recorder/android')
|
||||
|
||||
include ':send-intent'
|
||||
project(':send-intent').projectDir = new File('../node_modules/send-intent/android')
|
||||
|
||||
|
||||
@@ -130,6 +130,8 @@ const common = {
|
||||
'node_modules/prop-types/prop-types.min.js',
|
||||
'node_modules/interactjs/dist/interact.min.js',
|
||||
'node_modules/photoswipe/dist/umd/*.js',
|
||||
'node_modules/wavesurfer.js/dist/wavesurfer.min.js',
|
||||
'node_modules/wavesurfer.js/dist/plugins/record.min.js',
|
||||
'packages/amplify/dist/amplify.js',
|
||||
'packages/ui/dist/ui/ui.js',
|
||||
'node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3.wasm',
|
||||
|
||||
@@ -3,120 +3,122 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>APFiles</key>
|
||||
<dict>
|
||||
<key>APFileDescriptionKey</key>
|
||||
<string></string>
|
||||
<key>APFileDestinationPath</key>
|
||||
<string></string>
|
||||
<key>APFileName</key>
|
||||
<string></string>
|
||||
<key>APFileSourcePath</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Logseq</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>com.logseq.logseq</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>logseq</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string></string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
<true/>
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>We will access your camera when you take a photo, and embed it in your note.</string>
|
||||
<key>NSDocumentsFolderUsageDescription</key>
|
||||
<string></string>
|
||||
<key>NSDownloadsFolderUsageDescription</key>
|
||||
<string></string>
|
||||
<key>NSFileProviderDomainUsageDescription</key>
|
||||
<string></string>
|
||||
<key>NSFileProviderPresenceUsageDescription</key>
|
||||
<string></string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>We will access your microphone to record audio notes</string>
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
<string>We will access your album when you save a photo.</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>We will access your album when you choose a photo, and embed it in your note.</string>
|
||||
<key>NSUbiquitousContainers</key>
|
||||
<dict>
|
||||
<key>iCloud.com.logseq.logseq</key>
|
||||
<dict>
|
||||
<key>NSUbiquitousContainerIsDocumentScopePublic</key>
|
||||
<true/>
|
||||
<key>NSUbiquitousContainerName</key>
|
||||
<string>Logseq</string>
|
||||
<key>NSUbiquitousContainerSupportedFolderLevels</key>
|
||||
<string>ANY</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportsDocumentBrowser</key>
|
||||
<true/>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true/>
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string></string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>APFiles</key>
|
||||
<dict>
|
||||
<key>APFileDescriptionKey</key>
|
||||
<string></string>
|
||||
<key>APFileDestinationPath</key>
|
||||
<string></string>
|
||||
<key>APFileName</key>
|
||||
<string></string>
|
||||
<key>APFileSourcePath</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Logseq</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>com.logseq.logseq</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>logseq</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string></string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
<true/>
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>We will access your camera when you take a photo, and embed it in your note.</string>
|
||||
<key>NSDocumentsFolderUsageDescription</key>
|
||||
<string></string>
|
||||
<key>NSDownloadsFolderUsageDescription</key>
|
||||
<string></string>
|
||||
<key>NSFileProviderDomainUsageDescription</key>
|
||||
<string></string>
|
||||
<key>NSFileProviderPresenceUsageDescription</key>
|
||||
<string></string>
|
||||
<key>NSSpeechRecognitionUsageDescription</key>
|
||||
<string>We need access to speech recognition to convert your voice to text.</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>We will access your microphone to record audio notes</string>
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
<string>We will access your album when you save a photo.</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>We will access your album when you choose a photo, and embed it in your note.</string>
|
||||
<key>NSUbiquitousContainers</key>
|
||||
<dict>
|
||||
<key>iCloud.com.logseq.logseq</key>
|
||||
<dict>
|
||||
<key>NSUbiquitousContainerIsDocumentScopePublic</key>
|
||||
<true/>
|
||||
<key>NSUbiquitousContainerName</key>
|
||||
<string>Logseq</string>
|
||||
<key>NSUbiquitousContainerSupportedFolderLevels</key>
|
||||
<string>ANY</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportsDocumentBrowser</key>
|
||||
<true/>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true/>
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string></string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
<script defer src="./js/interact.min.js"></script>
|
||||
<script defer src="./js/marked.min.js"></script>
|
||||
<script defer src="./js/eventemitter3.umd.min.js"></script>
|
||||
<script defer src="./js/wavesurfer.min.js"></script>
|
||||
<script defer src="./js/record.min.js"></script>
|
||||
<script defer src="./js/photoswipe.umd.min.js"></script>
|
||||
<script defer src="./js/photoswipe-lightbox.umd.min.js"></script>
|
||||
<script defer src="./js/react.production.min.js"></script>
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"}]]))
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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!)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -342,13 +342,13 @@
|
||||
format (state/get-preferred-format repo)
|
||||
db-based? (config/db-based-graph? repo)
|
||||
create-f (fn []
|
||||
(p/do!
|
||||
(<create! title {:redirect? false
|
||||
:split-namespace? false
|
||||
:today-journal? true})
|
||||
(when-not db-based? (state/pub-event! [:journal/insert-template today-page]))
|
||||
(ui-handler/re-render-root!)
|
||||
(plugin-handler/hook-plugin-app :today-journal-created {:title today-page})))]
|
||||
(p/let [result (<create! title {:redirect? false
|
||||
:split-namespace? false
|
||||
:today-journal? true})]
|
||||
(when-not db-based? (state/pub-event! [:journal/insert-template today-page]))
|
||||
(ui-handler/re-render-root!)
|
||||
(plugin-handler/hook-plugin-app :today-journal-created {:title today-page})
|
||||
result))]
|
||||
(when-not (db/get-page today-page)
|
||||
(if db-based?
|
||||
(create-f)
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
(ns frontend.mobile.footer
|
||||
(:require [clojure.string :as string]
|
||||
[frontend.components.svg :as svg]
|
||||
[frontend.date :as date]
|
||||
[frontend.handler.editor :as editor-handler]
|
||||
[frontend.mobile.record :as record]
|
||||
[frontend.mobile.util :as mobile-util]
|
||||
[frontend.state :as state]
|
||||
[frontend.ui :as ui]
|
||||
@@ -13,40 +11,9 @@
|
||||
(rum/defc mobile-bar-command [command-handler icon]
|
||||
[:button.bottom-action
|
||||
{:on-pointer-down (fn [e]
|
||||
(util/stop e)
|
||||
(command-handler))}
|
||||
(if (= icon "player-stop")
|
||||
svg/circle-stop
|
||||
(ui/icon icon {:size 24}))])
|
||||
|
||||
(defn seconds->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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))))
|
||||
@@ -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?
|
||||
([]
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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"}))]])))
|
||||
|
||||
@@ -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)))}}))
|
||||
|
||||
208
src/main/mobile/components/recorder.cljs
Normal file
208
src/main/mobile/components/recorder.cljs
Normal file
@@ -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))))
|
||||
@@ -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/<get-block (state/get-current-repo) id
|
||||
{:children? false
|
||||
:skip-transact? true
|
||||
:skip-refresh? true})]
|
||||
(when block (mobile-state/open-block-modal! block)))))}
|
||||
[:div.flex.flex-col.gap-1.py-1
|
||||
(when header
|
||||
[:div.opacity-60.text-sm
|
||||
header])
|
||||
[:div.flex.flex-row.items-start.gap-1
|
||||
(when (and page? icon) (ui/icon icon {:size 15
|
||||
:class "text-muted-foreground mt-1"}))
|
||||
[:div text]]]]))]]]))
|
||||
(if (seq result)
|
||||
[: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/<get-block (state/get-current-repo) id
|
||||
{:children? false
|
||||
:skip-transact? true
|
||||
:skip-refresh? true})]
|
||||
(when block (mobile-state/open-block-modal! block)))))}
|
||||
[:div.flex.flex-col.gap-1.py-1
|
||||
(when header
|
||||
[:div.opacity-60.text-sm
|
||||
header])
|
||||
[:div.flex.flex-row.items-start.gap-1
|
||||
(when (and page? icon) (ui/icon icon {:size 15
|
||||
:class "text-muted-foreground mt-1"}))
|
||||
[:div text]]]]))]
|
||||
(when-not (string/blank? input)
|
||||
[:div.px-4.text-muted-foreground
|
||||
"No results"]))]]))
|
||||
|
||||
@@ -34,25 +34,25 @@
|
||||
{:class (when (= current-tab "home") "active")
|
||||
:data-tab "home"}
|
||||
(shui/button {:variant :icon}
|
||||
(shui/tabler-icon "home" {:size 24}))
|
||||
(shui/tabler-icon "home" {:size 24}))
|
||||
[:small "Journals"]]
|
||||
[:span.as-item
|
||||
{:class (when (= current-tab "search") "active")
|
||||
:data-tab "search"}
|
||||
(shui/button {:variant :icon}
|
||||
(shui/tabler-icon "search" {:size 24}))
|
||||
(shui/tabler-icon "search" {:size 24}))
|
||||
[:small "Search"]]
|
||||
[:span.as-item
|
||||
(shui/button
|
||||
{:variant :icon
|
||||
:on-click (fn [^js e]
|
||||
(util/stop e)
|
||||
(editor-handler/show-quick-add))}
|
||||
(shui/tabler-icon "plus" {:size 24}))
|
||||
{:variant :icon
|
||||
:on-click (fn [^js e]
|
||||
(util/stop e)
|
||||
(editor-handler/show-quick-add))}
|
||||
(shui/tabler-icon "plus" {:size 24}))
|
||||
[:small "Quick add"]]
|
||||
[:span.as-item
|
||||
{:class (when (= current-tab "settings") "active")
|
||||
:data-tab "settings"}
|
||||
(shui/button {:variant :icon}
|
||||
(shui/tabler-icon "settings" {:size 24}))
|
||||
(shui/tabler-icon "settings" {:size 24}))
|
||||
[:small "Settings"]]]))
|
||||
|
||||
@@ -33,6 +33,14 @@
|
||||
[data]
|
||||
(reset! *popup-data data))
|
||||
|
||||
(defn close-popup!
|
||||
[]
|
||||
(set-popup! nil))
|
||||
|
||||
(defn quick-add-open?
|
||||
[]
|
||||
(= :ls-quick-add (get-in @*popup-data [:opts :id])))
|
||||
|
||||
(defonce *left-sidebar-open? (atom false))
|
||||
|
||||
(defn open-left-sidebar!
|
||||
|
||||
41
yarn.lock
41
yarn.lock
@@ -132,13 +132,6 @@
|
||||
dependencies:
|
||||
"@babel/types" "^7.28.0"
|
||||
|
||||
"@babel/runtime@7.11.2":
|
||||
version "7.11.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736"
|
||||
integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/runtime@^7.10.2", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7":
|
||||
version "7.28.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.2.tgz#2ae5a9d51cc583bd1f5673b3bb70d6d819682473"
|
||||
@@ -338,16 +331,6 @@
|
||||
resolved "https://registry.yarnpkg.com/@capacitor/status-bar/-/status-bar-7.0.1.tgz#6bd3769ef35158c961ff2a6b571c03e9bce55809"
|
||||
integrity sha512-iDv3mXYo9CdxYRVwt3/pRyuk25p7Sn4GfaS/zMZyVIqTzsvKLCIIH3GdKK+ta+nsNcAVpCw/t5jFEBt1D18ctA==
|
||||
|
||||
"@capawesome/capacitor-background-task@7.0.1":
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@capawesome/capacitor-background-task/-/capacitor-background-task-7.0.1.tgz#5531717de4cea255156c7f83fd4bf0f1e472c534"
|
||||
integrity sha512-ILkJ0bCOLperUc+fezzhpiH3Bfnr/318TI9XSrPU/vwvBXjMH7p7xYxKtjDA4VpJfbVh1cHmWLtRSWIk2wUglg==
|
||||
|
||||
"@capgo/capacitor-navigation-bar@7.1.2":
|
||||
version "7.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@capgo/capacitor-navigation-bar/-/capacitor-navigation-bar-7.1.2.tgz#d017f22007e6e848c6a94aa38d70546b08d95473"
|
||||
integrity sha512-lganepu29pay05+clCE41yEICE34xDzB61dmvtwWxZlWccvlu+XWbS8WnMSncvIotqBUmU1owfivG+usfrp4CA==
|
||||
|
||||
"@colors/colors@1.5.0":
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
|
||||
@@ -2543,13 +2526,6 @@ canvas@^2.11.2:
|
||||
nan "^2.17.0"
|
||||
simple-get "^3.0.3"
|
||||
|
||||
capacitor-voice-recorder@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/capacitor-voice-recorder/-/capacitor-voice-recorder-5.0.0.tgz#ec8e421283de19063461838fd340d91f352c8875"
|
||||
integrity sha512-rCZgbmdmj9eXlotziRnIXWoo+7/aGKM1dSeSrgaEmayu9aTs8xkwhpx9eeVe24VDC6sfMHTnwMl5311Ryr/yFA==
|
||||
dependencies:
|
||||
get-blob-duration "^1.2.0"
|
||||
|
||||
chalk@2.4.2, chalk@^2.4.1:
|
||||
version "2.4.2"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
|
||||
@@ -4688,13 +4664,6 @@ gensync@^1.0.0-beta.2:
|
||||
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
|
||||
integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
|
||||
|
||||
get-blob-duration@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/get-blob-duration/-/get-blob-duration-1.2.0.tgz#73cf7dac2fbaa219a5a03d5e5093e06e43814d49"
|
||||
integrity sha512-2xNJa+oKznR21eC2ThMzw4a1931a3ogA8aHoY92xruZufc/02G7pl/P793GJZytkyI8xMJ2DepEQ7MWvg/tn/Q==
|
||||
dependencies:
|
||||
"@babel/runtime" "7.11.2"
|
||||
|
||||
get-caller-file@^1.0.1:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a"
|
||||
@@ -8652,11 +8621,6 @@ reflect.getprototypeof@^1.0.6, reflect.getprototypeof@^1.0.9:
|
||||
get-proto "^1.0.1"
|
||||
which-builtin-type "^1.2.1"
|
||||
|
||||
regenerator-runtime@^0.13.4:
|
||||
version "0.13.11"
|
||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
|
||||
integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
|
||||
|
||||
regex-not@^1.0.0, regex-not@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
|
||||
@@ -10815,6 +10779,11 @@ watchpack@^2.4.1:
|
||||
glob-to-regexp "^0.4.1"
|
||||
graceful-fs "^4.1.2"
|
||||
|
||||
wavesurfer.js@7.10.1:
|
||||
version "7.10.1"
|
||||
resolved "https://registry.yarnpkg.com/wavesurfer.js/-/wavesurfer.js-7.10.1.tgz#c2f799a05d939cbb1e5df8aa7e0485ab44ad7594"
|
||||
integrity sha512-tF1ptFCAi8SAqKbM1e7705zouLC3z4ulXCg15kSP5dQ7VDV30Q3x/xFRcuVIYTT5+jB/PdkhiBRCfsMshZG1Ug==
|
||||
|
||||
webidl-conversions@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
|
||||
|
||||
Reference in New Issue
Block a user