浏览代码

Merge branch 'logseq:whiteboards' into whiteboards

sawhney17 3 年之前
父节点
当前提交
39f567b1fa
共有 67 个文件被更改,包括 1653 次插入1256 次删除
  1. 1 1
      deps.edn
  2. 2 1
      deps/graph-parser/.clj-kondo/config.edn
  3. 10 6
      deps/graph-parser/src/logseq/graph_parser.cljs
  4. 11 6
      deps/graph-parser/src/logseq/graph_parser/cli.cljs
  5. 10 8
      deps/graph-parser/src/logseq/graph_parser/extract.cljc
  6. 17 5
      deps/graph-parser/test/logseq/graph_parser/cli_test.cljs
  7. 5 6
      deps/graph-parser/test/logseq/graph_parser/extract_test.cljs
  8. 1 1
      e2e-tests/basic.spec.ts
  9. 7 7
      e2e-tests/dnd.spec.ts
  10. 0 8
      ios/App/App.xcodeproj/project.pbxproj
  11. 0 13
      ios/App/App/DownloadiCloudFiles.m
  12. 0 109
      ios/App/App/DownloadiCloudFiles.swift
  13. 20 12
      ios/App/App/FileContainer.swift
  14. 11 11
      libs/package.json
  15. 1 1
      libs/src/LSPlugin.caller.ts
  16. 3 3
      libs/src/LSPlugin.core.ts
  17. 5 0
      libs/src/LSPlugin.ts
  18. 9 2
      libs/src/LSPlugin.user.ts
  19. 1 1
      libs/src/modules/LSPlugin.Experiments.ts
  20. 145 0
      libs/src/modules/LSPlugin.Request.ts
  21. 108 220
      libs/yarn.lock
  22. 0 0
      resources/js/lsplugin.core.js
  23. 1 1
      resources/js/lsplugin.core.js.LICENSE.txt
  24. 2 1
      resources/package.json
  25. 44 0
      src/electron/electron/handler.cljs
  26. 35 20
      src/main/frontend/commands.cljs
  27. 4 2
      src/main/frontend/components/block.cljs
  28. 2 1
      src/main/frontend/components/editor.cljs
  29. 19 15
      src/main/frontend/components/encryption.cljs
  30. 11 9
      src/main/frontend/components/header.cljs
  31. 9 3
      src/main/frontend/components/header.css
  32. 415 374
      src/main/frontend/components/plugins.cljs
  33. 37 0
      src/main/frontend/components/plugins.css
  34. 4 3
      src/main/frontend/components/theme.cljs
  35. 6 0
      src/main/frontend/db.cljs
  36. 15 0
      src/main/frontend/db/model.cljs
  37. 1 0
      src/main/frontend/dicts.cljc
  38. 0 7
      src/main/frontend/extensions/code.css
  39. 1 0
      src/main/frontend/extensions/pdf/assets.cljs
  40. 8 19
      src/main/frontend/fs/capacitor_fs.cljs
  41. 2 4
      src/main/frontend/handler.cljs
  42. 18 162
      src/main/frontend/handler/editor.cljs
  43. 95 15
      src/main/frontend/handler/events.cljs
  44. 13 12
      src/main/frontend/handler/file.cljs
  45. 175 0
      src/main/frontend/handler/paste.cljs
  46. 58 13
      src/main/frontend/handler/plugin.cljs
  47. 3 1
      src/main/frontend/handler/repo.cljs
  48. 5 3
      src/main/frontend/mobile/core.cljs
  49. 7 10
      src/main/frontend/mobile/util.cljs
  50. 53 38
      src/main/frontend/modules/outliner/core.cljs
  51. 2 1
      src/main/frontend/modules/shortcut/config.cljs
  52. 3 1
      src/main/frontend/modules/shortcut/dicts.cljc
  53. 17 18
      src/main/frontend/state.cljs
  54. 13 12
      src/main/frontend/ui.cljs
  55. 16 0
      src/main/logseq/api.cljs
  56. 44 13
      src/test/frontend/modules/outliner/core_test.cljs
  57. 10 7
      tldraw/apps/tldraw-logseq/src/app.tsx
  58. 86 60
      tldraw/apps/tldraw-logseq/src/hooks/usePaste.ts
  59. 14 3
      tldraw/apps/tldraw-logseq/src/lib/shapes/LogseqPortalShape.tsx
  60. 1 0
      tldraw/demo/src/App.jsx
  61. 14 0
      tldraw/packages/core/src/lib/TLApi/TLApi.ts
  62. 11 1
      tldraw/packages/core/src/lib/TLApp/TLApp.ts
  63. 5 1
      tldraw/packages/core/src/lib/TLHistory.ts
  64. 1 0
      tldraw/packages/core/src/lib/TLPage/TLPage.ts
  65. 1 0
      tldraw/packages/core/src/lib/tools/TLSelectTool/states/PointingCanvasState.ts
  66. 5 0
      tldraw/packages/react/src/hooks/useResizeObserver.ts
  67. 0 5
      yarn.lock

+ 1 - 1
deps.edn

@@ -18,7 +18,7 @@
   cljs-http/cljs-http                   {:mvn/version "0.1.46"}
   org.babashka/sci                      {:mvn/version "0.3.2"}
   hickory/hickory                       {:git/url "https://github.com/logseq/hickory"
-                                         :sha     "9c2c2f1fc2c45efaad906e0faabc3201278deeaa"}
+                                         :sha     "86baee0319a66b7921719a5d2345e09734b23650"}
   hiccups/hiccups                       {:mvn/version "0.3.0"}
   tongue/tongue                         {:mvn/version "0.4.4"}
   org.clojure/core.async                {:mvn/version "1.3.610"}

+ 2 - 1
deps/graph-parser/.clj-kondo/config.edn

@@ -1,6 +1,7 @@
 {:linters
  {:consistent-alias
-  {:aliases {datascript.core d
+  {:aliases {clojure.string string
+             datascript.core d
              logseq.graph-parser graph-parser
              logseq.graph-parser.text text
              logseq.graph-parser.block gp-block

+ 10 - 6
deps/graph-parser/src/logseq/graph_parser.cljs

@@ -24,14 +24,15 @@
   (db-set-file-content! conn file content)
   (let [format (gp-util/get-format file)
         file-content [{:file/path file}]
-        tx (if (contains? gp-config/mldoc-support-formats format)
+        {:keys [tx ast]}
+        (if (contains? gp-config/mldoc-support-formats format)
              (let [extract-options' (merge {:block-pattern (gp-config/get-block-pattern format)
                                             :date-formatter "MMM do, yyyy"
                                             :supported-formats (gp-config/supported-formats)}
                                            extract-options
                                            {:db @conn})
-                   [pages blocks]
-                   (extract/extract-blocks-pages file content extract-options')
+                   {:keys [pages blocks ast]}
+                   (extract/extract file content extract-options')
                    delete-blocks (delete-blocks-fn (first pages) file)
                    block-ids (map (fn [block] {:block/uuid (:block/uuid block)}) blocks)
                    block-refs-ids (->> (mapcat :block/refs blocks)
@@ -44,13 +45,16 @@
                    pages (extract/with-ref-pages pages blocks)
                    pages-index (map #(select-keys % [:block/name]) pages)]
                ;; does order matter?
-               (concat file-content pages-index delete-blocks pages block-ids blocks))
-             file-content)
+               {:tx (concat file-content pages-index delete-blocks pages block-ids blocks)
+                :ast ast})
+             {:tx file-content})
         tx (concat tx [(cond-> {:file/path file}
                                new?
                                ;; TODO: use file system timestamp?
                                (assoc :file/created-at (date-time-util/time-ms)))])]
-    (d/transact! conn (gp-util/remove-nils tx) (select-keys options [:new-graph? :from-disk?]))))
+    {:tx
+     (d/transact! conn (gp-util/remove-nils tx) (select-keys options [:new-graph? :from-disk?]))
+     :ast ast}))
 
 (defn filter-files
   "Filters files in preparation for parsing. Only includes files that are

+ 11 - 6
deps/graph-parser/src/logseq/graph_parser/cli.cljs

@@ -46,8 +46,12 @@ TODO: Fail fast when process exits 1"
   [conn files {:keys [config] :as options}]
   (let [extract-options (merge {:date-formatter (gp-config/get-date-formatter config)}
                                (select-keys options [:verbose]))]
-    (doseq [{:file/keys [path content]} files]
-      (graph-parser/parse-file conn path content {:extract-options extract-options}))))
+    (mapv
+     (fn [{:file/keys [path content]}]
+       (let [{:keys [ast]}
+             (graph-parser/parse-file conn path content {:extract-options extract-options})]
+         {:file path :ast ast}))
+     files)))
 
 (defn parse-graph
   "Parses a given graph directory and returns a datascript connection and all
@@ -61,8 +65,9 @@ TODO: Fail fast when process exits 1"
   ([dir options]
    (let [files (or (:files options) (build-graph-files dir))
          conn (ldb/start-conn)
-         config (read-config dir)]
-     (when-not (:files options) (println "Parsing" (count files) "files..."))
-     (parse-files conn files (merge options {:config config}))
+         config (read-config dir)
+        _ (when-not (:files options) (println "Parsing" (count files) "files..."))
+         asts (parse-files conn files (merge options {:config config}))]
      {:conn conn
-      :files (map :file/path files)})))
+      :files (map :file/path files)
+      :asts asts})))

+ 10 - 8
deps/graph-parser/src/logseq/graph_parser/extract.cljc

@@ -138,16 +138,17 @@
     (catch :default e
       (log/error :exception e))))
 
-(defn extract-blocks-pages
+(defn extract
+  "Extracts pages, blocks and ast from given file"
   [file content {:keys [user-config verbose] :or {verbose true} :as options}]
   (if (string/blank? content)
     []
     (let [format (gp-util/get-format file)
           _ (when verbose (println "Parsing start: " file))
           ast (gp-mldoc/->edn content (gp-mldoc/default-config format
-                                                         ;; {:parse_outline_only? true}
-                                                         )
-                           user-config)]
+                                        ;; {:parse_outline_only? true}
+                                        )
+                              user-config)]
       (when verbose (println "Parsing finished: " file))
       (let [first-block (ffirst ast)
             properties (let [properties (and (gp-property/properties-ast? first-block)
@@ -165,10 +166,11 @@
                              (update properties :filters
                                      (fn [v]
                                        (string/replace (or v "") "\\" "")))
-                             properties)))]
-        (extract-pages-and-blocks
-         format ast properties
-         file content options)))))
+                             properties)))
+            [pages blocks] (extract-pages-and-blocks format ast properties file content options)]
+        {:pages pages
+         :blocks blocks
+         :ast ast}))))
 
 (defn- with-block-uuid
   [pages]

+ 17 - 5
deps/graph-parser/test/logseq/graph_parser/cli_test.cljs

@@ -1,13 +1,25 @@
 (ns logseq.graph-parser.cli-test
-  (:require [cljs.test :refer [deftest]]
+  (:require [cljs.test :refer [deftest is testing]]
             [logseq.graph-parser.cli :as gp-cli]
-            [logseq.graph-parser.test.docs-graph-helper :as docs-graph-helper]))
+            [logseq.graph-parser.test.docs-graph-helper :as docs-graph-helper]
+            [clojure.string :as string]))
 
 ;; Integration test that test parsing a large graph like docs
 (deftest ^:integration parse-graph
   (let [graph-dir "test/docs"
         _ (docs-graph-helper/clone-docs-repo-if-not-exists graph-dir)
-        {:keys [conn files]} (gp-cli/parse-graph graph-dir)
-        db @conn]
+        {:keys [conn files asts]} (gp-cli/parse-graph graph-dir)]
 
-    (docs-graph-helper/docs-graph-assertions db files)))
+    (docs-graph-helper/docs-graph-assertions @conn files)
+
+    (testing "Asts"
+      (is (seq asts) "Asts returned are non-zero")
+      (is (= files (map :file asts))
+          "There's an ast returned for every file processed")
+      (is (empty? (remove #(or
+                            (seq (:ast %))
+                            ;; logseq files don't have ast
+                            ;; could also used gp-config but API isn't public yet
+                            (string/includes? (:file %) (str graph-dir "/logseq/")))
+                          asts))
+          "Parsed files shouldn't have empty asts"))))

+ 5 - 6
deps/graph-parser/test/logseq/graph_parser/extract_test.cljs

@@ -5,16 +5,15 @@
 
 (defn- extract
   [text]
-  (let [result (extract/extract-blocks-pages "a.md" text {:block-pattern "-"})
-          result (last result)
-          lefts (map (juxt :block/parent :block/left) result)]
+  (let [{:keys [blocks]} (extract/extract "a.md" text {:block-pattern "-"})
+          lefts (map (juxt :block/parent :block/left) blocks)]
     (if (not= (count lefts) (count (distinct lefts)))
       (do
-        (pprint/pprint (map (fn [x] (select-keys x [:block/uuid :block/level :block/content :block/left])) result))
+        (pprint/pprint (map (fn [x] (select-keys x [:block/uuid :block/level :block/content :block/left])) blocks))
         (throw (js/Error. ":block/parent && :block/left conflicts")))
-      (mapv :block/content result))))
+      (mapv :block/content blocks))))
 
