diff --git a/android/app/build.gradle b/android/app/build.gradle index 364e7d6f0d..84a6447f17 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -6,8 +6,8 @@ android { applicationId "com.logseq.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 22 - versionName "0.6.9" + versionCode 23 + versionName "0.6.10" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/android/app/src/main/assets/capacitor.config.json b/android/app/src/main/assets/capacitor.config.json index d188e8a97c..4882e518a9 100644 --- a/android/app/src/main/assets/capacitor.config.json +++ b/android/app/src/main/assets/capacitor.config.json @@ -10,6 +10,9 @@ "androidScaleType": "CENTER_CROP", "splashImmersive": false, "backgroundColor": "#002b36" + }, + "Keyboard": { + "resize": "none" } }, "ios": { diff --git a/android/app/src/main/java/com/logseq/app/FsWatcher.java b/android/app/src/main/java/com/logseq/app/FsWatcher.java index 9f322f553e..f927ed6418 100644 --- a/android/app/src/main/java/com/logseq/app/FsWatcher.java +++ b/android/app/src/main/java/com/logseq/app/FsWatcher.java @@ -1,5 +1,7 @@ package com.logseq.app; +import android.annotation.SuppressLint; +import android.os.Build; import android.system.ErrnoException; import android.system.Os; import android.system.StructStat; @@ -26,7 +28,6 @@ public class FsWatcher extends Plugin { List observers; private String mPath; - private Uri mPathUri; @Override public void load() { @@ -35,17 +36,23 @@ public class FsWatcher extends Plugin { @PluginMethod() public void watch(PluginCall call) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + call.reject("Android version not supported"); + return; + } String pathParam = call.getString("path"); // check file:// or no scheme uris Uri u = Uri.parse(pathParam); Log.i("FsWatcher", "watching " + u); if (u.getScheme() == null || u.getScheme().equals("file")) { - File pathObj = new File(u.getPath()); - if (pathObj == null) { + File pathObj; + try { + pathObj = new File(u.getPath()); + } catch (Exception e) { call.reject("invalid watch path: " + pathParam); return; } - mPathUri = Uri.fromFile(pathObj); + mPath = pathObj.getAbsolutePath(); int mask = FileObserver.CLOSE_WRITE | @@ -56,15 +63,16 @@ public class FsWatcher extends Plugin { call.reject("already watching"); return; } - observers = new ArrayList(); + observers = new ArrayList<>(); observers.add(new SingleFileObserver(pathObj, mask)); // NOTE: only watch first level of directory File[] files = pathObj.listFiles(); if (files != null) { - for (int i = 0; i < files.length; ++i) { - if (files[i].isDirectory() && !files[i].getName().startsWith(".")) { - observers.add(new SingleFileObserver(files[i], mask)); + for (File file : files) { + String filename = file.getName(); + if (file.isDirectory() && !filename.startsWith(".") && !filename.equals("bak") && !filename.equals("node_modules")) { + observers.add(new SingleFileObserver(file, mask)); } } } @@ -103,13 +111,14 @@ public class FsWatcher extends Plugin { } File[] files = pathObj.listFiles(); if (files != null) { - for (int i = 0; i < files.length; ++i) { - if (files[i].isDirectory() && !files[i].getName().startsWith(".") && !files[i].getName().equals("bak")) { - this.initialNotify(files[i], maxDepth - 1); - } else if (files[i].isFile() - && Pattern.matches("[^.].*?\\.(md|org|css|edn|text|markdown|yml|yaml|json|js)$", - files[i].getName())) { - this.onObserverEvent(FileObserver.CREATE, files[i].getAbsolutePath()); + for (File file : files) { + String filename = file.getName(); + if (file.isDirectory() && !filename.startsWith(".") && !filename.equals("bak") && !filename.equals("version-files") && !filename.equals("node_modules")) { + this.initialNotify(file, maxDepth - 1); + } else if (file.isFile() + && Pattern.matches("(?i)[^.].*?\\.(md|org|css|edn|js|markdown)$", + file.getName())) { + this.onObserverEvent(FileObserver.CREATE, file.getAbsolutePath()); } } } @@ -132,9 +141,7 @@ public class FsWatcher extends Plugin { try { obj.put("stat", getFileStat(path)); content = getFileContents(f); - } catch (IOException e) { - e.printStackTrace(); - } catch (ErrnoException e) { + } catch (IOException | ErrnoException e) { e.printStackTrace(); } obj.put("content", content); @@ -145,9 +152,7 @@ public class FsWatcher extends Plugin { try { obj.put("stat", getFileStat(path)); content = getFileContents(f); - } catch (IOException e) { - e.printStackTrace(); - } catch (ErrnoException e) { + } catch (IOException | ErrnoException e) { e.printStackTrace(); } obj.put("content", content); @@ -172,7 +177,7 @@ public class FsWatcher extends Plugin { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; - int length = 0; + int length; while ((length = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, length); @@ -183,22 +188,25 @@ public class FsWatcher extends Plugin { } public static JSObject getFileStat(final String path) throws ErrnoException { + File file = new File(path); StructStat stat = Os.stat(path); JSObject obj = new JSObject(); obj.put("atime", stat.st_atime); obj.put("mtime", stat.st_mtime); obj.put("ctime", stat.st_ctime); + obj.put("size", file.length()); return obj; } private class SingleFileObserver extends FileObserver { - private String mPath; + private final String mPath; public SingleFileObserver(String path, int mask) { super(path, mask); mPath = path; } + @SuppressLint("NewApi") public SingleFileObserver(File path, int mask) { super(path, mask); mPath = path.getAbsolutePath(); @@ -206,9 +214,9 @@ public class FsWatcher extends Plugin { @Override public void onEvent(int event, String path) { - if (path != null) { + if (path != null && !path.equals("graphs-txid.edn") && !path.equals("broken-config.edn")) { Log.d("FsWatcher", "got path=" + path + " event=" + event); - if (Pattern.matches("[^.].*?\\.(md|org|css|edn|text|markdown|yml|yaml|json|js)$", path)) { + if (Pattern.matches("(?i)[^.].*?\\.(md|org|css|edn|js|markdown)$", path)) { String fullPath = mPath + "/" + path; FsWatcher.this.onObserverEvent(event, fullPath); } diff --git a/capacitor.config.ts b/capacitor.config.ts index 2fbd42eec8..d41e12112a 100644 --- a/capacitor.config.ts +++ b/capacitor.config.ts @@ -13,6 +13,10 @@ const config: CapacitorConfig = { splashImmersive: false, backgroundColor: "#002b36" }, + + Keyboard: { + resize: "none" + } }, ios: { scheme: "Logseq" diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index 778dd4398e..4bed8505b7 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -542,7 +542,7 @@ INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MARKETING_VERSION = 0.6.6; + MARKETING_VERSION = 0.6.10; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -568,7 +568,7 @@ INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MARKETING_VERSION = 0.6.6; + MARKETING_VERSION = 0.6.10; PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; @@ -593,7 +593,7 @@ INFOPLIST_KEY_NSHumanReadableCopyright = ""; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - MARKETING_VERSION = 0.6.6; + MARKETING_VERSION = 0.6.10; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController; @@ -620,7 +620,7 @@ INFOPLIST_KEY_NSHumanReadableCopyright = ""; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - MARKETING_VERSION = 0.6.6; + MARKETING_VERSION = 0.6.10; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/ios/App/App/FileSync/Extensions.swift b/ios/App/App/FileSync/Extensions.swift index bf011836f9..4bbe03e3a6 100644 --- a/ios/App/App/FileSync/Extensions.swift +++ b/ios/App/App/FileSync/Extensions.swift @@ -8,11 +8,6 @@ import Foundation import CryptoKit - -import var CommonCrypto.CC_MD5_DIGEST_LENGTH -import func CommonCrypto.CC_MD5 -import typealias CommonCrypto.CC_LONG - // via https://github.com/krzyzanowskim/CryptoSwift extension Array where Element == UInt8 { public init(hex: String) { @@ -120,27 +115,8 @@ extension Data { extension String { var MD5: String { - // TODO: incremental hash - if #available(iOS 13.0, *) { - let computed = Insecure.MD5.hash(data: self.data(using: .utf8)!) - return computed.map { String(format: "%02hhx", $0) }.joined() - } else { - // Fallback on earlier versions, no CryptoKit - let length = Int(CC_MD5_DIGEST_LENGTH) - let messageData = self.data(using:.utf8)! - var digestData = Data(count: length) - - _ = digestData.withUnsafeMutableBytes { digestBytes -> UInt8 in - messageData.withUnsafeBytes { messageBytes -> UInt8 in - if let messageBytesBaseAddress = messageBytes.baseAddress, let digestBytesBlindMemory = digestBytes.bindMemory(to: UInt8.self).baseAddress { - let messageLength = CC_LONG(messageData.count) - CC_MD5(messageBytesBaseAddress, messageLength, digestBytesBlindMemory) - } - return 0 - } - } - return digestData.map { String(format: "%02hhx", $0) }.joined() - } + let computed = Insecure.MD5.hash(data: self.data(using: .utf8)!) + return computed.map { String(format: "%02hhx", $0) }.joined() } func encodeAsFname() -> String { diff --git a/ios/App/App/FileSync/FileSync.swift b/ios/App/App/FileSync/FileSync.swift index 2f7dbf3ec7..3f9e5370e4 100644 --- a/ios/App/App/FileSync/FileSync.swift +++ b/ios/App/App/FileSync/FileSync.swift @@ -17,6 +17,49 @@ var URL_BASE = URL(string: "https://api.logseq.com/file-sync/")! var BUCKET: String = "logseq-file-sync-bucket" var REGION: String = "us-east-2" + +public struct SyncMetadata: CustomStringConvertible, Equatable { + var md5: String + var size: Int + + public init?(of fileURL: URL) { + do { + let fileAttributes = try fileURL.resourceValues(forKeys:[.isRegularFileKey, .fileSizeKey]) + guard fileAttributes.isRegularFile! else { + return nil + } + size = fileAttributes.fileSize ?? 0 + + // incremental MD5sum + let bufferSize = 1024 * 1024 + let file = try FileHandle(forReadingFrom: fileURL) + defer { + file.closeFile() + } + var ctx = Insecure.MD5.init() + while autoreleasepool(invoking: { + let data = file.readData(ofLength: bufferSize) + if data.count > 0 { + ctx.update(data: data) + return true // continue + } else { + return false // eof + } + }) {} + + let computed = ctx.finalize() + md5 = computed.map { String(format: "%02hhx", $0) }.joined() + } catch { + return nil + } + } + + public var description: String { + return "SyncMetadata(md5=\(md5), size=\(size))" + } +} + + // MARK: FileSync Plugin @objc(FileSync) @@ -69,16 +112,16 @@ public class FileSync: CAPPlugin, SyncDebugDelegate { return } - var fileMd5Digests: [String: [String: Any]] = [:] + var fileMetadataDict: [String: [String: Any]] = [:] for filePath in filePaths { let url = baseURL.appendingPathComponent(filePath) - if let content = try? String(contentsOf: url, encoding: .utf8) { - fileMd5Digests[filePath] = ["md5": content.MD5, - "size": content.lengthOfBytes(using: .utf8)] + if let meta = SyncMetadata(of: url) { + fileMetadataDict[filePath] = ["md5": meta.md5, + "size": meta.size] } } - call.resolve(["result": fileMd5Digests]) + call.resolve(["result": fileMetadataDict]) } @objc func getLocalAllFilesMeta(_ call: CAPPluginCall) { @@ -88,21 +131,21 @@ public class FileSync: CAPPlugin, SyncDebugDelegate { return } - var fileMd5Digests: [String: [String: Any]] = [:] + var fileMetadataDict: [String: [String: Any]] = [:] if let enumerator = FileManager.default.enumerator(at: baseURL, includingPropertiesForKeys: [.isRegularFileKey], options: [.skipsPackageDescendants, .skipsHiddenFiles]) { for case let fileURL as URL in enumerator { if !fileURL.isSkipped() { - if let content = try? String(contentsOf: fileURL, encoding: .utf8) { - fileMd5Digests[fileURL.relativePath(from: baseURL)!] = ["md5": content.MD5, - "size": content.lengthOfBytes(using: .utf8)] + if let meta = SyncMetadata(of: fileURL) { + fileMetadataDict[fileURL.relativePath(from: baseURL)!] = ["md5": meta.md5, + "size": meta.size] } } else if fileURL.isICloudPlaceholder() { try? FileManager.default.startDownloadingUbiquitousItem(at: fileURL) } } } - call.resolve(["result": fileMd5Digests]) + call.resolve(["result": fileMetadataDict]) } diff --git a/ios/App/App/FsWatcher.swift b/ios/App/App/FsWatcher.swift index 1923b2869d..b877402959 100644 --- a/ios/App/App/FsWatcher.swift +++ b/ios/App/App/FsWatcher.swift @@ -52,23 +52,20 @@ public class FsWatcher: CAPPlugin, PollingWatcherDelegate { "dir": baseUrl?.description as Any, "path": url.description, ]) - case .Add: - let content = try? String(contentsOf: url, encoding: .utf8) - self.notifyListeners("watcher", data: ["event": "add", + case .Add, .Change: + var content: String? = nil + if url.shouldNotifyWithContent() { + content = try? String(contentsOf: url, encoding: .utf8) + } + self.notifyListeners("watcher", data: ["event": event.description, "dir": baseUrl?.description as Any, "path": url.description, "content": content as Any, - "stat": ["mtime": metadata?.contentModificationTimestamp, - "ctime": metadata?.creationTimestamp] + "stat": ["mtime": metadata?.contentModificationTimestamp ?? 0, + "ctime": metadata?.creationTimestamp ?? 0, + "size": metadata?.fileSize as Any] ]) - case .Change: - let content = try? String(contentsOf: url, encoding: .utf8) - self.notifyListeners("watcher", data: ["event": "change", - "dir": baseUrl?.description as Any, - "path": url.description, - "content": content as Any, - "stat": ["mtime": metadata?.contentModificationTimestamp, - "ctime": metadata?.creationTimestamp]]) + case .Error: // TODO: handle error? break @@ -84,16 +81,18 @@ extension URL { if self.lastPathComponent.starts(with: ".") { return true } - // NOTE: used by file-sync - if self.lastPathComponent == "graphs-txid.edn" { + if self.lastPathComponent == "graphs-txid.edn" || self.lastPathComponent == "broken-config.edn" { return true } - let allowedPathExtensions: Set = ["md", "markdown", "org", "css", "edn", "excalidraw"] + return false + } + + func shouldNotifyWithContent() -> Bool { + let allowedPathExtensions: Set = ["md", "markdown", "org", "js", "edn", "css", "excalidraw"] if allowedPathExtensions.contains(self.pathExtension.lowercased()) { - return false + return true } - // skip for other file types - return true + return false } func isICloudPlaceholder() -> Bool { @@ -110,13 +109,27 @@ public protocol PollingWatcherDelegate { func recevedNotification(_ url: URL, _ event: PollingWatcherEvent, _ metadata: SimpleFileMetadata?) } -public enum PollingWatcherEvent: String { +public enum PollingWatcherEvent { case Add case Change case Unlink case Error + + var description: String { + switch self { + case .Add: + return "add" + case .Change: + return "change" + case .Unlink: + return "unlink" + case .Error: + return "error" + } + } } + public struct SimpleFileMetadata: CustomStringConvertible, Equatable { var contentModificationTimestamp: Double var creationTimestamp: Double @@ -192,11 +205,11 @@ public class PollingWatcher { if isDirectory { // NOTE: URL.path won't end with a `/` - if fileURL.path.hasSuffix("/logseq/bak") || name == ".recycle" || name.hasPrefix(".") || name == "node_modules" { + if fileURL.path.hasSuffix("/logseq/bak") || fileURL.path.hasSuffix("/logseq/version-files") || name == ".recycle" || name.hasPrefix(".") || name == "node_modules" { enumerator.skipDescendants() } } - + if isRegularFile && !fileURL.isSkipped() { if let meta = SimpleFileMetadata(of: fileURL) { newMetaDb[fileURL] = meta diff --git a/ios/App/App/capacitor.config.json b/ios/App/App/capacitor.config.json index f301a40bb2..0e68ee3dd2 100644 --- a/ios/App/App/capacitor.config.json +++ b/ios/App/App/capacitor.config.json @@ -10,6 +10,9 @@ "androidScaleType": "CENTER_CROP", "splashImmersive": false, "backgroundColor": "#002b36" + }, + "Keyboard": { + "resize": "none" } }, "ios": { diff --git a/resources/package.json b/resources/package.json index b5110871b4..dda9c5f0ce 100644 --- a/resources/package.json +++ b/resources/package.json @@ -1,6 +1,6 @@ { "name": "Logseq", - "version": "0.6.9", + "version": "0.6.10", "main": "electron.js", "author": "Logseq", "license": "AGPL-3.0", diff --git a/shadow-cljs.edn b/shadow-cljs.edn index cb559ddba6..eef3f23777 100644 --- a/shadow-cljs.edn +++ b/shadow-cljs.edn @@ -35,7 +35,8 @@ :source-map true :externs ["datascript/externs.js" "externs.js"] - :warnings {:fn-deprecated false}} + :warnings {:fn-deprecated false + :redef false}} :closure-defines {goog.debug.LOGGING_ENABLED true} ;; NOTE: electron, browser/mobile-app use different asset-paths. @@ -60,7 +61,8 @@ :externs ["datascript/externs.js" "externs.js"] - :warnings {:fn-deprecated false}}} + :warnings {:fn-deprecated false + :redef false}}} :test {:target :node-test :output-to "static/tests.js" @@ -96,7 +98,8 @@ :output-feature-set :es-next :externs ["datascript/externs.js" "externs.js"] - :warnings {:fn-deprecated false}} + :warnings {:fn-deprecated false + :redef false}} :devtools {:before-load frontend.core/stop :after-load frontend.core/start :preloads [devtools.preload]}} diff --git a/src/electron/electron/fs_watcher.cljs b/src/electron/electron/fs_watcher.cljs index 443aef69e2..60bf51eae8 100644 --- a/src/electron/electron/fs_watcher.cljs +++ b/src/electron/electron/fs_watcher.cljs @@ -4,7 +4,6 @@ ["chokidar" :as watcher] [electron.utils :as utils] ["electron" :refer [app]] - [frontend.util.fs :as util-fs] [electron.window :as window])) ;; TODO: explore different solutions for different platforms @@ -30,10 +29,15 @@ (defn- publish-file-event! [dir path event] - (send-file-watcher! dir event {:dir (utils/fix-win-path! dir) - :path (utils/fix-win-path! path) - :content (utils/read-file path) - :stat (fs/statSync path)})) + (let [content (when (and (not= event "unlink") + (utils/should-read-content? path)) + (utils/read-file path)) + stat (when (not= event "unlink") + (fs/statSync path))] + (send-file-watcher! dir event {:dir (utils/fix-win-path! dir) + :path (utils/fix-win-path! path) + :content content + :stat stat}))) (defn watch-dir! "Watch a directory if no such file watcher exists" @@ -43,7 +47,7 @@ (let [watcher (.watch watcher dir (clj->js {:ignored (fn [path] - (util-fs/ignored-path? dir path)) + (utils/ignored-path? dir path)) :ignoreInitial false :ignorePermissionErrors true :interval polling-interval @@ -63,9 +67,7 @@ (publish-file-event! dir path "change"))) (.on watcher "unlink" (fn [path] - (send-file-watcher! dir "unlink" - {:dir (utils/fix-win-path! dir) - :path (utils/fix-win-path! path)}))) + (publish-file-event! dir path "unlink"))) (.on watcher "error" (fn [path] (println "Watch error happened: " diff --git a/src/electron/electron/utils.cljs b/src/electron/electron/utils.cljs index a8e7ef4c9c..f714b734ba 100644 --- a/src/electron/electron/utils.cljs +++ b/src/electron/electron/utils.cljs @@ -62,27 +62,27 @@ (when-let [agent (cfgs/get-item :settings/agent)] (set-fetch-agent agent))) -;; keep same as ignored-path? in src/main/frontend/util/fs.cljs -;; TODO: merge them (defn ignored-path? + "Ignore given path from file-watcher notification" [dir path] (when (string? path) (or (some #(string/starts-with? path (str dir "/" %)) - ["." ".recycle" "assets" "node_modules" "logseq/bak"]) + ["." ".recycle" "node_modules" "logseq/bak" "version-files"]) (some #(string/includes? path (str "/" % "/")) - ["." ".recycle" "assets" "node_modules" "logseq/bak"]) - (string/ends-with? path ".DS_Store") + ["." ".recycle" "node_modules" "logseq/bak" "version-files"]) + (some #(string/ends-with? path %) + [".DS_Store" "logseq/graphs-txid.edn" "logseq/broken-config.edn"]) ;; hidden directory or file (let [relpath (path/relative dir path)] (or (re-find #"/\.[^.]+" relpath) - (re-find #"^\.[^.]+" relpath))) - (let [path (string/lower-case path)] - (and - (not (string/blank? (path/extname path))) - (not - (some #(string/ends-with? path %) - [".md" ".markdown" ".org" ".js" ".edn" ".css"]))))))) + (re-find #"^\.[^.]+" relpath)))))) + +(defn should-read-content? + "Skip reading content of file while using file-watcher" + [path] + (let [ext (string/lower-case (path/extname path))] + (contains? #{".md" ".markdown" ".org" ".js" ".edn" ".css"} ext))) (defn fix-win-path! [path] @@ -131,4 +131,4 @@ (defn normalize-lc [s] - (normalize (string/lower-case s))) \ No newline at end of file + (normalize (string/lower-case s))) diff --git a/src/main/frontend/commands.cljs b/src/main/frontend/commands.cljs index 5bc58e6080..4534d87878 100644 --- a/src/main/frontend/commands.cljs +++ b/src/main/frontend/commands.cljs @@ -273,21 +273,16 @@ ["Query table function" [[:editor/input "{{function }}" {:backward-pos 2}]] "Create a query table function"] ["Calculator" [[:editor/input "```calc\n\n```" {:backward-pos 4}] [:codemirror/focus]] "Insert a calculator"] + ["draw" (draw-handler/initialize-excalidarw-file) "Draw a graph with Excalidraw"] - - (when (util/zh-CN-supported?) - ["Embed Bilibili video" [[:editor/input "{{bilibili }}" {:last-pattern (state/get-editor-command-trigger) - :backward-pos 2}]]]) + ["Embed HTML " (->inline "html")] - ["Embed Youtube video" [[:editor/input "{{youtube }}" {:last-pattern (state/get-editor-command-trigger) - :backward-pos 2}]]] + ["Embed Video URL" [[:editor/input "{{video }}" {:last-pattern (state/get-editor-command-trigger) + :backward-pos 2}]]] ["Embed Youtube timestamp" [[:youtube/insert-timestamp]]] - ["Embed Vimeo video" [[:editor/input "{{vimeo }}" {:last-pattern (state/get-editor-command-trigger) - :backward-pos 2}]]] - ["Embed Twitter tweet" [[:editor/input "{{tweet }}" {:last-pattern (state/get-editor-command-trigger) :backward-pos 2}]]]] diff --git a/src/main/frontend/components/block.cljs b/src/main/frontend/components/block.cljs index b8aa3694da..c0c78c88bd 100644 --- a/src/main/frontend/components/block.cljs +++ b/src/main/frontend/components/block.cljs @@ -264,14 +264,14 @@ (let [src (::src state) granted? (state/sub [:nfs/user-granted? (state/get-current-repo)]) href (config/get-local-asset-absolute-path href)] - (when (or granted? (util/electron?) (mobile-util/is-native-platform?)) + (when (or granted? (util/electron?) (mobile-util/native-platform?)) (p/then (editor-handler/make-asset-url href) #(reset! src %))) (when @src (let [ext (keyword (util/get-file-ext @src)) share-fn (fn [event] (util/stop event) - (when (mobile-util/is-native-platform?) + (when (mobile-util/native-platform?) (p/let [url (str (config/get-repo-dir (state/get-current-repo)) href)] (.share Share #js {:url url :title "Open file with your favorite app"}))))] @@ -884,7 +884,7 @@ (state/set-state! :pdf/current current)))} label-text] - (mobile-util/is-native-platform?) + (mobile-util/native-platform?) (asset-link config label-text s metadata full_text)) (contains? (config/doc-formats) ext) @@ -1113,42 +1113,80 @@ (defn- macro-vimeo-cp [_config arguments] (when-let [url (first arguments)] - (let [Vimeo-regex #"^((?:https?:)?//)?((?:www).)?((?:player.vimeo.com|vimeo.com)?)((?:/video/)?)([\w-]+)(\S+)?$"] - (when-let [vimeo-id (nth (util/safe-re-find Vimeo-regex url) 5)] - (when-not (string/blank? vimeo-id) - (let [width (min (- (util/get-width) 96) - 560) - height (int (* width (/ 315 560)))] - [:iframe - {:allow-full-screen "allowfullscreen" - :allow - "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope" - :frame-border "0" - :src (str "https://player.vimeo.com/video/" vimeo-id) - :height height - :width width}])))))) + (when-let [vimeo-id (nth (util/safe-re-find text/vimeo-regex url) 5)] + (when-not (string/blank? vimeo-id) + (let [width (min (- (util/get-width) 96) + 560) + height (int (* width (/ 315 560)))] + [:iframe + {:allow-full-screen "allowfullscreen" + :allow + "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope" + :frame-border "0" + :src (str "https://player.vimeo.com/video/" vimeo-id) + :height height + :width width}]))))) (defn- macro-bilibili-cp [_config arguments] (when-let [url (first arguments)] - (let [id-regex #"https?://www\.bilibili\.com/video/([^? ]+)"] - (when-let [id (cond - (<= (count url) 15) url - :else - (last (util/safe-re-find id-regex url)))] - (when-not (string/blank? id) - (let [width (min (- (util/get-width) 96) - 560) - height (int (* width (/ 315 560)))] - [:iframe - {:allowfullscreen true - :framespacing "0" - :frameborder "no" - :border "0" - :scrolling "no" - :src (str "https://player.bilibili.com/player.html?bvid=" id "&high_quality=1") - :width width - :height (max 500 height)}])))))) + (when-let [id (cond + (<= (count url) 15) url + :else + (nth (util/safe-re-find text/bilibili-regex url) 5))] + (when-not (string/blank? id) + (let [width (min (- (util/get-width) 96) + 560) + height (int (* width (/ 315 560)))] + [:iframe + {:allowfullscreen true + :framespacing "0" + :frameborder "no" + :border "0" + :scrolling "no" + :src (str "https://player.bilibili.com/player.html?bvid=" id "&high_quality=1") + :width width + :height (max 500 height)}]))))) + +(defn- macro-video-cp + [_config arguments] + (when-let [url (first arguments)] + (let [width (min (- (util/get-width) 96) + 560) + height (int (* width (/ 315 560))) + results (text/get-matched-video url) + src (match results + [_ _ _ (:or "youtube.com" "youtu.be" "y2u.be") _ id _] + (if (= (count id) 11) ["youtube-player" id] url) + + [_ _ _ "youtube-nocookie.com" _ id _] + (str "https://www.youtube-nocookie.com/embed/" id) + + [_ _ _ "loom.com" _ id _] + (str "https://www.loom.com/embed/" id) + + [_ _ _ (_ :guard #(string/ends-with? % "vimeo.com")) _ id _] + (str "https://player.vimeo.com/video/" id) + + [_ _ _ "bilibili.com" _ id _] + (str "https://player.bilibili.com/player.html?bvid=" id "&high_quality=1") + + :else + url)] + (if (and (coll? src) + (= (first src) "youtube-player")) + (youtube/youtube-video (last src)) + (when src + [:iframe + {:allowfullscreen true + :allow "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope" + :framespacing "0" + :frameborder "no" + :border "0" + :scrolling "no" + :src src + :width width + :height height}]))))) (defn- macro-else-cp [name config arguments] @@ -1261,13 +1299,12 @@ (= name "youtube") (when-let [url (first arguments)] - (let [YouTube-regex #"^((?:https?:)?//)?((?:www|m).)?((?:youtube.com|youtu.be))(/(?:[\w-]+\?v=|embed/|v/)?)([\w-]+)(\S+)?$"] - (when-let [youtube-id (cond - (== 11 (count url)) url - :else - (nth (util/safe-re-find YouTube-regex url) 5))] - (when-not (string/blank? youtube-id) - (youtube/youtube-video youtube-id))))) + (when-let [youtube-id (cond + (== 11 (count url)) url + :else + (nth (util/safe-re-find text/youtube-regex url) 5))] + (when-not (string/blank? youtube-id) + (youtube/youtube-video youtube-id)))) (= name "youtube-timestamp") (when-let [timestamp (first arguments)] @@ -1290,6 +1327,9 @@ (= name "bilibili") (macro-bilibili-cp config arguments) + (= name "video") + (macro-video-cp config arguments) + (contains? #{"tweet" "twitter"} name) (when-let [url (first arguments)] (let [id-regex #"/status/(\d+)"] @@ -1535,7 +1575,7 @@ "hide-inner-bullet"))} [:span.bullet {:blockid (str uuid)}]]]] (cond - (and (or (mobile-util/is-native-platform?) + (and (or (mobile-util/native-platform?) (:ui/show-empty-bullets? (state/get-config))) (not doc-mode?)) bullet @@ -1869,35 +1909,40 @@ (.stopPropagation e) (let [target (gobj/get e "target") button (gobj/get e "buttons") - shift? (gobj/get e "shiftKey")] - (when (contains? #{1 0} button) - (when-not (target-forbidden-edit? target) - (if (and shift? (state/get-selection-start-block)) - (editor-handler/highlight-selection-area! block-id) - (do - (editor-handler/clear-selection!) - (editor-handler/unhighlight-blocks!) - (let [f #(let [block (or (db/pull [:block/uuid (:block/uuid block)]) block) - cursor-range (util/caret-range (gdom/getElement block-id)) - {:block/keys [content format]} block - content (->> content - (property/remove-built-in-properties format) - (drawer/remove-logbook))] - ;; save current editing block - (let [{:keys [value] :as state} (editor-handler/get-state)] - (editor-handler/save-block! state value)) - (state/set-editing! - edit-input-id - content - block - cursor-range - false))] - ;; wait a while for the value of the caret range - (if (util/ios?) - (f) - (js/setTimeout f 5)) + shift? (gobj/get e "shiftKey") + meta? (util/meta-key? e)] + (if (and meta? (not (state/get-edit-input-id))) + (do + (util/stop e) + (state/conj-selection-block! (gdom/getElement block-id) :down)) + (when (contains? #{1 0} button) + (when-not (target-forbidden-edit? target) + (if (and shift? (state/get-selection-start-block)) + (editor-handler/highlight-selection-area! block-id) + (do + (editor-handler/clear-selection!) + (editor-handler/unhighlight-blocks!) + (let [f #(let [block (or (db/pull [:block/uuid (:block/uuid block)]) block) + cursor-range (util/caret-range (gdom/getElement block-id)) + {:block/keys [content format]} block + content (->> content + (property/remove-built-in-properties format) + (drawer/remove-logbook))] + ;; save current editing block + (let [{:keys [value] :as state} (editor-handler/get-state)] + (editor-handler/save-block! state value)) + (state/set-editing! + edit-input-id + content + block + cursor-range + false))] + ;; wait a while for the value of the caret range + (if (util/ios?) + (f) + (js/setTimeout f 5)) - (when block-id (state/set-selection-start-block! block-id))))))))) + (when block-id (state/set-selection-start-block! block-id)))))))))) (rum/defc dnd-separator-wrapper < rum/reactive [block block-id slide? top? block-content?] @@ -1970,7 +2015,8 @@ (when (and (state/in-selection-mode?) (not (string/includes? content "```")) - (not (gobj/get e "shiftKey"))) + (not (gobj/get e "shiftKey")) + (not (util/meta-key? e))) ;; clear highlighted text (util/clear-selection!)))} (not slide?) @@ -2227,20 +2273,21 @@ (defn- block-mouse-over [uuid e *control-show? block-id doc-mode?] - (util/stop e) - (when (or - (model/block-collapsed? uuid) - (editor-handler/collapsable? uuid {:semantic? true})) - (reset! *control-show? true)) - (when-let [parent (gdom/getElement block-id)] - (let [node (.querySelector parent ".bullet-container")] - (when doc-mode? - (dom/remove-class! node "hide-inner-bullet")))) - (when (and - (state/in-selection-mode?) - (non-dragging? e)) + (when-not @*dragging? (util/stop e) - (editor-handler/highlight-selection-area! block-id))) + (when (or + (model/block-collapsed? uuid) + (editor-handler/collapsable? uuid {:semantic? true})) + (reset! *control-show? true)) + (when-let [parent (gdom/getElement block-id)] + (let [node (.querySelector parent ".bullet-container")] + (when doc-mode? + (dom/remove-class! node "hide-inner-bullet")))) + (when (and + (state/in-selection-mode?) + (non-dragging? e)) + (util/stop e) + (editor-handler/highlight-selection-area! block-id)))) (defn- block-mouse-leave [e *control-show? block-id doc-mode?] diff --git a/src/main/frontend/components/block.css b/src/main/frontend/components/block.css index 4485453404..5900169388 100644 --- a/src/main/frontend/components/block.css +++ b/src/main/frontend/components/block.css @@ -185,14 +185,6 @@ } } -html.is-mobile, -html.is-native-iphone, -html.is-native-android { - .references .block-control { - margin-left: -20px; - } -} - .block-ref { border-bottom: 0.5px solid; border-bottom-color: var(--ls-block-ref-link-text-color); diff --git a/src/main/frontend/components/editor.cljs b/src/main/frontend/components/editor.cljs index be96d17472..5c75d95b2a 100644 --- a/src/main/frontend/components/editor.cljs +++ b/src/main/frontend/components/editor.cljs @@ -7,9 +7,6 @@ [frontend.components.datetime :as datetime-comp] [frontend.components.search :as search] [frontend.components.svg :as svg] - [frontend.mobile.camera :as mobile-camera] - [frontend.mobile.util :as mobile-util] - [frontend.config :as config] [frontend.db :as db] [frontend.db.model :as db-model] [frontend.extensions.zotero :as zotero] @@ -27,9 +24,7 @@ [goog.dom :as gdom] [promesa.core :as p] [rum.core :as rum] - [frontend.handler.history :as history] - [frontend.mobile.footer :as footer] - [frontend.handler.config :as config-handler])) + [frontend.mobile.footer :as footer])) (rum/defc commands < rum/reactive [id format] @@ -232,88 +227,6 @@ template) :class "black"})))))) -(rum/defc mobile-bar-indent-outdent [indent? icon] - [:div - [:button.bottom-action - {:on-mouse-down (fn [e] - (util/stop e) - (editor-handler/indent-outdent indent?))} - (ui/icon icon {:style {:fontSize ui/icon-size}})]]) - -(def ^:private mobile-bar-icons-keywords - [:checkbox :brackets :parentheses :command :tag :a-b :list :camera - :brand-youtube :link :rotate :rotate-clockwise :code :bold :italic :strikethrough :paint]) - -(def ^:private mobile-bar-commands-stats - (atom (into {} - (mapv (fn [name] [name {:counts 0}]) - mobile-bar-icons-keywords)))) - -(defn set-command-stats [icon] - (let [key (keyword icon) - counts (get-in @mobile-bar-commands-stats [key :counts])] - (swap! mobile-bar-commands-stats - assoc-in [key :counts] (inc counts)) - (config-handler/set-config! - :mobile/toolbar-stats @mobile-bar-commands-stats))) - -(rum/defc mobile-bar-command - [command-handler icon & [count? event?]] - [:div - [:button.bottom-action - {:on-mouse-down (fn [e] - (util/stop e) - (when count? - (set-command-stats icon)) - (if event? - (command-handler e) - (command-handler)))} - (ui/icon icon {:style {:fontSize ui/icon-size}})]]) - -(defn mobile-bar-commands - [_parent-state parent-id] - (let [viewport-fn (fn [] (when-let [input (gdom/getElement parent-id)] - (util/make-el-cursor-position-into-center-viewport input) - (.focus input)))] - (zipmap mobile-bar-icons-keywords - [(mobile-bar-command editor-handler/cycle-todo! "checkbox" true) - (mobile-bar-command #(editor-handler/toggle-page-reference-embed parent-id) "brackets" true) - (mobile-bar-command #(editor-handler/toggle-block-reference-embed parent-id) "parentheses" true) - (mobile-bar-command #(do (viewport-fn) (commands/simple-insert! parent-id "/" {})) "command" true) - (mobile-bar-command #(do (viewport-fn) (commands/simple-insert! parent-id "#" {})) "tag" true) - (mobile-bar-command editor-handler/cycle-priority! "a-b" true) - (mobile-bar-command editor-handler/toggle-list! "list" true) - (mobile-bar-command #(mobile-camera/embed-photo parent-id) "camera" true) - (mobile-bar-command commands/insert-youtube-timestamp "brand-youtube" true) - (mobile-bar-command editor-handler/html-link-format! "link" true) - (mobile-bar-command history/undo! "rotate" true true) - (mobile-bar-command history/redo! "rotate-clockwise" true true) - (mobile-bar-command #(do (viewport-fn) (commands/simple-insert! parent-id "<" {})) "code" true) - (mobile-bar-command editor-handler/bold-format! "bold" true) - (mobile-bar-command editor-handler/italics-format! "italic" true) - (mobile-bar-command editor-handler/strike-through-format! "strikethrough" true) - (mobile-bar-command editor-handler/highlight-format! "paint" true)]))) - -(rum/defc mobile-bar < rum/reactive - [parent-state parent-id] - (when-let [config-toolbar-stats (:mobile/toolbar-stats (state/get-config))] - (reset! mobile-bar-commands-stats config-toolbar-stats)) - (let [commands (mobile-bar-commands parent-state parent-id) - sorted-commands (sort-by (comp :counts second) > @mobile-bar-commands-stats)] - [:div#mobile-editor-toolbar.bg-base-2 - [:div.toolbar-commands - (mobile-bar-indent-outdent false "arrow-bar-left") - (mobile-bar-indent-outdent true "arrow-bar-right") - (mobile-bar-command (editor-handler/move-up-down true) "arrow-bar-to-up") - (mobile-bar-command (editor-handler/move-up-down false) "arrow-bar-to-down") - (mobile-bar-command #(if (state/sub :document/mode?) - (editor-handler/insert-new-block! nil) - (commands/simple-insert! parent-id "\n" {})) "arrow-back") - (for [command sorted-commands] - ((first command) commands))] - [:div.toolbar-hide-keyboard - (mobile-bar-command #(state/clear-edit!) "keyboard-show")]])) - (rum/defcs input < rum/reactive (rum/local {} ::input-value) (mixins/event-mixin @@ -607,7 +520,7 @@ (mixins/event-mixin setup-key-listener!) (shortcut/mixin :shortcut.handler/block-editing-only) lifecycle/lifecycle - [state {:keys [format block]} id config] + [state {:keys [format block]} id _config] (let [content (state/sub-edit-content) heading-class (get-editor-style-class content format)] [:div.editor-inner {:class (if block "block-editor" "non-block-editor")} @@ -616,11 +529,6 @@ [:div#audio-record-toolbar (footer/audio-record-cp)]) - (when (and (or (mobile-util/is-native-platform?) - config/mobile?) - (not (:review-cards? config))) - (mobile-bar state id)) - (ui/ls-textarea {:id id :cacheMeasurements (editor-row-height-unchanged?) ;; check when content updated (as the content variable is binded) @@ -629,7 +537,6 @@ :on-click (editor-handler/editor-on-click! id) :on-change (editor-handler/editor-on-change! block id search-timeout) :on-paste (editor-handler/editor-on-paste! id) - :on-height-change (editor-handler/editor-on-height-change! id) :auto-focus false :class heading-class}) diff --git a/src/main/frontend/components/editor.css b/src/main/frontend/components/editor.css index b90e1f2ecc..9d05628323 100644 --- a/src/main/frontend/components/editor.css +++ b/src/main/frontend/components/editor.css @@ -1,33 +1,3 @@ -#mobile-editor-toolbar { - position: fixed; - bottom: 0; - left: 0; - width: 100%; - /* height: 2.5rem; */ - z-index: 9999; - transition: none; - display: flex; - justify-content: space-between; - - button { - padding: 7px 10px; - } - - .toolbar-commands { - justify-content: space-between; - display: flex; - align-items: center; - overflow-x: overlay; - overflow-y: hidden; - width: 95%; - } - - .toolbar-hide-keyboard { - border-left: 1px solid; - border-color: var(--ls-quaternary-background-color); - } -} - #audio-record-toolbar { position: fixed; background-color: var(--ls-secondary-background-color); diff --git a/src/main/frontend/components/header.cljs b/src/main/frontend/components/header.cljs index 5ebeca2a61..3952bebc95 100644 --- a/src/main/frontend/components/header.cljs +++ b/src/main/frontend/components/header.cljs @@ -224,11 +224,10 @@ (let [repos (->> (state/sub [:me :repos]) (remove #(= (:url %) config/local-repo))) electron-mac? (and util/mac? (util/electron?)) - vw-state (state/sub :ui/visual-viewport-state) show-open-folder? (and (nfs/supported?) (or (empty? repos) (nil? (state/sub :git/current-repo))) - (not (mobile-util/is-native-platform?)) + (not (mobile-util/native-platform?)) (not config/publishing?))] [:div.cp__header#head {:class (util/classnames [{:electron-mac electron-mac? @@ -239,8 +238,7 @@ (when (and (util/electron?) (.. target -classList (contains "cp__header"))) (js/window.apis.toggleMaxOrMinActiveWindow)))) - :style {:fontSize 50 - :transform (str "translateY(" (or (:offset-top vw-state) 0) "px)")}} + :style {:fontSize 50}} [:div.l.flex (left-menu-button {:on-click (fn [] (open-fn) @@ -271,7 +269,7 @@ (mobile-util/native-ios?)) (back-and-forward)) - (when-not (mobile-util/is-native-platform?) + (when-not (mobile-util/native-platform?) (new-block-mode)) (when show-open-folder? diff --git a/src/main/frontend/components/header.css b/src/main/frontend/components/header.css index 1133e5fc51..66f6275461 100644 --- a/src/main/frontend/components/header.css +++ b/src/main/frontend/components/header.css @@ -16,6 +16,7 @@ user-select: none; line-height: 1; white-space: nowrap; + background-color: var(--ls-primary-background-color); > .l { width: var(--ls-left-sidebar-width); @@ -221,7 +222,9 @@ html.is-native-iphone-without-notch, html.is-native-ipad { #main-container { - padding-top: 0px; + padding-top: 0px; + display: flex; + flex-direction: column; } #main-content-container { diff --git a/src/main/frontend/components/onboarding/setups.cljs b/src/main/frontend/components/onboarding/setups.cljs index bd7fd05f14..73e640e3f1 100644 --- a/src/main/frontend/components/onboarding/setups.cljs +++ b/src/main/frontend/components/onboarding/setups.cljs @@ -71,10 +71,10 @@ [:section.a [:strong "Let’s get you set up."] [:small (str "Where on your " DEVICE " do you want to save your work?") - (when (mobile-util/is-native-platform?) + (when (mobile-util/native-platform?) (mobile-intro))] - (if (or (nfs/supported?) (mobile-util/is-native-platform?)) + (if (or (nfs/supported?) (mobile-util/native-platform?)) [:div.choose.flex.flex-col.items-center {:on-click #(page-handler/ls-dir-files! (fn [] diff --git a/src/main/frontend/components/page.cljs b/src/main/frontend/components/page.cljs index e056c6e054..4948b61407 100644 --- a/src/main/frontend/components/page.cljs +++ b/src/main/frontend/components/page.cljs @@ -183,7 +183,7 @@ (ui/foldable [:h2.font-bold.opacity-50 (util/format "Pages tagged with \"%s\"" tag)] [:ul.mt-2 - (for [[original-name name] (sort pages)] + (for [[original-name name] (sort-by last pages)] [:li {:key (str "tagged-page-" name)} [:a {:href (rfe/href :page {:name name})} original-name]])] @@ -231,7 +231,6 @@ (when (gp-util/wrapped-by-quotes? @*title-value) (swap! *title-value gp-util/unquote-string) (gobj/set (rum/deref input-ref) "value" @*title-value)) - (state/set-state! :editor/editing-page-title? false) (cond (= old-name @*title-value) (reset! *edit? false) @@ -266,7 +265,6 @@ (reset! *title-value old-name) (reset! *edit? false)))}]] [:a.page-title {:on-mouse-down (fn [e] - (state/set-state! :editor/editing-page-title? true) (when (util/right-click? e) (state/set-state! :page-title/context {:page page-name}))) :on-click (fn [e] @@ -364,7 +362,7 @@ [:div.relative (when (and (not sidebar?) (not block?)) [:div.flex.flex-row.space-between - (when (or (mobile-util/is-native-platform?) (util/mobile?)) + (when (or (mobile-util/native-platform?) (util/mobile?)) [:div.flex.flex-row.pr-2 {:style {:margin-left -15} :on-mouse-over (fn [e] diff --git a/src/main/frontend/components/page_menu.cljs b/src/main/frontend/components/page_menu.cljs index 55b45a24c5..30e11e72c3 100644 --- a/src/main/frontend/components/page_menu.cljs +++ b/src/main/frontend/components/page_menu.cljs @@ -84,7 +84,7 @@ (page-handler/unfavorite-page! page-original-name) (page-handler/favorite-page! page-original-name)))}} - (when-not (mobile-util/is-native-platform?) + (when-not (mobile-util/native-platform?) {:title (t :page/presentation-mode) :options {:on-click (fn [] (state/sidebar-add-block! @@ -103,7 +103,7 @@ :options {:on-click #(js/window.apis.openPath file-path)}}]) (when (or (util/electron?) - (mobile-util/is-native-platform?)) + (mobile-util/native-platform?)) {:title (t :page/copy-page-url) :options {:on-click #(util/copy-to-clipboard! (url-util/get-logseq-graph-page-url nil repo page-original-name))}}) diff --git a/src/main/frontend/components/repo.cljs b/src/main/frontend/components/repo.cljs index 91349d1419..a505cce884 100644 --- a/src/main/frontend/components/repo.cljs +++ b/src/main/frontend/components/repo.cljs @@ -44,7 +44,7 @@ [:div.pl-1.content.mt-3 [:div.flex.flex-row.my-4 (when (or (nfs-handler/supported?) - (mobile-util/is-native-platform?)) + (mobile-util/native-platform?)) [:div.mr-8 (ui/button (t :open-a-directory) @@ -102,7 +102,7 @@ (when (and nfs-repo? (not= current-repo config/local-repo) (or (nfs-handler/supported?) - (mobile-util/is-native-platform?))) + (mobile-util/native-platform?))) {:title (t :sync-from-local-files) :hover-detail (t :sync-from-local-files-detail) :options {:on-click diff --git a/src/main/frontend/components/settings.cljs b/src/main/frontend/components/settings.cljs index 3f6fd7723c..cee2681e66 100644 --- a/src/main/frontend/components/settings.cljs +++ b/src/main/frontend/components/settings.cljs @@ -132,7 +132,7 @@ :href href :on-click on-click))] (when-not (or (util/mobile?) - (mobile-util/is-native-platform?)) + (mobile-util/native-platform?)) [:div.text-sm desc])]]) (defn edit-config-edn [] @@ -161,7 +161,7 @@ (ui/toggle show-brackets? config-handler/toggle-ui-show-brackets! true)]] - (when (not (or (util/mobile?) (mobile-util/is-native-platform?))) + (when (not (or (util/mobile?) (mobile-util/native-platform?))) [:div {:style {:text-align "right"}} (ui/render-keyboard-shortcut (shortcut-helper/gen-shortcut-seq :ui/toggle-brackets))])]) @@ -543,9 +543,9 @@ (show-brackets-row t show-brackets?) (when (util/electron?) (switch-spell-check-row t)) (outdenting-row t logical-outdenting?) - (when-not (or (util/mobile?) (mobile-util/is-native-platform?)) + (when-not (or (util/mobile?) (mobile-util/native-platform?)) (shortcut-tooltip-row t enable-shortcut-tooltip?)) - (when-not (or (util/mobile?) (mobile-util/is-native-platform?)) + (when-not (or (util/mobile?) (mobile-util/native-platform?)) (tooltip-row t enable-tooltip?)) (timetracking-row t enable-timetracking?) (journal-row t enable-journals?) @@ -595,7 +595,7 @@ [:div.panel-wrap.is-advanced (when (and util/mac? (util/electron?)) (app-auto-update-row t)) (usage-diagnostics-row t instrument-disabled?) - (when-not (mobile-util/is-native-platform?) (developer-mode-row t developer-mode?)) + (when-not (mobile-util/native-platform?) (developer-mode-row t developer-mode?)) (when (util/electron?) (plugin-system-switcher-row)) (when (util/electron?) (https-user-agent-row https-agent-opts)) (clear-cache-row t) @@ -633,7 +633,7 @@ (for [[label text icon] [[:general (t :settings-page/tab-general) (ui/icon "adjustments" {:style {:font-size 20}})] [:editor (t :settings-page/tab-editor) (ui/icon "writing" {:style {:font-size 20}})] - (when-not (mobile-util/is-native-platform?) + (when-not (mobile-util/native-platform?) [:git (t :settings-page/tab-version-control) (ui/icon "history" {:style {:font-size 20}})]) [:advanced (t :settings-page/tab-advanced) (ui/icon "bulb" {:style {:font-size 20}})] (when plugins-of-settings diff --git a/src/main/frontend/components/sidebar.cljs b/src/main/frontend/components/sidebar.cljs index ead3ea0323..ed811a341d 100644 --- a/src/main/frontend/components/sidebar.cljs +++ b/src/main/frontend/components/sidebar.cljs @@ -4,37 +4,38 @@ [frontend.components.command-palette :as command-palette] [frontend.components.header :as header] [frontend.components.journal :as journal] + [frontend.components.onboarding :as onboarding] + [frontend.components.plugins :as plugins] [frontend.components.repo :as repo] [frontend.components.right-sidebar :as right-sidebar] + [frontend.components.select :as select] + [frontend.components.svg :as svg] [frontend.components.theme :as theme] [frontend.components.widgets :as widgets] - [frontend.components.plugins :as plugins] - [frontend.components.select :as select] [frontend.config :as config] [frontend.context.i18n :refer [t]] [frontend.db :as db] - [frontend.db.model :as db-model] - [frontend.components.svg :as svg] [frontend.db-mixins :as db-mixins] + [frontend.db.model :as db-model] + [frontend.extensions.pdf.assets :as pdf-assets] + [frontend.extensions.srs :as srs] [frontend.handler.editor :as editor-handler] - [frontend.handler.route :as route-handler] + [frontend.handler.mobile.swipe :as swipe] [frontend.handler.page :as page-handler] + [frontend.handler.route :as route-handler] [frontend.handler.user :as user-handler] [frontend.mixins :as mixins] + [frontend.mobile.footer :as footer] + [frontend.mobile.util :as mobile-util] + [frontend.mobile.mobile-bar :refer [mobile-bar]] [frontend.modules.shortcut.data-helper :as shortcut-dh] [frontend.state :as state] [frontend.ui :as ui] [frontend.util :as util] - [reitit.frontend.easy :as rfe] [goog.dom :as gdom] [goog.object :as gobj] - [rum.core :as rum] - [frontend.extensions.srs :as srs] - [frontend.extensions.pdf.assets :as pdf-assets] - [frontend.mobile.util :as mobile-util] - [frontend.handler.mobile.swipe :as swipe] - [frontend.components.onboarding :as onboarding] - [frontend.mobile.footer :as footer])) + [reitit.frontend.easy :as rfe] + [rum.core :as rum])) (rum/defc nav-content-item [name {:keys [class]} child] @@ -305,7 +306,10 @@ :data-is-full-width (or margin-less-pages? (contains? #{:all-files :all-pages :my-publishing} route-name))} - (when (and (not (mobile-util/is-native-platform?)) + (mobile-bar) + (footer/footer) + + (when (and (not (mobile-util/native-platform?)) (contains? #{:page :home} route-name)) (widgets/demo-graph-alert)) @@ -378,7 +382,10 @@ [page :page])] (state/sidebar-add-block! current-repo db-id block-type))) (reset! sidebar-inited? true)))) - state)} + state) + :did-mount (fn [state] + (state/set-state! :mobile/show-tabbar? true) + state)} [] (let [default-home (get-default-home-if-valid) current-repo (state/sub :git/current-repo) @@ -465,7 +472,8 @@ (defn- hide-context-menu-and-clear-selection [e] (state/hide-custom-context-menu!) - (when-not (gobj/get e "shiftKey") + (when-not (or (gobj/get e "shiftKey") + (util/meta-key? e)) (editor-handler/clear-selection!))) (rum/defcs ^:large-vars/cleanup-todo sidebar < @@ -550,12 +558,7 @@ :indexeddb-support? indexeddb-support? :light? light? :db-restoring? db-restoring? - :main-content main-content}) - - (when (and (mobile-util/is-native-platform?) - current-repo - (not (state/sub :modal/show?))) - (footer/footer))] + :main-content main-content})] (right-sidebar/sidebar) diff --git a/src/main/frontend/components/widgets.cljs b/src/main/frontend/components/widgets.cljs index c6ec4da6ef..6a9185725d 100644 --- a/src/main/frontend/components/widgets.cljs +++ b/src/main/frontend/components/widgets.cljs @@ -12,8 +12,8 @@ [] [:div.flex.flex-col [:h1.title (t :on-boarding/add-graph)] - (let [nfs-supported? (or (nfs/supported?) (mobile-util/is-native-platform?))] - (if (mobile-util/is-native-platform?) + (let [nfs-supported? (or (nfs/supported?) (mobile-util/native-platform?))] + (if (mobile-util/native-platform?) [:div.text-sm (ui/button "Open a local directory" :on-click #(page-handler/ls-dir-files! shortcut/refresh!)) diff --git a/src/main/frontend/config.cljs b/src/main/frontend/config.cljs index 049b2e5056..761be7f659 100644 --- a/src/main/frontend/config.cljs +++ b/src/main/frontend/config.cljs @@ -315,7 +315,7 @@ (and (util/electron?) (local-db? repo-url)) (get-local-dir repo-url) - (and (mobile-util/is-native-platform?) (local-db? repo-url)) + (and (mobile-util/native-platform?) (local-db? repo-url)) (let [dir (get-local-dir repo-url)] (if (string/starts-with? dir "file:") dir @@ -328,7 +328,7 @@ (defn get-repo-path [repo-url path] - (if (and (or (util/electron?) (mobile-util/is-native-platform?)) + (if (and (or (util/electron?) (mobile-util/native-platform?)) (local-db? repo-url)) path (util/node-path.join (get-repo-dir repo-url) path))) diff --git a/src/main/frontend/db/conn.cljs b/src/main/frontend/db/conn.cljs index bd87007dde..a6e3133fc1 100644 --- a/src/main/frontend/db/conn.cljs +++ b/src/main/frontend/db/conn.cljs @@ -23,7 +23,7 @@ (defn get-repo-name [repo] (cond - (mobile-util/is-native-platform?) + (mobile-util/native-platform?) (text/get-graph-name-from-path repo) (config/local-db? repo) @@ -36,7 +36,7 @@ "repo-path: output of `get-repo-name`" [repo-path] (if (or (util/electron?) - (mobile-util/is-native-platform?)) + (mobile-util/native-platform?)) (text/get-file-basename repo-path) repo-path)) diff --git a/src/main/frontend/db/model.cljs b/src/main/frontend/db/model.cljs index c4f0fe35d2..24a0e7c2cd 100644 --- a/src/main/frontend/db/model.cljs +++ b/src/main/frontend/db/model.cljs @@ -409,6 +409,29 @@ f)) form)) +(defn get-sorted-page-block-ids + [page-id] + (let [root (db-utils/entity page-id)] + (loop [result [] + children (sort-by-left (:block/_parent root) root)] + (if (seq children) + (let [child (first children)] + (recur (conj result (:db/id child)) + (concat + (sort-by-left (:block/_parent child) child) + (rest children)))) + result)))) + +(defn sort-page-random-blocks + "Blocks could be non consecutive." + [blocks] + (assert (every? #(= (:block/page %) (:block/page (first blocks))) blocks) "Blocks must to be in a same page.") + (let [page-id (:db/id (:block/page (first blocks))) + ;; TODO: there's no need to sort all the blocks + sorted-ids (get-sorted-page-block-ids page-id) + blocks-map (zipmap (map :db/id blocks) blocks)] + (keep blocks-map sorted-ids))) + (defn has-children? ([block-id] (has-children? (conn/get-db) block-id)) @@ -584,6 +607,57 @@ (recur parent))) false))))) +(defn get-prev-sibling + [db id] + (when-let [e (d/entity db id)] + (let [left (:block/left e)] + (when (not= (:db/id left) (:db/id (:block/parent e))) + left)))) + +(defn get-right-sibling + [db db-id] + (when-let [block (d/entity db db-id)] + (get-by-parent-&-left db + (:db/id (:block/parent block)) + db-id))) + +(defn last-child-block? + "The child block could be collapsed." + [db parent-id child-id] + (when-let [child (d/entity db child-id)] + (cond + (= parent-id child-id) + true + + (get-right-sibling db child-id) + false + + :else + (last-child-block? db parent-id (:db/id (:block/parent child)))))) + +(defn- consecutive-block? + [block-1 block-2] + (let [db (conn/get-db) + aux-fn (fn [block-1 block-2] + (and (= (:block/page block-1) (:block/page block-2)) + (or + ;; sibling or child + (= (:db/id (:block/left block-2)) (:db/id block-1)) + (when-let [prev-sibling (get-prev-sibling db (:db/id block-2))] + (last-child-block? db (:db/id prev-sibling) (:db/id block-1))))))] + (or (aux-fn block-1 block-2) (aux-fn block-2 block-1)))) + +(defn get-non-consecutive-blocks + [blocks] + (vec + (keep-indexed + (fn [i _block] + (when (< (inc i) (count blocks)) + (when-not (consecutive-block? (nth blocks i) + (nth blocks (inc i))) + (nth blocks i)))) + blocks))) + (defn- get-start-id-for-pagination-query [repo-url current-db {:keys [db-before tx-meta] :as tx-report} result outliner-op page-id block-id tx-block-ids] diff --git a/src/main/frontend/extensions/video/youtube.cljs b/src/main/frontend/extensions/video/youtube.cljs index 08ebef2ba8..1eba6742f7 100644 --- a/src/main/frontend/extensions/video/youtube.cljs +++ b/src/main/frontend/extensions/video/youtube.cljs @@ -109,7 +109,7 @@ (defn gen-youtube-ts-macro [] (if-let [player (get-player (state/get-input))] (util/format "{{youtube-timestamp %s}}" (Math/floor (.getCurrentTime ^js player))) - (when (mobile-util/is-native-platform?) + (when (mobile-util/native-platform?) (notification/show! "Please embed a YouTube video at first, then use this icon. Remember: You can paste a raw YouTube url as embedded video on mobile." @@ -123,9 +123,9 @@ Remember: You can paste a raw YouTube url as embedded video on mobile." reg-number #"^\d+$" timestamp (str timestamp) total-seconds (-> (re-matches reg-number timestamp) - parse-long) + util/safe-parse-int) [_ hours minutes seconds] (re-matches reg timestamp) - [hours minutes seconds] (map parse-long [hours minutes seconds])] + [hours minutes seconds] (map util/safe-parse-int [hours minutes seconds])] (cond total-seconds total-seconds diff --git a/src/main/frontend/fs.cljs b/src/main/frontend/fs.cljs index 6e8814545a..fa61e9171c 100644 --- a/src/main/frontend/fs.cljs +++ b/src/main/frontend/fs.cljs @@ -32,7 +32,7 @@ (and (util/electron?) (not bfs-local?)) node-record - (mobile-util/is-native-platform?) + (mobile-util/native-platform?) mobile-record (local-db? dir) @@ -109,7 +109,7 @@ :else (let [[old-path new-path] - (map #(if (or (util/electron?) (mobile-util/is-native-platform?)) + (map #(if (or (util/electron?) (mobile-util/native-platform?)) % (str (config/get-repo-dir repo) "/" %)) [old-path new-path])] @@ -125,7 +125,7 @@ (util/electron?) node-record - (mobile-util/is-native-platform?) + (mobile-util/native-platform?) mobile-record :else @@ -136,7 +136,7 @@ (let [record (get-record)] (p/let [result (protocol/open-dir record ok-handler)] (if (or (util/electron?) - (mobile-util/is-native-platform?)) + (mobile-util/native-platform?)) (let [[dir & paths] (bean/->clj result)] [(:path dir) paths]) result)))) @@ -145,7 +145,7 @@ [path-or-handle ok-handler] (let [record (get-record) electron? (util/electron?) - mobile? (mobile-util/is-native-platform?)] + mobile? (mobile-util/native-platform?)] (p/let [result (protocol/get-files record path-or-handle ok-handler)] (if (or electron? mobile?) (let [result (bean/->clj result)] diff --git a/src/main/frontend/handler.cljs b/src/main/frontend/handler.cljs index 0d9834de81..d8bcdd0fc8 100644 --- a/src/main/frontend/handler.cljs +++ b/src/main/frontend/handler.cljs @@ -93,7 +93,7 @@ (and (not (seq (db/get-files config/local-repo))) ;; Not native local directory (not (some config/local-db? (map :url repos))) - (not (mobile-util/is-native-platform?))) + (not (mobile-util/native-platform?))) ;; will execute `(state/set-db-restoring! false)` inside (repo-handler/setup-local-repo-if-not-exists!) @@ -193,7 +193,7 @@ (p/let [repos (get-repos)] (state/set-repos! repos) (restore-and-setup! repos db-schema) - (when (mobile-util/is-native-platform?) + (when (mobile-util/native-platform?) (p/do! (mobile-util/hide-splash)))) (reset! db/*sync-search-indice-f search/sync-search-indice!) diff --git a/src/main/frontend/handler/editor.cljs b/src/main/frontend/handler/editor.cljs index 8efe6d04d9..b3253cf34e 100644 --- a/src/main/frontend/handler/editor.cljs +++ b/src/main/frontend/handler/editor.cljs @@ -1,13 +1,15 @@ (ns frontend.handler.editor (:require ["/frontend/utils" :as utils] + ["path" :as path] [cljs.core.match :refer [match]] [clojure.set :as set] [clojure.string :as string] [clojure.walk :as w] [dommy.core :as dom] [frontend.commands :as commands - :refer [*angle-bracket-caret-pos *show-block-commands - *show-commands *slash-caret-pos]] + :refer [*angle-bracket-caret-pos + *show-block-commands *show-commands + *slash-caret-pos]] [frontend.config :as config] [frontend.date :as date] [frontend.db :as db] @@ -25,12 +27,12 @@ [frontend.handler.notification :as notification] [frontend.handler.repeated :as repeated] [frontend.handler.route :as route-handler] - [frontend.image :as image] [frontend.idb :as idb] + [frontend.image :as image] [frontend.mobile.util :as mobile-util] [frontend.modules.outliner.core :as outliner-core] - [frontend.modules.outliner.tree :as tree] [frontend.modules.outliner.transaction :as outliner-tx] + [frontend.modules.outliner.tree :as tree] [frontend.search :as search] [frontend.state :as state] [frontend.template :as template] @@ -40,21 +42,20 @@ [frontend.util.clock :as clock] [frontend.util.cursor :as cursor] [frontend.util.drawer :as drawer] - [frontend.util.marker :as marker] - [frontend.util.property :as property] - [frontend.util.priority :as priority] - [frontend.util.thingatpt :as thingatpt] + [frontend.util.keycode :as keycode] [frontend.util.list :as list] + [frontend.util.marker :as marker] + [frontend.util.priority :as priority] + [frontend.util.property :as property] + [frontend.util.thingatpt :as thingatpt] [goog.dom :as gdom] [goog.dom.classes :as gdom-classes] [goog.object :as gobj] [lambdaisland.glogi :as log] [promesa.core :as p] - [frontend.util.keycode :as keycode] [logseq.graph-parser.util :as gp-util] [logseq.graph-parser.mldoc :as gp-mldoc] - [logseq.graph-parser.block :as gp-block] - ["path" :as path])) + [logseq.graph-parser.block :as gp-block])) ;; FIXME: should support multiple images concurrently uploading @@ -1411,7 +1412,7 @@ (util/electron?) (str "assets://" repo-dir path) - (mobile-util/is-native-platform?) + (mobile-util/native-platform?) (mobile-util/convert-file-src (str repo-dir path)) :else @@ -1677,7 +1678,8 @@ (move-nodes blocks)) (when-let [input-id (state/get-edit-input-id)] (when-let [input (gdom/getElement input-id)] - (.focus input)))) + (.focus input) + (js/setTimeout #(util/scroll-editor-cursor input) 100)))) (let [ids (state/get-selection-block-ids)] (when (seq ids) (let [lookup-refs (map (fn [id] [:block/uuid id]) ids) @@ -2630,7 +2632,7 @@ ;; FIXME: On mobile, a backspace click to call keydown-backspace-handler ;; does not work sometimes in an empty block, hence the empty block ;; can't be deleted. Need to figure out why and find a better solution. - (and (mobile-util/is-native-platform?) + (and (mobile-util/native-platform?) (= key "Backspace") (= value "")) (do @@ -2813,24 +2815,9 @@ [id] (fn [_e] (let [input (gdom/getElement id)] + (util/scroll-editor-cursor input) (close-autocomplete-if-outside input)))) -(defonce mobile-toolbar-height 40) -(defn editor-on-height-change! - [id] - (fn [box-height ^js row-height] - (let [row-height (:rowHeight (js->clj row-height :keywordize-keys true)) - input (gdom/getElement id) - caret (cursor/get-caret-pos input) - cursor-bottom (if caret (+ row-height (:top caret)) box-height) - box-top (gobj/get (.getBoundingClientRect input) "top") - cursor-y (+ cursor-bottom box-top) - vw-height (.-height js/window.visualViewport)] - (when (< vw-height (+ cursor-y mobile-toolbar-height)) - (let [main-node (gdom/getElement "main-content-container") - scroll-top (.-scrollTop main-node)] - (set! (.-scrollTop main-node) (+ scroll-top row-height))))))) - (defn editor-on-change! [block id search-timeout] (fn [e] @@ -2842,7 +2829,9 @@ (js/setTimeout #(edit-box-on-change! e block id) timeout))) - (edit-box-on-change! e block id)))) + (let [input (gdom/getElement id)] + (edit-box-on-change! e block id) + (util/scroll-editor-cursor input))))) (defn- paste-text-parseable [format text] @@ -2878,6 +2867,18 @@ (recur (remove (set (map :block/uuid result)) (rest ids)) result)) result))) +(defn wrap-macro-url + [url] + (cond + (boolean (text/get-matched-video url)) + (util/format "{{video %s}}" url) + + (string/includes? url "twitter.com") + (util/format "{{twitter %s}}" url) + + :else + (notification/show! (util/format "No macro is available for %s" url) :warning))) + (defn- paste-copied-blocks-or-text [text e] (let [copied-blocks (state/get-copied-blocks) @@ -2905,18 +2906,7 @@ (and (gp-util/url? text) (not (string/blank? (util/get-selected-text)))) (html-link-format! text) - - (and (gp-util/url? text) - (or (string/includes? text "youtube.com") - (string/includes? text "youtu.be")) - (mobile-util/is-native-platform?)) - (commands/simple-insert! (state/get-edit-input-id) (util/format "{{youtube %s}}" text) nil) - - (and (gp-util/url? text) - (string/includes? text "twitter.com") - (mobile-util/is-native-platform?)) - (commands/simple-insert! (state/get-edit-input-id) (util/format "{{twitter %s}}" text) nil) - + (and (text/block-ref? text) (wrapped-by? input "((" "))")) (commands/simple-insert! (state/get-edit-input-id) (text/get-block-ref text) nil) @@ -2953,7 +2943,10 @@ (utils/getClipText (fn [clipboard-data] (when-let [_ (state/get-input)] - (state/append-current-edit-content! clipboard-data))) + (let [data (if (gp-util/url? clipboard-data) + (wrap-macro-url clipboard-data) + clipboard-data)] + (state/append-current-edit-content! data)))) (fn [error] (js/console.error error)))) @@ -2964,7 +2957,8 @@ (let [text (.getData (gobj/get e "clipboardData") "text") input (state/get-input)] (if-not (string/blank? text) - (if (thingatpt/org-admonition&src-at-point input) + (if (or (thingatpt/markdown-src-at-point input) + (thingatpt/org-admonition&src-at-point input)) (when-not (mobile-util/native-ios?) (util/stop e) (paste-text-in-one-block-at-point)) diff --git a/src/main/frontend/handler/editor/lifecycle.cljs b/src/main/frontend/handler/editor/lifecycle.cljs index d7ae20de13..b2576dd244 100644 --- a/src/main/frontend/handler/editor/lifecycle.cljs +++ b/src/main/frontend/handler/editor/lifecycle.cljs @@ -3,7 +3,6 @@ [frontend.handler.editor.keyboards :as keyboards-handler] [frontend.state :as state] [frontend.util :as util] - [frontend.mobile.util :as mobile-util] [goog.dom :as gdom])) (defn did-mount! @@ -21,9 +20,7 @@ (when-let [element (gdom/getElement id)] (.focus element) - (when (or (mobile-util/is-native-platform?) - (util/mobile?)) - (util/make-el-cursor-position-into-center-viewport element)))) + (js/setTimeout #(util/scroll-editor-cursor element) 50))) state) (defn did-remount! diff --git a/src/main/frontend/handler/events.cljs b/src/main/frontend/handler/events.cljs index 271cf031ac..84dfa8810c 100644 --- a/src/main/frontend/handler/events.cljs +++ b/src/main/frontend/handler/events.cljs @@ -137,7 +137,7 @@ [repo] (when (and (not (util/electron?)) - (not (mobile-util/is-native-platform?))) + (not (mobile-util/native-platform?))) (fn [close-fn] [:div [:p @@ -288,15 +288,28 @@ (reset! st/*inited? true) (st/consume-pending-shortcuts!))) +(defmethod handle :mobile/keyboard-will-show [[_ keyboard-height]] + (let [main-node (util/app-scroll-container-node)] + (state/set-state! :mobile/show-tabbar? false) + (state/set-state! :mobile/show-toolbar? true) + (when (mobile-util/native-ios?) + (reset! util/keyboard-height keyboard-height) + (set! (.. main-node -style -marginBottom) (str keyboard-height "px")) + (when-let [card-preview-el (js/document.querySelector ".cards-review")] + (set! (.. card-preview-el -style -marginBottom) (str keyboard-height "px"))) + (js/setTimeout (fn [] + (let [toolbar (.querySelector main-node "#mobile-editor-toolbar")] + (set! (.. toolbar -style -bottom) (str keyboard-height "px")))) + 100)))) -(defmethod handle :mobile/keyboard-will-show [[_]] - (when (and (state/get-left-sidebar-open?) - (state/editing?)) - (state/set-left-sidebar-open! false))) - -(defmethod handle :mobile/keyboard-did-show [[_]] - (when-let [input (state/get-input)] - (util/make-el-cursor-position-into-center-viewport input))) +(defmethod handle :mobile/keyboard-will-hide [[_]] + (let [main-node (util/app-scroll-container-node)] + (state/set-state! :mobile/show-toolbar? false) + (state/set-state! :mobile/show-tabbar? true) + (when (mobile-util/native-ios?) + (when-let [card-preview-el (js/document.querySelector ".cards-review")] + (set! (.. card-preview-el -style -marginBottom) "0px")) + (set! (.. main-node -style -marginBottom) "0px")))) (defmethod handle :plugin/consume-updates [[_ id pending? updated?]] (let [downloading? (:plugin/updates-downloading? @state/state)] diff --git a/src/main/frontend/handler/page.cljs b/src/main/frontend/handler/page.cljs index ef16780cb8..9d5a17c1a6 100644 --- a/src/main/frontend/handler/page.cljs +++ b/src/main/frontend/handler/page.cljs @@ -164,7 +164,7 @@ (db/transact! [[:db.fn/retractEntity [:file/path file-path]]]) (-> (p/let [_ (and (config/local-db? repo) - (mobile-util/is-native-platform?) + (mobile-util/native-platform?) (fs/delete-file! repo file-path file-path {})) _ (fs/unlink! repo (config/get-repo-path repo file-path) nil)]) (p/catch (fn [err] @@ -594,7 +594,7 @@ page) (let [journal? (date/valid-journal-title? page) ref-file-path (str - (if (or (util/electron?) (mobile-util/is-native-platform?)) + (if (or (util/electron?) (mobile-util/native-platform?)) (-> (config/get-repo-dir (state/get-current-repo)) js/decodeURI (string/replace #"/+$" "") @@ -728,7 +728,7 @@ (not (state/loading-files? repo))) (state/set-today! (date/today)) (when (or (config/local-db? repo) - (and (= "local" repo) (not (mobile-util/is-native-platform?)))) + (and (= "local" repo) (not (mobile-util/native-platform?)))) (let [title (date/today) today-page (util/page-name-sanity-lc title) format (state/get-preferred-format repo) diff --git a/src/main/frontend/handler/ui.cljs b/src/main/frontend/handler/ui.cljs index f3595301f2..5b00dc0c8c 100644 --- a/src/main/frontend/handler/ui.cljs +++ b/src/main/frontend/handler/ui.cljs @@ -135,7 +135,7 @@ (defn exec-js-if-exists-&-allowed! [t] - (when-not (mobile/is-native-platform?) + (when-not (mobile/native-platform?) (when-let [href (or (state/get-custom-js-link) (config/get-custom-js-path))] diff --git a/src/main/frontend/handler/web/nfs.cljs b/src/main/frontend/handler/web/nfs.cljs index c441348106..6181ba7244 100644 --- a/src/main/frontend/handler/web/nfs.cljs +++ b/src/main/frontend/handler/web/nfs.cljs @@ -26,12 +26,12 @@ [frontend.encrypt :as encrypt])) (defn remove-ignore-files - [files] + [files dir-name nfs?] (let [files (remove (fn [f] (let [path (:file/path f)] (or (string/starts-with? path ".git/") (string/includes? path ".git/") - (and (util-fs/ignored-path? "" path) + (and (util-fs/ignored-path? (if nfs? "" dir-name) path) (not= (:file/name f) ".gitignore"))))) files)] (if-let [ignore-file (some #(when (= (:file/name %) ".gitignore") @@ -55,7 +55,7 @@ :file/last-modified-at mtime :file/size size :file/content content}) - result) + result) electron? (map (fn [{:keys [path stat content]}] @@ -64,7 +64,7 @@ :file/last-modified-at mtime :file/size size :file/content content})) - result) + result) :else (let [result (flatten (bean/->clj result))] @@ -122,7 +122,7 @@ [ok-handler] (let [path-handles (atom {}) electron? (util/electron?) - mobile-native? (mobile-util/is-native-platform?) + mobile-native? (mobile-util/native-platform?) nfs? (and (not electron?) (not mobile-native?)) *repo (atom nil)] @@ -147,7 +147,7 @@ (nfs/add-nfs-file-handle! root-handle-path root-handle)) result (nth result 1) files (-> (->db-files mobile-native? electron? dir-name result) - remove-ignore-files) + (remove-ignore-files dir-name nfs?)) _ (when nfs? (let [file-paths (set (map :file/path files))] (swap! path-handles (fn [handles] @@ -282,7 +282,7 @@ handle-path (str config/local-handle-prefix dir-name) path-handles (atom {}) electron? (util/electron?) - mobile-native? (mobile-util/is-native-platform?) + mobile-native? (mobile-util/native-platform?) nfs? (and (not electron?) (not mobile-native?))] (when re-index? @@ -297,7 +297,7 @@ (when nfs? (swap! path-handles assoc path handle)))) new-files (-> (->db-files mobile-native? electron? dir-name files-result) - remove-ignore-files) + (remove-ignore-files dir-name nfs?)) _ (when nfs? (let [file-paths (set (map :file/path new-files))] (swap! path-handles (fn [handles] diff --git a/src/main/frontend/mobile/core.cljs b/src/main/frontend/mobile/core.cljs index f8ee58b3be..db24959869 100644 --- a/src/main/frontend/mobile/core.cljs +++ b/src/main/frontend/mobile/core.cljs @@ -1,5 +1,6 @@ (ns frontend.mobile.core (:require ["@capacitor/app" :refer [^js App]] + ["@capacitor/keyboard" :refer [^js Keyboard]] [clojure.string :as string] [frontend.fs.capacitor-fs :as fs] [frontend.handler.editor :as editor-handler] @@ -80,6 +81,15 @@ (fn [event] (state/pub-event! [:file-watcher/changed event]))) + (.addListener Keyboard "keyboardWillShow" + (fn [^js info] + (let [keyboard-height (.-keyboardHeight info)] + (state/pub-event! [:mobile/keyboard-will-show keyboard-height])))) + + (.addListener Keyboard "keyboardWillHide" + (fn [] + (state/pub-event! [:mobile/keyboard-will-hide]))) + (.addEventListener js/window "statusTap" #(util/scroll-to-top true)) @@ -96,6 +106,6 @@ (when (mobile-util/native-ios?) (ios-init)) - - (when (mobile-util/is-native-platform?) + + (when (mobile-util/native-platform?) (general-init))) diff --git a/src/main/frontend/mobile/footer.cljs b/src/main/frontend/mobile/footer.cljs index fa91d38a95..01bff9c7da 100644 --- a/src/main/frontend/mobile/footer.cljs +++ b/src/main/frontend/mobile/footer.cljs @@ -1,12 +1,12 @@ (ns frontend.mobile.footer - (:require [frontend.ui :as ui] - [rum.core :as rum] - [frontend.state :as state] - [frontend.mobile.record :as record] - [frontend.util :as util] + (:require [clojure.string :as string] + [frontend.date :as date] [frontend.handler.editor :as editor-handler] - [clojure.string :as string] - [frontend.date :as date])) + [frontend.mobile.record :as record] + [frontend.state :as state] + [frontend.ui :as ui] + [frontend.util :as util] + [rum.core :as rum])) (rum/defc mobile-bar-command [command-handler icon] [:div @@ -46,9 +46,8 @@ (rum/defc footer < rum/reactive [] - (when-not (or (state/sub :editor/editing?) - (state/sub :block/component-editing-mode?) - (state/sub :editor/editing-page-title?)) + (when (and (state/sub :mobile/show-tabbar?) + (state/get-current-repo)) [:div.cp__footer.w-full.bottom-0.justify-between (audio-record-cp) (mobile-bar-command #(state/toggle-document-mode!) "notes") diff --git a/src/main/frontend/mobile/index.css b/src/main/frontend/mobile/index.css index a755f902ca..f0021d977b 100644 --- a/src/main/frontend/mobile/index.css +++ b/src/main/frontend/mobile/index.css @@ -1,9 +1,10 @@ .cp__footer { position: absolute; bottom: 0px; + left: 0px; padding: 10px 20px; background-color: var(--ls-primary-background-color); - z-index: 1000; + z-index: 10; display: flex; flex: 0 0 auto; white-space: nowrap; @@ -22,6 +23,56 @@ } } +#mobile-editor-toolbar { + position: fixed; + bottom: 0; + transition: bottom 260ms; + /* transition-timing-function: cubic-bezier(.29, 1.01, 1, -0.68); */ + /* transition-timing-function: steps(10, jump-end); */ + /* transition-timing-function: steps(5, end); */ + transition-timing-function: ease-out; + left: 0; + width: 100%; + z-index: 9999; + display: flex; + justify-content: space-between; + + button { + padding: 7px 10px; + + .submenu { + background-color: red; + z-index: 100; + background-color: var(--ls-secondary-background-color); + border-radius: 5px; + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.02); + overflow-x: overlay; + overflow-y: hidden; + left: 0px; + height: 40px; + } + + .show-submenu { + display: block; + } + } + + .toolbar-commands { + justify-content: space-between; + display: flex; + align-items: center; + overflow-x: overlay; + overflow-y: hidden; + width: 95%; + } + + .toolbar-hide-keyboard { + border-left: 1px solid; + border-color: var(--ls-quaternary-background-color); + } +} + + html.is-native-ipad { .cp__footer { height: 55px; diff --git a/src/main/frontend/mobile/intent.cljs b/src/main/frontend/mobile/intent.cljs index f985257699..305458050e 100644 --- a/src/main/frontend/mobile/intent.cljs +++ b/src/main/frontend/mobile/intent.cljs @@ -1,22 +1,23 @@ (ns frontend.mobile.intent (:require ["@capacitor/filesystem" :refer [Filesystem]] + ["path" :as path] ["send-intent" :refer [^js SendIntent]] - [lambdaisland.glogi :as log] - [promesa.core :as p] + [clojure.pprint :as pprint] + [clojure.set :as set] [clojure.string :as string] + [frontend.config :as config] + [frontend.date :as date] [frontend.db :as db] [frontend.handler.editor :as editor-handler] - [frontend.state :as state] - [frontend.date :as date] - [frontend.util :as util] - [frontend.config :as config] - [logseq.graph-parser.mldoc :as gp-mldoc] - [logseq.graph-parser.config :as gp-config] - ["path" :as path] - [frontend.mobile.util :as mobile-util] [frontend.handler.notification :as notification] - [clojure.pprint :as pprint] - [clojure.set :as set])) + [frontend.mobile.util :as mobile-util] + [frontend.state :as state] + [frontend.util :as util] + [lambdaisland.glogi :as log] + [logseq.graph-parser.config :as gp-config] + [logseq.graph-parser.mldoc :as gp-mldoc] + [logseq.graph-parser.text :as text] + [promesa.core :as p])) (defn- handle-received-text [result] (let [{:keys [title url]} result @@ -33,9 +34,8 @@ (string/split url "\"\n")) text (some-> text (string/replace #"^\"" "")) url (and url - (cond (or (string/includes? url "youtube.com") - (string/includes? url "youtu.be")) - (util/format "{{youtube %s}}" url) + (cond (boolean (text/get-matched-video url)) + (util/format "{{video %s}}" url) (and (string/includes? url "twitter.com") (string/includes? url "status")) diff --git a/src/main/frontend/mobile/mobile_bar.cljs b/src/main/frontend/mobile/mobile_bar.cljs new file mode 100644 index 0000000000..f489fcfcc1 --- /dev/null +++ b/src/main/frontend/mobile/mobile_bar.cljs @@ -0,0 +1,141 @@ +(ns frontend.mobile.mobile-bar + (:require [dommy.core :as dom] + [frontend.commands :as commands] + [frontend.date :as date] + [frontend.handler.config :as config-handler] + [frontend.handler.editor :as editor-handler] + [frontend.handler.history :as history] + [frontend.handler.page :as page-handler] + [frontend.mobile.camera :as mobile-camera] + [frontend.state :as state] + [frontend.ui :as ui] + [frontend.util :as util] + [goog.dom :as gdom] + [rum.core :as rum])) + +(def ^:private icons-keywords + [:checkbox :brackets :parentheses :command :tag :a-b :list :camera + :brand-youtube :link :rotate :rotate-clockwise :code :calendar :bold :italic :strikethrough :paint]) + +(def ^:private commands-stats + (atom (into {} + (mapv (fn [name] [name {:counts 0}]) + icons-keywords)))) + +(defn set-command-stats [icon] + (let [key (keyword icon) + counts (get-in @commands-stats [key :counts])] + (swap! commands-stats + assoc-in [key :counts] (inc counts)) + (config-handler/set-config! + :mobile/toolbar-stats @commands-stats))) + +(rum/defc command + [command-handler icon & [count? event?]] + [:div + [:button.bottom-action + {:on-mouse-down (fn [e] + (util/stop e) + (when count? + (set-command-stats icon)) + (if event? + (command-handler e) + (command-handler)) + (state/set-state! :mobile/toolbar-update-observer (rand-int 1000000)))} + (ui/icon icon {:style {:fontSize ui/icon-size}})]]) + +(rum/defc indent-outdent [indent? icon] + [:div + [:button.bottom-action + {:on-mouse-down (fn [e] + (util/stop e) + (editor-handler/indent-outdent indent?))} + (ui/icon icon {:style {:fontSize ui/icon-size}})]]) + +(rum/defc timestamp-submenu + [parent-id] + (let [callback (fn [event] + (util/stop event) + (let [target (.-parentNode (.-target event))] + (dom/remove-class! target "show-submenu"))) + command-cp (fn [action description] + [:button + {:on-mouse-down (fn [e] + (action) + (callback e))} + description])] + [:div + [:button.bottom-action + {:on-mouse-down (fn [event] + (util/stop event) + (set-command-stats :calendar) + (state/set-state! :mobile/toolbar-update-observer (rand-int 1000000)) + (let [target (gdom/getNextElementSibling (.-target event))] + (dom/add-class! target "show-submenu")))} + (ui/icon "calendar" {:style {:fontSize ui/icon-size}}) + [:div.submenu.fixed.hidden.flex.flex-col.w-full.justify-evenly + {:style {:bottom @util/keyboard-height}} + (command-cp #(let [today (page-handler/get-page-ref-text (date/today))] + (commands/simple-insert! parent-id today {})) + "Today") + (command-cp #(let [tomorrow (page-handler/get-page-ref-text (date/tomorrow))] + (commands/simple-insert! parent-id tomorrow {})) + "Tomorrow") + (command-cp #(let [yesterday (page-handler/get-page-ref-text (date/yesterday))] + (commands/simple-insert! parent-id yesterday {})) + "Yesterday") + (command-cp #(let [timestamp (date/get-current-time)] + (commands/simple-insert! parent-id timestamp {})) + "Time")]]])) + +(defn commands + [parent-id] + (let [viewport-fn (fn [] (when-let [input (gdom/getElement parent-id)] + (util/scroll-editor-cursor input :to-vw-one-quarter? true) + (.focus input)))] + (zipmap icons-keywords + [(command editor-handler/cycle-todo! "checkbox" true) + (command #(do (viewport-fn) (editor-handler/toggle-page-reference-embed parent-id)) "brackets" true) + (command #(do (viewport-fn) (editor-handler/toggle-block-reference-embed parent-id)) "parentheses" true) + (command #(do (viewport-fn) (commands/simple-insert! parent-id "/" {})) "command" true) + (command #(do (viewport-fn) (commands/simple-insert! parent-id "#" {})) "tag" true) + (command editor-handler/cycle-priority! "a-b" true) + (command editor-handler/toggle-list! "list" true) + (command #(mobile-camera/embed-photo parent-id) "camera" true) + (command commands/insert-youtube-timestamp "brand-youtube" true) + (command editor-handler/html-link-format! "link" true) + (command history/undo! "rotate" true true) + (command history/redo! "rotate-clockwise" true true) + (timestamp-submenu parent-id) + (command #(commands/simple-insert! parent-id "<" {}) "code" true) + (command editor-handler/bold-format! "bold" true) + (command editor-handler/italics-format! "italic" true) + (command editor-handler/strike-through-format! "strikethrough" true) + (command editor-handler/highlight-format! "paint" true)]))) + +(rum/defc mobile-bar < rum/reactive + [] + (when (and (state/sub :mobile/toolbar-update-observer) + (state/sub :mobile/show-toolbar?)) + (when-let [config-toolbar-stats (:mobile/toolbar-stats (state/get-config))] + (prn :config-toolbar-stats config-toolbar-stats) + (reset! commands-stats config-toolbar-stats)) + (let [parent-id (state/get-edit-input-id) + commands (commands parent-id) + sorted-commands (sort-by (comp :counts second) > @commands-stats)] + (when (and (state/sub :mobile/show-toolbar?) + (state/sub :editor/editing?)) + [:div#mobile-editor-toolbar.bg-base-2 + [:div.toolbar-commands + (indent-outdent false "arrow-bar-left") + (indent-outdent true "arrow-bar-right") + (command (editor-handler/move-up-down true) "arrow-bar-to-up") + (command (editor-handler/move-up-down false) "arrow-bar-to-down") + (command #(if (state/sub :document/mode?) + (editor-handler/insert-new-block! nil) + (commands/simple-insert! parent-id "\n" {})) "arrow-back") + (for [command sorted-commands] + ((first command) commands))] + [:div.toolbar-hide-keyboard + (command #(state/clear-edit!) "keyboard-show")]])))) + diff --git a/src/main/frontend/mobile/util.cljs b/src/main/frontend/mobile/util.cljs index 3db32b68b1..cb0f6123eb 100644 --- a/src/main/frontend/mobile/util.cljs +++ b/src/main/frontend/mobile/util.cljs @@ -6,15 +6,15 @@ (defn platform [] (.getPlatform Capacitor)) -(defn is-native-platform? [] +(defn native-platform? [] (.isNativePlatform Capacitor)) (defn native-ios? [] - (and (is-native-platform?) + (and (native-platform?) (= (platform) "ios"))) (defn native-android? [] - (and (is-native-platform?) + (and (native-platform?) (= (platform) "android"))) (defn convert-file-src [path-str] @@ -27,7 +27,7 @@ (defonce file-sync (registerPlugin "FileSync"))) ;; NOTE: both iOS and android share the same FsWatcher API -(when (is-native-platform?) +(when (native-platform?) (defonce fs-watcher (registerPlugin "FsWatcher"))) (defn sync-icloud-repo [repo-dir] @@ -41,45 +41,6 @@ (defn hide-splash [] (.hide SplashScreen)) -(def idevice-info - (atom - {:iPadPro12.9 {:width 1024 :height 1366 :statusbar 40} - :iPadPro11 {:width 834 :height 1194 :statusbar 40} - :iPadPro10.5 {:width 834 :height 1112 :statusbar 40} - :iPadAir10.5 {:width 834 :height 1112 :statusbar 40} - :iPadAir10.9 {:width 820 :height 1180 :statusbar 40} - :iPad10.2 {:width 810 :height 1080 :statusbar 40} - :iPadPro9.7 {:width 768 :height 1024 :statusbar 40} - :iPadmini9.7 {:width 768 :height 1024 :statusbar 40} - :iPadAir9.7 {:width 768 :height 1024 :statusbar 40} - :iPad9.7 {:width 768 :height 1024 :statusbar 40} - :iPadmini8.3 {:width 744 :height 1133 :statusbar 40} - :iPhone7Plus {:width 476 :height 847 :statusbar 20} - :iPhone6sPlus {:width 476 :height 847 :statusbar 20} - :iPhone6Plus {:width 476 :height 847 :statusbar 20} - :iPhone13ProMax {:width 428 :height 926 :statusbar 47} - :iPhone12ProMax {:width 428 :height 926 :statusbar 47} - :iPhone11ProMax {:width 414 :height 896 :statusbar 44} - :iPhone11 {:width 414 :height 896 :statusbar 48} - :iPhoneXSMax {:width 414 :height 896 :statusbar 48} - :iPhoneXR {:width 414 :height 896 :statusbar 48} - :iPhone8Plus {:width 414 :height 736 :statusbar 20} - :iPhone13Pro {:width 390 :height 844 :statusbar 47} - :iPhone13 {:width 390 :height 844 :statusbar 47} - :iPhone12 {:width 390 :height 844 :statusbar 47} - :iPhone12Pro {:width 390 :height 844 :statusbar 47} - :iPhone11Pro {:width 375 :height 812 :statusbar 44} - :iPhoneXS {:width 375 :height 812 :statusbar 44} - :iPhoneX {:width 375 :height 812 :statusbar 44} - :iPhone8 {:width 375 :height 667 :statusbar 20} - :iPhone7 {:width 375 :height 667 :statusbar 20} - :iPhone6s {:width 375 :height 667 :statusbar 20} - :iPhone6 {:width 375 :height 667 :statusbar 20} - :iPhone13mini {:width 375 :height 812 :statusbar 44} - :iPhone12mini {:width 375 :height 812 :statusbar 44} - :iPhoneSE4 {:width 320 :height 568 :statusbar 20} - :iPodtouch5 {:width 320 :height 568 :statusbar 20}})) - (defn get-idevice-model [] (when (native-ios?) @@ -121,12 +82,3 @@ [] (when-let [model (get-idevice-model)] (string/starts-with? (first model) "iPad"))) - -(defn get-idevice-statusbar-height - [] - (let [[model landscape?] (get-idevice-model) - model (when-not (= model "Not a known Apple device!") - (keyword model))] - (if (and model landscape?) - 20 - (:statusbar (model @idevice-info))))) diff --git a/src/main/frontend/modules/instrumentation/sentry.cljs b/src/main/frontend/modules/instrumentation/sentry.cljs index c0e302cd79..9b41518cf3 100644 --- a/src/main/frontend/modules/instrumentation/sentry.cljs +++ b/src/main/frontend/modules/instrumentation/sentry.cljs @@ -18,7 +18,7 @@ :initialScope {:tags {:platform (cond (util/electron?) "electron" - (mobile-util/is-native-platform?) "mobile" + (mobile-util/native-platform?) "mobile" :else "web") :publishing config/publishing?}} :integrations [(new posthog/SentryIntegration posthog "logseq" 5311485) diff --git a/src/main/frontend/modules/outliner/core.cljs b/src/main/frontend/modules/outliner/core.cljs index 4101ea3cd6..a490228a7e 100644 --- a/src/main/frontend/modules/outliner/core.cljs +++ b/src/main/frontend/modules/outliner/core.cljs @@ -99,7 +99,7 @@ (-get-parent-id [this] (-> (get-in this [:data :block/parent]) - (outliner-u/->block-id))) + (outliner-u/->block-id))) (-set-parent-id [this parent-id] (outliner-u/check-block-id parent-id) @@ -107,7 +107,7 @@ (-get-left-id [this] (-> (get-in this [:data :block/left]) - (outliner-u/->block-id))) + (outliner-u/->block-id))) (-set-left-id [this left-id] (outliner-u/check-block-id left-id) @@ -170,7 +170,7 @@ (-del [this txs-state children?] (assert (ds/outliner-txs-state? txs-state) - "db should be satisfied outliner-tx-state?") + "db should be satisfied outliner-tx-state?") (let [block-id (tree/-get-id this) ids (set (if children? (let [children (db/get-block-children (state/get-current-repo) block-id) @@ -192,7 +192,7 @@ (assoc :block/left parent)))) immediate-children))) txs)) - txs)] + txs)] (swap! txs-state concat txs) block-id)) @@ -209,12 +209,7 @@ (defn get-right-sibling [db-id] (when db-id - (when-let [block (db/entity db-id)] - (db-model/get-by-parent-&-left (conn/get-db) - (:db/id (:block/parent block)) - db-id)))) - - + (db-model/get-right-sibling (conn/get-db) db-id))) (defn- assoc-level-aux [tree-vec children-key init-level] @@ -285,13 +280,13 @@ (loop [node node limit limit result []] - (if (zero? limit) - result - (if-let [left (tree/-get-left node)] - (if-not (= left parent) - (recur left (dec limit) (conj result (tree/-get-id left))) - result) - result))))) + (if (zero? limit) + result + (if-let [left (tree/-get-left node)] + (if-not (= left parent) + (recur left (dec limit) (conj result (tree/-get-id left))) + result) + result))))) (defn- page-first-child? [block] @@ -494,6 +489,48 @@ {:tx-data full-tx :blocks tx})))) +(defn- build-move-blocks-next-tx + [blocks] + (let [id->blocks (zipmap (map :db/id blocks) blocks) + top-level-blocks (get-top-level-blocks blocks) + top-level-blocks-ids (set (map :db/id top-level-blocks)) + right-block (get-right-sibling (:db/id (last top-level-blocks)))] + (when (and right-block + (not (contains? top-level-blocks-ids (:db/id right-block)))) + {:db/id (:db/id right-block) + :block/left (loop [block (:block/left right-block)] + (if (contains? top-level-blocks-ids (:db/id block)) + (recur (:block/left (get id->blocks (:db/id block)))) + (:db/id block)))}))) + +(defn- find-new-left + [block moved-ids target-block current-block sibling?] + (if (= (:db/id target-block) (:db/id (:block/left current-block))) + (if sibling? + (db/entity (last moved-ids)) + target-block) + (let [left (db/entity (:db/id (:block/left block)))] + (if (contains? (set moved-ids) (:db/id left)) + (find-new-left left moved-ids target-block current-block sibling?) + left)))) + +(defn- fix-non-consecutive-blocks + [blocks target-block sibling?] + (let [page-blocks (group-by :block/page blocks)] + (->> + (mapcat (fn [[_page blocks]] + (let [blocks (db-model/sort-page-random-blocks blocks) + non-consecutive-blocks (->> (conj (db-model/get-non-consecutive-blocks blocks) (last blocks)) + (util/distinct-by :db/id))] + (when (seq non-consecutive-blocks) + (mapv (fn [block] + (when-let [right (get-right-sibling (:db/id block))] + (when-let [new-left (find-new-left right (distinct (map :db/id blocks)) target-block block sibling?)] + {:db/id (:db/id right) + :block/left (:db/id new-left)}))) + non-consecutive-blocks)))) page-blocks) + (remove nil?)))) + (defn- delete-block "Delete block from the tree." [txs-state block' children?] @@ -556,23 +593,11 @@ (tree/-save new-right-node txs-state)))) (doseq [id block-ids] (let [node (block (db/pull id))] - (tree/-del node txs-state true))))) + (tree/-del node txs-state true))) + (let [fix-non-consecutive-tx (fix-non-consecutive-blocks blocks nil false)] + (swap! txs-state concat fix-non-consecutive-tx)))) {:tx-data @txs-state})) -(defn- build-move-blocks-next-tx - [blocks] - (let [id->blocks (zipmap (map :db/id blocks) blocks) - top-level-blocks (get-top-level-blocks blocks) - top-level-blocks-ids (set (map :db/id top-level-blocks)) - right-block (get-right-sibling (:db/id (last top-level-blocks)))] - (when (and right-block - (not (contains? top-level-blocks-ids (:db/id right-block)))) - {:db/id (:db/id right-block) - :block/left (loop [block (:block/left right-block)] - (if (contains? top-level-blocks-ids (:db/id block)) - (recur (:block/left (get id->blocks (:db/id block)))) - (:db/id block)))}))) - (defn move-blocks "Move `blocks` to `target-block` as siblings or children." [blocks target-block {:keys [sibling? outliner-op]}] @@ -598,7 +623,8 @@ (let [children-ids (mapcat #(db/get-block-children-ids (state/get-current-repo) (:block/uuid %)) blocks)] (map (fn [uuid] {:block/uuid uuid :block/page target-page}) children-ids))) - full-tx (util/concat-without-nil tx-data move-blocks-next-tx children-page-tx) + fix-non-consecutive-tx (fix-non-consecutive-blocks blocks target-block sibling?) + full-tx (util/concat-without-nil tx-data move-blocks-next-tx children-page-tx fix-non-consecutive-tx) tx-meta (cond-> {:move-blocks (mapv :db/id blocks) :target (:db/id target-block)} not-same-page? diff --git a/src/main/frontend/page.cljs b/src/main/frontend/page.cljs index 3950055542..bc2913a475 100644 --- a/src/main/frontend/page.cljs +++ b/src/main/frontend/page.cljs @@ -15,8 +15,7 @@ [] (try (comp - (ui/setup-active-keystroke!) - (ui/setup-patch-ios-visual-viewport-state!)) + (ui/setup-active-keystroke!)) (catch js/Error _e nil))) diff --git a/src/main/frontend/state.cljs b/src/main/frontend/state.cljs index f7940f6ae7..b378cefa20 100644 --- a/src/main/frontend/state.cljs +++ b/src/main/frontend/state.cljs @@ -58,6 +58,7 @@ :modal/close-btn? nil :modal/subsets [] + ;; right sidebar :ui/fullscreen? false :ui/settings-open? false @@ -88,9 +89,6 @@ :ui/shortcut-tooltip? (if (false? (storage/get :ui/shortcut-tooltip?)) false true) - :ui/visual-viewport-pending? false - :ui/visual-viewport-state nil - :document/mode? document-mode? :config {} @@ -114,7 +112,6 @@ :editor/args nil :editor/on-paste? false :editor/last-key-code nil - :editor/editing-page-title? false ;; for audio record :editor/record-status "NONE" @@ -146,6 +143,13 @@ :electron/updater {} :electron/user-cfgs nil + ;; mobile + :mobile/show-toolbar? false + ;;; toolbar icon doesn't update correctly when clicking after separate it from box, + ;;; add a random in (<= 1000000) to observer its update + :mobile/toolbar-update-observer 0 + :mobile/show-tabbar? false + ;; plugin :plugin/enabled (and (util/electron?) ;; true false :theme-only @@ -289,7 +293,7 @@ (defn get-current-repo [] (or (:git/current-repo @state) - (when-not (mobile-util/is-native-platform?) + (when-not (mobile-util/native-platform?) "local"))) (defn get-config @@ -853,10 +857,7 @@ (util/set-change-value input content)) (when move-cursor? - (cursor/move-cursor-to input pos)) - - (when (or (util/mobile?) (mobile-util/is-native-platform?)) - (util/make-el-center-if-near-top input)))))))) + (cursor/move-cursor-to input pos)))))))) (defn clear-edit! [] @@ -1193,7 +1194,7 @@ (defn enable-tooltip? [] - (if (or (util/mobile?) (mobile-util/is-native-platform?)) + (if (or (util/mobile?) (mobile-util/native-platform?)) false (get (get (sub-config) (get-current-repo)) :ui/enable-tooltip? @@ -1547,14 +1548,6 @@ [] (:editor/last-key-code @state)) -(defn set-visual-viewport-state - [input] - (set-state! :ui/visual-viewport-state input)) - -(defn get-visual-viewport-state - [] - (:ui/visual-viewport-state @state)) - (defn get-plugin-by-id [id] (when-let [id (and id (keyword id))] diff --git a/src/main/frontend/ui.cljs b/src/main/frontend/ui.cljs index 953c92e990..1c143b5a09 100644 --- a/src/main/frontend/ui.cljs +++ b/src/main/frontend/ui.cljs @@ -46,7 +46,7 @@ (util/safari?) (js/window.scrollTo 0 0))) -(defonce icon-size (if (mobile-util/is-native-platform?) 23 20)) +(defonce icon-size (if (mobile-util/native-platform?) 23 20)) (rum/defc ls-textarea < rum/reactive @@ -290,40 +290,6 @@ (.appendChild js/document.head node)) style))) -(defn setup-patch-ios-visual-viewport-state! - [] - (when-let [^js vp (and (or (and (util/mobile?) (util/safari?)) - (mobile-util/native-ios?)) - js/window.visualViewport)] - (let [raf-pending? (atom false) - set-raf-pending! #(reset! raf-pending? %) - on-viewport-changed - (fn [] - (let [update-vw-state - (debounce - (fn [] - (state/set-visual-viewport-state {:height (.-height vp) - :page-top (.-pageTop vp) - :offset-top (.-offsetTop vp)}) - (state/set-state! :ui/visual-viewport-pending? false)) - 20)] - (when-not @raf-pending? - (let [f (fn [] - (set-raf-pending! false) - (update-vw-state))] - (set-raf-pending! true) - (state/set-state! :ui/visual-viewport-pending? true) - (js/window.requestAnimationFrame f)))))] - - (.addEventListener vp "resize" on-viewport-changed) - (.addEventListener vp "scroll" on-viewport-changed) - - (fn [] - (.removeEventListener vp "resize" on-viewport-changed) - (.removeEventListener vp "scroll" on-viewport-changed) - (state/set-visual-viewport-state nil)))) - #()) - (defn apply-custom-theme-effect! [theme] (when plugin-handler/lsp-enabled? (when-let [custom-theme (state/sub [:ui/custom-theme (keyword theme)])] @@ -684,7 +650,7 @@ (assoc :on-mouse-down on-mouse-down :class "cursor")) [:div.flex.flex-row.items-center - (when-not (mobile-util/is-native-platform?) + (when-not (mobile-util/native-platform?) [:a.block-control.opacity-50.hover:opacity-100.mr-2 (cond-> {:style {:width 14 @@ -953,7 +919,7 @@ (rum/local true ::active?) [state content-fn sensor-opts {:keys [reset-height? once?]}] (let [*active? (::active? state)] - (if (or (util/mobile?) (mobile-util/is-native-platform?)) + (if (or (util/mobile?) (mobile-util/native-platform?)) (content-fn) (let [*visible? (::visible? state)] (visibility-sensor diff --git a/src/main/frontend/util.cljc b/src/main/frontend/util.cljc index 833325d7fd..05b67aa9d4 100644 --- a/src/main/frontend/util.cljc +++ b/src/main/frontend/util.cljc @@ -11,7 +11,7 @@ [cljs-time.coerce :as tc] [cljs-time.core :as t] [dommy.core :as d] - [frontend.mobile.util :refer [is-native-platform?]] + [frontend.mobile.util :refer [native-platform?]] [logseq.graph-parser.util :as gp-util] [goog.dom :as gdom] [goog.object :as gobj] @@ -89,7 +89,7 @@ #?(:cljs (def nfs? (and (not (electron?)) - (not (is-native-platform?))))) + (not (native-platform?))))) #?(:cljs (defn file-protocol? @@ -1040,29 +1040,6 @@ (string/replace """ "\"") (string/replace "'" "'"))) -#?(:cljs - (defn system-locales - [] - (when-not node-test? - (when-let [navigator (and js/window (.-navigator js/window))] - ;; https://zzz.buzz/2016/01/13/detect-browser-language-in-javascript/ - (when navigator - (let [v (js->clj - (or - (.-languages navigator) - (.-language navigator) - (.-userLanguage navigator) - (.-browserLanguage navigator) - (.-systemLanguage navigator)))] - (if (string? v) [v] v))))))) - -#?(:cljs - (defn zh-CN-supported? - [] - (let [system-locales (set (system-locales))] - (or (contains? system-locales "zh-CN") - (contains? system-locales "zh-Hans-CN"))))) - (comment (= (get-relative-path "journals/2020_11_18.org" "pages/grant_ideas.org") "../pages/grant_ideas.org") @@ -1216,6 +1193,12 @@ (defn meta-key-name [] (if mac? "Cmd" "Ctrl"))) +#?(:cljs + (defn meta-key? [e] + (if mac? + (gobj/get e "metaKey") + (gobj/get e "ctrlKey")))) + #?(:cljs (defn right-click? [e] @@ -1224,42 +1207,53 @@ (or (= which 3) (= button 2))))) +(def keyboard-height (atom nil)) #?(:cljs - (defn make-el-into-center-viewport - [^js/HTMLElement el] - (when el - (.scrollIntoView el #js {:block "center" :behavior "smooth"})))) + (defn scroll-editor-cursor + [^js/HTMLElement el & {:keys [to-vw-one-quarter?]}] + (when (and el (or (native-platform?) mobile?)) + (let [box-rect (.getBoundingClientRect el) + box-top (.-top box-rect) + box-bottom (.-bottom box-rect) -#?(:cljs - (defn make-el-cursor-position-into-center-viewport - [^js/HTMLElement el] - (when el - (let [main-node (gdom/getElement "main-content-container") - pos (get-selection-start el) - cursor-top (some-> (gdom/getElement "mock-text") - gdom/getChildren - array-seq - (nth-safe pos) - .-offsetTop) - box-caret (.getBoundingClientRect el) - box-top (.-top box-caret) - box-bottom (.-bottom box-caret) - vw-height (or (.-height js/window.visualViewport) - (.-clientHeight js/document.documentElement)) - scroll-top (.-scrollTop main-node) - cursor-y (if cursor-top (+ cursor-top box-top) box-bottom) - scroll (- cursor-y (/ vw-height 2))] - (when (> scroll 0) - (set! (.-scrollTop main-node) (+ scroll-top scroll))))))) + header-height (-> (gdom/getElementByClass "cp__header") + .-clientHeight) -#?(:cljs - (defn make-el-center-if-near-top - ([^js/HTMLElement el] - (make-el-center-if-near-top el 80)) - ([^js/HTMLElement el offset] - (let [target-top (.-top (.getBoundingClientRect el))] - (when (<= target-top (or (safe-parse-int offset) 0)) - (make-el-into-center-viewport el)))))) + main-node (app-scroll-container-node) + scroll-top (.-scrollTop main-node) + + current-pos (get-selection-start el) + mock-text (some-> (gdom/getElement "mock-text") + gdom/getChildren + array-seq + (nth-safe current-pos)) + offset-top (and mock-text (.-offsetTop mock-text)) + offset-height (and mock-text (.-offsetHeight mock-text)) + + cursor-y (if offset-top (+ offset-top box-top offset-height 2) box-bottom) + vw-height (or (.-height js/window.visualViewport) + (.-clientHeight js/document.documentElement)) + ;; mobile toolbar height: 40px + scroll (- cursor-y (- vw-height (+ @keyboard-height 40)))] + (cond + (and to-vw-one-quarter? (> cursor-y (* vw-height 0.4))) + (set! (.-scrollTop main-node) (+ scroll-top (- cursor-y (/ vw-height 4)))) + + (and (< cursor-y (+ header-height offset-height 4)) ;; 4 is top+bottom padding for per line + (>= cursor-y header-height)) + (.scrollBy main-node (bean/->js {:top (- (+ offset-height 4))})) + + (< cursor-y header-height) + (let [_ (.scrollIntoView el true) + main-node (app-scroll-container-node) + scroll-top (.-scrollTop main-node)] + (set! (.-scrollTop main-node) (- scroll-top (/ vw-height 4)))) + + (> scroll 0) + (set! (.-scrollTop main-node) (+ scroll-top scroll)) + + :else + nil))))) #?(:cljs (defn sm-breakpoint? diff --git a/src/main/frontend/util/fs.cljs b/src/main/frontend/util/fs.cljs index b708f021f5..16c6f1da66 100644 --- a/src/main/frontend/util/fs.cljs +++ b/src/main/frontend/util/fs.cljs @@ -4,17 +4,22 @@ ;; TODO: move all file path related util functions to here -;; keep same as ignored-path? in src/electron/electron/utils.cljs -;; TODO: merge them +;; NOTE: This is not the same ignored-path? as src/electron/electron/utils.cljs. +;; The assets directory is ignored. +;; +;; When in nfs-mode, dir is "", path is relative path to graph dir. +;; When in native-mode, dir and path are absolute paths. (defn ignored-path? + "Ignore path for ls-dir-files-with-handler! and reload-dir!" [dir path] (when (string? path) (or (some #(string/starts-with? path (str dir "/" %)) - ["." ".recycle" "assets" "node_modules" "logseq/bak"]) + ["." ".recycle" "assets" "node_modules" "logseq/bak" "version-files"]) (some #(string/includes? path (str "/" % "/")) - ["." ".recycle" "assets" "node_modules" "logseq/bak"]) - (string/ends-with? path ".DS_Store") + ["." ".recycle" "assets" "node_modules" "logseq/bak" "version-files"]) + (some #(string/ends-with? path %) + [".DS_Store" "logseq/graphs-txid.edn" "logseq/broken-config.edn"]) ;; hidden directory or file (let [relpath (path/relative dir path)] (or (re-find #"/\.[^.]+" relpath) @@ -24,4 +29,4 @@ (not (string/blank? (path/extname path))) (not (some #(string/ends-with? path %) - [".md" ".markdown" ".org" ".js" ".edn" ".css"]))))))) \ No newline at end of file + [".md" ".markdown" ".org" ".js" ".edn" ".css"]))))))) diff --git a/src/main/frontend/util/thingatpt.cljs b/src/main/frontend/util/thingatpt.cljs index beff6833e9..f13d0cbd24 100644 --- a/src/main/frontend/util/thingatpt.cljs +++ b/src/main/frontend/util/thingatpt.cljs @@ -147,7 +147,7 @@ :name name :end (+ (:end admonition&src) (count name)))))))) -(defn- markdown-src-at-point [& [input]] +(defn markdown-src-at-point [& [input]] (when-let [markdown-src (thing-at-point ["```" "```"] input)] (let [language (-> (:full-content markdown-src) string/split-lines diff --git a/src/main/frontend/version.cljs b/src/main/frontend/version.cljs index 124987105e..e12ef48451 100644 --- a/src/main/frontend/version.cljs +++ b/src/main/frontend/version.cljs @@ -1,3 +1,3 @@ (ns frontend.version) -(defonce version "0.6.9") +(defonce version "0.6.10") diff --git a/src/main/logseq/graph_parser/text.cljs b/src/main/logseq/graph_parser/text.cljs index a70b164605..b73a1fc691 100644 --- a/src/main/logseq/graph_parser/text.cljs +++ b/src/main/logseq/graph_parser/text.cljs @@ -122,6 +122,18 @@ [s] (string/split s #"(\"[^\"]*\")")) +(def bilibili-regex #"^((?:https?:)?//)?((?:www).)?((?:bilibili.com))(/(?:video/)?)([\w-]+)(\S+)?$") +(def loom-regex #"^((?:https?:)?//)?((?:www).)?((?:loom.com))(/(?:share/|embed/))([\w-]+)(\S+)?$") +(def vimeo-regex #"^((?:https?:)?//)?((?:www).)?((?:player.vimeo.com|vimeo.com))(/(?:video/)?)([\w-]+)(\S+)?$") +(def youtube-regex #"^((?:https?:)?//)?((?:www|m).)?((?:youtube.com|youtu.be|y2u.be|youtube-nocookie.com))(/(?:[\w-]+\?v=|embed/|v/)?)([\w-]+)([\S^\?]+)?$") + +(defn get-matched-video + [url] + (or (re-find youtube-regex url) + (re-find loom-regex url) + (re-find vimeo-regex url) + (re-find bilibili-regex url))) + (def markdown-link #"\[([^\[]+)\](\(.*\))") (defn split-page-refs-without-brackets