Browse Source

Merge branch 'master' into disable-webview-resize

llcc 3 years ago
parent
commit
df33aba4e6

+ 33 - 25
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<SingleFileObserver> 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<SingleFileObserver>();
+            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);
                 }

+ 2 - 26
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 {

+ 53 - 10
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])
     }
     
     

+ 35 - 22
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

+ 1 - 1
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",

+ 1 - 1
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": {

+ 6 - 3
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]}}

+ 11 - 9
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: "

+ 13 - 13
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)))
+  (normalize (string/lower-case s)))

+ 3 - 9
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}]]]]
 

+ 131 - 84
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))
-
-              (when block-id (state/set-selection-start-block! block-id)))))))))
+        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))))))))))
 
 (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}

+ 1 - 1
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]])]

+ 2 - 1
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 <

+ 74 - 0
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]

+ 2 - 2
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

+ 20 - 15
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))

+ 6 - 6
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]

+ 17 - 15
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.handler.notification :as notification]
+            [frontend.mobile.util :as mobile-util]
             [frontend.state :as state]
-            [frontend.date :as date]
             [frontend.util :as util]
-            [frontend.config :as config]
-            [logseq.graph-parser.mldoc :as gp-mldoc]
+            [lambdaisland.glogi :as log]
             [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]))
+            [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]

+ 59 - 33
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?

+ 1 - 1
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]

+ 6 - 23
src/main/frontend/util.cljc

@@ -1040,29 +1040,6 @@
       (string/replace "&quot;" "\"")
       (string/replace "&apos;" "'")))
 
-#?(: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]

+ 11 - 6
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"])))))))
+               [".md" ".markdown" ".org" ".js" ".edn" ".css"])))))))

+ 1 - 1
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

+ 12 - 0
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

+ 8 - 9
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