-(deftest test-extract-blocks-pages
+(deftest test-extract
   []
   (is (= ["a" "b" "c"]
          (extract

+ 1 - 1
e2e-tests/basic.spec.ts

@@ -223,5 +223,5 @@ test('invalid page props #3944', async ({ page, block }) => {
   await block.mustFill('public:: true\nsize:: 65535')
   await page.press('textarea >> nth=0', 'Enter')
   // Force rendering property block
-  await block.clickNext()
+  await block.enterNext()
 })

+ 7 - 7
e2e-tests/dnd.spec.ts

@@ -33,21 +33,21 @@ test('drop to left center', async ({ page }) => {
 })
 
 
-test('drop to upper left', async ({ page }) => {
+test('drop to upper left', async ({ page, block }) => {
   await createRandomPage(page)
 
-  await page.fill('textarea >> nth=0', 'block a')
-  await enterNextBlock(page)
+  await block.mustFill('block a')
+  await block.enterNext()
 
-  await page.fill('textarea >> nth=0', 'block b')
-  await page.press('textarea >> nth=0', 'Escape')
+  await block.mustFill('block b')
+  await block.escapeEditing()
 
   const bullet = page.locator('span.bullet-container >> nth=-1')
   const where = page.locator('.ls-block >> nth=0')
   await bullet.dragTo(where, {
     targetPosition: {
-      x: 30,
-      y: 5
+      x: 0,
+      y: 0
     }
   })
 

+ 0 - 8
ios/App/App.xcodeproj/project.pbxproj

@@ -14,8 +14,6 @@
 		504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; };
 		504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
 		50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
-		5FD5BB71278579F5008E6875 /* DownloadiCloudFiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FD5BB70278579F5008E6875 /* DownloadiCloudFiles.swift */; };
-		5FD5BB73278579FF008E6875 /* DownloadiCloudFiles.m in Sources */ = {isa = PBXBuildFile; fileRef = 5FD5BB72278579FF008E6875 /* DownloadiCloudFiles.m */; };
 		5FF8632A283B5ADB0047731B /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FF86329283B5ADB0047731B /* Utils.swift */; };
 		5FF8632C283B5BFD0047731B /* Utils.m in Sources */ = {isa = PBXBuildFile; fileRef = 5FF8632B283B5BFD0047731B /* Utils.m */; };
 		5FFF7D6D27E343FA00B00DA8 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FFF7D6C27E343FA00B00DA8 /* ShareViewController.swift */; };
@@ -70,8 +68,6 @@
 		504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
 		504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
 		50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
-		5FD5BB70278579F5008E6875 /* DownloadiCloudFiles.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadiCloudFiles.swift; sourceTree = "<group>"; };
-		5FD5BB72278579FF008E6875 /* DownloadiCloudFiles.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DownloadiCloudFiles.m; sourceTree = "<group>"; };
 		5FF86329283B5ADB0047731B /* Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = "<group>"; };
 		5FF8632B283B5BFD0047731B /* Utils.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Utils.m; sourceTree = "<group>"; };
 		5FFF7D6A27E343FA00B00DA8 /* ShareViewController.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareViewController.appex; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -144,8 +140,6 @@
 			children = (
 				5FF86329283B5ADB0047731B /* Utils.swift */,
 				5FF8632B283B5BFD0047731B /* Utils.m */,
-				5FD5BB72278579FF008E6875 /* DownloadiCloudFiles.m */,
-				5FD5BB70278579F5008E6875 /* DownloadiCloudFiles.swift */,
 				D32752BF2754C5AB0039291C /* AppDebug.entitlements */,
 				D32752BC275496A60039291C /* App.entitlements */,
 				50379B222058CBB4000EE86E /* capacitor.config.json */,
@@ -359,13 +353,11 @@
 			buildActionMask = 2147483647;
 			files = (
 				504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
-				5FD5BB71278579F5008E6875 /* DownloadiCloudFiles.swift in Sources */,
 				FE443F1E27FF54AA007ECE65 /* Payload.swift in Sources */,
 				5FF8632C283B5BFD0047731B /* Utils.m in Sources */,
 				FE8C946B27FD762700C8017B /* FileSync.swift in Sources */,
 				FE647FF427BDFEDE00F3206B /* FsWatcher.swift in Sources */,
 				5FF8632A283B5ADB0047731B /* Utils.swift in Sources */,
-				5FD5BB73278579FF008E6875 /* DownloadiCloudFiles.m in Sources */,
 				D3D62A0A275C92880003FBDC /* FileContainer.swift in Sources */,
 				D3D62A0C275C928F0003FBDC /* FileContainer.m in Sources */,
 				FE443F1C27FF5420007ECE65 /* Extensions.swift in Sources */,

+ 0 - 13
ios/App/App/DownloadiCloudFiles.m

@@ -1,13 +0,0 @@
-//
-//  DownloadiCloudFiles.m
-//  Logseq
-//
-//  Created by leizhe on 2021/12/29.
-//
-
-#import <Foundation/Foundation.h>
-#import <Capacitor/Capacitor.h>
-
-CAP_PLUGIN(DownloadiCloudFiles, "DownloadiCloudFiles",
-           CAP_PLUGIN_METHOD(syncGraph, CAPPluginReturnPromise);
-           )

+ 0 - 109
ios/App/App/DownloadiCloudFiles.swift

@@ -1,109 +0,0 @@
-//
-//  DownloadiCloudFiles.swift
-//  Logseq
-//
-//  Created by leizhe on 2021/12/29.
-//
-
-import Foundation
-import Capacitor
-
-@objc(DownloadiCloudFiles)
-public class DownloadiCloudFiles: CAPPlugin,  UIDocumentPickerDelegate  {
-    
-    public var _call: CAPPluginCall? = nil
-
-    let fileManager = FileManager.default
-    var filesNeededToDownload = Set<String>()
-    let extensions = [
-        "md",
-        "org",
-        "css",
-        "edn",
-        "excalidraw"
-    ]
-    
-    var containerUrl: URL? {
-        return fileManager.url(forUbiquityContainerIdentifier: nil)?.appendingPathComponent("Documents")
-    }
-    
-    var isDirectory: ObjCBool = false
-    var downloaded = false
-    
-    @objc func syncGraph(_ call: CAPPluginCall) {
-        
-        guard let graph = call.options["graph"] as? String else {
-            call.reject("Missing graph name")
-            return
-        }
- 
-        let ignores = [".git", ".trash", "bak", ".recycle"]
-        
-        if let url = self.containerUrl?.appendingPathComponent(graph) {
-            do {
-                downloaded = try self.downloadAllFilesFromCloud(at: url, ignorePattern: ignores)
-                handleDownloadFolderLoop()
-            } catch {
-                print(error.localizedDescription)
-            }
-        }
-        call.resolve(["success": downloaded])
-     }
-    
-    func appendUndownloadedFile(at url: URL){
-        var lastPathComponent = url.lastPathComponent
-        lastPathComponent.removeFirst()
-        let dirPath = url.deletingLastPathComponent().path
-        let filePath = dirPath + "/" + lastPathComponent.replacingOccurrences(of: ".icloud", with: "")
-        let neededToHandle = !extensions.allSatisfy{ !filePath.hasSuffix($0) }
-        
-        if neededToHandle {
-            filesNeededToDownload.insert(filePath)
-        }
-    }
-    
-    func downloadAllFilesFromCloud(at url: URL, ignorePattern ignores: [String] = []) throws -> Bool {
-
-        let files = try fileManager.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: [])
-
-        for file in files {
-            if file.pathExtension.lowercased() == "icloud" {
-                do {
-                    try fileManager.startDownloadingUbiquitousItem(at: file)
-                    appendUndownloadedFile(at: file)
-                } catch {
-                    print("Unexpected error: \(error).")
-                }
-                
-            } else {
-                if fileManager.fileExists(atPath: file.path, isDirectory:&isDirectory) {
-                    if isDirectory.boolValue && !ignores.contains(file.lastPathComponent) {
-                        if try downloadAllFilesFromCloud(at: file, ignorePattern: ignores) {
-                            downloaded = true
-                        }
-                    }
-                }
-            }
-        }
-        
-        return downloaded
-    }
-    
-
-    func handleDownloadFolder() {
-        for file in filesNeededToDownload {
-            if fileManager.fileExists(atPath: file) {
-                filesNeededToDownload.remove(file)
-            }
-        }
-    }
-    
-    func handleDownloadFolderLoop () {
-        while !filesNeededToDownload.isEmpty {
-            let count = filesNeededToDownload.count
-            let interval = min(Double(count) * 0.1, 2)
-            Thread.sleep(forTimeInterval: interval)
-            handleDownloadFolder()
-        }
-    }
-}

+ 20 - 12
ios/App/App/FileContainer.swift

@@ -11,17 +11,26 @@ import MobileCoreServices
 @objc(FileContainer)
 public class FileContainer: CAPPlugin, UIDocumentPickerDelegate {
 
-    public var _call: CAPPluginCall? = nil
-
-    var containerUrl: URL? {
+    var iCloudContainerUrl: URL? {
         return FileManager.default.url(forUbiquityContainerIdentifier: nil)?.appendingPathComponent("Documents")
     }
+    
+    var localContainerUrl: URL? {
+        return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
+    }
 
     @objc func ensureDocuments(_ call: CAPPluginCall) {
-        self._call = call
-
-        // check for container existence
-        if let url = self.containerUrl, !FileManager.default.fileExists(atPath: url.path, isDirectory: nil) {
+        
+        validateDocuments(at: self.iCloudContainerUrl!)
+        validateDocuments(at: self.localContainerUrl!)
+        
+        call.resolve(["path": [self.iCloudContainerUrl?.path as Any,
+                               self.localContainerUrl?.path as Any]])
+    }
+    
+    func validateDocuments(at url: URL) {
+        
+        if !FileManager.default.fileExists(atPath: url.path, isDirectory: nil) {
             do {
                 print("the url = " + url.path)
                 try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
@@ -31,20 +40,19 @@ public class FileContainer: CAPPlugin, UIDocumentPickerDelegate {
                 print(error.localizedDescription)
             }
         }
+        
 
         let str = ""
-        guard let filename = self.containerUrl?.appendingPathComponent(".logseq") else {
-            return
-        }
+        let filename = url.appendingPathComponent(".logseq", isDirectory: false)
 
         if !FileManager.default.fileExists(atPath: filename.path) {
             do {
                 try str.write(to: filename, atomically: true, encoding:  String.Encoding.utf8)
             }
             catch {
-                // failed to write file – bad permissions, bad filename, missing permissions, or more likely it can't be converted to the encoding
+                print("write .logseq failed")
+                print(error.localizedDescription)
             }
         }
-        self._call?.resolve(["path": self.containerUrl?.path as Any])
     }
 }

+ 11 - 11
libs/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@logseq/libs",
-  "version": "0.0.6",
+  "version": "0.0.7",
   "description": "Logseq SDK libraries",
   "main": "dist/lsplugin.user.js",
   "typings": "index.d.ts",
@@ -15,9 +15,9 @@
     "fix": "prettier --write \"src/**/*.{ts, js}\""
   },
   "dependencies": {
-    "csstype": "3.0.8",
-    "debug": "4.3.1",
-    "dompurify": "2.3.1",
+    "csstype": "3.1.0",
+    "debug": "4.3.4",
+    "dompurify": "2.3.8",
     "eventemitter3": "4.0.7",
     "fast-deep-equal": "3.1.3",
     "lodash-es": "4.17.21",
@@ -26,14 +26,14 @@
   },
   "devDependencies": {
     "@types/debug": "^4.1.5",
-    "@types/dompurify": "^2.2.1",
-    "@types/lodash-es": "^4.17.4",
+    "@types/dompurify": "2.3.3",
+    "@types/lodash-es": "4.17.6",
     "prettier": "^2.6.2",
     "prettier-config-standard": "^5.0.0",
-    "ts-loader": "^8.0.17",
-    "typescript": "^4.2.2",
-    "webpack": "^5.24.3",
-    "webpack-bundle-analyzer": "^4.4.0",
-    "webpack-cli": "^4.5.0"
+    "ts-loader": "9.3.0",
+    "typescript": "4.7.3",
+    "webpack": "5.73.0",
+    "webpack-bundle-analyzer": "4.5.0",
+    "webpack-cli": "4.9.2"
   }
 }

+ 1 - 1
libs/src/LSPlugin.caller.ts

@@ -281,7 +281,7 @@ class LSPluginCaller extends EventEmitter {
           })
 
           this._call = async (...args: any) => {
-            // parent all will get message before handshaked
+            // parent all will get message before handshake
             await refChild.call(LSPMSGFn(pl.id), {
               type: args[0],
               payload: Object.assign(args[1] || {}, {

+ 3 - 3
libs/src/LSPlugin.core.ts

@@ -1126,7 +1126,7 @@ class LSPluginCore
     | 'registered'
     | 'error'
     | 'unregistered'
-    | 'theme-changed'
+    | 'themes-changed'
     | 'theme-selected'
     | 'reset-custom-theme'
     | 'settings-changed'
@@ -1510,7 +1510,7 @@ class LSPluginCore
     }
 
     themes.push(opt)
-    this.emit('theme-changed', this.themes, { id, ...opt })
+    this.emit('themes-changed', this.themes, { id, ...opt })
   }
 
   async selectTheme(
@@ -1571,7 +1571,7 @@ class LSPluginCore
     }
 
     this._registeredThemes.delete(id)
-    this.emit('theme-changed', this.themes, { id })
+    this.emit('themes-changed', this.themes, { id })
     if (effect && this._currentTheme?.pid === id) {
       this._currentTheme.eject()
       this._currentTheme = null

+ 5 - 0
libs/src/LSPlugin.ts

@@ -4,6 +4,9 @@ import EventEmitter from 'eventemitter3'
 import { LSPluginCaller } from './LSPlugin.caller'
 import { LSPluginExperiments } from './modules/LSPlugin.Experiments'
 import { LSPluginFileStorage } from './modules/LSPlugin.Storage'
+import { LSPluginRequest } from './modules/LSPlugin.Request'
+
+export type WithOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
 
 export type PluginLocalIdentity = string
 
@@ -407,6 +410,7 @@ export interface IAppProxy {
   // hook events
   onCurrentGraphChanged: IUserHook
   onThemeModeChanged: IUserHook<{ mode: 'dark' | 'light' }>
+  onThemeChanged: IUserHook<Partial<{name: string, mode: string, pid: string, url: string}>>
   onBlockRendererSlotted: IUserSlotHook<{ uuid: BlockUUID }>
 
   /**
@@ -948,6 +952,7 @@ export interface ILSPluginUser extends EventEmitter<LSPluginUserEvents> {
   Git: IGitProxy
   UI: IUIProxy
 
+  Request: LSPluginRequest
   FileStorage: LSPluginFileStorage
   Experiments: LSPluginExperiments
 }

+ 9 - 2
libs/src/LSPlugin.user.ts

@@ -39,6 +39,7 @@ import * as CSS from 'csstype'
 import EventEmitter from 'eventemitter3'
 import { LSPluginFileStorage } from './modules/LSPlugin.Storage'
 import { LSPluginExperiments } from './modules/LSPlugin.Experiments'
+import { LSPluginRequest } from './modules/LSPlugin.Request'
 
 declare global {
   interface Window {
@@ -318,8 +319,7 @@ const KEY_MAIN_UI = 0
  */
 export class LSPluginUser
   extends EventEmitter<LSPluginUserEvents>
-  implements ILSPluginUser
-{
+  implements ILSPluginUser {
   // @ts-ignore
   private _version: string = LIB_VERSION
   private _debugTag: string = ''
@@ -333,6 +333,7 @@ export class LSPluginUser
   private _ui = new Map<number, uiState>()
 
   private _mFileStorage: LSPluginFileStorage
+  private _mRequest: LSPluginRequest
   private _mExperiments: LSPluginExperiments
 
   /**
@@ -673,6 +674,12 @@ export class LSPluginUser
     return m
   }
 
+  get Request(): LSPluginRequest {
+    let m = this._mRequest
+    if (!m) m = this._mRequest = new LSPluginRequest(this)
+    return m
+  }
+
   get Experiments(): LSPluginExperiments {
     let m = this._mExperiments
     if (!m) m = this._mExperiments = new LSPluginExperiments(this)

+ 1 - 1
libs/src/modules/LSPlugin.Experiments.ts

@@ -24,7 +24,7 @@ export class LSPluginExperiments {
     )
   }
 
-  private invokeExperMethod(type: string, ...args: Array<any>) {
+  public invokeExperMethod(type: string, ...args: Array<any>) {
     const host = this.ensureHostScope()
     type = safeSnakeCase(type)?.toLowerCase()
     return host.logseq.api['exper_' + type]?.apply(host, args)

+ 145 - 0
libs/src/modules/LSPlugin.Request.ts

@@ -0,0 +1,145 @@
+import { LSPluginUser, WithOptional } from '../LSPlugin.user'
+import { EventEmitter } from 'eventemitter3'
+
+export type IRequestOptions<R = any> = {
+  url: string
+  abortable: boolean
+  headers: Record<string, string>
+  method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
+  data: Object | ArrayBuffer
+  timeout: number
+  returnType: 'json' | 'text' | 'base64' | 'arraybuffer'
+  success: (result: R) => void
+  fail: (err: any) => void
+  final: () => void
+}
+
+export type RequestTaskID = string | number
+
+const CLIENT_MSG_CALLBACK = '#lsp#request#callback'
+const genTaskCallbackType = (id: RequestTaskID) => `task_callback_${id}`
+
+/**
+ * Request task
+ */
+export class LSPluginRequestTask<R = any> {
+  private readonly _promise: Promise<R>
+  private _aborted: boolean = false
+
+  constructor(
+    private _client: LSPluginRequest,
+    private _requestId: RequestTaskID,
+    private _requestOptions: Partial<IRequestOptions> = {}
+  ) {
+
+    this._promise = new Promise<any>((resolve, reject) => {
+      if (!this._requestId) {
+        return reject(null)
+      }
+
+      // task result listener
+      this._client.once(
+        genTaskCallbackType(this._requestId),
+        (e) => {
+          if (e && e instanceof Error) {
+            reject(e)
+          } else {
+            resolve(e)
+          }
+        }
+      )
+    })
+
+    const { success, fail, final } = this._requestOptions
+
+    this._promise
+      .then((res) => {
+        success?.(res)
+      })
+      .catch((e) => {
+        fail?.(e)
+      })
+      .finally(() => {
+        final?.()
+      })
+  }
+
+  abort() {
+    if (
+      !this._requestOptions.abortable ||
+      this._aborted
+    ) return
+
+    this._client.ctx._execCallableAPI(
+      'http_request_abort',
+      this._requestId
+    )
+
+    this._aborted = true
+  }
+
+  get promise(): Promise<R> {
+    return this._promise
+  }
+
+  get client(): LSPluginRequest {
+    return this._client
+  }
+
+  get requestId(): RequestTaskID {
+    return this._requestId
+  }
+}
+
+/**
+ * A simple request client
+ */
+export class LSPluginRequest extends EventEmitter {
+  constructor(private _ctx: LSPluginUser) {
+    super()
+
+    // request callback listener
+    this.ctx.caller.on(
+      CLIENT_MSG_CALLBACK,
+      (e: any) => {
+        const reqId = e?.requestId
+        if (!reqId) return
+
+        this.emit(genTaskCallbackType(reqId), e?.payload)
+      }
+    )
+  }
+
+  static createRequestTask(
+    client: LSPluginRequest,
+    requestID: RequestTaskID,
+    requestOptions: Partial<IRequestOptions>
+  ) {
+    return new LSPluginRequestTask(
+      client, requestID, requestOptions
+    )
+  }
+
+  async _request<R extends {},
+    T extends WithOptional<IRequestOptions<R>, keyof Omit<IRequestOptions, 'url'>>>(options: T):
+    Promise<T extends Pick<IRequestOptions, 'abortable'> ? LSPluginRequestTask<R> : R> {
+    const pid = this.ctx.baseInfo.id
+    const { success, fail, final, ...requestOptions } = options
+    const reqID = this.ctx.Experiments.invokeExperMethod('request', pid, requestOptions)
+
+    const task = LSPluginRequest.createRequestTask(
+      this.ctx.Request,
+      reqID, options
+    )
+
+    if (!requestOptions.abortable) {
+      return task.promise
+    }
+
+    return task as any
+  }
+
+  get ctx(): LSPluginUser {
+    return this._ctx
+  }
+}

+ 108 - 220
libs/yarn.lock

@@ -19,17 +19,17 @@
   dependencies:
     "@types/ms" "*"
 
-"@types/dompurify@^2.2.1":
-  version "2.2.3"
-  resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-2.2.3.tgz#6e89677a07902ac1b6821c345f34bd85da239b08"
-  integrity sha512-CLtc2mZK8+axmrz1JqtpklO/Kvn38arGc8o1l3UVopZaXXuer9ONdZwJ/9f226GrhRLtUmLr9WrvZsRSNpS8og==
+"@types/dompurify@2.3.3":
+  version "2.3.3"
+  resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-2.3.3.tgz#c24c92f698f77ed9cc9d9fa7888f90cf2bfaa23f"
+  integrity sha512-nnVQSgRVuZ/843oAfhA25eRSNzUFcBPk/LOiw5gm8mD9/X7CNcbRkQu/OsjCewO8+VIYfPxUnXvPEVGenw14+w==
   dependencies:
     "@types/trusted-types" "*"
 
-"@types/eslint-scope@^3.7.0":
-  version "3.7.1"
-  resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.1.tgz#8dc390a7b4f9dd9f1284629efce982e41612116e"
-  integrity sha512-SCFeogqiptms4Fg29WpOTk5nHIzfpKCemSN63ksBQYKTcXoJEmJagV+DhVmbapZzY4/5YaOV1nZwrsU79fFm1g==
+"@types/eslint-scope@^3.7.3":
+  version "3.7.3"
+  resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.3.tgz#125b88504b61e3c8bc6f870882003253005c3224"
+  integrity sha512-PB3ldyrcnAicT35TWPs5IcwKD8S333HMaa2VVv4+wdvebJkjWuW/xESoB8IwRcog8HYVYamb1g/R31Qv5Bx03g==
   dependencies:
     "@types/eslint" "*"
     "@types/estree" "*"
@@ -42,20 +42,25 @@
     "@types/estree" "*"
     "@types/json-schema" "*"
 
-"@types/estree@*", "@types/estree@^0.0.50":
+"@types/estree@*":
   version "0.0.50"
   resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.50.tgz#1e0caa9364d3fccd2931c3ed96fdbeaa5d4cca83"
   integrity sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==
 
+"@types/estree@^0.0.51":
+  version "0.0.51"
+  resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40"
+  integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==
+
 "@types/json-schema@*", "@types/json-schema@^7.0.8":
   version "7.0.9"
   resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d"
   integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==
 
-"@types/lodash-es@^4.17.4":
-  version "4.17.4"
-  resolved "https://registry.yarnpkg.com/@types/lodash-es/-/lodash-es-4.17.4.tgz#b2e440d2bf8a93584a9fd798452ec497986c9b97"
-  integrity sha512-BBz79DCJbD2CVYZH67MBeHZRX++HF+5p8Mo5MzjZi64Wac39S3diedJYHZtScbRVf4DjZyN6LzA0SB0zy+HSSQ==
+"@types/lodash-es@4.17.6":
+  version "4.17.6"
+  resolved "https://registry.yarnpkg.com/@types/lodash-es/-/lodash-es-4.17.6.tgz#c2ed4c8320ffa6f11b43eb89e9eaeec65966a0a0"
+  integrity sha512-R+zTeVUKDdfoRxpAryaQNRKk3105Rrgx2CFRClIgRGaqDTdjsm8h6IYA8ir584W3ePzkZfst5xIgDwYrlh9HLg==
   dependencies:
     "@types/lodash" "*"
 
@@ -200,22 +205,22 @@
     "@webassemblyjs/ast" "1.11.1"
     "@xtuc/long" "4.2.2"
 
-"@webpack-cli/configtest@^1.0.4":
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-1.0.4.tgz#f03ce6311c0883a83d04569e2c03c6238316d2aa"
-  integrity sha512-cs3XLy+UcxiP6bj0A6u7MLLuwdXJ1c3Dtc0RkKg+wiI1g/Ti1om8+/2hc2A2B60NbBNAbMgyBMHvyymWm/j4wQ==
+"@webpack-cli/configtest@^1.1.1":
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-1.1.1.tgz#9f53b1b7946a6efc2a749095a4f450e2932e8356"
+  integrity sha512-1FBc1f9G4P/AxMqIgfZgeOTuRnwZMten8E7zap5zgpPInnCrP8D4Q81+4CWIch8i/Nf7nXjP0v6CjjbHOrXhKg==
 
-"@webpack-cli/info@^1.3.0":
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-1.3.0.tgz#9d78a31101a960997a4acd41ffd9b9300627fe2b"
-  integrity sha512-ASiVB3t9LOKHs5DyVUcxpraBXDOKubYu/ihHhU+t1UPpxsivg6Od2E2qU4gJCekfEddzRBzHhzA/Acyw/mlK/w==
+"@webpack-cli/info@^1.4.1":
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-1.4.1.tgz#2360ea1710cbbb97ff156a3f0f24556e0fc1ebea"
+  integrity sha512-PKVGmazEq3oAo46Q63tpMr4HipI3OPfP7LiNOEJg963RMgT0rqheag28NCML0o3GIzA3DmxP1ZIAv9oTX1CUIA==
   dependencies:
     envinfo "^7.7.3"
 
-"@webpack-cli/serve@^1.5.2":
-  version "1.5.2"
-  resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.5.2.tgz#ea584b637ff63c5a477f6f21604b5a205b72c9ec"
-  integrity sha512-vgJ5OLWadI8aKjDlOH3rb+dYyPd2GTZuQC/Tihjct6F9GpXGZINo3Y/IVuZVTM1eDQB+/AOsjPUWH/WySDaXvw==
+"@webpack-cli/serve@^1.6.1":
+  version "1.6.1"
+  resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.6.1.tgz#0de2875ac31b46b6c5bb1ae0a7d7f0ba5678dffe"
+  integrity sha512-gNGTiTrjEVQ0OcVnzsRSqTxaBSr+dmTfm+qJsCDluky8uhdLWep7Gcr62QsAKHTMxjCS/8nEITsmFAhfIx+QSw==
 
 "@xtuc/ieee754@^1.2.0":
   version "1.2.0"
@@ -264,11 +269,6 @@ ansi-styles@^4.1.0:
   dependencies:
     color-convert "^2.0.1"
 
-big.js@^5.2.2:
-  version "5.2.2"
-  resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
-  integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==
-
 braces@^3.0.1:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
@@ -331,31 +331,26 @@ color-name@~1.1.4:
   resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
   integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
 
-colorette@^1.2.1, colorette@^1.3.0:
+colorette@^1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.3.0.tgz#ff45d2f0edb244069d3b772adeb04fed38d0a0af"
   integrity sha512-ecORCqbSFP7Wm8Y6lyqMJjexBQqXSF7SSeaTyGGphogUjBlFP9m9o08wy86HL2uB7fMTxtOUzLMk7ogKcxMg1w==
 
+colorette@^2.0.14:
+  version "2.0.17"
+  resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.17.tgz#5dd4c0d15e2984b7433cb4a9f2ead45063b80c47"
+  integrity sha512-hJo+3Bkn0NCHybn9Tu35fIeoOKGOk5OCC32y4Hz2It+qlCO2Q3DeQ1hRn/tDDMQKRYUEzqsl7jbF6dYKjlE60g==
+
 commander@^2.20.0:
   version "2.20.3"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
   integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
 
-commander@^6.2.0:
-  version "6.2.1"
-  resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
-  integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
-
-commander@^7.0.0:
+commander@^7.0.0, commander@^7.2.0:
   version "7.2.0"
   resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
   integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
 
-core-util-is@~1.0.0:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
-  integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
-
 cross-spawn@^7.0.3:
   version "7.0.3"
   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
@@ -365,22 +360,22 @@ cross-spawn@^7.0.3:
     shebang-command "^2.0.0"
     which "^2.0.1"
 
-csstype@3.0.8:
-  version "3.0.8"
-  resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.8.tgz#d2266a792729fb227cd216fb572f43728e1ad340"
-  integrity sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==
+csstype@3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.0.tgz#4ddcac3718d787cf9df0d1b7d15033925c8f29f2"
+  integrity sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==
 
[email protected].1:
-  version "4.3.1"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
-  integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
[email protected].4:
+  version "4.3.4"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
+  integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
   dependencies:
     ms "2.1.2"
 
[email protected].1:
-  version "2.3.1"
-  resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.3.1.tgz#a47059ca21fd1212d3c8f71fdea6943b8bfbdf6a"
-  integrity sha512-xGWt+NHAQS+4tpgbOAI08yxW0Pr256Gu/FNE2frZVTbgrBUn8M7tz7/ktS/LZ2MHeGqz6topj0/xY+y8R5FBFw==
[email protected].8:
+  version "2.3.8"
+  resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.3.8.tgz#224fe9ae57d7ebd9a1ae1ac18c1c1ca3f532226f"
+  integrity sha512-eVhaWoVibIzqdGYjwsBWodIQIaXFSB+cKDf4cfxLMsK0xiud6SE+/WCVx/Xw/UwQsa4cS3T2eITcdtmTg2UKcw==
 
 dot-case@^3.0.4:
   version "3.0.4"
@@ -400,24 +395,10 @@ electron-to-chromium@^1.3.811:
   resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.814.tgz#418fad80c3276a46103ca72a21a8290620d83c4a"
   integrity sha512-0mH03cyjh6OzMlmjauGg0TLd87ErIJqWiYxMcOLKf5w6p0YEOl7DJAj7BDlXEFmCguY5CQaKVOiMjAMODO2XDw==
 
-emojis-list@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
-  integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==
-
-enhanced-resolve@^4.0.0:
-  version "4.5.0"
-  resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz#2f3cfd84dbe3b487f18f2db2ef1e064a571ca5ec"
-  integrity sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg==
-  dependencies:
-    graceful-fs "^4.1.2"
-    memory-fs "^0.5.0"
-    tapable "^1.0.0"
-
-enhanced-resolve@^5.8.0:
-  version "5.8.2"
-  resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.8.2.tgz#15ddc779345cbb73e97c611cd00c01c1e7bf4d8b"
-  integrity sha512-F27oB3WuHDzvR2DOGNTaYy0D5o0cnrv8TeI482VM4kYgQd/FT9lUQwuNsJ0oOHtBUq7eiW5ytqzp7nBFknL+GA==
+enhanced-resolve@^5.0.0, enhanced-resolve@^5.9.3:
+  version "5.9.3"
+  resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.9.3.tgz#44a342c012cbc473254af5cc6ae20ebd0aae5d88"
+  integrity sha512-Bq9VSor+kjvW3f9/MiiR4eE3XYgOl7/rS8lnSxbRbF3kS0B2r+Y9w5krBWxZgDxASVZbdYrn5wT4j/Wb0J9qow==
   dependencies:
     graceful-fs "^4.2.4"
     tapable "^2.2.0"
@@ -427,17 +408,10 @@ envinfo@^7.7.3:
   resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475"
   integrity sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==
 
-errno@^0.1.3:
-  version "0.1.8"
-  resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f"
-  integrity sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==
-  dependencies:
-    prr "~1.0.1"
-
-es-module-lexer@^0.7.1:
-  version "0.7.1"
-  resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.7.1.tgz#c2c8e0f46f2df06274cdaf0dd3f3b33e0a0b267d"
-  integrity sha512-MgtWFl5No+4S3TmhDmCz2ObFGm6lEpTnzbQi+Dd+pw4mlTIZTmM2iAs5gRlmx5zS9luzobCSBSI90JM/1/JgOw==
+es-module-lexer@^0.9.0:
+  version "0.9.3"
+  resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19"
+  integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==
 
 escalade@^3.1.1:
   version "3.1.1"
@@ -544,6 +518,11 @@ graceful-fs@^4.1.2, graceful-fs@^4.2.4:
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a"
   integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==
 
+graceful-fs@^4.2.9:
+  version "4.2.10"
+  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
+  integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==
+
 gzip-size@^6.0.0:
   version "6.0.0"
   resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-6.0.0.tgz#065367fd50c239c0671cbcbad5be3e2eeb10e462"
@@ -581,11 +560,6 @@ [email protected]:
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
   integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
 
-inherits@~2.0.3:
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
-  integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
-
 interpret@^2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9"
@@ -615,11 +589,6 @@ is-stream@^2.0.0:
   resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
   integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==
 
-isarray@~1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
-  integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
-
 isexe@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
@@ -639,23 +608,16 @@ jest-worker@^27.0.2:
     merge-stream "^2.0.0"
     supports-color "^8.0.0"
 
-json-parse-better-errors@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
-  integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
+json-parse-even-better-errors@^2.3.1:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
+  integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
 
 json-schema-traverse@^0.4.1:
   version "0.4.1"
   resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
   integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
 
-json5@^2.1.2:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3"
-  integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==
-  dependencies:
-    minimist "^1.2.5"
-
 kind-of@^6.0.2:
   version "6.0.3"
   resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
@@ -666,15 +628,6 @@ loader-runner@^4.2.0:
   resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.2.0.tgz#d7022380d66d14c5fb1d496b89864ebcfd478384"
   integrity sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==
 
-loader-utils@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.0.tgz#e4cace5b816d425a166b5f097e10cd12b36064b0"
-  integrity sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==
-  dependencies:
-    big.js "^5.2.2"
-    emojis-list "^3.0.0"
-    json5 "^2.1.2"
-
 locate-path@^5.0.0:
   version "5.0.0"
   resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
@@ -706,14 +659,6 @@ lru-cache@^6.0.0:
   dependencies:
     yallist "^4.0.0"
 
-memory-fs@^0.5.0:
-  version "0.5.0"
-  resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.5.0.tgz#324c01288b88652966d161db77838720845a8e3c"
-  integrity sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==
-  dependencies:
-    errno "^0.1.3"
-    readable-stream "^2.0.1"
-
 merge-stream@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
@@ -749,11 +694,6 @@ mimic-fn@^2.1.0:
   resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
   integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
 
-minimist@^1.2.5:
-  version "1.2.6"
-  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
-  integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
-
 [email protected]:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
@@ -867,21 +807,11 @@ prettier@^2.6.2:
   resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.6.2.tgz#e26d71a18a74c3d0f0597f55f01fb6c06c206032"
   integrity sha512-PkUpF+qoXTqhOeWL9fu7As8LXsIUZ1WYaJiY/a7McAQzxjk82OF0tibkFXVCDImZtWxbvojFjerkiLb0/q8mew==
 
-process-nextick-args@~2.0.0:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
-  integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
-
 process@^0.11.1:
   version "0.11.10"
   resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
   integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI=
 
-prr@~1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
-  integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY=
-
 punycode@^2.1.0:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
@@ -894,19 +824,6 @@ randombytes@^2.1.0:
   dependencies:
     safe-buffer "^5.1.0"
 
-readable-stream@^2.0.1:
-  version "2.3.7"
-  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
-  integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
-  dependencies:
-    core-util-is "~1.0.0"
-    inherits "~2.0.3"
-    isarray "~1.0.0"
-    process-nextick-args "~2.0.0"
-    safe-buffer "~5.1.1"
-    string_decoder "~1.1.1"
-    util-deprecate "~1.0.1"
-
 rechoir@^0.7.0:
   version "0.7.1"
   resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.7.1.tgz#9478a96a1ca135b5e88fc027f03ee92d6c645686"
@@ -939,11 +856,6 @@ safe-buffer@^5.1.0:
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
   integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
 
-safe-buffer@~5.1.0, safe-buffer@~5.1.1:
-  version "5.1.2"
-  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
-  integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
-
 schema-utils@^3.0.0, schema-utils@^3.1.0:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281"
@@ -1026,13 +938,6 @@ source-map@~0.7.2:
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
   integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
 
-string_decoder@~1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
-  integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
-  dependencies:
-    safe-buffer "~5.1.0"
-
 strip-final-newline@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
@@ -1052,11 +957,6 @@ supports-color@^8.0.0:
   dependencies:
     has-flag "^4.0.0"
 
-tapable@^1.0.0:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2"
-  integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==
-
 tapable@^2.1.1, tapable@^2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.0.tgz#5c373d281d9c672848213d0e037d1c4165ab426b"
@@ -1095,14 +995,13 @@ totalist@^1.0.0:
   resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df"
   integrity sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==
 
-ts-loader@^8.0.17:
-  version "8.3.0"
-  resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-8.3.0.tgz#83360496d6f8004fab35825279132c93412edf33"
-  integrity sha512-MgGly4I6cStsJy27ViE32UoqxPTN9Xly4anxxVyaIWR+9BGxboV4EyJBGfR3RePV7Ksjj3rHmPZJeIt+7o4Vag==
+ts-loader@9.3.0:
+  version "9.3.0"
+  resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.3.0.tgz#980f4dbfb60e517179e15e10ed98e454b132159f"
+  integrity sha512-2kLLAdAD+FCKijvGKi9sS0OzoqxLCF3CxHpok7rVgCZ5UldRzH0TkbwG9XECKjBzHsAewntC5oDaI/FwKzEUog==
   dependencies:
     chalk "^4.1.0"
-    enhanced-resolve "^4.0.0"
-    loader-utils "^2.0.0"
+    enhanced-resolve "^5.0.0"
     micromatch "^4.0.0"
     semver "^7.3.4"
 
@@ -1111,10 +1010,10 @@ tslib@^2.0.3:
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
   integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
 
-typescript@^4.2.2:
-  version "4.3.5"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4"
-  integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==
+typescript@4.7.3:
+  version "4.7.3"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.3.tgz#8364b502d5257b540f9de4c40be84c98e23a129d"
+  integrity sha512-WOkT3XYvrpXx4vMMqlD+8R8R37fZkjyLGlxavMc4iB8lrl8L0DeTcHbYgw/v0N/z9wAFsgBhcsF0ruoySS22mA==
 
 uri-js@^4.2.2:
   version "4.4.1"
@@ -1123,11 +1022,6 @@ uri-js@^4.2.2:
   dependencies:
     punycode "^2.1.0"
 
-util-deprecate@~1.0.1:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
-  integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
-
 util@^0.10.3:
   version "0.10.4"
   resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901"
@@ -1135,51 +1029,45 @@ util@^0.10.3:
   dependencies:
     inherits "2.0.3"
 
-v8-compile-cache@^2.2.0:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
-  integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==
-
-watchpack@^2.2.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.2.0.tgz#47d78f5415fe550ecd740f99fe2882323a58b1ce"
-  integrity sha512-up4YAn/XHgZHIxFBVCdlMiWDj6WaLKpwVeGQk2I5thdYxF/KmF0aaz6TfJZ/hfl1h/XlcDr7k1KH7ThDagpFaA==
+watchpack@^2.3.1:
+  version "2.4.0"
+  resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"
+  integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==
   dependencies:
     glob-to-regexp "^0.4.1"
     graceful-fs "^4.1.2"
 
-webpack-bundle-analyzer@^4.4.0:
-  version "4.4.2"
-  resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.4.2.tgz#39898cf6200178240910d629705f0f3493f7d666"
-  integrity sha512-PIagMYhlEzFfhMYOzs5gFT55DkUdkyrJi/SxJp8EF3YMWhS+T9vvs2EoTetpk5qb6VsCq02eXTlRDOydRhDFAQ==
[email protected]:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.5.0.tgz#1b0eea2947e73528754a6f9af3e91b2b6e0f79d5"
+  integrity sha512-GUMZlM3SKwS8Z+CKeIFx7CVoHn3dXFcUAjT/dcZQQmfSZGvitPfMob2ipjai7ovFFqPvTqkEZ/leL4O0YOdAYQ==
   dependencies:
     acorn "^8.0.4"
     acorn-walk "^8.0.0"
     chalk "^4.1.0"
-    commander "^6.2.0"
+    commander "^7.2.0"
     gzip-size "^6.0.0"
     lodash "^4.17.20"
     opener "^1.5.2"
     sirv "^1.0.7"
     ws "^7.3.1"
 
-webpack-cli@^4.5.0:
-  version "4.8.0"
-  resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-4.8.0.tgz#5fc3c8b9401d3c8a43e2afceacfa8261962338d1"
-  integrity sha512-+iBSWsX16uVna5aAYN6/wjhJy1q/GKk4KjKvfg90/6hykCTSgozbfz5iRgDTSJt/LgSbYxdBX3KBHeobIs+ZEw==
+webpack-cli@4.9.2:
+  version "4.9.2"
+  resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-4.9.2.tgz#77c1adaea020c3f9e2db8aad8ea78d235c83659d"
+  integrity sha512-m3/AACnBBzK/kMTcxWHcZFPrw/eQuY4Df1TxvIWfWM2x7mRqBQCqKEd96oCUa9jkapLBaFfRce33eGDb4Pr7YQ==
   dependencies:
     "@discoveryjs/json-ext" "^0.5.0"
-    "@webpack-cli/configtest" "^1.0.4"
-    "@webpack-cli/info" "^1.3.0"
-    "@webpack-cli/serve" "^1.5.2"
-    colorette "^1.2.1"
+    "@webpack-cli/configtest" "^1.1.1"
+    "@webpack-cli/info" "^1.4.1"
+    "@webpack-cli/serve" "^1.6.1"
+    colorette "^2.0.14"
     commander "^7.0.0"
     execa "^5.0.0"
     fastest-levenshtein "^1.0.12"
     import-local "^3.0.2"
     interpret "^2.2.0"
     rechoir "^0.7.0"
-    v8-compile-cache "^2.2.0"
     webpack-merge "^5.7.3"
 
 webpack-merge@^5.7.3:
@@ -1190,18 +1078,18 @@ webpack-merge@^5.7.3:
     clone-deep "^4.0.1"
     wildcard "^2.0.0"
 
-webpack-sources@^3.2.0:
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.0.tgz#b16973bcf844ebcdb3afde32eda1c04d0b90f89d"
-  integrity sha512-fahN08Et7P9trej8xz/Z7eRu8ltyiygEo/hnRi9KqBUs80KeDcnf96ZJo++ewWd84fEf3xSX9bp4ZS9hbw0OBw==
+webpack-sources@^3.2.3:
+  version "3.2.3"
+  resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
+  integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
 
-webpack@^5.24.3:
-  version "5.51.1"
-  resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.51.1.tgz#41bebf38dccab9a89487b16dbe95c22e147aac57"
-  integrity sha512-xsn3lwqEKoFvqn4JQggPSRxE4dhsRcysWTqYABAZlmavcoTmwlOb9b1N36Inbt/eIispSkuHa80/FJkDTPos1A==
+webpack@5.73.0:
+  version "5.73.0"
+  resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.73.0.tgz#bbd17738f8a53ee5760ea2f59dce7f3431d35d38"
+  integrity sha512-svjudQRPPa0YiOYa2lM/Gacw0r6PvxptHj4FuEKQ2kX05ZLkjbVc5MnPs6its5j7IZljnIqSVo/OsY2X0IpHGA==
   dependencies:
-    "@types/eslint-scope" "^3.7.0"
-    "@types/estree" "^0.0.50"
+    "@types/eslint-scope" "^3.7.3"
+    "@types/estree" "^0.0.51"
     "@webassemblyjs/ast" "1.11.1"
     "@webassemblyjs/wasm-edit" "1.11.1"
     "@webassemblyjs/wasm-parser" "1.11.1"
@@ -1209,21 +1097,21 @@ webpack@^5.24.3:
     acorn-import-assertions "^1.7.6"
     browserslist "^4.14.5"
     chrome-trace-event "^1.0.2"
-    enhanced-resolve "^5.8.0"
-    es-module-lexer "^0.7.1"
+    enhanced-resolve "^5.9.3"
+    es-module-lexer "^0.9.0"
     eslint-scope "5.1.1"
     events "^3.2.0"
     glob-to-regexp "^0.4.1"
-    graceful-fs "^4.2.4"
-    json-parse-better-errors "^1.0.2"
+    graceful-fs "^4.2.9"
+    json-parse-even-better-errors "^2.3.1"
     loader-runner "^4.2.0"
     mime-types "^2.1.27"
     neo-async "^2.6.2"
     schema-utils "^3.1.0"
     tapable "^2.1.1"
     terser-webpack-plugin "^5.1.3"
-    watchpack "^2.2.0"
-    webpack-sources "^3.2.0"
+    watchpack "^2.3.1"
+    webpack-sources "^3.2.3"
 
 which@^2.0.1:
   version "2.0.2"

文件差异内容过多而无法显示
+ 0 - 0
resources/js/lsplugin.core.js


+ 1 - 1
resources/js/lsplugin.core.js.LICENSE.txt

@@ -1 +1 @@
-/*! @license DOMPurify 2.3.1 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/2.3.1/LICENSE */
+/*! @license DOMPurify 2.3.8 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/2.3.8/LICENSE */

+ 2 - 1
resources/package.json

@@ -37,7 +37,8 @@
     "@sentry/electron": "2.5.1",
     "posthog-js": "1.10.2",
     "@logseq/rsapi": "0.0.20",
-    "electron-deeplink": "1.0.10"
+    "electron-deeplink": "1.0.10",
+    "abort-controller": "3.0.0"
   },
   "devDependencies": {
     "@electron-forge/cli": "^6.0.0-beta.57",

+ 44 - 0
src/electron/electron/handler.cljs

@@ -8,6 +8,7 @@
             ["os" :as os]
             ["diff-match-patch" :as google-diff]
             ["/electron/utils" :as js-utils]
+            ["abort-controller" :as AbortController]
             [electron.fs-watcher :as watcher]
             [electron.configs :as cfgs]
             [promesa.core :as p]
@@ -368,6 +369,49 @@
 (defmethod handle :uninstallMarketPlugin [_ [_ id]]
   (plugin/uninstall! id))
 
+(def *request-abort-signals (atom {}))
+
+(defmethod handle :httpRequest [_ [_ _req-id opts]]
+  (let [{:keys [url abortable method data returnType headers]} opts]
+    (when-let [[method type] (and (not (string/blank? url))
+                                  [(keyword (string/upper-case (or method "GET")))
+                                   (keyword (string/lower-case (or returnType "json")))])]
+      (-> (utils/fetch url
+                       (-> {:method  method
+                            :headers (and headers (bean/->js headers))}
+                           (merge (when (and (not (contains? #{:GET :HEAD} method)) data)
+                                    ;; TODO: support type of arrayBuffer
+                                    {:body (js/JSON.stringify (bean/->js data))})
+
+                                  (when-let [^js controller (and abortable (AbortController.))]
+                                    (swap! *request-abort-signals assoc _req-id controller)
+                                    {:signal (.-signal controller)}))))
+          (p/then (fn [^js res]
+                    (case type
+                      :json
+                      (.json res)
+
+                      :arraybuffer
+                      (.arrayBuffer res)
+
+                      :base64
+                      (-> (.buffer res)
+                          (p/then #(.toString % "base64")))
+
+                      :text
+                      (.text res))))
+          (p/catch
+           (fn [^js e]
+             ;; TODO: handle special cases
+             (throw e)))
+          (p/finally
+           (fn []
+             (swap! *request-abort-signals dissoc _req-id)))))))
+
+(defmethod handle :httpRequestAbort [_ [_ _req-id]]
+  (when-let [^js controller (get @*request-abort-signals _req-id)]
+    (.abort controller)))
+
 (defmethod handle :quitAndInstall []
   (.quitAndInstall autoUpdater))
 

+ 35 - 20
src/main/frontend/commands.cljs

@@ -274,7 +274,7 @@
      ["Calculator" [[:editor/input "```calc\n\n```" {:backward-pos 4}]
                     [:codemirror/focus]] "Insert a calculator"]
      
-     ["draw" (draw-handler/initialize-excalidarw-file) "Draw a graph with Excalidraw"]
+     ["Draw" (draw-handler/initialize-excalidarw-file) "Draw a graph with Excalidraw"]
      
      ["Embed HTML " (->inline "html")]
 
@@ -399,25 +399,26 @@
     :as _option}]
   (let [selected? (not (string/blank? selected))
         input (gdom/getElement id)
-        edit-content (gobj/get input "value")
-        current-pos (cursor/pos input)
-        prefix (subs edit-content 0 current-pos)
-        postfix (if selected?
-                  (string/replace-first (subs edit-content current-pos)
-                                        selected
-                                        "")
-                  (subs edit-content current-pos))
-        new-value (str prefix value postfix)
-        new-pos (- (+ (count prefix)
-                      (count value)
-                      (or forward-pos 0))
-                   (or backward-pos 0))]
-    (state/set-block-content-and-last-pos! id new-value new-pos)
-    (cursor/move-cursor-to input new-pos)
-    (when selected?
-      (.setSelectionRange input new-pos (+ new-pos (count selected))))
-    (when check-fn
-      (check-fn new-value (dec (count prefix))))))
+        edit-content (gobj/get input "value")]
+    (when edit-content
+      (let [current-pos (cursor/pos input)
+            prefix (subs edit-content 0 current-pos)
+            postfix (if selected?
+                      (string/replace-first (subs edit-content current-pos)
+                                            selected
+                                            "")
+                      (subs edit-content current-pos))
+            new-value (str prefix value postfix)
+            new-pos (- (+ (count prefix)
+                          (count value)
+                          (or forward-pos 0))
+                       (or backward-pos 0))]
+        (state/set-block-content-and-last-pos! id new-value new-pos)
+        (cursor/move-cursor-to input new-pos)
+        (when selected?
+          (.setSelectionRange input new-pos (+ new-pos (count selected))))
+        (when check-fn
+          (check-fn new-value (dec (count prefix))))))))
 
 (defn delete-pair!
   [id]
@@ -431,6 +432,20 @@
     (state/set-block-content-and-last-pos! id new-value new-pos)
     (cursor/move-cursor-to input new-pos)))
 
+(defn delete-selection!
+  [id]
+  (let [input (gdom/getElement id)
+        edit-content (gobj/get input "value")
+        start (util/get-selection-start input)
+        end (util/get-selection-end input)]
+    (when-not (= start end)
+      (let [prefix (subs edit-content 0 start)
+            new-value (str prefix
+                           (subs edit-content end))
+            new-pos (count prefix)]
+        (state/set-block-content-and-last-pos! id new-value new-pos)
+        (cursor/move-cursor-to input new-pos)))))
+
 (defn get-matched-commands
   ([text]
    (get-matched-commands text @*initial-commands))

+ 4 - 2
src/main/frontend/components/block.cljs

@@ -1924,11 +1924,13 @@
     (if (and meta? (not (state/get-edit-input-id)))
       (do
         (util/stop e)
-        (state/conj-selection-block! (gdom/getElement block-id) :down))
+        (state/conj-selection-block! (gdom/getElement block-id) :down)
+        (when (and block-id (not (state/get-selection-start-block)))
+          (state/set-selection-start-block! block-id)))
       (when (contains? #{1 0} button)
         (when-not (target-forbidden-edit? target)
           (cond
-            (and shift? (state/get-selection-start-block))
+            (and shift? (state/get-selection-start-block-or-first))
             (editor-handler/highlight-selection-area! block-id)
 
             shift?

+ 2 - 1
src/main/frontend/components/editor.cljs

@@ -11,6 +11,7 @@
             [frontend.db.model :as db-model]
             [frontend.extensions.zotero :as zotero]
             [frontend.handler.editor :as editor-handler :refer [get-state]]
+            [frontend.handler.paste :as paste-handler]
             [frontend.handler.editor.lifecycle :as lifecycle]
             [frontend.handler.page :as page-handler]
             [frontend.mixins :as mixins]
@@ -530,7 +531,7 @@
        :minRows           (if (state/enable-grammarly?) 2 1)
        :on-click          (editor-handler/editor-on-click! id)
        :on-change         (editor-handler/editor-on-change! block id search-timeout)
-       :on-paste          (editor-handler/editor-on-paste! id)
+       :on-paste          (paste-handler/editor-on-paste! id)
        :auto-focus        false
        :class             heading-class})
 

+ 19 - 15
src/main/frontend/components/encryption.cljs

@@ -135,7 +135,20 @@
   (rum/local false ::loading)
   [state _repo-url db-encrypted-secret close-fn]
   (let [secret (::secret state)
-        loading (::loading state)]
+        loading (::loading state)
+        on-click-fn (fn []
+                      (reset! loading true)
+                      (let [value @secret]
+                        (when-not (string/blank? value) ; TODO: length or other checks
+                          (let [repo (state/get-current-repo)]
+                            (p/do!
+                             (-> (e/decrypt-with-passphrase value db-encrypted-secret)
+                                 (p/then (fn [keys]
+                                           (e/save-key-pair! repo keys)
+                                           (close-fn true)
+                                           (state/set-state! :encryption/graph-parsing? false)))
+                                 (p/catch #(notification/show! "The password is not matched." :warning true))
+                                 (p/finally #(reset! loading false))))))))]
     [:div
      [:div.sm:flex.sm:items-start
       [:div.mt-3.text-center.sm:mt-0.sm:text-left
@@ -146,25 +159,16 @@
       {:type "password"
        :auto-focus true
        :on-change (fn [e]
-                    (reset! secret (util/evalue e)))}]
+                    (reset! secret (util/evalue e)))
+       :on-key-down (fn [e]
+                      (when (= (.-key e) "Enter")
+                        (on-click-fn)))}]
 
      [:div.mt-5.sm:mt-4.sm:flex.sm:flex-row-reverse
       [:span.flex.w-full.rounded-md.shadow-sm.sm:ml-3.sm:w-auto
        [:button.inline-flex.justify-center.w-full.rounded-md.border.border-transparent.px-4.py-2.bg-indigo-600.text-base.leading-6.font-medium.text-white.shadow-sm.hover:bg-indigo-500.focus:outline-none.focus:border-indigo-700.focus:shadow-outline-indigo.transition.ease-in-out.duration-150.sm:text-sm.sm:leading-5
         {:type "button"
-         :on-click (fn []
-                     (reset! loading true)
-                     (let [value @secret]
-                       (when-not (string/blank? value) ; TODO: length or other checks
-                         (let [repo (state/get-current-repo)]
-                           (p/do!
-                            (-> (e/decrypt-with-passphrase value db-encrypted-secret)
-                                (p/then (fn [keys]
-                                          (e/save-key-pair! repo keys)
-                                          (close-fn true)
-                                          (state/set-state! :encryption/graph-parsing? false)))
-                                (p/catch #(notification/show! "The password is not matched." :warning true))
-                                (p/finally #(reset! loading false))))))))}
+         :on-click on-click-fn}
         (if @loading (ui/loading "Decrypting") "Decrypt")]]]]))
 
 (defn encryption-input-secret-dialog

+ 11 - 9
src/main/frontend/components/header.cljs

@@ -26,11 +26,10 @@
 (rum/defc home-button []
   (ui/with-shortcut :go/home "left"
     [:button.button.icon.inline
-     {:href     (rfe/href :home)
-      :on-click #(do
+     {:on-click #(do
                    (when (mobile-util/native-iphone?)
                      (state/set-left-sidebar-open! false))
-                   (route-handler/go-to-journals!))}
+                   (route-handler/redirect-to-home!))}
      (ui/icon "home" {:style {:fontSize ui/icon-size}})]))
 
 (rum/defc login < rum/reactive
@@ -229,9 +228,11 @@
                                (not (mobile-util/native-platform?))
                                (not config/publishing?))
         left-menu (left-menu-button {:on-click (fn []
-                                       (open-fn)
-                                       (state/set-left-sidebar-open!
-                                        (not (:ui/left-sidebar-open? @state/state))))})]
+                                                 (open-fn)
+                                                 (state/set-left-sidebar-open!
+                                                  (not (:ui/left-sidebar-open? @state/state))))})
+        custom-home-page? (and (state/custom-home-page?)
+                               (= (state/sub-default-home-page) (state/get-current-page)))]
     [:div.cp__header#head
      {:class           (util/classnames [{:electron-mac   electron-mac?
                                           :native-ios     (mobile-util/native-ios?)
@@ -254,7 +255,7 @@
                               (state/pub-event! [:go/search]))}
               (ui/icon "search" {:style {:fontSize ui/icon-size}})]))])
       (when (mobile-util/native-platform?)
-        (if (state/home?)
+        (if (or (state/home?) custom-home-page?)
           left-menu
           (ui/with-shortcut :go/backward "bottom"
             [:button.it.navigation.nav-left.button.icon
@@ -269,7 +270,8 @@
       (when plugin-handler/lsp-enabled?
         (plugins/hook-ui-items :toolbar))
 
-      (when (not= (state/get-current-route) :home)
+      (when (and (not= (state/get-current-route) :home)
+                 (not custom-home-page?))
         (home-button))
 
       (when (util/electron?)
@@ -283,7 +285,7 @@
          {:on-click #(route-handler/redirect! {:to :repo-add})}
          (ui/icon "folder-plus")
          (when-not config/mobile?
-           [:strong {:style {:margin-top (if electron-mac? 0 2)}}
+           [:span.ml-1 {:style {:margin-top (if electron-mac? 0 2)}}
             (t :on-boarding/add-graph)])])
 
       (when config/publishing?

+ 9 - 3
src/main/frontend/components/header.css

@@ -54,7 +54,8 @@
   }
 
   .ui-items-container .button {
-    width: 2rem;
+    width: auto;
+    padding: 0 8px;
   }
 
   svg.warning {
@@ -134,7 +135,7 @@
 }
 
 .is-electron.is-mac.is-fullscreen .cp__header > .l {
-  padding-left: 0;
+  padding-left: 1rem;
 }
 
 .cp__header a, .cp__header svg {
@@ -201,7 +202,12 @@
 }
 
 .button.icon {
-  @apply w-8 h-8 text-lg p-1;
+  @apply w-8 h-8 p-1;
+}
+
+.button.icon.add-graph-btn {
+    width: unset;
+    margin: 0 6px;
 }
 
 .is-mac.is-electron :is(.cp__header, .cp__right-sidebar-topbar) :is(button, .button, a) {

文件差异内容过多而无法显示
+ 415 - 374
src/main/frontend/components/plugins.cljs


+ 37 - 0
src/main/frontend/components/plugins.css

@@ -776,6 +776,43 @@
       @apply pr-3 opacity-30 hover:opacity-100 transition-opacity;
     }
   }
+
+  .toolbar-plugins-manager {
+    &-trigger {
+      .dropdown-wrapper {
+        max-height: 80vh;
+        overflow-y: auto;
+      }
+      
+      .menu-link {
+        padding: 3px 5px;
+      }
+
+      .item-wrap {
+        padding-right: 28px;
+        font-size: 13px;
+        position: relative;
+
+        div[data-injected-ui] .ti {
+          position: relative;
+          bottom: -1px;
+        }
+      }
+
+      .pin {
+        position: absolute;
+        top: 0;
+        right: 0;
+        height: 100%;
+        padding: 0 6px;
+
+        &.pinned {
+          color: var(--ls-link-ref-text-color);
+          opacity: 90;
+        }
+      }
+    }
+  }
 }
 
 .lsp-frame-readme {

+ 4 - 3
src/main/frontend/components/theme.cljs

@@ -12,7 +12,7 @@
             [rum.core :as rum]))
 
 (rum/defc container
-  [{:keys [t route theme on-click current-repo nfs-granted? db-restoring?
+  [{:keys [route theme on-click current-repo nfs-granted? db-restoring?
            settings-open? sidebar-open? system-theme? sidebar-blocks-len]} child]
   (let [mounted-fn (use-mounted)
         [restored-sidebar? set-restored-sidebar?] (rum/use-state false)]
@@ -37,8 +37,9 @@
 
     (rum/use-effect!
      #(when lsp-enabled?
-        (plugin-handler/setup-install-listener! t))
-     [t])
+        (plugin-handler/setup-install-listener!)
+        (plugin-handler/load-plugin-preferences))
+     [])
 
     (rum/use-effect!
      (fn []

+ 6 - 0
src/main/frontend/db.cljs

@@ -144,6 +144,12 @@
   (when-let [conn (get-db repo false)]
     (repo-listen-to-tx! repo conn)))
 
+(defn relisten-and-persist!
+  [repo]
+  (when-let [conn (get-db repo false)]
+    (d/unlisten! conn :persistence)
+    (listen-and-persist! repo)))
+
 (defn start-db-conn!
   ([repo]
    (start-db-conn! repo {}))

+ 15 - 0
src/main/frontend/db/model.cljs

@@ -170,6 +170,21 @@
          ;; (sort-by last)
          (reverse))))
 
+(defn get-files-v2
+  [repo]
+  (when-let [db (conn/get-db repo)]
+    (->> (d/q
+          '[:find ?file ?path
+            ;; ?modified-at
+            :where
+            [?file :file/path ?path]
+            ;; [?file :file/last-modified-at ?modified-at]
+            ]
+          db)
+         (seq)
+         ;; (sort-by last)
+         (reverse))))
+
 (defn get-files-blocks
   [repo-url paths]
   (let [paths (set paths)

+ 1 - 0
src/main/frontend/dicts.cljc

@@ -3608,6 +3608,7 @@
         :settings-page/tab-version-control "Sürüm denetimi"
         :settings-page/tab-advanced "Gelişmiş"
         :settings-page/plugin-system "Eklenti sistemi"
+        :settings-page/enable-flashcards "Bilgi kartları"
         :settings-page/network-proxy "Ağ ara sunucusu"
         :logseq "Logseq"
         :on "AÇIK"

+ 0 - 7
src/main/frontend/extensions/code.css

@@ -18,7 +18,6 @@
     right: 0;
     /* margin-top: 4px; */
     z-index: 9999;
-    display: none;
     padding: 4px 6px;
     line-height: 1em;
     opacity: .8;
@@ -42,12 +41,6 @@
       font-family: Fira Code, Monaco, Menlo, Consolas, 'COURIER NEW', monospace;
     }
   }
-
-  @screen md {
-    &-lang {
-      display: flex;
-    }
-  }
 }
 
 .CodeMirror {

+ 1 - 0
src/main/frontend/extensions/pdf/assets.cljs

@@ -152,6 +152,7 @@
 (defn resolve-ref-page
   [pdf-current]
   (let [page-name (:key pdf-current)
+        page-name (string/trim page-name)
         page-name (str "hls__" page-name)
         page (db-model/get-page page-name)
         url (:url pdf-current)

+ 8 - 19
src/main/frontend/fs/capacitor_fs.cljs

@@ -2,15 +2,15 @@
   (:require ["@capacitor/filesystem" :refer [Encoding Filesystem]]
             [cljs-bean.core :as bean]
             [clojure.string :as string]
+            [frontend.db :as db]
+            [frontend.encrypt :as encrypt]
             [frontend.fs.protocol :as protocol]
             [frontend.mobile.util :as mobile-util]
+            [frontend.state :as state]
             [frontend.util :as util]
             [lambdaisland.glogi :as log]
             [promesa.core :as p]
-            [rum.core :as rum]
-            [frontend.state :as state]
-            [frontend.db :as db]
-            [frontend.encrypt :as encrypt]))
+            [rum.core :as rum]))
 
 (when (mobile-util/native-ios?)
   (defn iOS-ensure-documents!
@@ -184,11 +184,6 @@
   [path localDocumentsPath]
   (string/includes? path localDocumentsPath))
 
-(defn- iCloud-container-path?
-  "Check whether `path' is logseq's iCloud container path on iOS"
-  [path]
-  (string/includes? path "iCloud~com~logseq~logseq"))
-
 (rum/defc instruction
   []
   [:div.instruction
@@ -280,16 +275,10 @@
             {:keys [path localDocumentsPath]} (p/chain
                                                (.pickFolder mobile-util/folder-picker)
                                                #(js->clj % :keywordize-keys true))
-            _ (when (mobile-util/native-ios?)
-                (cond
-                  (not (or (local-container-path? path localDocumentsPath)
-                           (iCloud-container-path? path)))
-                  (state/pub-event! [:modal/show-instruction])
-
-                  (iCloud-container-path? path)
-                  (mobile-util/sync-icloud-repo path)
-
-                  :else nil))
+            _ (when (and (mobile-util/native-ios?) 
+                         (not (or (local-container-path? path localDocumentsPath)
+                                  (mobile-util/iCloud-container-path? path))))
+                (state/pub-event! [:modal/show-instruction]))
             files (readdir path)
             files (js->clj files :keywordize-keys true)]
       (into [] (concat [{:path path}] files))))

+ 2 - 4
src/main/frontend/handler.cljs

@@ -19,7 +19,6 @@
             [frontend.handler.ui :as ui-handler]
             [frontend.handler.user :as user-handler]
             [frontend.extensions.srs :as srs]
-            [frontend.mobile.core :as mobile]
             [frontend.mobile.util :as mobile-util]
             [frontend.idb :as idb]
             [frontend.modules.instrumentation.core :as instrument]
@@ -111,7 +110,8 @@
 
          (watch-for-date!)
          (file-handler/watch-for-current-graph-dir!)
-         (state/pub-event! [:graph/ready (state/get-current-repo)])))
+         (state/pub-event! [:graph/ready (state/get-current-repo)])
+         (state/pub-event! [:graph/restored (state/get-current-repo)])))
       (p/catch (fn [error]
                  (log/error :exception error)))))
 
@@ -184,8 +184,6 @@
     (instrument/init)
     (set-network-watcher!)
 
-    (mobile/init!)
-
     (util/indexeddb-check?
      (fn [_error]
        (notification/show! "Sorry, it seems that your browser doesn't support IndexedDB, we recommend to use latest Chrome(Chromium) or Firefox(Non-private mode)." :error false)

+ 18 - 162
src/main/frontend/handler/editor.cljs

@@ -1,7 +1,5 @@
 (ns frontend.handler.editor
-  (:require ["/frontend/utils" :as utils]
-            ["path" :as path]
-            [cljs.core.match :refer [match]]
+  (:require ["path" :as path]
             [clojure.set :as set]
             [clojure.string :as string]
             [clojure.walk :as w]
@@ -56,8 +54,7 @@
             [promesa.core :as p]
             [logseq.graph-parser.util :as gp-util]
             [logseq.graph-parser.mldoc :as gp-mldoc]
-            [logseq.graph-parser.block :as gp-block]
-            [frontend.extensions.html-parser :as html-parser]))
+            [logseq.graph-parser.block :as gp-block]))
 
 ;; FIXME: should support multiple images concurrently uploading
 
@@ -65,7 +62,7 @@
 (defonce *asset-uploading-process (atom 0))
 (defonce *selected-text (atom nil))
 
-(defn- get-selection-and-format
+(defn get-selection-and-format
   []
   (when-let [block (state/get-edit-block)]
     (when (:block/uuid block)
@@ -977,6 +974,16 @@
                  (into [] (state/get-export-block-text-remove-options)))]
     [top-level-block-uuids content]))
 
+(defn- get-all-blocks-by-ids
+  [repo ids]
+  (loop [ids ids
+         result []]
+    (if (seq ids)
+      (let [blocks (db/get-block-and-children repo (first ids))
+            result (vec (concat result blocks))]
+        (recur (remove (set (map :block/uuid result)) (rest ids)) result))
+      result)))
+
 (defn copy-selection-blocks
   [html?]
   (when-let [blocks (seq (state/get-selection-blocks))]
@@ -988,7 +995,7 @@
       (when block
         (let [html (export/export-blocks-as-html repo top-level-block-uuids)]
           (common-handler/copy-to-clipboard-without-id-property! (:block/format block) content (when html? html)))
-        (state/set-copied-blocks content ids)
+        (state/set-copied-blocks! content (get-all-blocks-by-ids repo top-level-block-uuids))
         (notification/show! "Copied!" :success)))))
 
 (defn copy-block-refs
@@ -1071,7 +1078,6 @@
               sorted-blocks (mapcat (fn [block]
                                       (tree/get-sorted-block-and-children repo (:db/id block)))
                                     top-level-blocks)]
-          (state/set-copied-full-blocks nil sorted-blocks)
           (delete-blocks! repo (map :block/uuid sorted-blocks) sorted-blocks dom-blocks))))))
 
 (def url-regex
@@ -1202,7 +1208,7 @@
           [_top-level-block-uuids md-content] (compose-copied-blocks-contents repo [block-id])
           html (export/export-blocks-as-html repo [block-id])
           sorted-blocks (tree/get-sorted-block-and-children repo (:db/id block))]
-      (state/set-copied-full-blocks md-content sorted-blocks)
+      (state/set-copied-blocks! md-content sorted-blocks)
       (common-handler/copy-to-clipboard-without-id-property! (:block/format block) md-content html)
       (delete-block-aux! block true))))
 
@@ -1213,7 +1219,7 @@
 
 (defn highlight-selection-area!
   [end-block]
-  (when-let [start-block (state/get-selection-start-block)]
+  (when-let [start-block (state/get-selection-start-block-or-first)]
     (let [blocks (util/get-nodes-between-two-nodes start-block end-block "ls-block")
           direction (util/get-direction-between-two-nodes start-block end-block "ls-block")
 
@@ -1941,7 +1947,8 @@
                              (paste-block-cleanup block page exclude-properties format content-update-fn))
                         blocks)
               result (outliner-core/insert-blocks! blocks' target-block {:sibling? sibling?
-                                                                         :outliner-op :paste})]
+                                                                         :outliner-op :paste
+                                                                         :replace-empty-target? true})]
           (edit-last-block-after-inserted! result))))))
 
 (defn- block-tree->blocks
@@ -2848,157 +2855,6 @@
         (edit-box-on-change! e block id)
         (util/scroll-editor-cursor input)))))
 
