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/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/package.json b/package.json index e4db2f0a43..e6301fbe4e 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "cljs:app-watch": "clojure -M:cljs watch app", "cljs:electron-watch": "clojure -M:cljs watch app electron --config-merge '{:asset-path \"./js\"}'", "cljs:release": "clojure -M:cljs release app publishing electron", - "cljs:release-electron": "clojure -M:cljs release app publishing electron --debug", + "cljs:release-electron": "clojure -M:cljs release app electron --debug && clojure -M:cljs release publishing", "cljs:release-app": "clojure -M:cljs release app", "cljs:test": "clojure -M:test compile test", "cljs:run-test": "node static/tests.js", diff --git a/resources/package.json b/resources/package.json index 47d749ef8e..b5110871b4 100644 --- a/resources/package.json +++ b/resources/package.json @@ -36,7 +36,7 @@ "https-proxy-agent": "5.0.0", "@sentry/electron": "2.5.1", "posthog-js": "1.10.2", - "@logseq/rsapi": "0.0.11", + "@logseq/rsapi": "0.0.14", "electron-deeplink": "1.0.10" }, "devDependencies": { diff --git a/shadow-cljs.edn b/shadow-cljs.edn index e475782d89..e8df8c2715 100644 --- a/shadow-cljs.edn +++ b/shadow-cljs.edn @@ -29,7 +29,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. @@ -54,7 +55,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" @@ -86,7 +88,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 0bb20d020e..8cba732c8d 100644 --- a/src/main/frontend/commands.cljs +++ b/src/main/frontend/commands.cljs @@ -281,20 +281,14 @@ (p/let [_ (draw/create-draw-with-default-content path)] (println "draw file created, " path)) text)) "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 3480ba391c..0ea813dfeb 100644 --- a/src/main/frontend/components/block.cljs +++ b/src/main/frontend/components/block.cljs @@ -883,7 +883,7 @@ (mobile-util/native-platform?) (asset-link config label-text s metadata full_text)) - + (contains? (config/doc-formats) ext) (asset-link config label-text s metadata full_text) @@ -1110,42 +1110,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] @@ -1258,13 +1296,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)] @@ -1287,6 +1324,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+)"] @@ -1866,35 +1906,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?] @@ -1967,7 +2012,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?) @@ -2224,20 +2270,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?] @@ -3059,7 +3106,7 @@ (when (> (- (util/time-ms) (:start-time config)) 100) (load-more-blocks! config flat-blocks))) has-more? (and - (> (count flat-blocks) model/initial-blocks-length) + (>= (count flat-blocks) model/initial-blocks-length) (some? (model/get-next-open-block (db/get-db) (last flat-blocks) db-id))) dom-id (str "lazy-blocks-" (::id state))] [:div {:id dom-id} diff --git a/src/main/frontend/components/page.cljs b/src/main/frontend/components/page.cljs index b90d50af8b..f7d98eae68 100644 --- a/src/main/frontend/components/page.cljs +++ b/src/main/frontend/components/page.cljs @@ -177,7 +177,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]])] diff --git a/src/main/frontend/components/sidebar.cljs b/src/main/frontend/components/sidebar.cljs index fe5d329441..89ead885c2 100644 --- a/src/main/frontend/components/sidebar.cljs +++ b/src/main/frontend/components/sidebar.cljs @@ -465,7 +465,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 < 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 97181f9f9b..1eba6742f7 100644 --- a/src/main/frontend/extensions/video/youtube.cljs +++ b/src/main/frontend/extensions/video/youtube.cljs @@ -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/handler/editor.cljs b/src/main/frontend/handler/editor.cljs index 4cba866289..b3253cf34e 100644 --- a/src/main/frontend/handler/editor.cljs +++ b/src/main/frontend/handler/editor.cljs @@ -1346,7 +1346,7 @@ (defn get-asset-file-link [format url file-name image?] - (let [pdf? (and url (string/ends-with? url ".pdf"))] + (let [pdf? (and url (string/ends-with? (string/lower-case url) ".pdf"))] (case (keyword format) :markdown (util/format (str (when (or image? pdf?) "!") "[%s](%s)") file-name url) :org (if image? @@ -2867,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) @@ -2894,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/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/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) @@ -2942,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)))) @@ -2953,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/web/nfs.cljs b/src/main/frontend/handler/web/nfs.cljs index d7551be5bf..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))] @@ -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] @@ -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/intent.cljs b/src/main/frontend/mobile/intent.cljs index a6f053e18a..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")) @@ -78,10 +78,12 @@ (p/let [time (date/get-current-time) title (some-> (or title (path/basename url)) js/decodeURIComponent - util/node-path.name) + util/node-path.name + util/file-name-sanity + (string/replace "." "")) path (path/join (config/get-repo-dir (state/get-current-repo)) (config/get-pages-directory) - (path/basename url)) + (str (js/encodeURI title) (path/extname url))) _ (p/catch (.copy Filesystem (clj->js {:from url :to path})) (fn [error] 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/ui.cljs b/src/main/frontend/ui.cljs index 9c8eea6154..1c143b5a09 100644 --- a/src/main/frontend/ui.cljs +++ b/src/main/frontend/ui.cljs @@ -904,7 +904,7 @@ :style {:min-height @(::height state)}} (if visible? (when (fn? content-fn) (content-fn)) - [:div.shadow.rounded-md.p-4.w-full.mx-auto {:style {:height 64}} + [:div.shadow.rounded-md.p-4.w-full.mx-auto.fade-in.delay-1000.mb-5 {:style {:min-height 64}} [:div.animate-pulse.flex.space-x-4 [:div.flex-1.space-y-3.py-1 [:div.h-2.bg-base-4.rounded] diff --git a/src/main/frontend/util.cljc b/src/main/frontend/util.cljc index b7320b7a33..05b67aa9d4 100644 --- a/src/main/frontend/util.cljc +++ b/src/main/frontend/util.cljc @@ -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] 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/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 diff --git a/templates/tutorial-ja.md b/templates/tutorial-ja.md index 5cd34d6860..5ac00068e2 100644 --- a/templates/tutorial-ja.md +++ b/templates/tutorial-ja.md @@ -1,15 +1,15 @@ ## こんにちは、Logseq へようこそ! -- Logseq はプライバシーファーストで知識管理とコラボレーションを実現するオープンソースプラットフォームです。 +- Logseq はプライバシーファーストで知識管理とコラボレーションを実現する[オープンソース](https://github.com/logseq/logseq)プラットフォームです。 - 以下は Logseq の使い方が3分で判るチュートリアルです。ぜひやってみましょう! - 役に立つヒントがありますよ。 #+BEGIN_TIP ・ブロック(段落)を編集するにはクリックしてください。 -・新しいブロックを作成するには `Enter` キーを押してください。 +・編集中に新しいブロックを作成するには `Enter` キーを押してください。 ・ブロック内で新しい行を入力するには、`Shift+Enter` キーを押してください。 ・`/` キーを押すと全てのコマンドが表示されます。 #+END_TIP -- 1. [[見本のノートの作り方]]というページを開きましょう. 左のリンクをクリックすると開くことができます。`Shift+クリック` すると右のサイドバーで開くことができます! -クリックで開いた場合は「Linked References」と「Unlinked References」も表示されているはずです。Linked References はこのページへリンクしているページのリストです。Unlinked References はこのページのタイトルを本文中に含むページのリストです。 +- 1. [[見本のノートの作り方]]というページへ書き込んでみましょう。左のリンクをクリックすると開くことができます。`Shift+クリック` すると右のサイドバーで開くことができます! +「Linked References」と「Unlinked References」も表示されているはずです。Linked References はこのページへリンクしているページのリストです。Unlinked References はこのページのタイトルを本文中に含むページのリストです。 - 2. [[見本のノートの作り方]]上で「参照」をやってみましょう。下のブロック参照(リンク)を `Shift+クリック` して、右のサイドバーで開いてください。サイドバー側でブロックを修正すると、ブロック参照の側も同じように修正されます! - ((5f713e91-8a3c-4b04-a33a-c39482428e2d)) : これはブロック参照です。 @@ -18,14 +18,13 @@ - 3. タグは使えますか? - もちろん。これは #ダミー のタグです。 -- 4. 「ToDo」「作業中」(Doing)「完了」(Done)や優先度のようなタスク管理はサポートしていますか? +- 4. todo/doing/done(ToDo/作業中/完了)や優先度といったタスク管理はサポートしていますか? - はい。キーボードで`/`とタイプし、表示されるメニューからToDo管理のための TODO、DOING、DONE、NOW、LATER や優先度の A、B、Cという語をタイプするか選んでください。(下はその例です) - NOW [#A] "見本のノートの作り方" のチュートリアル - - LATER [#A] [:a {:href "https://twitter.com/TechWithEd" :target "_blank"} "@TechWithEd"] の作ったこちらのビデオを見てください(※ビデオは英語です。)これは Logseq でローカルフォルダを開く方法を示しています。 - - {{tutorial-video}} + - LATER [#A] [:a {:href "https://twitter.com/shuomi3" :target "_blank"} "@shuomi3"] の作ったこちらのビデオを見てください(※ビデオは英語です。)これは Logseq でノートをとって暮らしの計画を立てる方法を示しています。 + {{youtube https://www.youtube.com/watch?v=BhHfF0P9A80&ab_channel=ShuOmi}} - DONE ページ作成 - CANCELED [#C] 1000ブロック以上のページを作成する - 以上です!ここから、さらにブロックを作成したり、ローカルディレクトリを開いてノートをインポートすることができます! -- デスクトップアプリをダウンロードするならこちら: https://github.com/logseq/logseq/releases +- デスクトップアプリのダウンロードはこちらから: https://github.com/logseq/logseq/releases