-(defn- paste-text-parseable
-  [format text]
-  (when-let [editing-block (state/get-edit-block)]
-    (let [page-id (:db/id (:block/page editing-block))
-          blocks (block/extract-blocks
-                  (mldoc/->edn text (gp-mldoc/default-config format)) text true format)
-          blocks' (gp-block/with-parent-and-left page-id blocks)]
-      (paste-blocks blocks' {}))))
-
-(defn- paste-segmented-text
-  [format text]
-  (let [paragraphs (string/split text #"(?:\r?\n){2,}")
-        updated-paragraphs
-        (string/join "\n"
-                     (mapv (fn [p] (->> (string/trim p)
-                                        ((fn [p]
-                                           (if (util/safe-re-find (if (= format :org)
-                                                                    #"\s*\*+\s+"
-                                                                    #"\s*-\s+") p)
-                                             p
-                                             (str (if (= format :org) "* " "- ") p))))))
-                           paragraphs))]
-    (paste-text-parseable format updated-paragraphs)))
-
-(defn- get-all-blocks-by-ids
-  [repo ids]
-  (loop [ids ids
-         result []]
-    (if (seq ids)
-      (let [blocks (db/get-block-and-children repo (first ids))
-            result (vec (concat result blocks))]
-        (recur (remove (set (map :block/uuid result)) (rest ids)) result))
-      result)))
-
-(defn wrap-macro-url
-  [url]
-  (cond
-    (boolean (text-util/get-matched-video url))
-    (util/format "{{video %s}}" url)
-
-    (string/includes? url "twitter.com")
-    (util/format "{{twitter %s}}" url)
-
-    :else
-    (do
-      (notification/show! (util/format "No macro is available for %s" url) :warning)
-      nil)))
-
-(defn- paste-copied-blocks-or-text
-  [initial-text text e]
-  (let [copied-blocks (state/get-copied-blocks)
-        copied-block-ids (:copy/block-ids copied-blocks)
-        copied-graph (:copy/graph copied-blocks)
-        input (state/get-input)]
-    (cond
-      ;; Internal blocks by either copy or cut blocks
-      (and
-       (= copied-graph (state/get-current-repo))
-       (or (seq copied-block-ids)
-           (seq (:copy/full-blocks copied-blocks)))
-       initial-text
-       ;; not copied from the external clipboard
-       (= (string/replace (string/trim initial-text) "\r" "")
-          (string/replace (string/trim (or (:copy/content copied-blocks) "")) "\r" "")))
-      (do
-        (util/stop e)
-        (let [blocks (or
-                      (:copy/full-blocks copied-blocks)
-                      (get-all-blocks-by-ids (state/get-current-repo) copied-block-ids))]
-          (when (seq blocks)
-            (state/set-copied-full-blocks! blocks)
-            (paste-blocks blocks {}))))
-
-      (and (gp-util/url? text)
-           (not (string/blank? (util/get-selected-text))))
-      (do
-        (util/stop e)
-        (html-link-format! text))
-
-      (and (text/block-ref? text)
-           (wrapped-by? input "((" "))"))
-      (do
-        (util/stop e)
-        (commands/simple-insert! (state/get-edit-input-id) (text/get-block-ref text) nil))
-
-      :else
-      ;; from external
-      (let [format (or (db/get-page-format (state/get-current-page)) :markdown)]
-        (util/stop e)
-        (match [format
-                (nil? (util/safe-re-find #"(?m)^\s*(?:[-+*]|#+)\s+" text))
-                (nil? (util/safe-re-find #"(?m)^\s*\*+\s+" text))
-                (nil? (util/safe-re-find #"(?:\r?\n){2,}" text))]
-          [:markdown false _ _]
-          (paste-text-parseable format text)
-
-          [:org _ false _]
-          (paste-text-parseable format text)
-
-          [:markdown true _ false]
-          (paste-segmented-text format text)
-
-          [:markdown true _ true]
-          (commands/simple-insert! (state/get-edit-input-id) text nil)
-
-          [:org _ true false]
-          (paste-segmented-text format text)
-
-          [:org _ true true]
-          (commands/simple-insert! (state/get-edit-input-id) text nil))))))
-
-(defn paste-text-in-one-block-at-point
-  []
-  (utils/getClipText
-   (fn [clipboard-data]
-     (when-let [_ (state/get-input)]
-       (let [data (or (when (gp-util/url? clipboard-data)
-                        (wrap-macro-url clipboard-data))
-                      clipboard-data)]
-         (insert data true))))
-   (fn [error]
-     (js/console.error error))))
-
-(defn editor-on-paste!
-  [id]
-  (fn [e]
-    (state/set-state! :editor/on-paste? true)
-    (let [clipboard-data (gobj/get e "clipboardData")
-          html (.getData clipboard-data "text/html")
-          edit-block (state/get-edit-block)
-          format (or (:block/format edit-block) :markdown)
-          initial-text (.getData clipboard-data "text")
-          text (or (when-not (string/blank? html)
-                     (html-parser/convert format html))
-                   initial-text)
-          input (state/get-input)]
-      (if-not (string/blank? text)
-        (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))
-          (paste-copied-blocks-or-text initial-text text e))
-        (let [_handled
-              (let [clipboard-data (gobj/get e "clipboardData")
-                    files (.-files clipboard-data)]
-                (when-let [file (first files)]
-                  (when-let [block (state/get-edit-block)]
-                    (upload-asset id #js[file] (:block/format block) *asset-uploading? true))))]
-          (util/stop e))))))
-
 (defn- cut-blocks-and-clear-selections!
   [copy?]
   (cut-selection-blocks copy?)

+ 95 - 15
src/main/frontend/handler/events.cljs

@@ -1,8 +1,10 @@
 (ns frontend.handler.events
   (:refer-clojure :exclude [run!])
-  (:require [clojure.core.async :as async]
+  (:require ["@capacitor/filesystem" :refer [Directory Filesystem]]
+            [clojure.core.async :as async]
             [clojure.set :as set]
             [clojure.string :as string]
+            [datascript.core :as d]
             [frontend.commands :as commands]
             [frontend.components.diff :as diff]
             [frontend.components.encryption :as encryption]
@@ -13,7 +15,9 @@
             [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
             [frontend.db :as db]
-            [logseq.db.schema :as db-schema]
+            [frontend.db.conn :as conn]
+            [frontend.db.model :as db-model]
+            [frontend.db.persist :as db-persist]
             [frontend.encrypt :as encrypt]
             [frontend.extensions.srs :as srs]
             [frontend.fs :as fs]
@@ -32,15 +36,18 @@
             [frontend.handler.search :as search-handler]
             [frontend.handler.ui :as ui-handler]
             [frontend.handler.web.nfs :as nfs-handler]
+            [frontend.mobile.core :as mobile]
             [frontend.mobile.util :as mobile-util]
             [frontend.modules.instrumentation.posthog :as posthog]
             [frontend.modules.outliner.file :as outliner-file]
             [frontend.modules.shortcut.core :as st]
+            [frontend.search :as search-db]
             [frontend.state :as state]
             [frontend.ui :as ui]
             [frontend.util :as util]
             [frontend.util.persist-var :as persist-var]
             [goog.dom :as gdom]
+            [logseq.db.schema :as db-schema]
             [promesa.core :as p]
             [rum.core :as rum]))
 
@@ -63,19 +70,25 @@
          (sync/sync-start)
 ))
 
-(defn- graph-switch [graph]
-  (state/set-current-repo! graph)
-  ;; load config
-  (common-handler/reset-config! graph nil)
-  (st/refresh!)
-  (when-not (= :draw (state/get-current-route))
-    (route-handler/redirect-to-home!))
-  (when-let [dir-name (config/get-repo-dir graph)]
-    (fs/watch-dir! dir-name))
-  (srs/update-cards-due-count!)
-  (state/pub-event! [:graph/ready graph])
-
-  (file-sync-stop-when-switch-graph))
+(defn- graph-switch
+  ([graph]
+   (graph-switch graph false))
+  ([graph skip-ios-check?]
+   (if (and (mobile-util/native-ios?) (not skip-ios-check?))
+     (state/pub-event! [:validate-appId graph-switch graph])
+     (do
+       (state/set-current-repo! graph)
+       ;; load config
+       (common-handler/reset-config! graph nil)
+       (st/refresh!)
+       (when-not (= :draw (state/get-current-route))
+         (route-handler/redirect-to-home!))
+       (when-let [dir-name (config/get-repo-dir graph)]
+         (fs/watch-dir! dir-name))
+       (srs/update-cards-due-count!)
+       (state/pub-event! [:graph/ready graph])
+
+       (file-sync-stop-when-switch-graph)))))
 
 (def persist-db-noti-m
   {:before     #(notification/show!
@@ -330,6 +343,60 @@
       (when-let [right-sidebar-node (gdom/getElementByClass "sidebar-item-list")]
         (set! (.. right-sidebar-node -style -paddingBottom) "150px")))))
 
+(defn update-file-path [deprecated-repo current-repo deprecated-app-id current-app-id]
+  (let [files (db-model/get-files-v2 deprecated-repo)
+        conn (conn/get-db deprecated-repo false)
+        tx (mapv (fn [[id path]]
+                   (let [new-path (string/replace path deprecated-app-id current-app-id)]
+                     {:db/id id
+                      :file/path new-path}))
+                 files)]
+    (d/transact! conn tx)
+    (reset! conn/conns
+            (update-keys @conn/conns
+                         (fn [key] (if (string/includes? key deprecated-repo)
+                                     (string/replace key deprecated-repo current-repo)
+                                     key))))))
+
+(defn get-ios-app-id
+  [repo-url]
+  (when repo-url
+    (let [app-id (-> (first (string/split repo-url "/Documents"))
+                     (string/split "/")
+                     last)]
+      app-id)))
+
+(defmethod handle :validate-appId [[_ graph-switch-f graph]]
+  (when-let [deprecated-repo (or graph (state/get-current-repo))]
+    ;; Installation is will not be changed for iCloud
+    (if (mobile-util/iCloud-container-path? deprecated-repo)
+      (when graph-switch-f (graph-switch-f graph true))
+      (p/let [deprecated-app-id (get-ios-app-id deprecated-repo)
+              current-document-url (.getUri Filesystem #js {:path ""
+                                                            :directory (.-Documents Directory)})
+              current-app-id (-> (js->clj current-document-url :keywordize-keys true)
+                                 get-ios-app-id)]
+        (if (= deprecated-app-id current-app-id)
+          (when graph-switch-f (graph-switch-f graph true))
+          (do
+            (.unwatch mobile-util/fs-watcher)
+            (let [current-repo (string/replace deprecated-repo deprecated-app-id current-app-id)
+                  current-repo-dir (config/get-repo-dir current-repo)]
+              (try
+                (update-file-path deprecated-repo current-repo deprecated-app-id current-app-id)
+                (db-persist/delete-graph! deprecated-repo)
+                (search-db/remove-db! deprecated-repo)
+                (state/delete-repo! {:url deprecated-repo})
+                (state/add-repo! {:url current-repo :nfs? true})
+                (catch :default e
+                  (js/console.error e)))
+              (state/set-current-repo! current-repo)
+              (db/relisten-and-persist! current-repo)
+              (db/persist-if-idle! current-repo)
+              (file-handler/restore-config! current-repo false)
+              (.watch mobile-util/fs-watcher #js {:path current-repo-dir})
+              (when graph-switch-f (graph-switch-f current-repo true)))))))))
+
 (defmethod handle :plugin/consume-updates [[_ id pending? updated?]]
   (let [downloading? (:plugin/updates-downloading? @state/state)]
 
@@ -441,6 +508,9 @@
   (route-handler/redirect! {:to :whiteboard
                             :path-params {:name link}}))
 
+(defmethod handle :graph/restored [[_ _graph]]
+  (mobile/init!))
+
 (defmethod handle :graph/dir-gone [[_ dir]]
   (state/pub-event! [:notification/show
                      {:content (str "The directory " dir " has been renamed or deleted, the editor will be disabled for this graph, you can unlink the graph.")
@@ -474,3 +544,13 @@
                                               :error error}])))))
       (recur))
     chan))
+
+(comment
+  (let [{:keys [deprecated-app-id current-app-id]} {:deprecated-app-id "AFDADF9A-7466-4ED8-B74F-AAAA0D4565B9", :current-app-id "7563518E-0EFD-4AD2-8577-10CFFD6E4596"}]
+    (def deprecated-app-id deprecated-app-id)
+    (def current-app-id current-app-id))
+  (def deprecated-repo (state/get-current-repo))
+  (def new-repo (string/replace deprecated-repo deprecated-app-id current-app-id))
+
+  (update-file-path deprecated-repo new-repo deprecated-app-id current-app-id)
+  )

+ 13 - 12
src/main/frontend/handler/file.cljs

@@ -130,18 +130,19 @@
                 file)
          file (gp-util/path-normalize file)
          new? (nil? (db/entity [:file/path file]))]
-     (graph-parser/parse-file
-      (db/get-db repo-url false)
-      file
-      content
-      (merge options
-             {:new? new?
-              :delete-blocks-fn (partial get-delete-blocks repo-url)
-              :extract-options {:user-config (state/get-config)
-                                :date-formatter (state/get-date-formatter)
-                                :page-name-order (state/page-name-order)
-                                :block-pattern (config/get-block-pattern (gp-util/get-format file))
-                                :supported-formats (gp-config/supported-formats)}})))))
+     (:tx
+      (graph-parser/parse-file
+       (db/get-db repo-url false)
+       file
+       content
+       (merge options
+              {:new? new?
+               :delete-blocks-fn (partial get-delete-blocks repo-url)
+               :extract-options {:user-config (state/get-config)
+                                 :date-formatter (state/get-date-formatter)
+                                 :page-name-order (state/page-name-order)
+                                 :block-pattern (config/get-block-pattern (gp-util/get-format file))
+                                 :supported-formats (gp-config/supported-formats)}}))))))
 
 ;; TODO: Remove this function in favor of `alter-files`
 (defn alter-file

+ 175 - 0
src/main/frontend/handler/paste.cljs

@@ -0,0 +1,175 @@
+(ns frontend.handler.paste
+  (:require [frontend.state :as state]
+            [frontend.db :as db]
+            [frontend.format.block :as block]
+            [logseq.graph-parser.util :as gp-util]
+            [logseq.graph-parser.mldoc :as gp-mldoc]
+            [logseq.graph-parser.block :as gp-block]
+            [clojure.string :as string]
+            [frontend.util :as util]
+            [frontend.handler.editor :as editor-handler]
+            [frontend.extensions.html-parser :as html-parser]
+            [goog.object :as gobj]
+            [frontend.mobile.util :as mobile-util]
+            [frontend.util.thingatpt :as thingatpt]
+            ["/frontend/utils" :as utils]
+            [frontend.commands :as commands]
+            [cljs.core.match :refer [match]]
+            [logseq.graph-parser.text :as text]
+            [frontend.handler.notification :as notification]
+            [frontend.util.text :as text-util]
+            [frontend.format.mldoc :as mldoc]
+            [lambdaisland.glogi :as log]))
+
+(defn- paste-text-parseable
+  [format text]
+  (when-let [editing-block (state/get-edit-block)]
+    (let [page-id (:db/id (:block/page editing-block))
+          blocks (block/extract-blocks
+                  (mldoc/->edn text (gp-mldoc/default-config format)) text true format)
+          blocks' (gp-block/with-parent-and-left page-id blocks)]
+      (editor-handler/paste-blocks blocks' {}))))
+
+(defn- paste-segmented-text
+  [format text]
+  (let [paragraphs (string/split text #"(?:\r?\n){2,}")
+        updated-paragraphs
+        (string/join "\n"
+                     (mapv (fn [p] (->> (string/trim p)
+                                        ((fn [p]
+                                           (if (util/safe-re-find (if (= format :org)
+                                                                    #"\s*\*+\s+"
+                                                                    #"\s*-\s+") p)
+                                             p
+                                             (str (if (= format :org) "* " "- ") p))))))
+                           paragraphs))]
+    (paste-text-parseable format updated-paragraphs)))
+
+(defn- wrap-macro-url
+  [url]
+  (cond
+    (boolean (text-util/get-matched-video url))
+    (util/format "{{video %s}}" url)
+
+    (string/includes? url "twitter.com")
+    (util/format "{{twitter %s}}" url)
+
+    :else
+    (do
+      (notification/show! (util/format "No macro is available for %s" url) :warning)
+      nil)))
+
+(defn- paste-copied-blocks-or-text
+  [text e html]
+  (util/stop e)
+  (let [copied-blocks (state/get-copied-blocks)
+        input (state/get-input)
+        text (string/replace text "\r\n" "\n") ;; Fix for Windows platform
+        internal-paste? (and
+                         (seq (:copy/blocks copied-blocks))
+                         ;; not copied from the external clipboard
+                         (= text (:copy/content copied-blocks)))]
+    (if internal-paste?
+      (let [blocks (:copy/blocks copied-blocks)]
+        (when (seq blocks)
+          (editor-handler/paste-blocks blocks {})))
+      (let [{:keys [value]} (editor-handler/get-selection-and-format)]
+        (cond
+          (and (or (gp-util/url? text)
+                   (and value (gp-util/url? (string/trim value))))
+               (not (string/blank? (util/get-selected-text))))
+          (editor-handler/html-link-format! text)
+
+          (and (text/block-ref? text)
+               (editor-handler/wrapped-by? input "((" "))"))
+          (commands/simple-insert! (state/get-edit-input-id) (text/get-block-ref text) nil)
+
+          :else
+          ;; from external
+          (let [format (or (db/get-page-format (state/get-current-page)) :markdown)
+                html-text (let [result (when-not (string/blank? html)
+                                         (try
+                                           (html-parser/convert format html)
+                                           (catch :default e
+                                             (log/error :exception e)
+                                             nil)))]
+                            (if (string/blank? result) nil result))
+                text (or html-text text)
+                input-id (state/get-edit-input-id)
+                replace-text-f (fn []
+                                 (commands/delete-selection! input-id)
+                                 (commands/simple-insert! input-id text nil))]
+            (match [format
+                    (nil? (util/safe-re-find #"(?m)^\s*(?:[-+*]|#+)\s+" text))
+                    (nil? (util/safe-re-find #"(?m)^\s*\*+\s+" text))
+                    (nil? (util/safe-re-find #"(?:\r?\n){2,}" text))]
+              [:markdown false _ _]
+              (paste-text-parseable format text)
+
+              [:org _ false _]
+              (paste-text-parseable format text)
+
+              [:markdown true _ false]
+              (paste-segmented-text format text)
+
+              [:markdown true _ true]
+              (replace-text-f)
+
+              [:org _ true false]
+              (paste-segmented-text format text)
+
+              [:org _ true true]
+              (replace-text-f))))))))
+
+(defn paste-text-in-one-block-at-point
+  []
+  (utils/getClipText
+   (fn [clipboard-data]
+     (when-let [_ (state/get-input)]
+       (let [data (or (when (gp-util/url? clipboard-data)
+                        (wrap-macro-url clipboard-data))
+                      clipboard-data)]
+         (editor-handler/insert data true))))
+   (fn [error]
+     (js/console.error error))))
+
+(defn- paste-text-or-blocks-aux
+  [input e text html]
+  (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))
+    (paste-copied-blocks-or-text text e html)))
+
+(defn editor-on-paste!
+  ([id]
+   (editor-on-paste! id false))
+  ([id raw-paste?]
+   (fn [e]
+     (state/set-state! :editor/on-paste? true)
+     (let [input (state/get-input)]
+       (if raw-paste?
+        (utils/getClipText
+         (fn [clipboard-data]
+           (when-let [_ (state/get-input)]
+             (let [text (or (when (gp-util/url? clipboard-data)
+                              (wrap-macro-url clipboard-data))
+                            clipboard-data)]
+               (paste-text-or-blocks-aux input e text nil))))
+         (fn [error]
+           (js/console.error error)))
+        (let [clipboard-data (gobj/get e "clipboardData")
+              html (when-not raw-paste? (.getData clipboard-data "text/html"))
+              text (.getData clipboard-data "text")]
+          (if-not (string/blank? text)
+            (paste-text-or-blocks-aux input e text html)
+            (when id
+              (let [_handled
+                    (let [clipboard-data (gobj/get e "clipboardData")
+                          files (.-files clipboard-data)]
+                      (when-let [file (first files)]
+                        (when-let [block (state/get-edit-block)]
+                          (editor-handler/upload-asset id #js[file] (:block/format block)
+                                                       editor-handler/*asset-uploading? true))))]
+                (util/stop e))))))))))

+ 58 - 13
src/main/frontend/handler/plugin.cljs

@@ -14,6 +14,7 @@
             [clojure.string :as string]
             [lambdaisland.glogi :as log]
             [frontend.components.svg :as svg]
+            [frontend.context.i18n :refer [t]]
             [frontend.format :as format]))
 
 (defonce lsp-enabled?
@@ -42,6 +43,23 @@
 (defonce stats-url (str central-endpoint "stats.json"))
 (declare select-a-plugin-theme)
 
+(defn load-plugin-preferences
+  []
+  (-> (invoke-exported-api "load_user_preferences")
+      (p/then #(bean/->clj %))
+      (p/then #(state/set-state! :plugin/preferences %))
+      (p/catch
+       #(js/console.error %))))
+
+(defn save-plugin-preferences!
+  ([input] (save-plugin-preferences! input true))
+  ([input reload-state?]
+   (when-let [^js input (and (map? input) (bean/->js input))]
+     (p/then
+      (js/LSPluginCore.saveUserPreferences input)
+      #(when reload-state?
+         (load-plugin-preferences))))))
+
 (defn gh-repo-url [repo]
   (str "https://github.com/" repo))
 
@@ -55,14 +73,17 @@
   (if (or refresh? (nil? (:plugin/marketplace-pkgs @state/state)))
     (p/create
       (fn [resolve reject]
-        (-> (ipc/ipc :httpFetchJSON plugins-url)
-            (p/then (fn [res]
+        (let [on-ok (fn [res]
                       (if-let [res (and res (bean/->clj res))]
                         (let [pkgs (:packages res)]
                           (state/set-state! :plugin/marketplace-pkgs pkgs)
                           (resolve pkgs))
-                        (reject nil))))
-            (p/catch reject))))
+                        (reject nil)))]
+          (if (state/http-proxy-enabled-or-val?)
+            (-> (ipc/ipc :httpFetchJSON plugins-url)
+                (p/then on-ok)
+                (p/catch reject))
+            (util/fetch plugins-url on-ok reject)))))
     (p/resolved (:plugin/marketplace-pkgs @state/state))))
 
 (defn load-marketplace-stats
@@ -70,8 +91,7 @@
   (if (or refresh? (nil? (:plugin/marketplace-stats @state/state)))
     (p/create
       (fn [resolve reject]
-        (-> (ipc/ipc :httpFetchJSON stats-url)
-            (p/then (fn [^js res]
+        (let [on-ok (fn [^js res]
                       (if-let [res (and res (bean/->clj res))]
                         (do
                           (state/set-state!
@@ -82,8 +102,12 @@
                                                      (reduce (fn [a b] (+ a (get b 2))) 0 (:releases stat)))])
                                          res)))
                           (resolve nil))
-                        (reject nil))))
-            (p/catch reject))))
+                        (reject nil)))]
+          (if (state/http-proxy-enabled-or-val?)
+            (-> (ipc/ipc :httpFetchJSON stats-url)
+                (p/then on-ok)
+                (p/catch reject))
+            (util/fetch stats-url on-ok reject)))))
     (p/resolved nil)))
 
 (defn installed?
@@ -159,7 +183,7 @@
     (filter #(has-setting-schema? (:id %)) plugins)))
 
 (defn setup-install-listener!
-  [t]
+  []
   (let [channel (name :lsp-installed)
         listener (fn [^js _ ^js e]
                    (js/console.debug :lsp-installed e)
@@ -299,11 +323,13 @@
   (swap! state/state medley/dissoc-in [:plugin/simple-commands (keyword pid)]))
 
 (defn register-plugin-ui-item
-  [pid {:keys [type] :as opts}]
+  [pid {:keys [key type] :as opts}]
   (when-let [pid (keyword pid)]
     (when (contains? (:plugin/installed-plugins @state/state) pid)
-      (swap! state/state update-in [:plugin/installed-ui-items pid]
-             (fnil conj []) [type opts pid])
+      (let [items (or (get-in @state/state [:plugin/installed-ui-items pid]) [])
+            items (filter #(not= key (:key (second %))) items)]
+        (swap! state/state assoc-in [:plugin/installed-ui-items pid]
+               (conj items [type opts pid])))
       true)))
 
 (defn unregister-plugin-ui-items
@@ -531,6 +557,24 @@
                         (into {} (map (fn [v] [(keyword (:id v)) v]) plugins)))
       (state/pub-event! [:plugin/consume-updates]))))
 
+(defn call-plugin
+  [^js pl type payload]
+  (when pl
+    (.call (.-caller pl) (name type) (bean/->js payload))))
+
+(defn request-callback
+  [^js pl req-id payload]
+  (call-plugin pl :#lsp#request#callback {:requestId req-id :payload payload}))
+
+(defn op-pinned-toolbar-item!
+  [key op]
+  (let [pinned (state/sub [:plugin/preferences :pinnedToolbarItems])
+        pinned (into #{} pinned)]
+    (when-let [op-fn (case op
+                       :add conj
+                       :remove disj)]
+      (save-plugin-preferences! {:pinnedToolbarItems (op-fn pinned (name key))}))))
+
 ;; components
 (rum/defc lsp-indicator < rum/reactive
   []
@@ -596,7 +640,7 @@
                                   (clear-commands! pid)
                                   (unregister-plugin-themes pid)))
 
-                (.on "theme-changed" (fn [^js themes]
+                (.on "themes-changed" (fn [^js themes]
                                        (swap! state/state assoc :plugin/installed-themes
                                               (vec (mapcat (fn [[pid vs]] (mapv #(assoc % :pid pid) (bean/->clj vs))) (bean/->clj themes))))))
 
@@ -607,6 +651,7 @@
                                           (when mode
                                             (state/set-custom-theme! mode theme)
                                             (state/set-theme-mode! mode))
+                                          (hook-plugin-app :theme-changed theme)
                                           (state/set-state! :plugin/selected-theme url))))
 
                 (.on "reset-custom-theme" (fn [^js themes]

+ 3 - 1
src/main/frontend/handler/repo.cljs

@@ -336,7 +336,9 @@
                         (db-persist/delete-graph! url)
                         (search/remove-db! url)
                         (state/delete-repo! repo)
-                        (when graph-exists? (ipc/ipc "graphUnlinked" repo))))]
+                        (when graph-exists? (ipc/ipc "graphUnlinked" repo))
+                        (when (= (state/get-current-repo) url)
+                          (state/set-current-repo! (:url (first (state/get-repos)))))))]
     (when (or (config/local-db? url) (= url "local"))
       (p/let [_ (idb/clear-local-db! url)] ; clear file handles
         (delete-db-f)))))

+ 5 - 3
src/main/frontend/mobile/core.cljs

@@ -2,7 +2,7 @@
   (:require ["@capacitor/app" :refer [^js App]]
             ["@capacitor/keyboard" :refer [^js Keyboard]]
             [clojure.string :as string]
-            [frontend.fs.capacitor-fs :as fs]
+            [frontend.fs.capacitor-fs :as mobile-fs]
             [frontend.handler.editor :as editor-handler]
             [frontend.mobile.deeplink :as deeplink]
             [frontend.mobile.intent :as intent]
@@ -20,8 +20,10 @@
 (defn- ios-init
   "Initialize iOS-specified event listeners"
   []
-  (let [path (fs/iOS-ensure-documents!)]
+  (let [path (mobile-fs/iOS-ensure-documents!)]
     (println "iOS container path: " path))
+
+  (state/pub-event! [:validate-appId])
   
   (.addEventListener js/window
                      "load"
@@ -29,7 +31,7 @@
                        (when @*url
                          (js/setTimeout #(deeplink/deeplink @*url)
                                         1000))))
-
+  
   (mobile-util/check-ios-zoomed-display)
   
   (.removeAllListeners mobile-util/file-sync)

+ 7 - 10
src/main/frontend/mobile/util.cljs

@@ -23,7 +23,6 @@
 
 (defonce folder-picker (registerPlugin "FolderPicker"))
 (when (native-ios?)
-  (defonce download-icloud-files (registerPlugin "DownloadiCloudFiles"))
   (defonce ios-utils (registerPlugin "Utils"))
   (defonce ios-file-container (registerPlugin "FileContainer"))
   (defonce file-sync (registerPlugin "FileSync")))
@@ -32,14 +31,6 @@
 (when (native-platform?)
   (defonce fs-watcher (registerPlugin "FsWatcher")))
 
-(defn sync-icloud-repo [repo-dir]
-  (let [repo-name (-> (string/split repo-dir "Documents/")
-                      last
-                      string/trim
-                      js/decodeURI)]
-    (.syncGraph download-icloud-files
-                (clj->js {:graph repo-name}))))
-
 (defn hide-splash []
   (.hide SplashScreen))
 
@@ -92,4 +83,10 @@
                               #(js->clj % :keywordize-keys true))]
     (when (:isZoomed is-zoomed?)
       (let [^js cl (.-classList js/document.documentElement)]
-        (.add cl "is-zoomed-native-ios")))))
+        (.add cl "is-zoomed-native-ios")))))
+
+(defn iCloud-container-path?
+  "Check whether `path' is logseq's iCloud container path on iOS"
+  [path]
+  (string/includes? path "iCloud~com~logseq~logseq"))
+

+ 53 - 38
src/main/frontend/modules/outliner/core.cljs

@@ -370,20 +370,29 @@
 
 ;;; ### insert-blocks, delete-blocks, move-blocks
 
-(defn- fix-top-level-blocks
+(defn fix-top-level-blocks
   "Blocks with :block/level"
   [blocks]
-  (loop [blocks blocks
-         last-top-level-block nil
-         result []]
-    (if-let [block (first blocks)]
-      (if (= 1 (:block/level block))
-        (let [block' (assoc block
-                            :block/left {:db/id (:db/id last-top-level-block)}
-                            :block/parent (:block/parent last-top-level-block))]
-          (recur (rest blocks) block (conj result block')))
-        (recur (rest blocks) last-top-level-block (conj result block)))
-      result)))
+  (let [top-level-blocks (filter #(= (:block/level %) 1) blocks)
+        id->block (zipmap (map :db/id top-level-blocks) top-level-blocks)
+        uuid->block (zipmap (map :block/uuid top-level-blocks) top-level-blocks)]
+    (if (every? (fn [block]
+                  (let [left (:block/left block)
+                        id (if (map? left) (:db/id left) (second left))]
+                    (some? (or (get id->block id) (get uuid->block id))))) (rest top-level-blocks))
+      ;; no need to fix
+      blocks
+      (loop [blocks blocks
+             last-top-level-block nil
+             result []]
+        (if-let [block (first blocks)]
+          (if (= 1 (:block/level block))
+            (let [block' (assoc block
+                                :block/left {:db/id (:db/id last-top-level-block)}
+                                :block/parent (:block/parent last-top-level-block))]
+              (recur (rest blocks) block (conj result block')))
+            (recur (rest blocks) last-top-level-block (conj result block)))
+          result)))))
 
 (defn- insert-blocks-aux
   [blocks target-block {:keys [sibling? replace-empty-target? keep-uuid? move? outliner-op]}]
@@ -469,7 +478,7 @@
                                      (> (count blocks) 1)
                                      (not move?)))
         blocks' (blocks-with-level blocks)
-        blocks' (if (= outliner-op ::paste)
+        blocks' (if (= outliner-op :paste)
                   (fix-top-level-blocks blocks')
                   blocks')
         insert-opts {:sibling? sibling?
@@ -509,13 +518,18 @@
          :blocks tx}))))
 
 (defn- build-move-blocks-next-tx
-  [blocks]
+  [blocks non-consecutive-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))))
+               (not (contains? top-level-blocks-ids (:db/id right-block)))
+               (or (and
+                    non-consecutive-blocks?
+                    (not= (:db/id (last top-level-blocks))
+                          (:db/id (:block/left right-block))))
+                   true))
       {:db/id (:db/id right-block)
        :block/left (loop [block (:block/left right-block)]
                      (if (contains? top-level-blocks-ids (:db/id block))
@@ -535,20 +549,21 @@
 
 (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?))))
+  (when (> (count blocks) 1)
+    (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."
@@ -618,20 +633,20 @@
     {:tx-data @txs-state}))
 
 (defn- move-to-original-position?
-  [blocks target-block sibling?]
-  (let [non-consecutive-blocks (db-model/get-non-consecutive-blocks blocks)]
-    (and (empty? non-consecutive-blocks)
-         (= (:db/id (:block/left (first blocks))) (:db/id target-block))
-         (not= (= (:db/id (:block/parent (first blocks)))
-                  (:db/id target-block))
-               sibling?))))
+  [blocks target-block sibling? non-consecutive-blocks?]
+  (and (not non-consecutive-blocks?)
+       (= (:db/id (:block/left (first blocks))) (:db/id target-block))
+       (not= (= (:db/id (:block/parent (first blocks)))
+                (:db/id target-block))
+             sibling?)))
 
 (defn move-blocks
   "Move `blocks` to `target-block` as siblings or children."
   [blocks target-block {:keys [sibling? outliner-op]}]
   [:pre [(seq blocks)
          (s/valid? ::block-map-or-entity target-block)]]
-  (let [original-position? (move-to-original-position? blocks target-block sibling?)]
+  (let [non-consecutive-blocks? (seq (db-model/get-non-consecutive-blocks blocks))
+        original-position? (move-to-original-position? blocks target-block sibling? non-consecutive-blocks?)]
     (when (and (not (contains? (set (map :db/id blocks)) (:db/id target-block)))
                (not original-position?))
       (let [parents (->> (db/get-block-parents (state/get-current-repo) (:block/uuid target-block))
@@ -648,7 +663,7 @@
                     target-page (or (:db/id (:block/page target-block))
                                     (:db/id target-block))
                     not-same-page? (not= first-block-page target-page)
-                    move-blocks-next-tx [(build-move-blocks-next-tx blocks)]
+                    move-blocks-next-tx [(build-move-blocks-next-tx blocks non-consecutive-blocks?)]
                     children-page-tx (when not-same-page?
                                        (let [children-ids (mapcat #(db/get-block-children-ids (state/get-current-repo) (:block/uuid %)) blocks)]
                                          (map (fn [uuid] {:block/uuid uuid

+ 2 - 1
src/main/frontend/modules/shortcut/config.cljs

@@ -4,6 +4,7 @@
             [frontend.extensions.pdf.utils :as pdf-utils]
             [frontend.handler.config :as config-handler]
             [frontend.handler.editor :as editor-handler]
+            [frontend.handler.paste :as paste-handler]
             [frontend.handler.history :as history]
             [frontend.handler.page :as page-handler]
             [frontend.handler.route :as route-handler]
@@ -149,7 +150,7 @@
                        :fn      editor-handler/copy-current-block-embed}
 
    :editor/paste-text-in-one-block-at-point {:binding "mod+shift+v"
-                                             :fn      editor-handler/paste-text-in-one-block-at-point}
+                                             :fn      (fn [_state e] ((paste-handler/editor-on-paste! nil true) e))}
 
    :editor/insert-youtube-timestamp         {:binding "mod+shift+y"
                                              :fn      commands/insert-youtube-timestamp}

+ 3 - 1
src/main/frontend/modules/shortcut/dicts.cljc

@@ -971,6 +971,7 @@
              :command.date-picker/next-week        "Tarih seçici: Sonraki haftayı seç"
              :command.pdf/previous-page            "Geçerli pdf belgesinin önceki sayfası"
              :command.pdf/next-page                "Geçerli pdf belgesinin sonraki sayfası"
+             :command.pdf/close                    "Geçerli pdf görüntüleyiciyi kapat"
              :command.auto-complete/complete       "Otomatik tamamlama: Seçili öğeyi seç"
              :command.auto-complete/prev           "Otomatik tamamlama: Önceki öğeyi seç"
              :command.auto-complete/next           "Otomatik tamamlama: Sonraki öğeyi seç"
@@ -994,6 +995,7 @@
              :command.editor/strike-through        "Üstü çizili"
              :command.editor/clear-block           "Tüm blok içeriğini sil"
              :command.editor/kill-line-before      "İmleç konumundan önceki satırı sil"
+             :command.editor/copy-embed            "Geçerli bloğu işaret eden bir blok eklemesi kopyala"
              :command.editor/kill-line-after       "İmleç konumundan sonraki satırı sil"
              :command.editor/beginning-of-block    "İmleci bir bloğun başına taşı"
              :command.editor/end-of-block          "İmleci bir bloğun sonuna taşı"
@@ -1191,4 +1193,4 @@
              :command.editor/copy-text                        "선택한 영역을 텍스트로 복사"
              :command.pdf/close                               "PDF 닫기"
              }
-})
+})

+ 17 - 18
src/main/frontend/state.cljs

@@ -160,6 +160,7 @@
      :plugin/enabled                        (and (util/electron?)
                                                  ;; true false :theme-only
                                                  ((fnil identity true) (storage/get :lsp-core-enabled)))
+     :plugin/preferences                    nil
      :plugin/indicator-text                 nil
      :plugin/installed-plugins              {}
      :plugin/installed-themes               []
@@ -193,8 +194,8 @@
 
      ;; copied blocks
      :copy/blocks                           {:copy/content nil
-                                             :copy/block-ids nil
-                                             :copy/graph nil}
+                                             :copy/graph nil
+                                             :copy/blocks nil}
 
      :copy/export-block-text-indent-style   (or (storage/get :copy/export-block-text-indent-style)
                                                 "dashes")
@@ -532,9 +533,7 @@
            (->> (remove #(= (:url repo)
                             (:url %))
                         repos)
-                (util/distinct-by :url))))
-  (when (= (get-current-repo) (:url repo))
-    (set-current-repo! (:url (first (get-repos))))))
+                (util/distinct-by :url)))))
 
 (defn set-timestamp-block!
   [value]
@@ -703,6 +702,12 @@
                 (uuid id)))
        (distinct)))
 
+(defn get-selection-start-block-or-first
+  []
+  (or (get-selection-start-block)
+      (some-> (first (get-selection-blocks))
+              (gobj/get "id"))))
+
 (defn in-selection-mode?
   []
   (:selection/mode @state))
@@ -1436,22 +1441,11 @@
   []
   (:copy/blocks @state))
 
-(defn set-copied-blocks
-  [content ids]
-  (set-state! :copy/blocks {:copy/graph (get-current-repo)
-                            :copy/content content
-                            :copy/block-ids ids
-                            :copy/full-blocks nil}))
-
-(defn set-copied-full-blocks
+(defn set-copied-blocks!
   [content blocks]
   (set-state! :copy/blocks {:copy/graph (get-current-repo)
                             :copy/content (or content (get-in @state [:copy/blocks :copy/content]))
-                            :copy/full-blocks blocks}))
-
-(defn set-copied-full-blocks!
-  [blocks]
-  (set-state! [:copy/blocks :copy/full-blocks] blocks))
+                            :copy/blocks blocks}))
 
 (defn get-export-block-text-indent-style []
   (:copy/export-block-text-indent-style @state))
@@ -1698,6 +1692,11 @@
                  (if (fn? m) m
                    (fn [old-value] (merge old-value m)))))
 
+(defn http-proxy-enabled-or-val? []
+  (when-let [agent-opts (sub [:electron/user-cfgs :settings/agent])]
+    (when (every? not-empty (vals agent-opts))
+      (str (:protocol agent-opts) "://" (:host agent-opts) ":" (:port agent-opts)))))
+
 (defn enable-encryption?
   [repo]
   (:feature/enable-encryption?

+ 13 - 12
src/main/frontend/ui.cljs

@@ -100,11 +100,11 @@
 ;; public exports
 (rum/defcs dropdown < (mixins/modal :open?)
   [state content-fn modal-content-fn
-   & [{:keys [modal-class z-index]
+   & [{:keys [modal-class z-index trigger-class]
        :or   {z-index 999}}]]
   (let [{:keys [open?]} state
         modal-content (modal-content-fn state)]
-    [:div.relative.ui__dropdown-trigger {:style {:z-index z-index}}
+    [:div.relative.ui__dropdown-trigger {:style {:z-index z-index} :class trigger-class}
      (content-fn state)
      (css-transition
       {:in @open? :timeout 0}
@@ -125,20 +125,21 @@
    (fn [{:keys [close-fn]}]
      [:div.py-1.rounded-md.shadow-xs
       (when links-header links-header)
-      (for [{:keys [options title icon hr hover-detail]} (if (fn? links) (links) links)]
+      (for [{:keys [options title icon hr hover-detail item]} (if (fn? links) (links) links)]
         (let [new-options
               (merge options
-                     {:title hover-detail
+                     {:title    hover-detail
                       :on-click (fn [e]
-                                  (when-let [on-click-fn (:on-click options)]
-                                    (on-click-fn e))
-                                  (close-fn))})
+                                  (when-not (false? (when-let [on-click-fn (:on-click options)]
+                                                      (on-click-fn e)))
+                                    (close-fn)))})
               child (if hr
                       nil
-                      [:div.flex.items-center
-                       (when icon icon)
-                       [:div {:style {:margin-right "8px"
-                                      :margin-left "4px"}} title]])]
+                      (or item
+                          [:div.flex.items-center
+                           (when icon icon)
+                           [:div {:style {:margin-right "8px"
+                                          :margin-left  "4px"}} title]]))]
           (if hr
             [:hr.my-1 {:key "dropdown-hr"}]
             (rum/with-key
@@ -296,7 +297,7 @@
     (when-let [custom-theme (state/sub [:ui/custom-theme (keyword theme)])]
       (when-let [url (:url custom-theme)]
         (js/LSPluginCore.selectTheme (bean/->js custom-theme)
-                                     (bean/->js {:emit false}))
+                                     (bean/->js {:emit true}))
         (state/set-state! :plugin/selected-theme url)))))
 
 (defn setup-system-theme-effect!

+ 16 - 0
src/main/logseq/api.cljs

@@ -808,6 +808,22 @@
     (plugin-handler/register-extensions-enhancer
       (keyword pid) type {:enhancer enhancer})))
 
+(defonce *request-k (volatile! 0))
+
+(defn ^:export exper_request
+  [pid ^js options]
+  (when-let [^js _pl (plugin-handler/get-plugin-inst pid)]
+    (let [req-id (vreset! *request-k (inc @*request-k))
+          req-cb #(plugin-handler/request-callback _pl req-id %)]
+      (-> (ipc/ipc :httpRequest req-id options)
+          (p/then #(req-cb %))
+          (p/catch #(req-cb %)))
+      req-id)))
+
+(defn ^:export http_request_abort
+  [req-id]
+  (ipc/ipc :httpRequestAbort req-id))
+
 ;; helpers
 (defn ^:export query_element_by_id
   [id]

+ 44 - 13
src/test/frontend/modules/outliner/core_test.cljs

@@ -215,6 +215,37 @@
       (outliner-core/indent-outdent-blocks! [(get-block 13) (get-block 14)] false))
     (is (= [2 12 13 14 16] (get-children 22)))))
 
+(deftest test-fix-top-level-blocks
+  (testing "no need to fix"
+    (let [blocks [{:block/uuid #uuid "62aa668b-e258-445d-aef6-5510054ff495",
+                   :block/properties {},
+                   :block/left #:db{:id 144},
+                   :block/format :markdown,
+                   :block/level 1,
+                   :block/content "a",
+                   :db/id 145,
+                   :block/parent #:db{:id 144},
+                   :block/page #:db{:id 144}}
+                  {:block/uuid #uuid "62aa668d-65d1-440c-849b-a0717f691193",
+                   :block/properties {},
+                   :block/left #:db{:id 145},
+                   :block/format :markdown,
+                   :block/level 1,
+                   :block/content "b",
+                   :db/id 146,
+                   :block/parent #:db{:id 144},
+                   :block/page #:db{:id 144}}
+                  {:block/uuid #uuid "62aa668e-f866-48ee-b8fe-737e101c548d",
+                   :block/properties {},
+                   :block/left #:db{:id 146},
+                   :block/format :markdown,
+                   :block/level 1,
+                   :block/content "c",
+                   :db/id 147,
+                   :block/parent #:db{:id 144},
+                   :block/page #:db{:id 144}}]]
+      (= blocks (outliner-core/fix-top-level-blocks blocks)))))
+
 (deftest test-outdent-blocks
   (testing "
   [1 [[2 [[3]
@@ -416,7 +447,9 @@
 (defn transact-random-tree!
   []
   (let [tree (gen-safe-tree)]
-    (transact-tree! tree)))
+    (if (seq tree)
+      (transact-tree! tree)
+      (transact-random-tree!))))
 
 (defn get-datoms
   []
@@ -458,9 +491,6 @@
                                   (set/union old (set (map :block/uuid blocks)))))
           (insert-blocks! blocks (get-random-block)))
         (let [total (get-blocks-count)]
-          ;; (when (not= total (count @*random-blocks))
-          ;;   (defonce wrong-db (db/get-db test-db))
-          ;;   (defonce random-blocks @*random-blocks))
           (is (= total (count @*random-blocks))))))))
 
 (deftest ^:long random-deletes
@@ -518,16 +548,17 @@
           *random-blocks (atom c1)]
       (dotimes [_i 100]
         ;; (prn "Random move indent/outdent: " i)
-        (let [blocks (gen-blocks)]
+        (let [new-blocks (gen-blocks)]
           (swap! *random-blocks (fn [old]
-                                  (set/union old (set (map :block/uuid blocks)))))
-          (insert-blocks! blocks (get-random-block)))
-        (let [blocks (get-random-successive-blocks)]
-          (when (seq blocks)
-            (outliner-tx/transact! {:graph test-db}
-              (outliner-core/indent-outdent-blocks! blocks (gen/generate gen/boolean)))
-            (let [total (get-blocks-count)]
-              (is (= total (count @*random-blocks))))))))))
+                                  (set/union old (set (map :block/uuid new-blocks)))))
+          (insert-blocks! new-blocks (get-random-block))
+          (let [blocks (get-random-successive-blocks)
+                indent? (gen/generate gen/boolean)]
+            (when (seq blocks)
+              (outliner-tx/transact! {:graph test-db}
+                (outliner-core/indent-outdent-blocks! blocks indent?))
+              (let [total (get-blocks-count)]
+                (is (= total (count @*random-blocks)))))))))))
 
 (deftest ^:long random-mixed-ops
   (testing "Random mixed operations"

+ 10 - 7
tldraw/apps/tldraw-logseq/src/app.tsx

@@ -5,7 +5,8 @@ import {
   AppCanvas,
   AppProvider,
   TLReactCallbacks,
-  TLReactComponents, TLReactToolConstructor
+  TLReactComponents,
+  TLReactToolConstructor,
 } from '@tldraw/react'
 import * as React from 'react'
 import { AppUI } from '~components/AppUI'
@@ -25,7 +26,7 @@ import {
   PencilTool,
   PolygonTool,
   TextTool,
-  YouTubeTool
+  YouTubeTool,
 } from '~lib/tools'
 
 const components: TLReactComponents<Shape> = {
@@ -54,16 +55,18 @@ interface LogseqTldrawProps {
   onPersist?: TLReactCallbacks<Shape>['onPersist']
 }
 
-export const App = function App(props: LogseqTldrawProps): JSX.Element {
+export const App = function App({
+  searchHandler,
+  PageComponent,
+  ...props
+}: LogseqTldrawProps): JSX.Element {
   const onFileDrop = useFileDrop()
   const onPaste = usePaste()
 
-  const Page = React.useMemo(() => React.memo(props.PageComponent), [])
-
+  const Page = React.useMemo(() => React.memo(PageComponent), [])
   return (
-    <LogseqContext.Provider value={{ Page, search: props.searchHandler }}>
+    <LogseqContext.Provider value={{ Page, search: searchHandler }}>
       <AppProvider
-        onMount={props.onMount}
         Shapes={shapes}
         Tools={tools}
         onFileDrop={onFileDrop}

+ 86 - 60
tldraw/apps/tldraw-logseq/src/hooks/usePaste.ts

@@ -8,18 +8,19 @@ import {
   uniqueId,
 } from '@tldraw/core'
 import type { TLReactCallbacks } from '@tldraw/react'
+import { transaction } from 'mobx'
 import * as React from 'react'
-import type { Shape } from '~lib'
+import { LogseqPortalShape, Shape } from '~lib'
 
 export function usePaste() {
-  return React.useCallback<TLReactCallbacks<Shape>['onFileDrop']>(async (app, { point }) => {
+  return React.useCallback<TLReactCallbacks<Shape>['onPaste']>(async (app, { point }) => {
     const assetId = uniqueId()
     interface ImageAsset extends TLAsset {
       size: number[]
     }
 
     const assetsToCreate: ImageAsset[] = []
-    const shapesToCreate: TLShapeModel[] = []
+    const shapesToCreate: Shape['props'][] = []
     const bindingsToCreate: TLBinding[] = []
 
     async function handleImage(item: ClipboardItem) {
@@ -47,64 +48,83 @@ export function usePaste() {
     }
 
     async function handleLogseqShapes(item: ClipboardItem) {
-      const plainTextType = item.types.find(type => type.startsWith('text/plain'))
-      if (plainTextType) {
-        const blob = await item.getType(plainTextType)
-        const rawText = await blob.text()
-        const data = JSON.parse(rawText)
-        if (data.type === 'logseq/whiteboard-shapes') {
-          const shapes = data.shapes as TLShapeModel[]
-          const commonBounds = BoundsUtils.getCommonBounds(
-            shapes.map(shape => ({
-              minX: shape.point?.[0] ?? point[0],
-              minY: shape.point?.[1] ?? point[1],
-              width: shape.size?.[0] ?? 4,
-              height: shape.size?.[1] ?? 4,
-              maxX: (shape.point?.[0] ?? point[0]) + (shape.size?.[0] ?? 4),
-              maxY: (shape.point?.[1] ?? point[1]) + (shape.size?.[1] ?? 4),
-            }))
-          )
-          const clonedShapes = shapes.map((shape: TLShapeModel) => {
-            return {
-              ...shape,
-              id: uniqueId(),
-              parentId: app.currentPageId,
-              point: [
-                point[0] + shape.point![0] - commonBounds.minX,
-                point[1] + shape.point![1] - commonBounds.minY,
-              ],
-            }
-          })
-          shapesToCreate.push(...clonedShapes)
+      if (item.types.includes('text/plain')) {
+        const blob = await item.getType('text/plain')
+        const rawText = (await blob.text()).trim()
 
-          // Try to rebinding the shapes to the new assets
-          shapesToCreate.forEach((s, idx) => {
-            if (s.handles) {
-              Object.values(s.handles).forEach(h => {
-                if (h.bindingId) {
-                  // try to bind the new shape
-                  const binding = app.currentPage.bindings[h.bindingId]
-                  // if the copied binding from/to is in the source
-                  const oldFromIdx = shapes.findIndex(s => s.id === binding.fromId)
-                  const oldToIdx = shapes.findIndex(s => s.id === binding.toId)
-                  if (binding && oldFromIdx !== -1 && oldToIdx !== -1) {
-                    const newBinding: TLBinding = {
-                      ...binding,
-                      id: uniqueId(),
-                      fromId: shapesToCreate[oldFromIdx].id,
-                      toId: shapesToCreate[oldToIdx].id,
+        try {
+          const data = JSON.parse(rawText)
+          if (data.type === 'logseq/whiteboard-shapes') {
+            const shapes = data.shapes as TLShapeModel[]
+            const commonBounds = BoundsUtils.getCommonBounds(
+              shapes.map(shape => ({
+                minX: shape.point?.[0] ?? point[0],
+                minY: shape.point?.[1] ?? point[1],
+                width: shape.size?.[0] ?? 4,
+                height: shape.size?.[1] ?? 4,
+                maxX: (shape.point?.[0] ?? point[0]) + (shape.size?.[0] ?? 4),
+                maxY: (shape.point?.[1] ?? point[1]) + (shape.size?.[1] ?? 4),
+              }))
+            )
+            const clonedShapes = shapes.map(shape => {
+              return {
+                ...shape,
+                id: uniqueId(),
+                parentId: app.currentPageId,
+                point: [
+                  point[0] + shape.point![0] - commonBounds.minX,
+                  point[1] + shape.point![1] - commonBounds.minY,
+                ],
+              }
+            })
+            // @ts-expect-error - This is a valid shape
+            shapesToCreate.push(...clonedShapes)
+
+            // Try to rebinding the shapes to the new assets
+            shapesToCreate.forEach((s, idx) => {
+              if (s.handles) {
+                Object.values(s.handles).forEach(h => {
+                  if (h.bindingId) {
+                    // try to bind the new shape
+                    const binding = app.currentPage.bindings[h.bindingId]
+                    // if the copied binding from/to is in the source
+                    const oldFromIdx = shapes.findIndex(s => s.id === binding.fromId)
+                    const oldToIdx = shapes.findIndex(s => s.id === binding.toId)
+                    if (binding && oldFromIdx !== -1 && oldToIdx !== -1) {
+                      const newBinding: TLBinding = {
+                        ...binding,
+                        id: uniqueId(),
+                        fromId: shapesToCreate[oldFromIdx].id,
+                        toId: shapesToCreate[oldToIdx].id,
+                      }
+                      bindingsToCreate.push(newBinding)
+                      h.bindingId = newBinding.id
+                    } else {
+                      h.bindingId = undefined
                     }
-                    bindingsToCreate.push(newBinding)
-                    h.bindingId = newBinding.id
-                  } else {
-                    h.bindingId = undefined
                   }
-                }
-              })
-            }
-          })
+                })
+              }
+            })
+            return true
+          }
+        } catch {
+          const blockRefEg = '((62af02d0-0443-42e8-a284-946c162b0f89))'
+          if (/^\(\(.*\)\)$/.test(rawText) && rawText.length === blockRefEg.length) {
+            const blockRef = rawText.slice(2, -2)
+            shapesToCreate.push({
+              ...LogseqPortalShape.defaultProps,
+              id: uniqueId(),
+              parentId: app.currentPageId,
+              point: [point[0], point[1]],
+              size: [600, 400],
+              pageId: blockRef,
+              blockType: 'B',
+            })
+          }
         }
       }
+      return false
     }
 
     // TODO: supporting other pasting formats
@@ -131,10 +151,16 @@ export function usePaste() {
       })),
       ...shapesToCreate,
     ]
-    app.createAssets(assetsToCreate)
-    app.createShapes(allShapesToAdd)
-    app.currentPage.updateBindings(Object.fromEntries(bindingsToCreate.map(b => [b.id, b])))
 
-    app.setSelectedShapes(allShapesToAdd.map(s => s.id))
+    app.transaction(() => {
+      if (assetsToCreate.length > 0) {
+        app.createAssets(assetsToCreate)
+      }
+      if (allShapesToAdd.length > 0) {
+        app.createShapes(allShapesToAdd)
+      }
+      app.currentPage.updateBindings(Object.fromEntries(bindingsToCreate.map(b => [b.id, b])))
+      app.setSelectedShapes(allShapesToAdd.map(s => s.id))
+    })
   }, [])
 }

+ 14 - 3
tldraw/apps/tldraw-logseq/src/lib/shapes/LogseqPortalShape.tsx

@@ -18,8 +18,9 @@ const HEADER_HEIGHT = 40
 export interface LogseqPortalShapeProps extends TLBoxShapeProps, CustomStyleProps {
   type: 'logseq-portal'
   pageId: string // page name or UUID
-  collapsed: boolean
-  collapsedHeight: number
+  blockType?: 'P' | 'B'
+  collapsed?: boolean
+  collapsedHeight?: number
 }
 
 interface LogseqQuickSearchProps {
@@ -125,6 +126,14 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
     }
   }
 
+  static isPageOrBlock(id: string): 'P' | 'B' | false {
+    const blockRefEg = '((62af02d0-0443-42e8-a284-946c162b0f89))'
+    if (id) {
+      return /^\(\(.*\)\)$/.test(id) && id.length === blockRefEg.length ? 'B' : 'P'
+    }
+    return false
+  }
+
   ReactContextBar = observer(() => {
     return (
       <>
@@ -203,9 +212,11 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
 
     const onPageNameChanged = React.useCallback((id: string) => {
       transaction(() => {
+        app.history.resume()
         this.update({
           pageId: id,
           size: [600, 320],
+          blockType: 'page',
         })
         this.setDraft(false)
         app.setActivatedShapes([])
@@ -254,7 +265,7 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
                 '--ls-title-text-color': stroke,
               }}
             >
-              <LogseqPortalShapeHeader type="P" pageId={pageId} />
+              <LogseqPortalShapeHeader type={this.props.blockType ?? 'P'} pageId={pageId} />
               {(!this.props.collapsed || isActivated) && (
                 <div
                   style={{

+ 1 - 0
tldraw/demo/src/App.jsx

@@ -5,6 +5,7 @@ import { App as TldrawApp } from 'tldraw-logseq'
 const storingKey = 'playground.index'
 
 const onPersist = app => {
+  console.log('onPersist', app)
   window.sessionStorage.setItem(storingKey, JSON.stringify(app.serialized))
 }
 

+ 14 - 0
tldraw/packages/core/src/lib/TLApi/TLApi.ts

@@ -1,3 +1,4 @@
+import Vec from '@tldraw/vec'
 import type { TLApp, TLPage, TLShapeModel, TLShape } from '~lib'
 import type { TLEventMap } from '~types'
 import { BoundsUtils } from '~utils'
@@ -132,6 +133,19 @@ export class TLApi<S extends TLShape = TLShape, K extends TLEventMap = TLEventMa
     return this
   }
 
+  cameraToCenter = (): this => {
+    const { shapes } = this.app.currentPage
+    // Viewport should be focused to existing shapes
+    const commonBounds = BoundsUtils.getCommonBounds(shapes.map(shape => shape.bounds))
+    this.app.viewport.update({
+      point: Vec.add(Vec.neg(BoundsUtils.getBoundsCenter(commonBounds)), [
+        this.app.viewport.currentView.width / 2,
+        this.app.viewport.currentView.height/ 2,
+      ]),
+    })
+    return this
+  }
+
   /** Zoom to fit the current selection in the viewport. */
   zoomToSelection = (): this => {
     const { selectionBounds } = this.app

+ 11 - 1
tldraw/packages/core/src/lib/TLApp/TLApp.ts

@@ -2,7 +2,7 @@
 /* eslint-disable @typescript-eslint/no-non-null-assertion */
 /* eslint-disable @typescript-eslint/no-explicit-any */
 import { Vec } from '@tldraw/vec'
-import { action, computed, makeObservable, observable, toJS } from 'mobx'
+import { action, computed, makeObservable, observable, toJS, transaction } from 'mobx'
 import { BoundsUtils, KeyUtils } from '~utils'
 import {
   TLSelectTool,
@@ -191,6 +191,7 @@ export class TLApp<
   loadDocumentModel(model: TLDocumentModel<S>): this {
     this.history.deserialize(model)
     if (model.assets) this.addAssets(model.assets)
+
     return this
   }
 
@@ -719,6 +720,15 @@ export class TLApp<
     return Shape
   }
 
+  transaction = (fn: () => void) => {
+    transaction(() => {
+      this.history.pause()
+      fn()
+      this.history.resume()
+      this.persist()
+    })
+  }
+
   /* ------------------ Subscriptions ----------------- */
 
   private subscriptions = new Set<TLSubscription<S, K, this, any>>([])

+ 5 - 1
tldraw/packages/core/src/lib/TLHistory.ts

@@ -1,5 +1,6 @@
 import { TLApp, TLPage, TLDocumentModel, TLShape } from '~lib'
 import type { TLEventMap } from '~types'
+import { deepEqual } from '~utils'
 
 export class TLHistory<S extends TLShape = TLShape, K extends TLEventMap = TLEventMap> {
   constructor(app: TLApp<S, K>) {
@@ -12,7 +13,7 @@ export class TLHistory<S extends TLShape = TLShape, K extends TLEventMap = TLEve
   isPaused = true
 
   get creating() {
-    return this.app.selectedTool.currentState.id === 'creating';
+    return this.app.selectedTool.currentState.id === 'creating'
   }
 
   pause = () => {
@@ -38,6 +39,9 @@ export class TLHistory<S extends TLShape = TLShape, K extends TLEventMap = TLEve
 
     const { serialized } = this.app
 
+    // Do not persist if the serialized state is the same as the last one
+    if (deepEqual(this.stack[this.pointer], serialized)) return
+
     if (this.pointer < this.stack.length) {
       this.stack = this.stack.slice(0, this.pointer + 1)
     }

+ 1 - 0
tldraw/packages/core/src/lib/TLPage/TLPage.ts

@@ -280,6 +280,7 @@ export class TLPage<S extends TLShape = TLShape, E extends TLEventMap = TLEventM
 
     if (!deepEqual(updated, curr) || shapesToDelete.length) {
       transaction(() => {
+        this.app.history.resume()
         this.update({
           bindings: updated.bindings,
         })

+ 1 - 0
tldraw/packages/core/src/lib/tools/TLSelectTool/states/PointingCanvasState.ts

@@ -53,6 +53,7 @@ export class PointingCanvasState<
           point: [...this.app.inputs.originPoint],
         })
         shape.setDraft(true)
+        this.app.history.pause()
         this.app.setActivatedShapes([shape.id])
         this.app.currentPage.addShapes(shape)
       }

+ 5 - 0
tldraw/packages/react/src/hooks/useResizeObserver.ts

@@ -1,5 +1,6 @@
 import * as React from 'react'
 import { TLViewport, TLBounds, debounce } from '@tldraw/core'
+import { useApp } from './useApp'
 
 const getNearestScrollableContainer = (element: HTMLElement): HTMLElement | Document => {
   let parent = element.parentElement
@@ -25,6 +26,7 @@ export function useResizeObserver<T extends HTMLElement>(
   viewport: TLViewport,
   onBoundsChange?: (bounds: TLBounds) => void
 ) {
+  const app = useApp()
   const rIsMounted = React.useRef(false)
 
   // When the element resizes, update the bounds (stored in inputs)
@@ -81,5 +83,8 @@ export function useResizeObserver<T extends HTMLElement>(
 
   React.useLayoutEffect(() => {
     updateBounds()
+    setTimeout(() => {
+      app.api.cameraToCenter()
+    })
   }, [ref])
 }

+ 0 - 5
yarn.lock

@@ -6811,11 +6811,6 @@ [email protected]:
     react-draggable "3.x"
     react-resizable "1.x"
 
[email protected]:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/react-icon-base/-/react-icon-base-2.1.0.tgz#a196e33fdf1e7aaa1fda3aefbb68bdad9e82a79d"
-  integrity sha512-9wwKJa2LB8ujtJB5MAXYYEM7JfYThZTj0YnfGxzLLWkifaLIGc7iTde2EpJ7ka5MjneRHnlxbIn5VV9k2WjUVA==
-
 react-icon-base@^2.1.2:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/react-icon-base/-/react-icon-base-2.1.2.tgz#a17101dad9c1192652356096860a9ab43a0766c7"

部分文件因为文件数量过多而无法显示