浏览代码

Merge branch 'master' into fix/namespaced-alias

Tienson Qin 2 年之前
父节点
当前提交
2220fef445
共有 100 个文件被更改,包括 2811 次插入1123 次删除
  1. 1 0
      .clj-kondo/config.edn
  2. 2 0
      .github/workflows/build-ios.yml
  3. 11 11
      .github/workflows/build-stage.yml
  4. 8 1
      README.md
  5. 1 0
      android/app/capacitor.build.gradle
  6. 4 0
      android/app/src/main/assets/capacitor.plugins.json
  7. 2 0
      android/app/src/main/res/values/colors.xml
  8. 2 0
      android/app/src/main/res/values/styles.xml
  9. 3 0
      android/capacitor.settings.gradle
  10. 1 1
      deps/db/bb.edn
  11. 56 55
      deps/db/src/logseq/db/rules.cljc
  12. 16 7
      deps/graph-parser/src/logseq/graph_parser/whiteboard.cljs
  13. 1 1
      e2e-tests/accessibility.spec.ts
  14. 35 0
      e2e-tests/editor.spec.ts
  15. 2 1
      e2e-tests/page-rename.spec.ts
  16. 3 4
      e2e-tests/random.spec.ts
  17. 9 0
      e2e-tests/sanitization.spec.ts
  18. 1 1
      e2e-tests/utils.ts
  19. 22 22
      e2e-tests/whiteboards.spec.ts
  20. 1 0
      ios/App/Podfile
  21. 1 0
      libs/.npmignore
  22. 30 0
      libs/CHANGELOG.md
  23. 10 0
      libs/babel.config.json
  24. 9 2
      libs/package.json
  25. 1 0
      libs/src/LSPlugin.caller.ts
  26. 13 4
      libs/src/LSPlugin.core.ts
  27. 39 2
      libs/src/LSPlugin.ts
  28. 25 3
      libs/src/LSPlugin.user.ts
  29. 1 1
      libs/src/modules/LSPlugin.Experiments.ts
  30. 82 0
      libs/src/modules/LSPlugin.Search.ts
  31. 16 2
      libs/webpack.config.js
  32. 956 4
      libs/yarn.lock
  33. 2 1
      package.json
  34. 0 11
      public/index.html
  35. 2 8
      resources/css/common.css
  36. 52 20
      resources/css/tabler-extension.css
  37. 二进制
      resources/fonts/tabler-icons-extension.woff2
  38. 0 0
      resources/js/lsplugin.core.js
  39. 0 0
      resources/js/lsplugin.user.js
  40. 9 0
      resources/js/swiped-events.min.js
  41. 1 1
      src/electron/electron/plugin.cljs
  42. 56 38
      src/main/electron/listener.cljs
  43. 1 0
      src/main/frontend/commands.cljs
  44. 13 21
      src/main/frontend/components/block.cljs
  45. 3 6
      src/main/frontend/components/content.cljs
  46. 9 5
      src/main/frontend/components/editor.cljs
  47. 3 4
      src/main/frontend/components/file_sync.cljs
  48. 4 4
      src/main/frontend/components/header.cljs
  49. 5 3
      src/main/frontend/components/header.css
  50. 24 10
      src/main/frontend/components/lazy_editor.cljs
  51. 6 4
      src/main/frontend/components/page.cljs
  52. 2 1
      src/main/frontend/components/page_menu.cljs
  53. 56 13
      src/main/frontend/components/search.cljs
  54. 32 35
      src/main/frontend/components/search.css
  55. 5 5
      src/main/frontend/components/settings.cljs
  56. 205 116
      src/main/frontend/components/sidebar.cljs
  57. 39 11
      src/main/frontend/components/sidebar.css
  58. 20 19
      src/main/frontend/components/svg.cljs
  59. 1 1
      src/main/frontend/components/theme.cljs
  60. 77 47
      src/main/frontend/components/whiteboard.cljs
  61. 36 19
      src/main/frontend/db/model.cljs
  62. 54 25
      src/main/frontend/dicts.cljc
  63. 5 2
      src/main/frontend/extensions/code.cljs
  64. 1 3
      src/main/frontend/extensions/html_parser.cljs
  65. 48 19
      src/main/frontend/extensions/pdf/assets.cljs
  66. 81 71
      src/main/frontend/extensions/pdf/highlights.cljs
  67. 17 1
      src/main/frontend/extensions/pdf/pdf.css
  68. 12 2
      src/main/frontend/extensions/pdf/toolbar.cljs
  69. 14 0
      src/main/frontend/extensions/pdf/utils.cljs
  70. 22 8
      src/main/frontend/extensions/sci.cljs
  71. 39 17
      src/main/frontend/extensions/tldraw.cljs
  72. 14 12
      src/main/frontend/fs.cljs
  73. 4 2
      src/main/frontend/fs/capacitor_fs.cljs
  74. 325 296
      src/main/frontend/fs/sync.cljs
  75. 1 1
      src/main/frontend/fs/watcher_handler.cljs
  76. 6 4
      src/main/frontend/handler.cljs
  77. 1 1
      src/main/frontend/handler/block.cljs
  78. 3 3
      src/main/frontend/handler/config.cljs
  79. 1 1
      src/main/frontend/handler/conversion.cljs
  80. 40 20
      src/main/frontend/handler/editor.cljs
  81. 2 2
      src/main/frontend/handler/events.cljs
  82. 4 2
      src/main/frontend/handler/file.cljs
  83. 19 11
      src/main/frontend/handler/file_sync.cljs
  84. 2 57
      src/main/frontend/handler/mobile/swipe.cljs
  85. 4 2
      src/main/frontend/handler/page.cljs
  86. 4 3
      src/main/frontend/handler/paste.cljs
  87. 16 5
      src/main/frontend/handler/plugin.cljs
  88. 1 1
      src/main/frontend/handler/plugin_config.cljs
  89. 1 1
      src/main/frontend/handler/repo.cljs
  90. 1 1
      src/main/frontend/handler/repo_config.cljs
  91. 7 5
      src/main/frontend/handler/route.cljs
  92. 2 1
      src/main/frontend/handler/search.cljs
  93. 10 8
      src/main/frontend/handler/whiteboard.cljs
  94. 1 1
      src/main/frontend/idb.cljs
  95. 1 2
      src/main/frontend/mobile/core.cljs
  96. 1 1
      src/main/frontend/mobile/graph_picker.cljs
  97. 6 3
      src/main/frontend/mobile/index.css
  98. 9 2
      src/main/frontend/mobile/util.cljs
  99. 3 2
      src/main/frontend/modules/file/core.cljs
  100. 2 0
      src/main/frontend/modules/outliner/pipeline.cljs

+ 1 - 0
.clj-kondo/config.edn

@@ -42,6 +42,7 @@
              frontend.db.query-react query-react
              frontend.db.query-react query-react
              frontend.diff diff
              frontend.diff diff
              frontend.encrypt encrypt
              frontend.encrypt encrypt
+             frontend.extensions.sci sci
              frontend.format.mldoc mldoc
              frontend.format.mldoc mldoc
              frontend.format.block block
              frontend.format.block block
              frontend.fs fs
              frontend.fs fs

+ 2 - 0
.github/workflows/build-ios.yml

@@ -8,10 +8,12 @@ on:
     branches: [master]
     branches: [master]
     paths:
     paths:
       - 'ios/App'
       - 'ios/App'
+      - package.json
   pull_request:
   pull_request:
     branches: [master]
     branches: [master]
     paths:
     paths:
       - 'ios/App'
       - 'ios/App'
+      - package.json
 
 
 env:
 env:
   CLOJURE_VERSION: '1.10.1.763'
   CLOJURE_VERSION: '1.10.1.763'

+ 11 - 11
.github/workflows/build-stage.yml

@@ -37,16 +37,16 @@ jobs:
         run: yarn cache clean && yarn install --frozen-lockfile
         run: yarn cache clean && yarn install --frozen-lockfile
 
 
       - name: Build Released-Web
       - name: Build Released-Web
-        run: yarn gulp:build && clojure -M:cljs release app  --config-merge '{:asset-path "${{env.asset-path}}"}'
+        run: |
+          yarn gulp:build && clojure -M:cljs release app  --config-merge '{:asset-path "${{env.asset-path}}" :compiler-options {:source-map-include-sources-content false :source-map-detail-level :symbols}}'
+          ls -ah ./static/js
 
 
-      - uses: jakejarvis/s3-sync-action@master
+      - name: Publish to Cloudflare Pages
+        uses: cloudflare/pages-action@1
         with:
         with:
-            #args: --acl public-read --follow-symlinks --delete
-            args: --acl public-read --follow-symlinks
-        env:
-          AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
-          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
-          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
-          AWS_REGION: 'us-west-1'   # optional: defaults to us-east-1
-          SOURCE_DIR: 'static'      # optional: defaults to entire repository
-          DEST_DIR: ${GITHUB_REF##*/}/static
+          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+          accountId: 2553ea8236c11ea0f88de28fce1cbfee
+          projectName: 'logseq-demo'
+          directory: 'static'
+          gitHubToken: ${{ secrets.GITHUB_TOKEN }}
+          branch: 'production'

+ 8 - 1
README.md

@@ -83,10 +83,17 @@ We have [a dedicated overview page](https://github.com/logseq/logseq/blob/master
 ## Set up development environment
 ## Set up development environment
 * For setting up web app / desktop app development environment on macOS / Linux, please refer to [Develop Logseq](docs/develop-logseq.md).
 * For setting up web app / desktop app development environment on macOS / Linux, please refer to [Develop Logseq](docs/develop-logseq.md).
 
 
-* For Windows users, please refer to [Develop LogSeq on Windows](docs/develop-logseq-on-windows.md) in addition.
+* For Windows users, please refer to [Develop Logseq on Windows](docs/develop-logseq-on-windows.md) in addition.
 
 
 There are more guides in [docs/](docs/), e.g. the [Guide for contributing to translations](docs/contributing-to-translations.md) and the [Docker web app guide](docs/docker-web-app-guide.md)
 There are more guides in [docs/](docs/), e.g. the [Guide for contributing to translations](docs/contributing-to-translations.md) and the [Docker web app guide](docs/docker-web-app-guide.md)
 
 
+## How to contribute with a PR
+If you would like to contribute by solving an open issue, please fork this repository and then create a branch for the fix.
+
+Once you push your code to your fork you we'll be able to open a PR into Logseq repository. For more info you can follow this guide from [Github docs](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork)
+
+And here a list of some [good firt issues](https://github.com/logseq/logseq/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22)!
+
 ## Thanks
 ## Thanks
 
 
 [![JetBrains](docs/assets/jetbrains.svg)](https://www.jetbrains.com/?from=logseq)
 [![JetBrains](docs/assets/jetbrains.svg)](https://www.jetbrains.com/?from=logseq)

+ 1 - 0
android/app/capacitor.build.gradle

@@ -19,6 +19,7 @@ dependencies {
     implementation project(':capacitor-splash-screen')
     implementation project(':capacitor-splash-screen')
     implementation project(':capacitor-status-bar')
     implementation project(':capacitor-status-bar')
     implementation project(':capawesome-capacitor-background-task')
     implementation project(':capawesome-capacitor-background-task')
+    implementation project(':hugotomazi-capacitor-navigation-bar')
     implementation project(':logseq-capacitor-file-sync')
     implementation project(':logseq-capacitor-file-sync')
     implementation project(':capacitor-voice-recorder')
     implementation project(':capacitor-voice-recorder')
     implementation project(':send-intent')
     implementation project(':send-intent')

+ 4 - 0
android/app/src/main/assets/capacitor.plugins.json

@@ -39,6 +39,10 @@
 		"pkg": "@capawesome/capacitor-background-task",
 		"pkg": "@capawesome/capacitor-background-task",
 		"classpath": "io.capawesome.capacitorjs.plugins.backgroundtask.BackgroundTaskPlugin"
 		"classpath": "io.capawesome.capacitorjs.plugins.backgroundtask.BackgroundTaskPlugin"
 	},
 	},
+	{
+		"pkg": "@hugotomazi/capacitor-navigation-bar",
+		"classpath": "br.com.tombus.capacitor.plugin.navigationbar.NavigationBarPlugin"
+	},
 	{
 	{
 		"pkg": "@logseq/capacitor-file-sync",
 		"pkg": "@logseq/capacitor-file-sync",
 		"classpath": "com.logseq.app.filesync.FileSyncPlugin"
 		"classpath": "com.logseq.app.filesync.FileSyncPlugin"

+ 2 - 0
android/app/src/main/res/values/colors.xml

@@ -1,4 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
 <resources>
     <color name="logoPrimary">#002b36</color>
     <color name="logoPrimary">#002b36</color>
+    <color name="colorPrimary">#ffffff</color>
+    <color name="colorPrimaryDark">#002b36</color>
 </resources>
 </resources>

+ 2 - 0
android/app/src/main/res/values/styles.xml

@@ -14,6 +14,8 @@
         <item name="windowNoTitle">true</item>
         <item name="windowNoTitle">true</item>
         <item name="android:background">@null</item>
         <item name="android:background">@null</item>
         <item name="android:windowIsTranslucent">true</item>
         <item name="android:windowIsTranslucent">true</item>
+        <item name="android:navigationBarColor">@color/colorPrimary</item>
+        <item name="android:statusBarColor">@color/colorPrimary</item>
     </style>
     </style>
 
 
     <!-- App Starting -->
     <!-- App Starting -->

+ 3 - 0
android/capacitor.settings.gradle

@@ -32,6 +32,9 @@ project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacit
 include ':capawesome-capacitor-background-task'
 include ':capawesome-capacitor-background-task'
 project(':capawesome-capacitor-background-task').projectDir = new File('../node_modules/@capawesome/capacitor-background-task/android')
 project(':capawesome-capacitor-background-task').projectDir = new File('../node_modules/@capawesome/capacitor-background-task/android')
 
 
+include ':hugotomazi-capacitor-navigation-bar'
+project(':hugotomazi-capacitor-navigation-bar').projectDir = new File('../node_modules/@hugotomazi/capacitor-navigation-bar/android')
+
 include ':logseq-capacitor-file-sync'
 include ':logseq-capacitor-file-sync'
 project(':logseq-capacitor-file-sync').projectDir = new File('../node_modules/@logseq/capacitor-file-sync/android')
 project(':logseq-capacitor-file-sync').projectDir = new File('../node_modules/@logseq/capacitor-file-sync/android')
 
 

+ 1 - 1
deps/db/bb.edn

@@ -27,7 +27,7 @@
               [logseq.db.rules :as rules])
               [logseq.db.rules :as rules])
    :doc "Lint datalog rules for parsability and unbound variables"
    :doc "Lint datalog rules for parsability and unbound variables"
    :task (datalog/lint-rules
    :task (datalog/lint-rules
-          (into rules/rules
+          (into (mapcat val rules/rules)
                 (-> rules/query-dsl-rules
                 (-> rules/query-dsl-rules
                     ;; TODO: Update linter to handle false positive on ?str-val
                     ;; TODO: Update linter to handle false positive on ?str-val
                     (dissoc :property)
                     (dissoc :property)

+ 56 - 55
deps/db/src/logseq/db/rules.cljc

@@ -2,62 +2,63 @@
   "Datalog rules for use with logseq.db.schema")
   "Datalog rules for use with logseq.db.schema")
 
 
 (def ^:large-vars/data-var rules
 (def ^:large-vars/data-var rules
+  "Rules used mainly in frontend.db.model"
   ;; rule "parent" is optimized for parent node -> child node nesting queries
   ;; rule "parent" is optimized for parent node -> child node nesting queries
-  '[[(parent ?p ?c)
-     [?c :block/parent ?p]]
-    [(parent ?p ?c)
-     [?c :block/parent ?t]
-     (parent ?p ?t)]
-
-  ;; rule "child" is optimized for child node -> parent node nesting queries
-    [(child ?p ?c)
-     [?c :block/parent ?p]]
-    [(child ?p ?c)
-     [?t :block/parent ?p]
-     (child ?t ?c)]
-
-  ;; rule "namespace" is optimized for child node -> node of upper namespace level nesting queries
-    [(namespace ?p ?c)
-     [?c :block/namespace ?p]]
-    [(namespace ?p ?c)
-     [?t :block/namespace ?p]
-     (namespace ?t ?c)]
-
-    ;; Select rules carefully, as it is critical for performance.
-    ;; The rules have different clause order and resolving directions.
-    ;; Clause order Reference:
-    ;; https://docs.datomic.com/on-prem/query/query-executing.html#clause-order
-    ;; Recursive optimization Reference:
-    ;; https://stackoverflow.com/questions/42457136/recursive-datalog-queries-for-datomic-really-slow
-    ;; Should optimize for query the decendents of a block
-    ;; Quote:
-    ;; My theory is that your rules are not written in a way that Datalog can optimize for this read pattern - probably resulting in a traversal of all the entities. I suggest to rewrite them as follows:
-    ;; [[(ubersymbol ?c ?p)
-    ;;   (?c :ml/parent ?p)]
-    ;;  [(ubersymbol ?c ?p)
-    ;;   ;; we bind a child of the ancestor, instead of a parent of the descendant
-    ;;   (?c1 :ml/parent ?p)
-    ;;   (ubersymbol ?c ?c1)]]
-
-    ;; This way of writing the ruleset is optimized to find the descendants of some node. The way you originally wrote it is optimized to find the anscestors of some node.
-
-    ;; from https://stackoverflow.com/questions/43784258/find-entities-whose-ref-to-many-attribute-contains-all-elements-of-input
-    ;; Quote:
-    ;; You're tackling the general problem of 'dynamic conjunction' in Datomic's Datalog.
-    ;; Write a dynamic Datalog query which uses 2 negations and 1 disjunction or a recursive rule
-    ;; Datalog has no direct way of expressing dynamic conjunction (logical AND / 'for all ...' / set intersection).
-    ;; However, you can achieve it in pure Datalog by combining one disjunction
-    ;; (logical OR / 'exists ...' / set union) and two negations, i.e
-    ;; (For all ?g in ?Gs p(?e,?g)) <=> NOT(Exists ?g in ?Gs, such that NOT(p(?e, ?g)))
-
-    ;; [(matches-all ?e ?a ?vs)
-    ;;  [(first ?vs) ?v0]
-    ;;  [?e ?a ?v0]
-    ;;  (not-join [?e ?vs]
-    ;;            [(identity ?vs) [?v ...]]
-    ;;            (not-join [?e ?v]
-    ;;                      [?e ?a ?v]))]
-    ])
+  {:namespace
+   '[[(namespace ?p ?c)
+      [?c :block/namespace ?p]]
+     [(namespace ?p ?c)
+      [?t :block/namespace ?p]
+      (namespace ?t ?c)]]
+
+   :alias
+   '[[(alias ?e2 ?e1)
+      [?e2 :block/alias ?e1]]
+     [(alias ?e2 ?e1)
+      [?e1 :block/alias ?e2]]
+     [(alias ?e1 ?e3)
+      [?e1 :block/alias ?e2]
+      [?e2 :block/alias ?e3]]
+     [(alias ?e3 ?e1)
+      [?e1 :block/alias ?e2]
+      [?e2 :block/alias ?e3]]]})
+
+;; Rules writing advice
+;; ====================
+;; Select rules carefully, as it is critical for performance.
+;; The rules have different clause order and resolving directions.
+;; Clause order Reference:
+;; https://docs.datomic.com/on-prem/query/query-executing.html#clause-order
+;; Recursive optimization Reference:
+;; https://stackoverflow.com/questions/42457136/recursive-datalog-queries-for-datomic-really-slow
+;; Should optimize for query the decendents of a block
+;; Quote:
+;; My theory is that your rules are not written in a way that Datalog can optimize for this read pattern - probably resulting in a traversal of all the entities. I suggest to rewrite them as follows:
+;; [[(ubersymbol ?c ?p)
+;;   (?c :ml/parent ?p)]
+;;  [(ubersymbol ?c ?p)
+;;   ;; we bind a child of the ancestor, instead of a parent of the descendant
+;;   (?c1 :ml/parent ?p)
+;;   (ubersymbol ?c ?c1)]]
+
+;; This way of writing the ruleset is optimized to find the descendants of some node. The way you originally wrote it is optimized to find the anscestors of some node.
+
+;; from https://stackoverflow.com/questions/43784258/find-entities-whose-ref-to-many-attribute-contains-all-elements-of-input
+;; Quote:
+;; You're tackling the general problem of 'dynamic conjunction' in Datomic's Datalog.
+;; Write a dynamic Datalog query which uses 2 negations and 1 disjunction or a recursive rule
+;; Datalog has no direct way of expressing dynamic conjunction (logical AND / 'for all ...' / set intersection).
+;; However, you can achieve it in pure Datalog by combining one disjunction
+;; (logical OR / 'exists ...' / set union) and two negations, i.e
+;; (For all ?g in ?Gs p(?e,?g)) <=> NOT(Exists ?g in ?Gs, such that NOT(p(?e, ?g)))
+
+;; [(matches-all ?e ?a ?vs)
+;;  [(first ?vs) ?v0]
+;;  [?e ?a ?v0]
+;;  (not-join [?e ?vs]
+;;            [(identity ?vs) [?v ...]]
+;;            (not-join [?e ?v]
+;;                      [?e ?a ?v]))]
 
 
 (def ^:large-vars/data-var query-dsl-rules
 (def ^:large-vars/data-var query-dsl-rules
   "Rules used by frontend.db.query-dsl. The symbols ?b and ?p respectively refer
   "Rules used by frontend.db.query-dsl. The symbols ?b and ?p respectively refer

+ 16 - 7
deps/graph-parser/src/logseq/graph_parser/whiteboard.cljs

@@ -43,15 +43,24 @@
 
 
 
 
 (defn- get-shape-refs [shape]
 (defn- get-shape-refs [shape]
-  (when (= "logseq-portal" (:type shape))
-    [(if (= (:blockType shape) "P")
-       {:block/name (gp-util/page-name-sanity-lc (:pageId shape))}
-       {:block/uuid (uuid (:pageId shape))})]))
+  (let [portal-refs (when (= "logseq-portal" (:type shape))
+                      [(if (= (:blockType shape) "P")
+                         {:block/name (gp-util/page-name-sanity-lc (:pageId shape))}
+                         {:block/uuid (uuid (:pageId shape))})])
+        shape-link-refs (->> (:refs shape)
+                             (filter (complement empty?))
+                             (map (fn [ref] (if (parse-uuid ref)
+                                              {:block/uuid (parse-uuid ref)}
+                                              {:block/name (gp-util/page-name-sanity-lc ref)}))))]
+    (concat portal-refs shape-link-refs)))
 
 
 (defn- with-whiteboard-block-refs
 (defn- with-whiteboard-block-refs
-  [shape]
+  [shape page-name]
   (let [refs (or (get-shape-refs shape) [])]
   (let [refs (or (get-shape-refs shape) [])]
-    (merge {:block/refs refs})))
+    (merge {:block/refs (if (seq refs) refs [])
+            :block/path-refs (if (seq refs)
+                               (conj refs {:block/name page-name})
+                               [])})))
 
 
 (defn- with-whiteboard-content
 (defn- with-whiteboard-content
   "Main purpose of this function is to populate contents when shapes are used as references in outliner."
   "Main purpose of this function is to populate contents when shapes are used as references in outliner."
@@ -72,7 +81,7 @@
     (merge (if shape?
     (merge (if shape?
              (merge
              (merge
               {:block/uuid (uuid (:id shape))}
               {:block/uuid (uuid (:id shape))}
-              (with-whiteboard-block-refs shape)
+              (with-whiteboard-block-refs shape page-name)
               (with-whiteboard-content shape))
               (with-whiteboard-content shape))
 
 
              ;; TODO: remove?
              ;; TODO: remove?

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

@@ -11,5 +11,5 @@ test('should not have any automatically detectable accessibility issues', async
         .setLegacyMode()
         .setLegacyMode()
         .analyze()
         .analyze()
 
 
-    expect(accessibilityScanResults.violations).toEqual([]);
+        expect(accessibilityScanResults.violations).toEqual([]);
 })
 })

+ 35 - 0
e2e-tests/editor.spec.ts

@@ -29,6 +29,41 @@ test('hashtag and quare brackets in same line #4178', async ({ page }) => {
   )
   )
 })
 })
 
 
+test('hashtag search page auto-complete', async ({ page, block }) => {
+  await createRandomPage(page)
+
+  await block.activeEditing(0)
+
+  await page.type('textarea >> nth=0', '#', { delay: 100 })
+  await page.waitForSelector('text="Search for a page"', { state: 'visible' })
+  await page.keyboard.press('Escape', { delay: 50 })
+
+  await block.mustFill("done")
+
+  await enterNextBlock(page)
+  await page.type('textarea >> nth=0', 'Some#', { delay: 100 })
+  await page.waitForSelector('text="Search for a page"', { state: 'visible' })
+  await page.keyboard.press('Escape', { delay: 50 })
+
+  await block.mustFill("done")
+
+  await enterNextBlock(page)
+  await page.type('textarea >> nth=0', 'Some #', { delay: 100 })
+  await page.waitForSelector('text="Search for a page"', { state: 'visible' })
+  await page.keyboard.press('Escape', { delay: 50 })
+
+  await block.mustFill("done")
+
+  await enterNextBlock(page)
+  await page.type('textarea >> nth=0', 'SomeInner', { delay: 100 })
+  for (let i = 0; i < 5; i++) {
+    await page.press('textarea >> nth=0', 'ArrowLeft', { delay: 50 })
+  }
+  await page.type('textarea >> nth=0', '#', { delay: 50 })
+  await page.waitForSelector('text="Search for a page"', { state: 'visible' })
+  await page.keyboard.press('Escape', { delay: 50 })
+})
+
 test('disappeared children #4814', async ({ page, block }) => {
 test('disappeared children #4814', async ({ page, block }) => {
   await createRandomPage(page)
   await createRandomPage(page)
 
 

+ 2 - 1
e2e-tests/page-rename.spec.ts

@@ -74,7 +74,8 @@ async function homepage_rename_test(page: Page, original_page_name: string, new_
 }
 }
 
 
 test('page rename test', async ({ page }) => {
 test('page rename test', async ({ page }) => {
-  await homepage_rename_test(page, "abcd", "a/b/c/d")
+  // TODO: Fix commented out test. Started failing after https://github.com/logseq/logseq/pull/6945
+  // await homepage_rename_test(page, "abcd", "a/b/c/d")
   await page_rename_test(page, "abcd", "a.b.c.d")
   await page_rename_test(page, "abcd", "a.b.c.d")
   await page_rename_test(page, "abcd", "a/b/c/d")
   await page_rename_test(page, "abcd", "a/b/c/d")
 
 

+ 3 - 4
e2e-tests/random.spec.ts

@@ -1,7 +1,7 @@
 import { expect } from '@playwright/test'
 import { expect } from '@playwright/test'
 import { test } from './fixtures'
 import { test } from './fixtures'
 import {
 import {
-  createRandomPage, randomInt, randomInsert, randomEditDelete, randomEditMoveUpDown, IsMac, randomString,
+  createRandomPage, randomInt, IsMac, randomString,
 } from './utils'
 } from './utils'
 
 
 /**
 /**
@@ -96,7 +96,8 @@ const generateRandomTest = (size: number): RandomTestStep[] => {
   return steps
   return steps
 }
 }
 
 
-test('Random editor operations', async ({ page, block }) => {
+// TODO: Fix test that intermittently started failing after https://github.com/logseq/logseq/pull/6945
+test.skip('Random editor operations', async ({ page, block }) => {
   const steps = generateRandomTest(20)
   const steps = generateRandomTest(20)
 
 
   await createRandomPage(page)
   await createRandomPage(page)
@@ -175,7 +176,5 @@ test('Random editor operations', async ({ page, block }) => {
 
 
     // FIXME: CHECK await block.waitForBlocks(expectedBlocks)
     // FIXME: CHECK await block.waitForBlocks(expectedBlocks)
     await page.waitForTimeout(50)
     await page.waitForTimeout(50)
-
   }
   }
-
 })
 })

+ 9 - 0
e2e-tests/sanitization.spec.ts

@@ -37,3 +37,12 @@ test('custom hiccup should not spawn any dialogs', async ({ page, block }) => {
 
 
   expect(true).toBeTruthy()
   expect(true).toBeTruthy()
 })
 })
+
+test('"is" attribute should be allowed for plugin purposes', async ({ page, block }) => {
+  await createRandomPage(page)
+
+  await page.keyboard.type('[:div {:is "custom-element" :id "custom-element-id"}]', { delay: 5 })
+  await block.enterNext()
+
+  await expect(page.locator('#custom-element-id')).toHaveAttribute('is', 'custom-element');
+})

+ 1 - 1
e2e-tests/utils.ts

@@ -2,7 +2,7 @@ import { Page, Locator } from 'playwright'
 import { expect, ConsoleMessage } from '@playwright/test'
 import { expect, ConsoleMessage } from '@playwright/test'
 import * as process from 'process'
 import * as process from 'process'
 import { Block } from './types'
 import { Block } from './types'
-import pathlib from 'path'
+import * as pathlib from 'path'
 
 
 export const IsMac = process.platform === 'darwin'
 export const IsMac = process.platform === 'darwin'
 export const IsLinux = process.platform === 'linux'
 export const IsLinux = process.platform === 'linux'

+ 22 - 22
e2e-tests/whiteboards.spec.ts

@@ -2,7 +2,7 @@ import { expect } from '@playwright/test'
 import { test } from './fixtures'
 import { test } from './fixtures'
 import { IsMac } from './utils'
 import { IsMac } from './utils'
 
 
-test('enable whiteboards', async ({ page }) => {
+test.skip('enable whiteboards', async ({ page }) => {
     await page.evaluate(() => {
     await page.evaluate(() => {
         window.localStorage.removeItem('ls-onboarding-whiteboard?')
         window.localStorage.removeItem('ls-onboarding-whiteboard?')
     })
     })
@@ -16,17 +16,17 @@ test('enable whiteboards', async ({ page }) => {
     await expect(page.locator('.nav-header .whiteboard')).toBeVisible()
     await expect(page.locator('.nav-header .whiteboard')).toBeVisible()
 })
 })
 
 
-test('create new whiteboard', async ({ page }) => {
+test.skip('create new whiteboard', async ({ page }) => {
     await page.click('.nav-header .whiteboard')
     await page.click('.nav-header .whiteboard')
     await page.click('#tl-create-whiteboard')
     await page.click('#tl-create-whiteboard')
     await expect(page.locator('.logseq-tldraw')).toBeVisible()
     await expect(page.locator('.logseq-tldraw')).toBeVisible()
 })
 })
 
 
-test('check if the page contains the onboarding whiteboard', async ({ page }) => {
+test.skip('check if the page contains the onboarding whiteboard', async ({ page }) => {
     await expect(page.locator('.tl-text-shape-wrapper >> text=Welcome to')).toHaveCount(1)
     await expect(page.locator('.tl-text-shape-wrapper >> text=Welcome to')).toHaveCount(1)
 })
 })
 
 
-test('cleanup the shapes', async ({ page }) => {
+test.skip('cleanup the shapes', async ({ page }) => {
     if (IsMac) {
     if (IsMac) {
         await page.keyboard.press('Meta+a')
         await page.keyboard.press('Meta+a')
     } else {
     } else {
@@ -36,19 +36,19 @@ test('cleanup the shapes', async ({ page }) => {
     await expect(page.locator('[data-type=Shape]')).toHaveCount(0)
     await expect(page.locator('[data-type=Shape]')).toHaveCount(0)
 })
 })
 
 
-test('can right click title to show context menu', async ({ page }) => {
+test.skip('can right click title to show context menu', async ({ page }) => {
     await page.click('.whiteboard-page-title', {
     await page.click('.whiteboard-page-title', {
         button: 'right',
         button: 'right',
     })
     })
-  
+
     await expect(page.locator('#custom-context-menu')).toBeVisible()
     await expect(page.locator('#custom-context-menu')).toBeVisible()
-  
+
     await page.keyboard.press('Escape')
     await page.keyboard.press('Escape')
-  
+
     await expect(page.locator('#custom-context-menu')).toHaveCount(0)
     await expect(page.locator('#custom-context-menu')).toHaveCount(0)
 })
 })
 
 
-test('set whiteboard title', async ({ page }) => {
+test.skip('set whiteboard title', async ({ page }) => {
     const title = "my-whiteboard"
     const title = "my-whiteboard"
     // Newly created whiteboard should have a default title
     // Newly created whiteboard should have a default title
     await expect(page.locator('.whiteboard-page-title .title')).toContainText("Untitled");
     await expect(page.locator('.whiteboard-page-title .title')).toContainText("Untitled");
@@ -68,16 +68,16 @@ test('set whiteboard title', async ({ page }) => {
     await expect(page.locator('.whiteboard-page-title .title')).toContainText(title + "-2");
     await expect(page.locator('.whiteboard-page-title .title')).toContainText(title + "-2");
 })
 })
 
 
-test('select rectangle tool', async ({ page }) => {
-    await page.keyboard.press('8')
+test.skip('select rectangle tool', async ({ page }) => {
+    await page.keyboard.press('7')
     await expect(page.locator('.tl-geometry-tools-pane-anchor [title*="Rectangle"]')).toHaveAttribute('data-selected', 'true')
     await expect(page.locator('.tl-geometry-tools-pane-anchor [title*="Rectangle"]')).toHaveAttribute('data-selected', 'true')
 })
 })
 
 
-test('draw a rectangle', async ({ page }) => {
+test.skip('draw a rectangle', async ({ page }) => {
     const canvas = await page.waitForSelector('.logseq-tldraw');
     const canvas = await page.waitForSelector('.logseq-tldraw');
     const bounds = (await canvas.boundingBox())!;
     const bounds = (await canvas.boundingBox())!;
 
 
-    await page.keyboard.press('8')
+    await page.keyboard.press('7')
 
 
     await page.mouse.move(bounds.x + 5, bounds.y + 5);
     await page.mouse.move(bounds.x + 5, bounds.y + 5);
     await page.mouse.down();
     await page.mouse.down();
@@ -88,35 +88,35 @@ test('draw a rectangle', async ({ page }) => {
     await expect(page.locator('.logseq-tldraw .tl-positioned-svg rect')).not.toHaveCount(0);
     await expect(page.locator('.logseq-tldraw .tl-positioned-svg rect')).not.toHaveCount(0);
 })
 })
 
 
-test('zoom in', async ({ page }) => {
+test.skip('zoom in', async ({ page }) => {
     await page.click('#tl-zoom-in')
     await page.click('#tl-zoom-in')
     await expect(page.locator('#tl-zoom')).toContainText('125%');
     await expect(page.locator('#tl-zoom')).toContainText('125%');
 })
 })
 
 
-test('zoom out', async ({ page }) => {
+test.skip('zoom out', async ({ page }) => {
     await page.click('#tl-zoom-out')
     await page.click('#tl-zoom-out')
     await expect(page.locator('#tl-zoom')).toContainText('100%');
     await expect(page.locator('#tl-zoom')).toContainText('100%');
 })
 })
 
 
-test('open context menu', async ({ page }) => {
+test.skip('open context menu', async ({ page }) => {
     await page.locator('.logseq-tldraw').click({ button: "right" })
     await page.locator('.logseq-tldraw').click({ button: "right" })
     await expect(page.locator('.tl-context-menu')).toBeVisible()
     await expect(page.locator('.tl-context-menu')).toBeVisible()
 })
 })
 
 
-test('close context menu on esc', async ({ page }) => {
+test.skip('close context menu on esc', async ({ page }) => {
     await page.keyboard.press('Escape')
     await page.keyboard.press('Escape')
     await expect(page.locator('.tl-context-menu')).toBeHidden()
     await expect(page.locator('.tl-context-menu')).toBeHidden()
 })
 })
 
 
-test('quick add another whiteboard', async ({ page }) => {
+test.skip('quick add another whiteboard', async ({ page }) => {
     // create a new board first
     // create a new board first
     await page.click('.nav-header .whiteboard')
     await page.click('.nav-header .whiteboard')
     await page.click('#tl-create-whiteboard')
     await page.click('#tl-create-whiteboard')
-    
+
     await page.click('.whiteboard-page-title')
     await page.click('.whiteboard-page-title')
     await page.fill('.whiteboard-page-title input', "my-whiteboard-3")
     await page.fill('.whiteboard-page-title input', "my-whiteboard-3")
     await page.keyboard.press('Enter')
     await page.keyboard.press('Enter')
-    
+
     const canvas = await page.waitForSelector('.logseq-tldraw');
     const canvas = await page.waitForSelector('.logseq-tldraw');
     await canvas.dblclick({
     await canvas.dblclick({
         position: {
         position: {
@@ -135,10 +135,10 @@ test('quick add another whiteboard', async ({ page }) => {
     await expect(page.locator('.tl-logseq-portal-container >> text=my-whiteboard-2')).toBeVisible()
     await expect(page.locator('.tl-logseq-portal-container >> text=my-whiteboard-2')).toBeVisible()
 })
 })
 
 
-test('go to another board and check reference', async ({ page }) => {
+test.skip('go to another board and check reference', async ({ page }) => {
     await page.locator('.tl-logseq-portal-container >> text=my-whiteboard-2').click()
     await page.locator('.tl-logseq-portal-container >> text=my-whiteboard-2').click()
     await expect(page.locator('.whiteboard-page-title .title')).toContainText("my-whiteboard-2");
     await expect(page.locator('.whiteboard-page-title .title')).toContainText("my-whiteboard-2");
-  
+
     const pageRefCount$ = page.locator('.whiteboard-page-refs-count')
     const pageRefCount$ = page.locator('.whiteboard-page-refs-count')
     await expect(pageRefCount$.locator('.open-page-ref-link')).toContainText('1')
     await expect(pageRefCount$.locator('.open-page-ref-link')).toContainText('1')
 
 

+ 1 - 0
ios/App/Podfile

@@ -21,6 +21,7 @@ def capacitor_pods
   pod 'CapacitorSplashScreen', :path => '../../node_modules/@capacitor/splash-screen'
   pod 'CapacitorSplashScreen', :path => '../../node_modules/@capacitor/splash-screen'
   pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
   pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
   pod 'CapawesomeCapacitorBackgroundTask', :path => '../../node_modules/@capawesome/capacitor-background-task'
   pod 'CapawesomeCapacitorBackgroundTask', :path => '../../node_modules/@capawesome/capacitor-background-task'
+  pod 'HugotomaziCapacitorNavigationBar', :path => '../../node_modules/@hugotomazi/capacitor-navigation-bar'
   pod 'LogseqCapacitorFileSync', :path => '../../node_modules/@logseq/capacitor-file-sync'
   pod 'LogseqCapacitorFileSync', :path => '../../node_modules/@logseq/capacitor-file-sync'
   pod 'CapacitorVoiceRecorder', :path => '../../node_modules/capacitor-voice-recorder'
   pod 'CapacitorVoiceRecorder', :path => '../../node_modules/capacitor-voice-recorder'
   pod 'SendIntent', :path => '../../node_modules/send-intent'
   pod 'SendIntent', :path => '../../node_modules/send-intent'

+ 1 - 0
libs/.npmignore

@@ -1,3 +1,4 @@
 src/
 src/
 webpack.*
 webpack.*
 .DS_Store
 .DS_Store
+docs/

+ 30 - 0
libs/CHANGELOG.md

@@ -0,0 +1,30 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+## [Unreleased]
+
+## [0.0.11]
+
+### Added
+
+- All configurations of current graph.
+  `App.getCurrentGraphConfigs: () => Promise<any>`
+- All favorite pages list of current graph.
+  `App.getCurrentGraphFavorites: () => Promise<Array<string> | null>`
+- All recent pages list of current graph.
+  `App.getCurrentGraphRecent: () => Promise<Array<string> | null>`
+- Clear right sidebar blocks.
+  `App.clearRightSidebarBlocks: (opts?: { close: boolean }) => void`
+- Support register `CodeMirror` enhancer. _#Experiment feature_
+  `Experiments.registerExtensionsEnhancer<T = any>(type: 'katex' | 'codemirror', enhancer: (v: T) => Promise<any>)`
+- Support hooks for app search service. _#Alpha stage_
+  `App.registerSearchService<T extends IPluginSearchServiceHooks>(s: T): void`
+- Support `focus` option for `App.insertBlock`. Credit
+  to [[[tennox](https://github.com/tennox)]] [#PR](https://github.com/logseq/logseq/commit/4217057a44de65e5c64be37857af2fb4e9534b24)
+
+### Fixed
+
+- Adjust build script to be compatible for `shadow-cljs` bundler.
+  > How to set up a clojurescript project with shadow-cljs?
+  > https://github.com/rlhk/logseq-url-plus/blob/main/doc/dev-notes.md

+ 10 - 0
libs/babel.config.json

@@ -0,0 +1,10 @@
+{
+  "presets": [
+    [
+      "@babel/preset-env",
+      {
+        "targets": "chrome 72"
+      }
+    ]
+  ]
+}

+ 9 - 2
libs/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@logseq/libs",
   "name": "@logseq/libs",
-  "version": "0.0.10",
+  "version": "0.0.11",
   "description": "Logseq SDK libraries",
   "description": "Logseq SDK libraries",
   "main": "dist/lsplugin.user.js",
   "main": "dist/lsplugin.user.js",
   "typings": "index.d.ts",
   "typings": "index.d.ts",
@@ -12,7 +12,8 @@
     "dev:core": "npm run build:core -- --mode development --watch",
     "dev:core": "npm run build:core -- --mode development --watch",
     "build": "tsc && rm dist/*.js && npm run build:user",
     "build": "tsc && rm dist/*.js && npm run build:user",
     "lint": "prettier --check \"src/**/*.{ts, js}\"",
     "lint": "prettier --check \"src/**/*.{ts, js}\"",
-    "fix": "prettier --write \"src/**/*.{ts, js}\""
+    "fix": "prettier --write \"src/**/*.{ts, js}\"",
+    "build:docs": "typedoc --plugin typedoc-plugin-lsp-docs src/LSPlugin.user.ts && typedoc --json docs/out.json ./src/LSPlugin.user.ts"
   },
   },
   "dependencies": {
   "dependencies": {
     "csstype": "3.1.0",
     "csstype": "3.1.0",
@@ -25,12 +26,18 @@
     "snake-case": "3.0.4"
     "snake-case": "3.0.4"
   },
   },
   "devDependencies": {
   "devDependencies": {
+    "@babel/core": "^7.20.2",
+    "@babel/preset-env": "^7.20.2",
     "@types/debug": "^4.1.5",
     "@types/debug": "^4.1.5",
     "@types/dompurify": "2.3.3",
     "@types/dompurify": "2.3.3",
     "@types/lodash-es": "4.17.6",
     "@types/lodash-es": "4.17.6",
+    "babel-loader": "^9.1.0",
     "prettier": "^2.6.2",
     "prettier": "^2.6.2",
     "prettier-config-standard": "^5.0.0",
     "prettier-config-standard": "^5.0.0",
+    "terser-webpack-plugin": "^5.3.6",
     "ts-loader": "9.3.0",
     "ts-loader": "9.3.0",
+    "typedoc": "^0.23.17",
+    "typedoc-plugin-lsp-docs": "^0.0.1",
     "typescript": "4.7.3",
     "typescript": "4.7.3",
     "webpack": "5.73.0",
     "webpack": "5.73.0",
     "webpack-bundle-analyzer": "4.5.0",
     "webpack-bundle-analyzer": "4.5.0",

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

@@ -279,6 +279,7 @@ class LSPluginCaller extends EventEmitter {
             debug(`[user -> *host] `, type, payload)
             debug(`[user -> *host] `, type, payload)
 
 
             this._pluginLocal?.emit(type, payload || {})
             this._pluginLocal?.emit(type, payload || {})
+            this._pluginLocal?.caller.emit(type, payload || {})
           })
           })
 
 
           this._call = async (...args: any) => {
           this._call = async (...args: any) => {

+ 13 - 4
libs/src/LSPlugin.core.ts

@@ -139,7 +139,12 @@ class PluginLogger extends EventEmitter<'change'> {
     super()
     super()
   }
   }
 
 
-  write(type: string, payload: any[]) {
+  write(type: string, payload: any[], inConsole?: boolean) {
+    if (payload?.length && (true === payload[payload.length - 1])) {
+      inConsole = true
+      payload.pop()
+    }
+
     const msg = payload.reduce((ac, it) => {
     const msg = payload.reduce((ac, it) => {
       if (it && it instanceof Error) {
       if (it && it instanceof Error) {
         ac += `${it.message} ${it.stack}`
         ac += `${it.message} ${it.stack}`
@@ -150,6 +155,11 @@ class PluginLogger extends EventEmitter<'change'> {
     }, `[${this._tag}][${new Date().toLocaleTimeString()}] `)
     }, `[${this._tag}][${new Date().toLocaleTimeString()}] `)
 
 
     this._logs.push([type, msg])
     this._logs.push([type, msg])
+
+    if (inConsole) {
+      console?.['ERROR' === type ? 'error' : 'debug'](`${type}: ${msg}`)
+    }
+
     this.emit('change')
     this.emit('change')
   }
   }
 
 
@@ -907,9 +917,9 @@ class PluginLocal extends EventEmitter<'loaded'
 
 
       this._dispose(cleanInjectedScripts.bind(this))
       this._dispose(cleanInjectedScripts.bind(this))
     } catch (e) {
     } catch (e) {
-      console.error('[Load Plugin Error] ', e)
-      this.logger?.error(e)
+      this.logger?.error('[Load Plugin]', e, true)
 
 
+      this.dispose().catch(null)
       this._status = PluginLocalLoadStatus.ERROR
       this._status = PluginLocalLoadStatus.ERROR
       this._loadErr = e
       this._loadErr = e
     } finally {
     } finally {
@@ -1329,7 +1339,6 @@ class LSPluginCore
           }
           }
         }
         }
 
 
-
         pluginLocal.settings?.on('change', (a) => {
         pluginLocal.settings?.on('change', (a) => {
           this.emit('settings-changed', pluginLocal.id, a)
           this.emit('settings-changed', pluginLocal.id, a)
           pluginLocal.caller?.callUserModel(LSPMSG_SETTINGS, { payload: a })
           pluginLocal.caller?.callUserModel(LSPMSG_SETTINGS, { payload: a })

+ 39 - 2
libs/src/LSPlugin.ts

@@ -289,6 +289,34 @@ export type ExternalCommandType =
 
 
 export type UserProxyTags = 'app' | 'editor' | 'db' | 'git' | 'ui' | 'assets'
 export type UserProxyTags = 'app' | 'editor' | 'db' | 'git' | 'ui' | 'assets'
 
 
+export type SearchIndiceInitStatus = boolean
+export type SearchBlockItem = { id: EntityID, uuid: BlockIdentity, content: string, page: EntityID }
+export type SearchPageItem = string
+export type SearchFileItem = string
+
+export interface IPluginSearchServiceHooks {
+  name: string
+  options?: Record<string, any>
+
+  onQuery: (
+    graph: string,
+    key: string,
+    opts: Partial<{ limit: number }>
+  ) =>
+    Promise<{
+      graph: string,
+      key: string,
+      blocks?: Array<Partial<SearchBlockItem>>,
+      pages?: Array<SearchPageItem>,
+      files?: Array<SearchFileItem>
+    }>
+
+  onIndiceInit: (graph: string) => Promise<SearchIndiceInitStatus>
+  onIndiceReset: (graph: string) => Promise<void>
+  onBlocksChanged: (graph: string, changes: { added: Array<SearchBlockItem>, removed: Array<BlockEntity> }) => Promise<void>
+  onGraphRemoved: (graph: string, opts?: {}) => Promise<any>
+}
+
 /**
 /**
  * App level APIs
  * App level APIs
  */
  */
@@ -302,6 +330,9 @@ export interface IAppProxy {
   getUserInfo: () => Promise<AppUserInfo | null>
   getUserInfo: () => Promise<AppUserInfo | null>
   getUserConfigs: () => Promise<AppUserConfigs>
   getUserConfigs: () => Promise<AppUserConfigs>
 
 
+  // services
+  registerSearchService<T extends IPluginSearchServiceHooks>(s: T): void
+
   // commands
   // commands
   registerCommand: (
   registerCommand: (
     type: string,
     type: string,
@@ -352,6 +383,7 @@ export interface IAppProxy {
    * @param path
    * @param path
    */
    */
   getStateFromStore: <T = any>(path: string | Array<string>) => Promise<T>
   getStateFromStore: <T = any>(path: string | Array<string>) => Promise<T>
+  setStateFromStore: (path: string | Array<string>, value: any) => Promise<void>
 
 
   // native
   // native
   relaunch: () => Promise<void>
   relaunch: () => Promise<void>
@@ -367,6 +399,9 @@ export interface IAppProxy {
 
 
   // graph
   // graph
   getCurrentGraph: () => Promise<AppGraphInfo | null>
   getCurrentGraph: () => Promise<AppGraphInfo | null>
+  getCurrentGraphConfigs: () => Promise<any>
+  getCurrentGraphFavorites: () => Promise<Array<string> | null>
+  getCurrentGraphRecent: () => Promise<Array<string> | null>
 
 
   // router
   // router
   pushState: (
   pushState: (
@@ -403,6 +438,7 @@ export interface IAppProxy {
   setFullScreen: (flag: boolean | 'toggle') => void
   setFullScreen: (flag: boolean | 'toggle') => void
   setLeftSidebarVisible: (flag: boolean | 'toggle') => void
   setLeftSidebarVisible: (flag: boolean | 'toggle') => void
   setRightSidebarVisible: (flag: boolean | 'toggle') => void
   setRightSidebarVisible: (flag: boolean | 'toggle') => void
+  clearRightSidebarBlocks: (opts?: { close: boolean }) => void
 
 
   registerUIItem: (
   registerUIItem: (
     type: 'toolbar' | 'pagebar',
     type: 'toolbar' | 'pagebar',
@@ -592,6 +628,7 @@ export interface IEditorProxy extends Record<string, any> {
       before: boolean
       before: boolean
       sibling: boolean
       sibling: boolean
       isPageBlock: boolean
       isPageBlock: boolean
+      focus: boolean
       customUUID: string
       customUUID: string
       properties: {}
       properties: {}
     }>
     }>
@@ -804,14 +841,14 @@ export interface IAssetsProxy {
    * @added 0.0.2
    * @added 0.0.2
    * @param exts
    * @param exts
    */
    */
-  listFilesOfCurrentGraph(exts?: string | string[]): Promise<{
+  listFilesOfCurrentGraph(exts?: string | string[]): Promise<Array<{
     path: string
     path: string
     size: number
     size: number
     accessTime: number
     accessTime: number
     modifiedTime: number
     modifiedTime: number
     changeTime: number
     changeTime: number
     birthTime: number
     birthTime: number
-  }>
+  }>>
 
 
   /**
   /**
    * @example https://github.com/logseq/logseq/pull/6488
    * @example https://github.com/logseq/logseq/pull/6488

+ 25 - 3
libs/src/LSPlugin.user.ts

@@ -33,7 +33,7 @@ import {
   BlockEntity,
   BlockEntity,
   IDatom,
   IDatom,
   IAssetsProxy,
   IAssetsProxy,
-  AppInfo,
+  AppInfo, IPluginSearchServiceHooks,
 } from './LSPlugin'
 } from './LSPlugin'
 import Debug from 'debug'
 import Debug from 'debug'
 import * as CSS from 'csstype'
 import * as CSS from 'csstype'
@@ -41,6 +41,7 @@ import EventEmitter from 'eventemitter3'
 import { IAsyncStorage, LSPluginFileStorage } from './modules/LSPlugin.Storage'
 import { IAsyncStorage, LSPluginFileStorage } from './modules/LSPlugin.Storage'
 import { LSPluginExperiments } from './modules/LSPlugin.Experiments'
 import { LSPluginExperiments } from './modules/LSPlugin.Experiments'
 import { LSPluginRequest } from './modules/LSPlugin.Request'
 import { LSPluginRequest } from './modules/LSPlugin.Request'
+import { LSPluginSearchService } from './modules/LSPlugin.Search'
 
 
 declare global {
 declare global {
   interface Window {
   interface Window {
@@ -86,13 +87,15 @@ function registerSimpleCommand(
     method: 'register-plugin-simple-command',
     method: 'register-plugin-simple-command',
     args: [
     args: [
       this.baseInfo.id,
       this.baseInfo.id,
-      [{ key, label, type, desc, keybinding, extras}, ['editor/hook', eventKey]],
+      [{ key, label, type, desc, keybinding, extras }, ['editor/hook', eventKey]],
       palette,
       palette,
     ],
     ],
   })
   })
 }
 }
 
 
 let _appBaseInfo: AppInfo = null
 let _appBaseInfo: AppInfo = null
+let _searchServices: Map<string, LSPluginSearchService> = new Map()
+
 const app: Partial<IAppProxy> = {
 const app: Partial<IAppProxy> = {
   async getInfo(
   async getInfo(
     this: LSPluginUser,
     this: LSPluginUser,
@@ -106,6 +109,17 @@ const app: Partial<IAppProxy> = {
 
 
   registerCommand: registerSimpleCommand,
   registerCommand: registerSimpleCommand,
 
 
+  registerSearchService<T extends IPluginSearchServiceHooks>(
+    this: LSPluginUser,
+    s: T
+  ) {
+    if (_searchServices.has(s.name)) {
+      throw new Error(`SearchService: #${s.name} has registered!`)
+    }
+
+    _searchServices.set(s.name, new LSPluginSearchService(this, s))
+  },
+
   registerCommandPalette(
   registerCommandPalette(
     opts: { key: string; label: string; keybinding?: SimpleCommandKeybinding },
     opts: { key: string; label: string; keybinding?: SimpleCommandKeybinding },
     action: SimpleCommandCallback
     action: SimpleCommandCallback
@@ -356,7 +370,11 @@ type uiState = {
 const KEY_MAIN_UI = 0
 const KEY_MAIN_UI = 0
 
 
 /**
 /**
- * User plugin instance
+ * User plugin instance from global namespace `logseq`.
+ * @example
+ * ```ts
+ * logseq.UI.showMsg('Hello, Logseq')
+ * ```
  * @public
  * @public
  */
  */
 export class LSPluginUser
 export class LSPluginUser
@@ -420,6 +438,7 @@ export class LSPluginUser
     })
     })
   }
   }
 
 
+  // Life related
   async ready(model?: any, callback?: any) {
   async ready(model?: any, callback?: any) {
     if (this._connected) return
     if (this._connected) return
 
 
@@ -495,6 +514,7 @@ export class LSPluginUser
     return this
     return this
   }
   }
 
 
+  // Settings related
   useSettingsSchema(schema: Array<SettingSchemaDesc>) {
   useSettingsSchema(schema: Array<SettingSchemaDesc>) {
     if (this.connected) {
     if (this.connected) {
       this.caller.call('settings:schema', {
       this.caller.call('settings:schema', {
@@ -526,6 +546,7 @@ export class LSPluginUser
     this.caller.call('settings:visible:changed', { visible: false })
     this.caller.call('settings:visible:changed', { visible: false })
   }
   }
 
 
+  // UI related
   setMainUIAttrs(attrs: Partial<UIContainerAttrs>): void {
   setMainUIAttrs(attrs: Partial<UIContainerAttrs>): void {
     this.caller.call('main-ui:attrs', attrs)
     this.caller.call('main-ui:attrs', attrs)
   }
   }
@@ -566,6 +587,7 @@ export class LSPluginUser
     }
     }
   }
   }
 
 
+  // Getters
   get version(): string {
   get version(): string {
     return this._version
     return this._version
   }
   }

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

@@ -60,7 +60,7 @@ export class LSPluginExperiments {
   }
   }
 
 
   registerExtensionsEnhancer<T = any>(
   registerExtensionsEnhancer<T = any>(
-    type: 'katex',
+    type: 'katex' | 'codemirror',
     enhancer: (v: T) => Promise<any>
     enhancer: (v: T) => Promise<any>
   ) {
   ) {
     const host = this.ensureHostScope()
     const host = this.ensureHostScope()

+ 82 - 0
libs/src/modules/LSPlugin.Search.ts

@@ -0,0 +1,82 @@
+import { IPluginSearchServiceHooks } from '../LSPlugin'
+import { LSPluginUser } from '../LSPlugin.user'
+import { isArray, isFunction, mapKeys } from 'lodash-es'
+
+export class LSPluginSearchService {
+
+  /**
+   * @param ctx
+   * @param serviceHooks
+   */
+  constructor(
+    private ctx: LSPluginUser,
+    private serviceHooks: IPluginSearchServiceHooks
+  ) {
+    ctx._execCallableAPI(
+      'register-search-service',
+      ctx.baseInfo.id,
+      serviceHooks.name,
+      serviceHooks.options
+    )
+
+    // hook events TODO: remove listeners
+    const wrapHookEvent = (k) => `service:search:${k}:${serviceHooks.name}`
+
+    Object.entries(
+      {
+        query: {
+          f: 'onQuery', args: ['graph', 'q', true], reply: true,
+          transformOutput: (data: any) => {
+            // TODO: transform keys?
+            if (isArray(data?.blocks)) {
+              data.blocks = data.blocks.map(it => {
+                return it && mapKeys(it, (_, k) => `block/${k}`)
+              })
+            }
+
+            return data
+          }
+        },
+        rebuildBlocksIndice: { f: 'onIndiceInit', args: ['graph', 'blocks'] },
+        transactBlocks: { f: 'onBlocksChanged', args: ['graph', 'data'] },
+        truncateBlocks: { f: 'onIndiceReset', args: ['graph'] },
+        removeDb: { f: 'onGraph', args: ['graph'] }
+      }
+    ).forEach(
+      ([k, v]) => {
+        const hookEvent = wrapHookEvent(k)
+        ctx.caller.on(hookEvent, async (payload: any) => {
+          if (isFunction(serviceHooks?.[v.f])) {
+            let ret = null
+
+            try {
+              ret = await serviceHooks[v.f].apply(
+                serviceHooks, (v.args || []).map((prop: any) => {
+                  if (!payload) return
+                  if (prop === true) return payload
+                  if (payload.hasOwnProperty(prop)) {
+                    const ret = payload[prop]
+                    delete payload[prop]
+                    return ret
+                  }
+                })
+              )
+
+              if (v.transformOutput) {
+                ret = v.transformOutput(ret)
+              }
+            } catch (e) {
+              console.error('[SearchService] ', e)
+              ret = e
+            } finally {
+              if (v.reply) {
+                ctx.caller.call(
+                  `${hookEvent}:reply`, ret
+                )
+              }
+            }
+          }
+        })
+      })
+  }
+}

+ 16 - 2
libs/webpack.config.js

@@ -2,6 +2,7 @@ const pkg = require('./package.json')
 const path = require('path')
 const path = require('path')
 const webpack = require('webpack')
 const webpack = require('webpack')
 const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
 const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
+const TerserPlugin = require('terser-webpack-plugin')
 
 
 module.exports = {
 module.exports = {
   entry: './src/LSPlugin.user.ts',
   entry: './src/LSPlugin.user.ts',
@@ -9,14 +10,27 @@ module.exports = {
     rules: [
     rules: [
       {
       {
         test: /\.tsx?$/,
         test: /\.tsx?$/,
-        use: 'ts-loader',
+        use: [
+          {
+            loader: 'babel-loader'
+          },
+          {
+            loader: 'ts-loader'
+          }
+        ],
         exclude: /node_modules/,
         exclude: /node_modules/,
-      },
+      }
     ],
     ],
   },
   },
   resolve: {
   resolve: {
     extensions: ['.tsx', '.ts', '.js'],
     extensions: ['.tsx', '.ts', '.js'],
   },
   },
+  optimization: {
+    minimize: true,
+    minimizer: [
+      new TerserPlugin()
+    ]
+  },
   plugins: [
   plugins: [
     new webpack.ProvidePlugin({
     new webpack.ProvidePlugin({
       process: 'process/browser',
       process: 'process/browser',

文件差异内容过多而无法显示
+ 956 - 4
libs/yarn.lock


+ 2 - 1
package.json

@@ -90,8 +90,9 @@
         "@capacitor/status-bar": "^4.0.0",
         "@capacitor/status-bar": "^4.0.0",
         "@capawesome/capacitor-background-task": "^2.0.0",
         "@capawesome/capacitor-background-task": "^2.0.0",
         "@excalidraw/excalidraw": "0.12.0",
         "@excalidraw/excalidraw": "0.12.0",
+        "@hugotomazi/capacitor-navigation-bar": "^2.0.0",
         "@kanru/rage-wasm": "^0.3.0",
         "@kanru/rage-wasm": "^0.3.0",
-        "@logseq/capacitor-file-sync": "0.0.13",
+        "@logseq/capacitor-file-sync": "0.0.14",
         "@logseq/react-tweet-embed": "1.3.1-1",
         "@logseq/react-tweet-embed": "1.3.1-1",
         "@sentry/react": "^6.18.2",
         "@sentry/react": "^6.18.2",
         "@sentry/tracing": "^6.18.2",
         "@sentry/tracing": "^6.18.2",

+ 0 - 11
public/index.html

@@ -56,16 +56,5 @@
 <script defer src="/static/js/age-encryption.js"></script>
 <script defer src="/static/js/age-encryption.js"></script>
 <script defer src="/static/js/tldraw.js"></script>
 <script defer src="/static/js/tldraw.js"></script>
 <script defer src="/static/js/excalidraw.js"></script>
 <script defer src="/static/js/excalidraw.js"></script>
-<script>
-  /*!
- * swiped-events.js - v1.1.6
- * Pure JavaScript swipe events
- * https://github.com/john-doherty/swiped-events
- * @inspiration https://stackoverflow.com/questions/16348031/disable-scrolling-when-touch-moving-certain-element
- * @author John Doherty <www.johndoherty.info>
- * @license MIT
- */
-!function(t,e){"use strict";"function"!=typeof t.CustomEvent&&(t.CustomEvent=function(t,n){n=n||{bubbles:!1,cancelable:!1,detail:void 0};var a=e.createEvent("CustomEvent");return a.initCustomEvent(t,n.bubbles,n.cancelable,n.detail),a},t.CustomEvent.prototype=t.Event.prototype),e.addEventListener("touchstart",function(t){if("true"===t.target.getAttribute("data-swipe-ignore"))return;s=t.target,r=Date.now(),n=t.touches[0].clientX,a=t.touches[0].clientY,u=0,i=0},!1),e.addEventListener("touchmove",function(t){if(!n||!a)return;var e=t.touches[0].clientX,r=t.touches[0].clientY;u=n-e,i=a-r},!1),e.addEventListener("touchend",function(t){if(s!==t.target)return;var e=parseInt(l(s,"data-swipe-threshold","20"),10),o=parseInt(l(s,"data-swipe-timeout","500"),10),c=Date.now()-r,d="",p=t.changedTouches||t.touches||[];Math.abs(u)>Math.abs(i)?Math.abs(u)>e&&c<o&&(d=u>0?"swiped-left":"swiped-right"):Math.abs(i)>e&&c<o&&(d=i>0?"swiped-up":"swiped-down");if(""!==d){var b={dir:d.replace(/swiped-/,""),touchType:(p[0]||{}).touchType||"direct",xStart:parseInt(n,10),xEnd:parseInt((p[0]||{}).clientX||-1,10),yStart:parseInt(a,10),yEnd:parseInt((p[0]||{}).clientY||-1,10)};s.dispatchEvent(new CustomEvent("swiped",{bubbles:!0,cancelable:!0,detail:b})),s.dispatchEvent(new CustomEvent(d,{bubbles:!0,cancelable:!0,detail:b}))}n=null,a=null,r=null},!1);var n=null,a=null,u=null,i=null,r=null,s=null;function l(t,n,a){for(;t&&t!==e.documentElement;){var u=t.getAttribute(n);if(u)return u;t=t.parentNode}return a}}(window,document);
-</script>
 </body>
 </body>
 </html>
 </html>

+ 2 - 8
resources/css/common.css

@@ -12,7 +12,7 @@
   --ls-headbar-height: 3rem;
   --ls-headbar-height: 3rem;
   --ls-headbar-inner-top-padding: 0px;
   --ls-headbar-inner-top-padding: 0px;
   --ls-left-sidebar-width: 246px;
   --ls-left-sidebar-width: 246px;
-  --ls-left-sidebar-sm-width: 70%;
+  --ls-left-sidebar-sm-width: 74vw;
   --ls-left-sidebar-nav-btn-size: 38px;
   --ls-left-sidebar-nav-btn-size: 38px;
   --ls-error-color: var(--color-red-500);
   --ls-error-color: var(--color-red-500);
   --ls-warning-color: var(--color-orange-500);
   --ls-warning-color: var(--color-orange-500);
@@ -48,12 +48,6 @@ html[data-theme='dark'] {
   --ls-block-properties-background-color: #06323e;
   --ls-block-properties-background-color: #06323e;
   --ls-page-properties-background-color: #02171d;
   --ls-page-properties-background-color: #02171d;
   --ls-block-ref-link-text-color: #1a6376;
   --ls-block-ref-link-text-color: #1a6376;
-  --ls-search-background-color: linear-gradient(
-    to right,
-    #021c23 0,
-    #021b21 200px,
-    #002b36 100%
-  );
   --ls-border-color: #0e5263;
   --ls-border-color: #0e5263;
   --ls-secondary-border-color: #126277;
   --ls-secondary-border-color: #126277;
   --ls-tertiary-border-color: rgba(0, 2, 0, 0.10);
   --ls-tertiary-border-color: rgba(0, 2, 0, 0.10);
@@ -131,7 +125,6 @@ html[data-theme='light'] {
   --ls-block-properties-background-color: #f7f7f7;
   --ls-block-properties-background-color: #f7f7f7;
   --ls-page-properties-background-color: #f7f7f7;
   --ls-page-properties-background-color: #f7f7f7;
   --ls-block-ref-link-text-color: #d8e1e8;
   --ls-block-ref-link-text-color: #d8e1e8;
-  --ls-search-background-color: var(--ls-primary-background-color);
   --ls-border-color: #ccc;
   --ls-border-color: #ccc;
   --ls-secondary-border-color: #e2e2e2;
   --ls-secondary-border-color: #e2e2e2;
   --ls-tertiary-border-color: rgba(200, 200, 200, 0.30);
   --ls-tertiary-border-color: rgba(200, 200, 200, 0.30);
@@ -151,6 +144,7 @@ html[data-theme='light'] {
   --ls-block-bullet-color: rgba(67, 63, 56, 0.25);
   --ls-block-bullet-color: rgba(67, 63, 56, 0.25);
   --ls-block-highlight-color: #c0e6fd;
   --ls-block-highlight-color: #c0e6fd;
   --ls-selection-background-color: #e4f2ff;
   --ls-selection-background-color: #e4f2ff;
+  --ls-selection-text-color: var(--ls-secondary-text-color);
   --ls-page-checkbox-color: #9dbbd8;
   --ls-page-checkbox-color: #9dbbd8;
   --ls-page-checkbox-border-color: var(--ls-page-checkbox-color);
   --ls-page-checkbox-border-color: var(--ls-page-checkbox-color);
   --ls-page-blockquote-color: var(--ls-primary-text-color);
   --ls-page-blockquote-color: var(--ls-primary-text-color);

+ 52 - 20
resources/css/tabler-extension.css

@@ -28,82 +28,114 @@
   -moz-osx-font-smoothing: grayscale;
   -moz-osx-font-smoothing: grayscale;
 }
 }
 
 
-.tie-app-feature::before {
+.tie-add-link::before {
   content: "\ea01";
   content: "\ea01";
 }
 }
 
 
-.tie-block::before {
+.tie-app-feature::before {
   content: "\ea02";
   content: "\ea02";
 }
 }
 
 
-.tie-block-search::before {
+.tie-block::before {
   content: "\ea03";
   content: "\ea03";
 }
 }
 
 
-.tie-connector::before {
+.tie-block-search::before {
   content: "\ea04";
   content: "\ea04";
 }
 }
 
 
-.tie-h-auto::before {
+.tie-connector::before {
   content: "\ea05";
   content: "\ea05";
 }
 }
 
 
-.tie-heading-off::before {
+.tie-h-auto::before {
   content: "\ea06";
   content: "\ea06";
 }
 }
 
 
-.tie-new-block::before {
+.tie-heading-off::before {
   content: "\ea07";
   content: "\ea07";
 }
 }
 
 
-.tie-new-page::before {
+.tie-internal-link::before {
   content: "\ea08";
   content: "\ea08";
 }
 }
 
 
-.tie-new-whiteboard::before {
+.tie-link-to-block::before {
   content: "\ea09";
   content: "\ea09";
 }
 }
 
 
-.tie-new-whiteboard-element::before {
+.tie-link-to-page::before {
   content: "\ea0a";
   content: "\ea0a";
 }
 }
 
 
-.tie-object-compact::before {
+.tie-link-to-whiteboard::before {
   content: "\ea0b";
   content: "\ea0b";
 }
 }
 
 
-.tie-object-expanded::before {
+.tie-move-to-sidebar-right::before {
   content: "\ea0c";
   content: "\ea0c";
 }
 }
 
 
-.tie-page::before {
+.tie-new-block::before {
   content: "\ea0d";
   content: "\ea0d";
 }
 }
 
 
-.tie-page-search::before {
+.tie-new-page::before {
   content: "\ea0e";
   content: "\ea0e";
 }
 }
 
 
-.tie-references-hide::before {
+.tie-new-whiteboard::before {
   content: "\ea0f";
   content: "\ea0f";
 }
 }
 
 
-.tie-references-show::before {
+.tie-new-whiteboard-element::before {
   content: "\ea10";
   content: "\ea10";
 }
 }
 
 
-.tie-select-cursor::before {
+.tie-object-compact::before {
   content: "\ea11";
   content: "\ea11";
 }
 }
 
 
-.tie-text::before {
+.tie-object-expanded::before {
   content: "\ea12";
   content: "\ea12";
 }
 }
 
 
-.tie-whiteboard::before {
+.tie-open-as-page::before {
   content: "\ea13";
   content: "\ea13";
 }
 }
 
 
-.tie-whiteboard-element::before {
+.tie-page::before {
   content: "\ea14";
   content: "\ea14";
 }
 }
+
+.tie-page-search::before {
+  content: "\ea15";
+}
+
+.tie-references-hide::before {
+  content: "\ea16";
+}
+
+.tie-references-show::before {
+  content: "\ea17";
+}
+
+.tie-select-cursor::before {
+  content: "\ea18";
+}
+
+.tie-text::before {
+  content: "\ea19";
+}
+
+.tie-whiteboard::before {
+  content: "\ea1a";
+}
+
+.tie-whiteboard-element::before {
+  content: "\ea1b";
+}
+
+.tie-whiteboard-search::before {
+  content: "\ea1c";
+}

二进制
resources/fonts/tabler-icons-extension.woff2


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


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


+ 9 - 0
resources/js/swiped-events.min.js

@@ -0,0 +1,9 @@
+/*!
+ * swiped-events.js - v1.1.6
+ * Pure JavaScript swipe events
+ * https://github.com/john-doherty/swiped-events
+ * @inspiration https://stackoverflow.com/questions/16348031/disable-scrolling-when-touch-moving-certain-element
+ * @author John Doherty <www.johndoherty.info>
+ * @license MIT
+ */
+!function(t,e){"use strict";"function"!=typeof t.CustomEvent&&(t.CustomEvent=function(t,n){n=n||{bubbles:!1,cancelable:!1,detail:void 0};var a=e.createEvent("CustomEvent");return a.initCustomEvent(t,n.bubbles,n.cancelable,n.detail),a},t.CustomEvent.prototype=t.Event.prototype),e.addEventListener("touchstart",function(t){if("true"===t.target.getAttribute("data-swipe-ignore"))return;s=t.target,r=Date.now(),n=t.touches[0].clientX,a=t.touches[0].clientY,u=0,i=0},!1),e.addEventListener("touchmove",function(t){if(!n||!a)return;var e=t.touches[0].clientX,r=t.touches[0].clientY;u=n-e,i=a-r},!1),e.addEventListener("touchend",function(t){if(s!==t.target)return;var e=parseInt(l(s,"data-swipe-threshold","20"),10),o=parseInt(l(s,"data-swipe-timeout","500"),10),c=Date.now()-r,d="",p=t.changedTouches||t.touches||[];Math.abs(u)>Math.abs(i)?Math.abs(u)>e&&c<o&&(d=u>0?"swiped-left":"swiped-right"):Math.abs(i)>e&&c<o&&(d=i>0?"swiped-up":"swiped-down");if(""!==d){var b={dir:d.replace(/swiped-/,""),touchType:(p[0]||{}).touchType||"direct",xStart:parseInt(n,10),xEnd:parseInt((p[0]||{}).clientX||-1,10),yStart:parseInt(a,10),yEnd:parseInt((p[0]||{}).clientY||-1,10)};s.dispatchEvent(new CustomEvent("swiped",{bubbles:!0,cancelable:!0,detail:b})),s.dispatchEvent(new CustomEvent(d,{bubbles:!0,cancelable:!0,detail:b}))}n=null,a=null,r=null},!1);var n=null,a=null,u=null,i=null,r=null,s=null;function l(t,n,a){for(;t&&t!==e.documentElement;){var u=t.getAttribute(n);if(u)return u;t=t.parentNode}return a}}(window,document);

+ 1 - 1
src/electron/electron/plugin.cljs

@@ -39,9 +39,9 @@
            api #(str "https://api.github.com/repos/" repo "/" %)
            api #(str "https://api.github.com/repos/" repo "/" %)
            endpoint (api url-suffix)
            endpoint (api url-suffix)
            ^js res (fetch endpoint)
            ^js res (fetch endpoint)
+           _ (debug "[Release URL] " endpoint "[Response Status/Text]" (.-status res) "-")
            res (response-transform res)
            res (response-transform res)
            res (.json res)
            res (.json res)
-           _ (debug "[Release URL] " endpoint)
            res (bean/->clj res)
            res (bean/->clj res)
            version (:tag_name res)
            version (:tag_name res)
            asset (first (filter #(string/ends-with? (:name %) ".zip") (:assets res)))]
            asset (first (filter #(string/ends-with? (:name %) ".zip") (:assets res)))]

+ 56 - 38
src/main/electron/listener.cljs

@@ -1,45 +1,49 @@
 (ns electron.listener
 (ns electron.listener
   "System-component-like ns that defines listeners by event name to receive ipc
   "System-component-like ns that defines listeners by event name to receive ipc
   messages from electron's main process"
   messages from electron's main process"
-  (:require [frontend.state :as state]
-            [frontend.context.i18n :refer [t]]
-            [frontend.date :as date]
-            [frontend.handler.route :as route-handler]
-            [frontend.handler.editor :as editor-handler]
-            [frontend.handler.ui :as ui-handler]
-            [frontend.handler.file-sync :as file-sync-handler]
-            [frontend.config :as config]
-            [clojure.string :as string]
-            [cljs-bean.core :as bean]
-            [frontend.fs.watcher-handler :as watcher-handler]
-            [frontend.fs.sync :as sync]
-            [frontend.db :as db]
-            [frontend.db.model :as db-model]
-            [datascript.core :as d]
-            [electron.ipc :as ipc]
-            [frontend.ui :as ui]
-            [frontend.handler.notification :as notification]
-            [frontend.handler.repo :as repo-handler]
-            [frontend.handler.user :as user]
-            [dommy.core :as dom]))
+  (:require
+    [cljs-bean.core :as bean]
+    [clojure.string :as string]
+    [datascript.core :as d]
+    [dommy.core :as dom]
+    [electron.ipc :as ipc]
+    [frontend.config :as config]
+    [frontend.context.i18n :refer [t]]
+    [frontend.date :as date]
+    [frontend.db :as db]
+    [frontend.db.model :as db-model]
+    [frontend.fs.sync :as sync]
+    [frontend.fs.watcher-handler :as watcher-handler]
+    [frontend.handler.editor :as editor-handler]
+    [frontend.handler.file-sync :as file-sync-handler]
+    [frontend.handler.notification :as notification]
+    [frontend.handler.repo :as repo-handler]
+    [frontend.handler.route :as route-handler]
+    [frontend.handler.ui :as ui-handler]
+    [frontend.handler.user :as user]
+    [frontend.state :as state]
+    [frontend.ui :as ui]))
+
 
 
 (defn persist-dbs!
 (defn persist-dbs!
   []
   []
   ;; only persist current db!
   ;; only persist current db!
   ;; TODO rename the function and event to persist-db
   ;; TODO rename the function and event to persist-db
   (repo-handler/persist-db! {:before     #(notification/show!
   (repo-handler/persist-db! {:before     #(notification/show!
-                                           (ui/loading (t :graph/persist))
-                                           :warning)
+                                            (ui/loading (t :graph/persist))
+                                            :warning)
                              :on-success #(ipc/ipc "persistent-dbs-saved")
                              :on-success #(ipc/ipc "persistent-dbs-saved")
                              :on-error   #(ipc/ipc "persistent-dbs-error")}))
                              :on-error   #(ipc/ipc "persistent-dbs-error")}))
 
 
+
 (defn listen-persistent-dbs!
 (defn listen-persistent-dbs!
   []
   []
   ;; TODO: move "file-watcher" to electron.ipc.channels
   ;; TODO: move "file-watcher" to electron.ipc.channels
   (js/window.apis.on
   (js/window.apis.on
-   "persistent-dbs"
-   (fn [_req]
-     (persist-dbs!))))
+    "persistent-dbs"
+    (fn [_req]
+      (persist-dbs!))))
+
 
 
 (defn ^:large-vars/cleanup-todo listen-to-electron!
 (defn ^:large-vars/cleanup-todo listen-to-electron!
   []
   []
@@ -92,10 +96,13 @@
                        (let [{:keys [page-name block-id file]} (bean/->clj data)]
                        (let [{:keys [page-name block-id file]} (bean/->clj data)]
                          (cond
                          (cond
                            page-name
                            page-name
-                           (let [db-page-name (db-model/get-redirect-page-name page-name)]
+                           (let [db-page-name (db-model/get-redirect-page-name page-name)
+                                 whiteboard? (db-model/whiteboard-page? db-page-name)]
                              ;; No error handling required, as a page name is always valid
                              ;; No error handling required, as a page name is always valid
                              ;; Open new page if the page does not exist
                              ;; Open new page if the page does not exist
-                             (editor-handler/insert-first-page-block-if-not-exists! db-page-name))
+                             (if whiteboard?
+                               (route-handler/redirect-to-whiteboard! page-name {:block-id block-id})
+                               (editor-handler/insert-first-page-block-if-not-exists! db-page-name)))
 
 
                            block-id
                            block-id
                            (if (db-model/get-block-by-uuid block-id)
                            (if (db-model/get-block-by-uuid block-id)
@@ -121,14 +128,14 @@
                      (fn [data]
                      (fn [data]
                        (let [repo (bean/->clj data)
                        (let [repo (bean/->clj data)
                              before-f #(notification/show!
                              before-f #(notification/show!
-                                        (ui/loading (t :graph/persist))
-                                        :warning)
+                                         (ui/loading (t :graph/persist))
+                                         :warning)
                              after-f #(ipc/ipc "broadcastPersistGraphDone")
                              after-f #(ipc/ipc "broadcastPersistGraphDone")
                              error-f (fn []
                              error-f (fn []
                                        (after-f)
                                        (after-f)
                                        (notification/show!
                                        (notification/show!
-                                        (t :graph/persist-error)
-                                        :error))
+                                         (t :graph/persist-error)
+                                         :error))
                              handlers {:before     before-f
                              handlers {:before     before-f
                                        :on-success after-f
                                        :on-success after-f
                                        :on-error   error-f}]
                                        :on-error   error-f}]
@@ -150,31 +157,42 @@
   (js/window.apis.on "quickCapture"
   (js/window.apis.on "quickCapture"
                      (fn [args]
                      (fn [args]
                        (let [{:keys [url title content]} (bean/->clj args)
                        (let [{:keys [url title content]} (bean/->clj args)
-                             page (or (state/get-current-page)
-                                      (string/lower-case (date/journal-name)))
+                             insert-today? (get-in (state/get-config)
+                                                   [:quick-capture-options :insert-today]
+                                                   false)
+                             today-page (string/lower-case (date/today))
+                             page (if (true? insert-today?)
+                                    today-page
+                                    (or (state/get-current-page)
+                                        today-page
+                                        "Quick Capture"))
                              format (db/get-page-format page)
                              format (db/get-page-format page)
                              time (date/get-current-time)
                              time (date/get-current-time)
                              text (or (and content (not-empty (string/trim content))) "")
                              text (or (and content (not-empty (string/trim content))) "")
-                             link (if (not-empty title) (config/link-format format title url) url)
+                             link (if (string/includes? url "www.youtube.com/watch") (str title " {{video " url "}}") (if (not-empty title) (config/link-format format title url) url))
                              template (get-in (state/get-config)
                              template (get-in (state/get-config)
                                               [:quick-capture-templates :text]
                                               [:quick-capture-templates :text]
                                               "**{time}** [[quick capture]]: {text} {url}")
                                               "**{time}** [[quick capture]]: {text} {url}")
                              content (-> template
                              content (-> template
                                          (string/replace "{time}" time)
                                          (string/replace "{time}" time)
                                          (string/replace "{url}" link)
                                          (string/replace "{url}" link)
-                                         (string/replace "{text}" text))]
-                         (if (and (state/get-edit-block) (state/editing?))
-                           (editor-handler/insert content)
+                                         (string/replace "{text}" text))
+                             edit-content (state/get-edit-content)
+                             edit-content-include-capture? (and edit-content (string/includes? edit-content "[[quick capture]]"))]
+                         (if (and (state/editing?) (not edit-content-include-capture?))
+                           (editor-handler/insert (str "\n" content))
                            (editor-handler/api-insert-new-block! content {:page page
                            (editor-handler/api-insert-new-block! content {:page page
                                                                           :edit-block? false
                                                                           :edit-block? false
                                                                           :replace-empty-target? true})))))
                                                                           :replace-empty-target? true})))))
 
 
+
   (js/window.apis.on "openNewWindowOfGraph"
   (js/window.apis.on "openNewWindowOfGraph"
                      ;; Handle open new window in renderer, until the destination graph doesn't rely on setting local storage
                      ;; Handle open new window in renderer, until the destination graph doesn't rely on setting local storage
                      ;; No db cache persisting ensured. Should be handled by the caller
                      ;; No db cache persisting ensured. Should be handled by the caller
                      (fn [repo]
                      (fn [repo]
                        (ui-handler/open-new-window! repo))))
                        (ui-handler/open-new-window! repo))))
 
 
+
 (defn listen!
 (defn listen!
   []
   []
   (listen-to-electron!)
   (listen-to-electron!)

+ 1 - 0
src/main/frontend/commands.cljs

@@ -28,6 +28,7 @@
 ;; TODO: move to frontend.handler.editor.commands
 ;; TODO: move to frontend.handler.editor.commands
 
 
 (defonce angle-bracket "<")
 (defonce angle-bracket "<")
+(defonce hashtag "#")
 (defonce colon ":")
 (defonce colon ":")
 (defonce *current-command (atom nil))
 (defonce *current-command (atom nil))
 
 

+ 13 - 21
src/main/frontend/components/block.cljs

@@ -61,6 +61,7 @@
             [frontend.util.drawer :as drawer]
             [frontend.util.drawer :as drawer]
             [frontend.util.property :as property]
             [frontend.util.property :as property]
             [frontend.util.text :as text-util]
             [frontend.util.text :as text-util]
+            [frontend.handler.notification :as notification]
             [goog.dom :as gdom]
             [goog.dom :as gdom]
             [goog.object :as gobj]
             [goog.object :as gobj]
             [lambdaisland.glogi :as log]
             [lambdaisland.glogi :as log]
@@ -264,14 +265,6 @@
     (when (seq images)
     (when (seq images)
       (lightbox/preview-images! images))))
       (lightbox/preview-images! images))))
 
 
-(defn copy-image-to-clipboard
-  [src]
-  (-> (js/fetch src)
-      (.then (fn [data]
-               (-> (.blob data)
-                   (.then (fn [blob]
-                            (js/navigator.clipboard.write (clj->js [(js/ClipboardItem. (clj->js {(.-type blob) blob}))])))))))))
-
 (defonce *resizing-image? (atom false))
 (defonce *resizing-image? (atom false))
 (rum/defcs resizable-image <
 (rum/defcs resizable-image <
   (rum/local nil ::size)
   (rum/local nil ::size)
@@ -353,12 +346,13 @@
             (ui/icon "trash")]
             (ui/icon "trash")]
 
 
            [:button.asset-action-btn
            [:button.asset-action-btn
-            {:title (t :asset/copy)
-             :tabIndex "-1"
+            {:title         (t :asset/copy)
+             :tabIndex      "-1"
              :on-mouse-down util/stop
              :on-mouse-down util/stop
-             :on-click (fn [e]
-                         (util/stop e)
-                         (copy-image-to-clipboard image-src))}
+             :on-click      (fn [e]
+                              (util/stop e)
+                              (-> (util/copy-image-to-clipboard image-src)
+                                  (p/then #(notification/show! "Copied!" :success))))}
             (ui/icon "copy")]
             (ui/icon "copy")]
 
 
            [:button.asset-action-btn
            [:button.asset-action-btn
@@ -687,10 +681,7 @@
           inner (page-inner config
           inner (page-inner config
                             page-name-in-block
                             page-name-in-block
                             page-name
                             page-name
-                            redirect-page-name page-entity contents-page? children html-export? label whiteboard-page?)
-          inner (if whiteboard-page?
-                  [:<> [:span.text-gray-500 (ui/icon "whiteboard" {:extension? true}) " "] inner]
-                  inner)]
+                            redirect-page-name page-entity contents-page? children html-export? label whiteboard-page?)]
       (cond
       (cond
         (:breadcrumb? config)
         (:breadcrumb? config)
         (or (:block/original-name page)
         (or (:block/original-name page)
@@ -1045,7 +1036,7 @@
         [:a.asset-ref.is-pdf
         [:a.asset-ref.is-pdf
          {:on-mouse-down (fn [_event]
          {:on-mouse-down (fn [_event]
                            (when-let [current (pdf-assets/inflate-asset s)]
                            (when-let [current (pdf-assets/inflate-asset s)]
-                             (state/set-state! :pdf/current current)))}
+                             (state/set-current-pdf! current)))}
          (or label-text
          (or label-text
              (->elem :span (map-inline config label)))]
              (->elem :span (map-inline config label)))]
 
 
@@ -2540,7 +2531,7 @@
                             (filterv identity)
                             (filterv identity)
                             (map (fn [x] (if (vector? x)
                             (map (fn [x] (if (vector? x)
                                            (let [[block label] x]
                                            (let [[block label] x]
-                                             (breadcrumb-fragment config block label opts))
+                                             (rum/with-key (breadcrumb-fragment config block label opts) (:block/uuid block)))
                                            [:span.opacity-70 "⋯"])))
                                            [:span.opacity-70 "⋯"])))
                             (interpose (breadcrumb-separator)))]
                             (interpose (breadcrumb-separator)))]
         [:div.breadcrumb.block-parents.flex-row.flex-1
         [:div.breadcrumb.block-parents.flex-row.flex-1
@@ -2763,7 +2754,8 @@
                         (block-handler/on-touch-move event block uuid edit? *show-left-menu? *show-right-menu?))
                         (block-handler/on-touch-move event block uuid edit? *show-left-menu? *show-right-menu?))
        :on-touch-end (fn [event]
        :on-touch-end (fn [event]
                        (block-handler/on-touch-end event block uuid *show-left-menu? *show-right-menu?))
                        (block-handler/on-touch-end event block uuid *show-left-menu? *show-right-menu?))
-       :on-touch-cancel block-handler/on-touch-cancel
+       :on-touch-cancel (fn [_e]
+                          (block-handler/on-touch-cancel *show-left-menu? *show-right-menu?))
        :on-mouse-over (fn [e]
        :on-mouse-over (fn [e]
                         (block-mouse-over e *control-show? block-id doc-mode?))
                         (block-mouse-over e *control-show? block-id doc-mode?))
        :on-mouse-leave (fn [e]
        :on-mouse-leave (fn [e]
@@ -3609,7 +3601,7 @@
             (when (seq blocks)
             (when (seq blocks)
               (let [alias? (:block/alias? page)
               (let [alias? (:block/alias? page)
                     page (db/entity (:db/id page))
                     page (db/entity (:db/id page))
-                    whiteboard? (= "whiteboard" (:block/type page))]
+                    whiteboard? (model/whiteboard-page? page)]
                 [:div.my-2 (cond-> {:key (str "page-" (:db/id page))}
                 [:div.my-2 (cond-> {:key (str "page-" (:db/id page))}
                              (:ref? config)
                              (:ref? config)
                              (assoc :class "color-level px-2 sm:px-7 py-2 rounded"))
                              (assoc :class "color-level px-2 sm:px-7 py-2 rounded"))

+ 3 - 6
src/main/frontend/components/content.cljs

@@ -362,12 +362,9 @@
     (let [page-menu-options (page-menu/page-menu page)]
     (let [page-menu-options (page-menu/page-menu page)]
       [:.menu-links-wrapper
       [:.menu-links-wrapper
        (for [{:keys [title options]} page-menu-options]
        (for [{:keys [title options]} page-menu-options]
-         (ui/menu-link
-          (merge
-           {:key title}
-           options)
-          title
-          nil))])))
+         (rum/with-key
+           (ui/menu-link options title nil)
+           title))])))
 
 
 ;; TODO: content could be changed
 ;; TODO: content could be changed
 ;; Also, keyboard bindings should only be activated after
 ;; Also, keyboard bindings should only be activated after

+ 9 - 5
src/main/frontend/components/editor.cljs

@@ -181,7 +181,7 @@
      result
      result
      {:on-chosen   chosen-handler
      {:on-chosen   chosen-handler
       :on-enter    non-exist-block-handler
       :on-enter    non-exist-block-handler
-      :empty-placeholder   [:div.text-gray-500.pl-4.pr-4 (t :editor/block-search)]
+      :empty-placeholder   [:div.text-gray-500.text-sm.px-4.py-2 (t :editor/block-search)]
       :item-render (fn [{:block/keys [page uuid]}]  ;; content returned from search engine is normalized
       :item-render (fn [{:block/keys [page uuid]}]  ;; content returned from search engine is normalized
                      (let [page (or (:block/original-name page)
                      (let [page (or (:block/original-name page)
                                     (:block/name page))
                                     (:block/name page))
@@ -191,7 +191,7 @@
                            content (:block/content block)]
                            content (:block/content block)]
                        (when-not (string/blank? content)
                        (when-not (string/blank? content)
                          [:.py-2 (search/block-search-result-item repo uuid format content q :block)])))
                          [:.py-2 (search/block-search-result-item repo uuid format content q :block)])))
-      :class       "black"})))
+      :class       "ac-block-search"})))
 
 
 (rum/defcs block-search < rum/reactive
 (rum/defcs block-search < rum/reactive
   {:will-unmount (fn [state]
   {:will-unmount (fn [state]
@@ -339,10 +339,14 @@
 
 
 (rum/defc absolute-modal < rum/static
 (rum/defc absolute-modal < rum/static
   [cp modal-name set-default-width? {:keys [top left rect]}]
   [cp modal-name set-default-width? {:keys [top left rect]}]
-  (let [max-height 370
-        max-width 300
-        offset-top 24
+  (let [vw-width js/window.innerWidth
         vw-height js/window.innerHeight
         vw-height js/window.innerHeight
+        vw-max-width (- vw-width (:left rect))
+        vw-max-height (- vw-height (:top rect))
+        sm? (< vw-width 415)
+        max-height (min (- vw-max-height 20) 800)
+        max-width (if sm? 300 (min (max 400 (/ vw-max-width 2)) 600))
+        offset-top 24
         to-max-height (if (and (seq rect) (> vw-height max-height))
         to-max-height (if (and (seq rect) (> vw-height max-height))
                         (let [delta-height (- vw-height (+ (:top rect) top offset-top))]
                         (let [delta-height (- vw-height (+ (:top rect) top offset-top))]
                           (if (< delta-height max-height)
                           (if (< delta-height max-height)

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

@@ -379,20 +379,19 @@
                                                         (first @graphs-txid)))))
                                                         (first @graphs-txid)))))
                                            nil
                                            nil
 
 
-                                           (and synced-file-graph?
-                                                (second @graphs-txid)
+                                           (and (second @graphs-txid)
                                                 (fs-sync/graph-sync-off? (second @graphs-txid))
                                                 (fs-sync/graph-sync-off? (second @graphs-txid))
                                                 (async/<! (fs-sync/<check-remote-graph-exists (second @graphs-txid))))
                                                 (async/<! (fs-sync/<check-remote-graph-exists (second @graphs-txid))))
                                            (fs-sync/<sync-start)
                                            (fs-sync/<sync-start)
 
 
                                            ;; remote graph already has been deleted, clear repos first, then create-remote-graph
                                            ;; remote graph already has been deleted, clear repos first, then create-remote-graph
-                                           synced-file-graph?  ; <check-remote-graph-exists -> false
+                                           (second @graphs-txid) ; <check-remote-graph-exists -> false
                                            (do (state/set-repos!
                                            (do (state/set-repos!
                                                 (map (fn [r]
                                                 (map (fn [r]
                                                        (if (= (:url r) current-repo)
                                                        (if (= (:url r) current-repo)
                                                          (dissoc r :GraphUUID :GraphName :remote?)
                                                          (dissoc r :GraphUUID :GraphName :remote?)
                                                          r))
                                                          r))
-                                                     (state/get-repos)))
+                                                  (state/get-repos)))
                                                (create-remote-graph-fn))
                                                (create-remote-graph-fn))
 
 
                                            (second @graphs-txid) ; sync not started yet
                                            (second @graphs-txid) ; sync not started yet

+ 4 - 4
src/main/frontend/components/header.cljs

@@ -166,17 +166,17 @@
         custom-home-page? (and (state/custom-home-page?)
         custom-home-page? (and (state/custom-home-page?)
                                (= (state/sub-default-home-page) (state/get-current-page)))
                                (= (state/sub-default-home-page) (state/get-current-page)))
         sync-enabled? (file-sync-handler/enable-sync?)]
         sync-enabled? (file-sync-handler/enable-sync?)]
-    [:div.cp__header#head
+    [:div.cp__header.drag-region#head
      {:class           (util/classnames [{:electron-mac   electron-mac?
      {:class           (util/classnames [{:electron-mac   electron-mac?
                                           :native-ios     (mobile-util/native-ios?)
                                           :native-ios     (mobile-util/native-ios?)
                                           :native-android (mobile-util/native-android?)}])
                                           :native-android (mobile-util/native-android?)}])
       :on-double-click (fn [^js e]
       :on-double-click (fn [^js e]
                          (when-let [target (.-target e)]
                          (when-let [target (.-target e)]
                            (when (and (util/electron?)
                            (when (and (util/electron?)
-                                      (.. target -classList (contains "cp__header")))
+                                      (.. target -classList (contains "drag-region")))
                              (js/window.apis.toggleMaxOrMinActiveWindow))))
                              (js/window.apis.toggleMaxOrMinActiveWindow))))
       :style           {:fontSize  50}}
       :style           {:fontSize  50}}
-     [:div.l.flex
+     [:div.l.flex.drag-region
       (when-not (mobile-util/native-platform?)
       (when-not (mobile-util/native-platform?)
         [left-menu
         [left-menu
          (when current-repo ;; this is for the Search button
          (when current-repo ;; this is for the Search button
@@ -196,7 +196,7 @@
              {:title "Go back" :on-click #(js/window.history.back)}
              {:title "Go back" :on-click #(js/window.history.back)}
              (ui/icon "chevron-left" {:size 26})])))]
              (ui/icon "chevron-left" {:size 26})])))]
 
 
-     [:div.r.flex
+     [:div.r.flex.drag-region
       (when (and current-repo
       (when (and current-repo
                  (not (config/demo-graph? current-repo))
                  (not (config/demo-graph? current-repo))
                  (user-handler/alpha-or-beta-user?))
                  (user-handler/alpha-or-beta-user?))

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

@@ -1,8 +1,6 @@
 .cp__header {
 .cp__header {
   @apply z-10;
   @apply z-10;
 
 
-  -webkit-app-region: drag;
-
   padding-top: var(--ls-headbar-inner-top-padding);
   padding-top: var(--ls-headbar-inner-top-padding);
   height: calc(var(--ls-headbar-height) + var(--ls-headbar-inner-top-padding));
   height: calc(var(--ls-headbar-height) + var(--ls-headbar-inner-top-padding));
   display: flex;
   display: flex;
@@ -13,7 +11,6 @@
   top: 0;
   top: 0;
   left: 0;
   left: 0;
   right: 0;
   right: 0;
-  user-select: none;
   line-height: 1;
   line-height: 1;
   white-space: nowrap;
   white-space: nowrap;
 
 
@@ -351,3 +348,8 @@ html.is-zoomed-native-ios {
     --ls-headbar-height: 2.5rem;
     --ls-headbar-height: 2.5rem;
   }
   }
 }
 }
+
+.drag-region {
+  user-select: none;
+  -webkit-app-region: drag;
+}

+ 24 - 10
src/main/frontend/components/lazy_editor.cljs

@@ -3,7 +3,10 @@
             [rum.core :as rum]
             [rum.core :as rum]
             [shadow.lazy :as lazy]
             [shadow.lazy :as lazy]
             [frontend.ui :as ui]
             [frontend.ui :as ui]
-            [frontend.state :as state]))
+            [frontend.config :as config]
+            [frontend.state :as state]
+            [frontend.handler.plugin :refer [hook-extensions-enhancer-by-type]]
+            [promesa.core :as p]))
 
 
 ;; TODO: Why does shadow fail when code is required
 ;; TODO: Why does shadow fail when code is required
 #_:clj-kondo/ignore
 #_:clj-kondo/ignore
@@ -11,17 +14,28 @@
 
 
 (defonce loaded? (atom false))
 (defonce loaded? (atom false))
 
 
-(rum/defc editor < rum/reactive
-  {:will-mount (fn [state]
-                 (lazy/load lazy-editor
-                            (fn []
-                              (reset! loaded? true)))
-                 state)}
+(rum/defc editor <
+  rum/reactive
+  {:will-mount
+   (fn [state]
+     (lazy/load lazy-editor
+                (fn []
+                  (if-not @loaded?
+                    (p/finally
+                     (p/all (when-let [enhancers (and config/lsp-enabled?
+                                                      (seq (hook-extensions-enhancer-by-type :codemirror)))]
+                              (for [{f :enhancer} enhancers]
+                                (when (fn? f) (f (. js/window -CodeMirror))))))
+                     (fn []
+                       (-> (p/delay 200)
+                           (p/then #(reset! loaded? true)))))
+                    (reset! loaded? true))))
+     state)}
   [config id attr code options]
   [config id attr code options]
   (let [loaded? (rum/react loaded?)
   (let [loaded? (rum/react loaded?)
-        theme (state/sub :ui/theme)
-        code (or code "")
-        code (string/replace-first code #"\n$" "")] ;; See-also: #3410
+        theme   (state/sub :ui/theme)
+        code    (or code "")
+        code    (string/replace-first code #"\n$" "")]      ;; See-also: #3410
     (if loaded?
     (if loaded?
       (@lazy-editor config id attr code theme options)
       (@lazy-editor config id attr code theme options)
       (ui/loading "CodeMirror"))))
       (ui/loading "CodeMirror"))))

+ 6 - 4
src/main/frontend/components/page.cljs

@@ -16,6 +16,7 @@
             [frontend.db.model :as model]
             [frontend.db.model :as model]
             [frontend.extensions.graph :as graph]
             [frontend.extensions.graph :as graph]
             [frontend.extensions.pdf.assets :as pdf-assets]
             [frontend.extensions.pdf.assets :as pdf-assets]
+            [frontend.extensions.pdf.utils :as pdf-utils]
             [frontend.format.block :as block]
             [frontend.format.block :as block]
             [frontend.handler.common :as common-handler]
             [frontend.handler.common :as common-handler]
             [frontend.handler.config :as config-handler]
             [frontend.handler.config :as config-handler]
@@ -282,7 +283,7 @@
           whiteboard-page? (model/whiteboard-page? page-name)
           whiteboard-page? (model/whiteboard-page? page-name)
           untitled? (and whiteboard-page? (parse-uuid page-name)) ;; normal page cannot be untitled right?
           untitled? (and whiteboard-page? (parse-uuid page-name)) ;; normal page cannot be untitled right?
           title (if hls-page?
           title (if hls-page?
-                  [:a.asset-ref (pdf-assets/fix-local-asset-pagename title)]
+                  [:a.asset-ref (pdf-utils/fix-local-asset-pagename title)]
                   (if fmt-journal? (date/journal-title->custom-format title) title))
                   (if fmt-journal? (date/journal-title->custom-format title) title))
           old-name (or title page-name)]
           old-name (or title page-name)]
       [:h1.page-title.flex.cursor-pointer.gap-1.w-full
       [:h1.page-title.flex.cursor-pointer.gap-1.w-full
@@ -369,8 +370,8 @@
           journal? (db/journal-page? page-name)
           journal? (db/journal-page? page-name)
           fmt-journal? (boolean (date/journal-title->int page-name))
           fmt-journal? (boolean (date/journal-title->int page-name))
           sidebar? (:sidebar? option)
           sidebar? (:sidebar? option)
-          whiteboard? (:whiteboard? option)
-          whiteboard-page? (model/whiteboard-page? page-name)
+          whiteboard? (:whiteboard? option) ;; in a whiteboard portal shape?
+          whiteboard-page? (model/whiteboard-page? page-name) ;; is this page a whiteboard?
           route-page-name path-page-name
           route-page-name path-page-name
           page (if block?
           page (if block?
                  (->> (:db/id (:block/page (db/entity repo [:block/uuid block-id])))
                  (->> (:db/id (:block/page (db/entity repo [:block/uuid block-id])))
@@ -400,7 +401,7 @@
               {:key path-page-name
               {:key path-page-name
                :class (util/classnames [{:is-journals (or journal? fmt-journal?)}])})
                :class (util/classnames [{:is-journals (or journal? fmt-journal?)}])})
 
 
-       (if whiteboard-page?
+       (if (and whiteboard-page? (not sidebar?))
          [:div ((state/get-component :whiteboard/tldraw-preview) page-name)] ;; FIXME: this is not reactive
          [:div ((state/get-component :whiteboard/tldraw-preview) page-name)] ;; FIXME: this is not reactive
          [:div.relative
          [:div.relative
           (when (and (not sidebar?) (not block?))
           (when (and (not sidebar?) (not block?))
@@ -1050,6 +1051,7 @@
                                              (swap! *checks update idx not))})]
                                              (swap! *checks update idx not))})]
 
 
                 [:td.name [:a {:on-click (fn [e]
                 [:td.name [:a {:on-click (fn [e]
+                                          (.preventDefault e)
                                            (let [repo (state/get-current-repo)]
                                            (let [repo (state/get-current-repo)]
                                              (when (gobj/get e "shiftKey")
                                              (when (gobj/get e "shiftKey")
                                                (state/sidebar-add-block!
                                                (state/sidebar-add-block!

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

@@ -113,7 +113,8 @@
             {:title   (t :page/delete)
             {:title   (t :page/delete)
              :options {:on-click #(state/set-modal! (delete-page-dialog page-name))}})
              :options {:on-click #(state/set-modal! (delete-page-dialog page-name))}})
 
 
-          (when-not (mobile-util/native-platform?)
+          (when (and (not (mobile-util/native-platform?)) 
+                     (state/get-current-page))
             {:title (t :page/presentation-mode)
             {:title (t :page/presentation-mode)
              :options {:on-click (fn []
              :options {:on-click (fn []
                                    (state/sidebar-add-block!
                                    (state/sidebar-add-block!

+ 56 - 13
src/main/frontend/components/search.cljs

@@ -13,7 +13,7 @@
             [frontend.db.model :as model]
             [frontend.db.model :as model]
             [frontend.handler.search :as search-handler]
             [frontend.handler.search :as search-handler]
             [frontend.handler.whiteboard :as whiteboard-handler]
             [frontend.handler.whiteboard :as whiteboard-handler]
-            [frontend.extensions.pdf.assets :as pdf-assets]
+            [frontend.extensions.pdf.utils :as pdf-utils]
             [frontend.ui :as ui]
             [frontend.ui :as ui]
             [frontend.state :as state]
             [frontend.state :as state]
             [frontend.mixins :as mixins]
             [frontend.mixins :as mixins]
@@ -203,7 +203,7 @@
 (defn- search-item-render
 (defn- search-item-render
   [search-q {:keys [type data alias]}]
   [search-q {:keys [type data alias]}]
   (let [search-mode (state/get-search-mode)
   (let [search-mode (state/get-search-mode)
-        data (if (string? data) (pdf-assets/fix-local-asset-pagename data) data)]
+        data (if (string? data) (pdf-utils/fix-local-asset-pagename data) data)]
     [:div {:class "py-2"}
     [:div {:class "py-2"}
      (case type
      (case type
        :graph-add-filter
        :graph-add-filter
@@ -231,25 +231,33 @@
                            (highlight-exact-query data search-q))
                            (highlight-exact-query data search-q))
 
 
        :block
        :block
-       (let [{:block/keys [page uuid]} data  ;; content here is normalized
+       (let [{:block/keys [page uuid content]} data  ;; content here is normalized
              page (util/get-page-original-name page)
              page (util/get-page-original-name page)
              repo (state/sub :git/current-repo)
              repo (state/sub :git/current-repo)
              format (db/get-page-format page)
              format (db/get-page-format page)
-             block (model/query-block-by-uuid uuid)
-             content (:block/content block)]
+             block (when-not (string/blank? uuid)
+                     (model/query-block-by-uuid uuid))
+             content' (if block (:block/content block) content)]
          [:span {:data-block-ref uuid}
          [:span {:data-block-ref uuid}
           (search-result-item {:name "block"
           (search-result-item {:name "block"
                                :title (t :search-item/block)
                                :title (t :search-item/block)
                                :extension? true}
                                :extension? true}
-                              (if block
-                                (block-search-result-item repo uuid format content search-q search-mode)
+
+                              (cond
+                                (some? block)
+                                (block-search-result-item repo uuid format content' search-q search-mode)
+
+                                (not (string/blank? content'))
+                                content'
+
+                                :else
                                 (do (log/error "search result with non-existing uuid: " data)
                                 (do (log/error "search result with non-existing uuid: " data)
                                     (str "Cache is outdated. Please click the 'Re-index' button in the graph's dropdown menu."))))])
                                     (str "Cache is outdated. Please click the 'Re-index' button in the graph's dropdown menu."))))])
 
 
        nil)]))
        nil)]))
 
 
 (rum/defc search-auto-complete
 (rum/defc search-auto-complete
-  [{:keys [pages files blocks has-more?] :as result} search-q all?]
+  [{:keys [engine pages files blocks has-more?] :as result} search-q all?]
   (let [pages (when-not all? (map (fn [page]
   (let [pages (when-not all? (map (fn [page]
                                     (let [alias (model/get-redirect-page-name page)]
                                     (let [alias (model/get-redirect-page-name page)]
                                       (cond->
                                       (cond->
@@ -264,6 +272,7 @@
         blocks (map (fn [block] {:type :block :data block}) blocks)
         blocks (map (fn [block] {:type :block :data block}) blocks)
         search-mode (state/sub :search/mode)
         search-mode (state/sub :search/mode)
         new-page (if (or
         new-page (if (or
+                      (some? engine)
                       (and (seq pages)
                       (and (seq pages)
                            (= (util/safe-page-name-sanity-lc search-q)
                            (= (util/safe-page-name-sanity-lc search-q)
                               (util/safe-page-name-sanity-lc (:data (first pages)))))
                               (util/safe-page-name-sanity-lc (:data (first pages)))))
@@ -401,14 +410,17 @@
       state
       state
       :on-hide (fn []
       :on-hide (fn []
                  (search-handler/clear-search!)))))
                  (search-handler/clear-search!)))))
+  (rum/local nil ::active-engine-tab)
   [state]
   [state]
   (let [search-result (state/sub :search/result)
   (let [search-result (state/sub :search/result)
         search-q (state/sub :search/q)
         search-q (state/sub :search/q)
         search-mode (state/sub :search/mode)
         search-mode (state/sub :search/mode)
+        engines (state/sub :search/engines)
+        *active-engine-tab (::active-engine-tab state)
         timeout 300
         timeout 300
         in-page-search? (= search-mode :page)]
         in-page-search? (= search-mode :page)]
     [:div.cp__palette.cp__palette-main
     [:div.cp__palette.cp__palette-main
-     [:div.ls-search.p-2
+     [:div.ls-search.p-2.md:p-0
       [:div.input-wrap
       [:div.input-wrap
       [:input.cp__palette-input.w-full
       [:input.cp__palette-input.w-full
        {:type          "text"
        {:type          "text"
@@ -421,7 +433,12 @@
                          (default-placeholder search-mode))
                          (default-placeholder search-mode))
         :auto-complete (if (util/chrome?) "chrome-off" "off") ; off not working here
         :auto-complete (if (util/chrome?) "chrome-off" "off") ; off not working here
         :value         search-q
         :value         search-q
-        :on-change     (fn [e]
+        :on-key-down   (fn [^js e]
+                         (when (= 27 (.-keyCode e))
+                           (when-not (string/blank? search-q)
+                             (util/stop e)
+                             (search-handler/clear-search!))))
+        :on-change     (fn [^js e]
                          (when @search-timeout
                          (when @search-timeout
                            (js/clearTimeout @search-timeout))
                            (js/clearTimeout @search-timeout))
                          (let [value (util/evalue e)
                          (let [value (util/evalue e)
@@ -443,9 +460,35 @@
                                             (search-handler/search (state/get-current-repo) value)))
                                             (search-handler/search (state/get-current-repo) value)))
                                         timeout))))))}]]
                                         timeout))))))}]]
       [:div.search-results-wrap
       [:div.search-results-wrap
-       (if (seq search-result)
-         (search-auto-complete search-result search-q false)
-         (recent-search-and-pages in-page-search?))]]]))
+        ;; list registered search engines
+       (when (seq engines)
+         [:ul.search-results-engines-tabs
+          [:li
+           {:class (when-not @*active-engine-tab "is-active")}
+           (ui/button
+            [:span.flex.items-center
+             (svg/logo 14) [:span.pl-2 "Default"]]
+            :background "orange"
+            :on-click #(reset! *active-engine-tab nil))]
+
+          (for [[k v] engines]
+            [:li
+             {:key k
+              :class (if (= k @*active-engine-tab) "is-active" "")}
+             (ui/button [:span.flex.items-center
+                         [:span.pr-2 (ui/icon "puzzle")]
+                         (:name v)
+                         (when-let [result (and v (:result v))]
+                           (str " (" (count (:blocks result)) ")"))]
+                        :on-click #(reset! *active-engine-tab k))])])
+
+       (if-not (nil? @*active-engine-tab)
+         (let [active-engine-result (get-in engines [@*active-engine-tab :result])]
+           (search-auto-complete
+            (merge active-engine-result {:engine @*active-engine-tab}) search-q false))
+         (if (seq search-result)
+           (search-auto-complete search-result search-q false)
+           (recent-search-and-pages in-page-search?)))]]]))
 
 
 (rum/defc more < rum/reactive
 (rum/defc more < rum/reactive
   [route]
   [route]

+ 32 - 35
src/main/frontend/components/search.css

@@ -1,6 +1,9 @@
 #search {
 #search {
   > .inner {
   > .inner {
     width: 100%;
     width: 100%;
+    max-width: 100%;
+
+    border-radius: 4px;
   }
   }
 
 
   .search-result {
   .search-result {
@@ -8,52 +11,46 @@
   }
   }
 }
 }
 
 
-.search-ac {
-    background-color: var(--ls-primary-background-color);
-}
-
 #search-wrapper svg {
 #search-wrapper svg {
-    color: var(--ls-search-icon-color, #9fa6b2);
-    opacity: 0.6;
-    transition: .3s;
+  color: var(--ls-search-icon-color, #9fa6b2);
+  opacity: 0.6;
+  transition: .3s;
 }
 }
 
 
 #search-wrapper:hover svg, #search-wrapper:focus-within svg {
 #search-wrapper:hover svg, #search-wrapper:focus-within svg {
-    color: var(--ls-link-text-hover-color, #4b5563);
-    opacity: 0.8;
-}
-
-#search-field {
-  background-color: var(--ls-search-background-color, #fff);
-  color: var(--ls-secondary-text-color, #161e2e);
-  transition: background .3s;
-  max-width: 545px;
-  opacity: 0;
+  color: var(--ls-link-text-hover-color, #4b5563);
+  opacity: 0.8;
 }
 }
 
 
 #search-wrapper {
 #search-wrapper {
-    transition: .3s;
-    padding-right: 12px;
+  transition: .3s;
+  padding-right: 12px;
 }
 }
 
 
-#search-field:hover,
-#search-field:focus-within {
-    opacity: 1;
+.search-item svg {
+  transform: scale(0.8);
 }
 }
 
 
-#search>.inner {
-    max-width: 100%;
-    border-radius: 4px;
-}
+.search-results-engines {
+  &-tabs {
+    @apply flex list-none -mx-2 mb-2;
 
 
-#search-field:focus {
-    background: var(--ls-search-background-color);
-}
+    background: var(--ls-primary-background-color);
 
 
-.dark-theme #search-field:focus {
-    box-shadow: 0px 0px 20px 0px rgba(18, 18, 18, .3);
-}
+    > li {
+      @apply p-0 m-0 min-w-[150px] flex-1;
 
 
-.search-item svg {
-    transform: scale(0.8);
-}
+      .ui__button {
+        @apply w-full h-full justify-center border;
+
+        background: transparent;
+        border-radius: 0;
+        line-height: 1;
+      }
+
+      &.is-active .ui__button {
+        background: var(--ls-quaternary-background-color);
+      }
+    }
+  }
+}

+ 5 - 5
src/main/frontend/components/settings.cljs

@@ -357,11 +357,11 @@
           logical-outdenting?
           logical-outdenting?
           config-handler/toggle-logical-outdenting!))
           config-handler/toggle-logical-outdenting!))
 
 
-(defn perferred-pasting-file [t perferred-pasting-file?]
+(defn preferred-pasting-file [t preferred-pasting-file?]
   (toggle "preferred_pasting_file"
   (toggle "preferred_pasting_file"
           (t :settings-page/preferred-pasting-file)
           (t :settings-page/preferred-pasting-file)
-          perferred-pasting-file?
-          config-handler/toggle-perferred-pasting-file!))
+          preferred-pasting-file?
+          config-handler/toggle-preferred-pasting-file!))
 
 
 (defn tooltip-row [t enable-tooltip?]
 (defn tooltip-row [t enable-tooltip?]
   (toggle "enable_tooltip"
   (toggle "enable_tooltip"
@@ -572,7 +572,7 @@
         enable-timetracking? (state/enable-timetracking?)
         enable-timetracking? (state/enable-timetracking?)
         enable-all-pages-public? (state/all-pages-public?)
         enable-all-pages-public? (state/all-pages-public?)
         logical-outdenting? (state/logical-outdenting?)
         logical-outdenting? (state/logical-outdenting?)
-        perferred-pasting-file? (state/perferred-pasting-file?)
+        preferred-pasting-file? (state/preferred-pasting-file?)
         enable-tooltip? (state/enable-tooltip?)
         enable-tooltip? (state/enable-tooltip?)
         enable-shortcut-tooltip? (state/sub :ui/shortcut-tooltip?)
         enable-shortcut-tooltip? (state/sub :ui/shortcut-tooltip?)
         show-brackets? (state/show-brackets?)
         show-brackets? (state/show-brackets?)
@@ -586,7 +586,7 @@
      (show-brackets-row t show-brackets?)
      (show-brackets-row t show-brackets?)
      (when (util/electron?) (switch-spell-check-row t))
      (when (util/electron?) (switch-spell-check-row t))
      (outdenting-row t logical-outdenting?)
      (outdenting-row t logical-outdenting?)
-     (perferred-pasting-file t perferred-pasting-file?)
+     (preferred-pasting-file t preferred-pasting-file?)
      (when-not (or (util/mobile?) (mobile-util/native-platform?))
      (when-not (or (util/mobile?) (mobile-util/native-platform?))
        (shortcut-tooltip-row t enable-shortcut-tooltip?))
        (shortcut-tooltip-row t enable-shortcut-tooltip?))
      (when-not (or (util/mobile?) (mobile-util/native-platform?))
      (when-not (or (util/mobile?) (mobile-util/native-platform?))

+ 205 - 116
src/main/frontend/components/sidebar.cljs

@@ -2,6 +2,7 @@
   (:require [cljs-drag-n-drop.core :as dnd]
   (:require [cljs-drag-n-drop.core :as dnd]
             [clojure.string :as string]
             [clojure.string :as string]
             [frontend.components.command-palette :as command-palette]
             [frontend.components.command-palette :as command-palette]
+            [frontend.components.find-in-page :as find-in-page]
             [frontend.components.header :as header]
             [frontend.components.header :as header]
             [frontend.components.journal :as journal]
             [frontend.components.journal :as journal]
             [frontend.components.onboarding :as onboarding]
             [frontend.components.onboarding :as onboarding]
@@ -12,17 +13,15 @@
             [frontend.components.svg :as svg]
             [frontend.components.svg :as svg]
             [frontend.components.theme :as theme]
             [frontend.components.theme :as theme]
             [frontend.components.widgets :as widgets]
             [frontend.components.widgets :as widgets]
-            [frontend.components.find-in-page :as find-in-page]
             [frontend.config :as config]
             [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
             [frontend.context.i18n :refer [t]]
             [frontend.db :as db]
             [frontend.db :as db]
             [frontend.db-mixins :as db-mixins]
             [frontend.db-mixins :as db-mixins]
             [frontend.db.model :as db-model]
             [frontend.db.model :as db-model]
-            [frontend.extensions.pdf.assets :as pdf-assets]
+            [frontend.extensions.pdf.utils :as pdf-utils]
             [frontend.extensions.srs :as srs]
             [frontend.extensions.srs :as srs]
             [frontend.handler.common :as common-handler]
             [frontend.handler.common :as common-handler]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.editor :as editor-handler]
-            [frontend.handler.mobile.swipe :as swipe]
             [frontend.handler.page :as page-handler]
             [frontend.handler.page :as page-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.user :as user-handler]
             [frontend.handler.user :as user-handler]
@@ -89,7 +88,7 @@
               (route-handler/redirect-to-whiteboard! name)
               (route-handler/redirect-to-whiteboard! name)
               (route-handler/redirect-to-page! name {:click-from-recent? recent?})))))}
               (route-handler/redirect-to-page! name {:click-from-recent? recent?})))))}
      [:span.page-icon (if whiteboard-page? (ui/icon "whiteboard" {:extension? true}) icon)]
      [:span.page-icon (if whiteboard-page? (ui/icon "whiteboard" {:extension? true}) icon)]
-     [:span.page-title (pdf-assets/fix-local-asset-pagename original-name)]]))
+     [:span.page-title (pdf-utils/fix-local-asset-pagename original-name)]]))
 
 
 (defn get-page-icon [page-entity]
 (defn get-page-icon [page-entity]
   (let [default-icon (ui/icon "page" {:extension? true})
   (let [default-icon (ui/icon "page" {:extension? true})
@@ -226,7 +225,7 @@
 (defn close-sidebar-on-mobile!
 (defn close-sidebar-on-mobile!
   []
   []
   (and (util/sm-breakpoint?)
   (and (util/sm-breakpoint?)
-    (state/toggle-left-sidebar!)))
+       (state/toggle-left-sidebar!)))
 
 
 (defn create-dropdown
 (defn create-dropdown
   []
   []
@@ -256,115 +255,207 @@
                            :extension? true})}])
                            :extension? true})}])
    {}))
    {}))
 
 
-(rum/defc sidebar-nav < rum/reactive
-  [route-match close-modal-fn left-sidebar-open? srs-open?]
-  (let [default-home (get-default-home-if-valid)
-        route-name (get-in route-match [:data :name])
+(rum/defc ^:large-vars/cleanup-todo sidebar-nav
+  [route-match close-modal-fn left-sidebar-open? srs-open?
+   *closing? close-signal touching-x-offset]
+  (let [[local-closing? set-local-closing?] (rum/use-state false)
+        [el-rect set-el-rect!] (rum/use-state nil)
+        ref-el              (rum/use-ref nil)
+        ref-open?           (rum/use-ref left-sidebar-open?)
+        default-home        (get-default-home-if-valid)
+        route-name          (get-in route-match [:data :name])
         enable-whiteboards? (state/enable-whiteboards?)
         enable-whiteboards? (state/enable-whiteboards?)
-        on-contents-scroll #(when-let [^js el (.-target %)]
-                              (let [top (.-scrollTop el)
-                                    cls (.-classList el)
-                                    cls' "is-scrolled"]
-                                (if (> top 2)
-                                  (.add cls cls')
-                                  (.remove cls cls'))))]
-
-    [:div.left-sidebar-inner.flex-1.flex.flex-col.min-h-0
-     {:on-click #(when-let [^js target (and (util/sm-breakpoint?) (.-target %))]
-                   (when (some (fn [sel] (boolean (.closest target sel)))
-                               [".favorites .bd" ".recent .bd" ".dropdown-wrapper" ".nav-header"])
-                     (close-modal-fn)))}
-
-     [:div.flex.flex-col.wrap.gap-1.relative
-      (when (mobile-util/native-platform?)
-        [:div.fake-bar.absolute
-         [:button
-          {:on-click state/toggle-left-sidebar!}
-          (ui/icon "menu-2" {:size ui/icon-size})]])
-
-      [:nav.px-4.flex.flex-col.gap-1.cp__menubar-repos
-       {:aria-label "Navigation menu"}
-       (repo/repos-dropdown)
-
-       [:div.nav-header.flex.gap-1.flex-col
-        (let [page (:page default-home)]
-          (if (and page (not (state/enable-journals? (state/get-current-repo))))
-            (sidebar-item
-             {:class            "home-nav"
-              :title            page
-              :on-click-handler route-handler/redirect-to-home!
-              :active           (and (not srs-open?)
-                                     (= route-name :page)
-                                     (= page (get-in route-match [:path-params :name])))
-              :icon             "home"})
-            (sidebar-item
-             {:class            "journals-nav"
-              :active           (and (not srs-open?)
-                                     (or (= route-name :all-journals) (= route-name :home)))
-              :title            (t :left-side-bar/journals)
-              :on-click-handler (fn [e]
-                                  (if (gobj/get e "shiftKey")
-                                    (route-handler/sidebar-journals!)
-                                    (route-handler/go-to-journals!)))
-              :icon             "calendar"})))
-
-        (when (state/enable-flashcards? (state/get-current-repo))
-          [:div.flashcards-nav
-           (flashcards srs-open?)])
-
-        (sidebar-item
-         {:class  "graph-view-nav"
-          :title  (t :right-side-bar/graph-view)
-          :href   (rfe/href :graph)
-          :active (and (not srs-open?) (= route-name :graph))
-          :icon   "hierarchy"})
-
-        (sidebar-item
-         {:class  "all-pages-nav"
-          :title  (t :right-side-bar/all-pages)
-          :href   (rfe/href :all-pages)
-          :active (and (not srs-open?) (= route-name :all-pages))
-          :icon   "files"})
-
-        (when enable-whiteboards?
-          (sidebar-item
-           {:class "whiteboard"
-            :title (t :right-side-bar/whiteboards)
-            :href  (rfe/href :whiteboards)
-            :active (and (not srs-open?) (#{:whiteboard :whiteboards} route-name))
-            :icon  "whiteboard"
-            :icon-extension? true}))]]
-
-      [:div.nav-contents-container.flex.flex-col.gap-1.pt-1
-       {:on-scroll on-contents-scroll}
-       (when left-sidebar-open?
-         (favorites t))
-
-       (when (and left-sidebar-open? (not config/publishing?))
-         (recent-pages t))]
-
-      [:footer.px-2 {:class "create"}
-       (when-not config/publishing?
-         (if enable-whiteboards?
-           (create-dropdown)
-           [:a.item.group.flex.items-center.px-2.py-2.text-sm.font-medium.rounded-md.new-page-link
-            {:on-click (fn []
-                         (and (util/sm-breakpoint?)
-                              (state/toggle-left-sidebar!))
-                         (state/pub-event! [:go/search]))}
-            (ui/icon "circle-plus" {:style {:font-size 20}})
-            [:span.flex-1 (t :right-side-bar/new-page)]]))]]]))
-
-(rum/defc left-sidebar < rum/reactive
-  [{:keys [left-sidebar-open? route-match]}]
-  (let [close-fn #(state/set-left-sidebar-open! false)
-        srs-open? (= :srs (state/sub :modal/id))]
+        on-contents-scroll  #(when-let [^js el (.-target %)]
+                               (let [top  (.-scrollTop el)
+                                     cls  (.-classList el)
+                                     cls' "is-scrolled"]
+                                 (if (> top 2)
+                                   (.add cls cls')
+                                   (.remove cls cls'))))
+        close-fn            #(set-local-closing? true)
+        touching-x-offset (when (number? touching-x-offset)
+                            (if-not left-sidebar-open?
+                              (when (> touching-x-offset 0)
+                                (min touching-x-offset (:width el-rect)))
+                              (when (< touching-x-offset 0)
+                                (max touching-x-offset (- 0 (:width el-rect))))))
+        offset-ratio (and (number? touching-x-offset)
+                            (some->> (:width el-rect)
+                                     (/ touching-x-offset)))]
+
+    (rum/use-effect!
+     #(js/setTimeout
+       (fn [] (some-> (rum/deref ref-el)
+                      (.getBoundingClientRect)
+                      (.toJSON)
+                      (js->clj :keywordize-keys true)
+                      (set-el-rect!)))
+       16)
+     [])
+
+    (rum/use-layout-effect!
+     (fn []
+       (when (and (rum/deref ref-open?) local-closing?)
+         (reset! *closing? true))
+       (rum/set-ref! ref-open? left-sidebar-open?)
+       #())
+     [local-closing? left-sidebar-open?])
+
+    (rum/use-effect!
+     (fn []
+       (when-not (neg? close-signal)
+         (close-fn)))
+     [close-signal])
+
+    [:<>
+     [:div.left-sidebar-inner.flex-1.flex.flex-col.min-h-0
+      {:ref               ref-el
+       :style             (cond-> {}
+                            (and (number? offset-ratio)
+                                 (> touching-x-offset 0))
+                            (assoc :transform (str "translate3d(calc(" touching-x-offset "px - 100%), 0, 0)"))
+
+                            (and (number? offset-ratio)
+                                 (< touching-x-offset 0))
+                            (assoc :transform (str "translate3d(" (* offset-ratio 100) "%, 0, 0)")))
+       :on-transition-end (fn []
+                            (when local-closing?
+                              (reset! *closing? false)
+                              (set-local-closing? false)
+                              (close-modal-fn)))
+       :on-click          #(when-let [^js target (and (util/sm-breakpoint?) (.-target %))]
+                             (when (some (fn [sel] (boolean (.closest target sel)))
+                                         [".favorites .bd" ".recent .bd" ".dropdown-wrapper" ".nav-header"])
+                               (close-fn)))}
+
+      [:div.flex.flex-col.wrap.gap-1.relative
+       ;; temporarily remove fake hamburger menu
+       ;(when (mobile-util/native-platform?)
+       ;  [:div.fake-bar.absolute
+       ;   [:button
+       ;    {:on-click state/toggle-left-sidebar!}
+       ;    (ui/icon "menu-2" {:size ui/icon-size})]])
+
+       [:nav.px-4.flex.flex-col.gap-1.cp__menubar-repos
+        {:aria-label "Navigation menu"}
+        (repo/repos-dropdown)
+
+        [:div.nav-header.flex.gap-1.flex-col
+         (let [page (:page default-home)]
+           (if (and page (not (state/enable-journals? (state/get-current-repo))))
+             (sidebar-item
+              {:class            "home-nav"
+               :title            page
+               :on-click-handler route-handler/redirect-to-home!
+               :active           (and (not srs-open?)
+                                      (= route-name :page)
+                                      (= page (get-in route-match [:path-params :name])))
+               :icon             "home"})
+             (sidebar-item
+              {:class            "journals-nav"
+               :active           (and (not srs-open?)
+                                      (or (= route-name :all-journals) (= route-name :home)))
+               :title            (t :left-side-bar/journals)
+               :on-click-handler (fn [e]
+                                   (if (gobj/get e "shiftKey")
+                                     (route-handler/sidebar-journals!)
+                                     (route-handler/go-to-journals!)))
+               :icon             "calendar"})))
+
+         (when (state/enable-flashcards? (state/get-current-repo))
+           [:div.flashcards-nav
+            (flashcards srs-open?)])
+
+         (sidebar-item
+          {:class  "graph-view-nav"
+           :title  (t :right-side-bar/graph-view)
+           :href   (rfe/href :graph)
+           :active (and (not srs-open?) (= route-name :graph))
+           :icon   "hierarchy"})
+
+         (sidebar-item
+          {:class  "all-pages-nav"
+           :title  (t :right-side-bar/all-pages)
+           :href   (rfe/href :all-pages)
+           :active (and (not srs-open?) (= route-name :all-pages))
+           :icon   "files"})
+
+         (when enable-whiteboards?
+           (sidebar-item
+            {:class           "whiteboard"
+             :title           (t :right-side-bar/whiteboards)
+             :href            (rfe/href :whiteboards)
+             :active          (and (not srs-open?) (#{:whiteboard :whiteboards} route-name))
+             :icon            "whiteboard"
+             :icon-extension? true}))]]
+
+       [:div.nav-contents-container.flex.flex-col.gap-1.pt-1
+        {:on-scroll on-contents-scroll}
+        (favorites t)
+
+        (when (not config/publishing?)
+          (recent-pages t))]
+
+       [:footer.px-2 {:class "create"}
+        (when-not config/publishing?
+          (if enable-whiteboards?
+            (create-dropdown)
+            [:a.item.group.flex.items-center.px-2.py-2.text-sm.font-medium.rounded-md.new-page-link
+             {:on-click (fn []
+                          (and (util/sm-breakpoint?)
+                               (state/toggle-left-sidebar!))
+                          (state/pub-event! [:go/search]))}
+             (ui/icon "circle-plus" {:style {:font-size 20}})
+             [:span.flex-1 (t :right-side-bar/new-page)]]))]]]
+     [:span.shade-mask
+      (cond-> {:on-click close-fn}
+        (number? offset-ratio)
+        (assoc :style {:opacity (cond-> offset-ratio
+                                  (neg? offset-ratio)
+                                  (+ 1))}))]]))
+
+(rum/defcs left-sidebar < rum/reactive
+  (rum/local false ::closing?)
+  (rum/local -1 ::close-signal)
+  (rum/local nil ::touch-state)
+  [s {:keys [left-sidebar-open? route-match]}]
+  (let [close-fn             #(state/set-left-sidebar-open! false)
+        *closing?            (::closing? s)
+        *touch-state         (::touch-state s)
+        *close-signal        (::close-signal s)
+        touch-point-fn       (fn [^js e] (some-> (gobj/get e "touches") (aget 0) (#(hash-map :x (.-clientX %) :y (.-clientY %)))))
+        srs-open?            (= :srs (state/sub :modal/id))
+        touching-x-offset    (and (some-> @*touch-state :after)
+                                  (some->> @*touch-state
+                                           ((juxt :after :before))
+                                           (map :x) (apply -)))
+        touch-pending?       (> (abs touching-x-offset) 20)]
+
     [:div#left-sidebar.cp__sidebar-left-layout
     [:div#left-sidebar.cp__sidebar-left-layout
-     {:class (util/classnames [{:is-open left-sidebar-open?}])}
+     {:class (util/classnames [{:is-open     left-sidebar-open?
+                                :is-closing  @*closing?
+                                :is-touching touch-pending?}])
+      :on-touch-start
+      (fn [^js e]
+        (reset! *touch-state {:before (touch-point-fn e)}))
+      :on-touch-move
+      (fn [^js e]
+        (when @*touch-state
+          (some-> *touch-state (swap! assoc :after (touch-point-fn e)))))
+      :on-touch-end
+      (fn []
+        (when touch-pending?
+          (cond
+            (and (not left-sidebar-open?) (> touching-x-offset 40))
+            (state/set-left-sidebar-open! true)
+
+            (and left-sidebar-open? (< touching-x-offset -30))
+            (reset! *close-signal (inc @*close-signal))))
+        (reset! *touch-state nil))}
 
 
      ;; sidebar contents
      ;; sidebar contents
-     (sidebar-nav route-match close-fn left-sidebar-open? srs-open?)
-     [:span.shade-mask {:on-click close-fn}]]))
+     (sidebar-nav route-match close-fn left-sidebar-open? srs-open? *closing?
+                  @*close-signal (and touch-pending? touching-x-offset))]))
 
 
 (rum/defc recording-bar
 (rum/defc recording-bar
   []
   []
@@ -392,14 +483,16 @@
                             (when-let [id (state/get-edit-input-id)]
                             (when-let [id (state/get-edit-input-id)]
                               (let [format (:block/format (state/get-edit-block))]
                               (let [format (:block/format (state/get-edit-block))]
                                 (editor-handler/upload-asset id files format editor-handler/*asset-uploading? true))))})
                                 (editor-handler/upload-asset id files format editor-handler/*asset-uploading? true))))})
-                  (common-handler/listen-to-scroll! element))
+                  (common-handler/listen-to-scroll! element)
+                  (when (:margin-less-pages? (first (:rum/args state))) ;; makes sure full screen pages displaying without scrollbar
+                    (set! (.. element -scrollTop) 0)))
                 state)}
                 state)}
   [{:keys [route-match margin-less-pages? route-name indexeddb-support? db-restoring? main-content show-action-bar? show-recording-bar?]}]
   [{:keys [route-match margin-less-pages? route-name indexeddb-support? db-restoring? main-content show-action-bar? show-recording-bar?]}]
   (let [left-sidebar-open? (state/sub :ui/left-sidebar-open?)
   (let [left-sidebar-open? (state/sub :ui/left-sidebar-open?)
         onboarding-and-home? (and (or (nil? (state/get-current-repo)) (config/demo-graph?))
         onboarding-and-home? (and (or (nil? (state/get-current-repo)) (config/demo-graph?))
                                   (not config/publishing?)
                                   (not config/publishing?)
                                   (= :home route-name))
                                   (= :home route-name))
-        margin-less-pages? (or onboarding-and-home? margin-less-pages?)]
+        margin-less-pages? (or (and (mobile-util/native-platform?) onboarding-and-home?) margin-less-pages?)]
     [:div#main-container.cp__sidebar-main-layout.flex-1.flex
     [:div#main-container.cp__sidebar-main-layout.flex-1.flex
      {:class (util/classnames [{:is-left-sidebar-open left-sidebar-open?}])}
      {:class (util/classnames [{:is-left-sidebar-open left-sidebar-open?}])}
 
 
@@ -572,7 +665,6 @@
                   :exit 300}}
                   :exit 300}}
        (render-custom-context-menu links position)))))
        (render-custom-context-menu links position)))))
 
 
-
 (rum/defc new-block-mode < rum/reactive
 (rum/defc new-block-mode < rum/reactive
   []
   []
   (when (state/sub [:document/mode?])
   (when (state/sub [:document/mode?])
@@ -617,9 +709,6 @@
                                    (:editor/editing? @state/state))))
                                    (:editor/editing? @state/state))))
                           (state/close-modal!)
                           (state/close-modal!)
                           (hide-context-menu-and-clear-selection e)))))))
                           (hide-context-menu-and-clear-selection e)))))))
-  {:did-mount (fn [state]
-                (swipe/setup-listeners!)
-                state)}
   [state route-match main-content]
   [state route-match main-content]
   (let [{:keys [open-fn]} state
   (let [{:keys [open-fn]} state
         current-repo (state/sub :git/current-repo)
         current-repo (state/sub :git/current-repo)

+ 39 - 11
src/main/frontend/components/sidebar.css

@@ -72,6 +72,7 @@
 #main-content-container[data-is-margin-less-pages=true] {
 #main-content-container[data-is-margin-less-pages=true] {
   padding: 0 !important;
   padding: 0 !important;
   position: relative;
   position: relative;
+  overflow: auto;
 }
 }
 
 
 .left-sidebar-inner {
 .left-sidebar-inner {
@@ -90,7 +91,7 @@
 
 
   > .wrap {
   > .wrap {
     height: calc(100vh - var(--ls-headbar-inner-top-padding) - 50px);
     height: calc(100vh - var(--ls-headbar-inner-top-padding) - 50px);
-    margin-top: 40px;
+    margin-top: 30px;
     width: 100%;
     width: 100%;
 
 
     > .fake-bar {
     > .fake-bar {
@@ -330,36 +331,50 @@
 
 
 .cp__sidebar-left-layout {
 .cp__sidebar-left-layout {
   position: fixed;
   position: fixed;
-  width: 0;
-  top: var(--ls-headbar-inner-top-padding);
+  top: 0;
+
   left: 0;
   left: 0;
   z-index: var(--ls-z-index-level-5);
   z-index: var(--ls-z-index-level-5);
-  transition: width .3s;
+  width: 10px;
 
 
   a {
   a {
+    @apply opacity-90 hover:opacity-100;
+
     color: var(--ls-header-button-background);
     color: var(--ls-header-button-background);
-    opacity: 0.9;
   }
   }
 
 
-  a:hover {
-    opacity: 1;
+  > .left-sidebar-inner {
+    padding-top: var(--ls-headbar-inner-top-padding);
   }
   }
 
 
   > .shade-mask {
   > .shade-mask {
-    background-color: rgba(0, 0, 0, .5);
+    background-color: rgba(0, 0, 0, .7);
     position: absolute;
     position: absolute;
     top: 0;
     top: 0;
     left: 0;
     left: 0;
     bottom: 0;
     bottom: 0;
     right: 0;
     right: 0;
-    z-index: -1;
+    z-index: 1;
     opacity: 0;
     opacity: 0;
-    transition: opacity .1s;
+    transition: opacity .4s;
     touch-action: none;
     touch-action: none;
   }
   }
 
 
-  &.is-open {
+  &.is-touching {
+    width: 100%;
     transition: none;
     transition: none;
+
+    .left-sidebar-inner {
+      transition: none !important;
+    }
+
+    > .shade-mask {
+      transition: none !important;
+      z-index: 1;
+    }
+  }
+
+  &.is-open {
     width: 100%;
     width: 100%;
 
 
     .left-sidebar-inner {
     .left-sidebar-inner {
@@ -373,6 +388,18 @@
     }
     }
   }
   }
 
 
+  &.is-closing {
+    .left-sidebar-inner {
+      transition: transform .3s;
+      transform: translate3d(-100%, 0, 0) !important;
+    }
+
+    > .shade-mask {
+      opacity: 0;
+      z-index: 1;
+    }
+  }
+
   &:before {
   &:before {
     content: " ";
     content: " ";
     height: 3rem;
     height: 3rem;
@@ -387,6 +414,7 @@
   @screen sm {
   @screen sm {
     width: 0;
     width: 0;
     z-index: var(--ls-z-index-level-1);
     z-index: var(--ls-z-index-level-1);
+    transition: width .3s;
 
 
     &:before {
     &:before {
       background-color: var(--ls-secondary-background-color);
       background-color: var(--ls-secondary-background-color);

+ 20 - 19
src/main/frontend/components/svg.cljs

@@ -166,25 +166,26 @@
     {:d         "M0 384.662V127.338c0-17.818 21.543-26.741 34.142-14.142l128.662 128.662c7.81 7.81 7.81 20.474 0 28.284L34.142 398.804C21.543 411.404 0 402.48 0 384.662z"
     {:d         "M0 384.662V127.338c0-17.818 21.543-26.741 34.142-14.142l128.662 128.662c7.81 7.81 7.81 20.474 0 28.284L34.142 398.804C21.543 411.404 0 402.48 0 384.662z"
      :fill-rule "evenodd"}]])
      :fill-rule "evenodd"}]])
 
 
-(rum/defc logo
-  [_dark?]
-  [:svg
-   {:fill "currentColor", :view-box "0 0 21 21", :height "21", :width "21"}
-   [:ellipse
-    {:transform
-         "matrix(0.987073 0.160274 -0.239143 0.970984 11.7346 2.59206)"
-     :ry "2.04373"
-     :rx "3.29236"}]
-   [:ellipse
-    {:transform
-         "matrix(-0.495846 0.868411 -0.825718 -0.564084 3.97209 5.54515)"
-     :ry "3.37606"
-     :rx "2.95326"}]
-   [:ellipse
-    {:transform
-         "matrix(0.987073 0.160274 -0.239143 0.970984 13.0843 14.72)"
-     :ry "6.13006"
-     :rx "7.78547"}]])
+(defn logo
+  ([] (logo 20))
+  ([size]
+   [:svg
+    {:fill "currentColor", :view-box "0 0 21 21", :height size, :width size}
+    [:ellipse
+     {:transform
+      "matrix(0.987073 0.160274 -0.239143 0.970984 11.7346 2.59206)"
+      :ry "2.04373"
+      :rx "3.29236"}]
+    [:ellipse
+     {:transform
+      "matrix(-0.495846 0.868411 -0.825718 -0.564084 3.97209 5.54515)"
+      :ry "3.37606"
+      :rx "2.95326"}]
+    [:ellipse
+     {:transform
+      "matrix(0.987073 0.160274 -0.239143 0.970984 13.0843 14.72)"
+      :ry "6.13006"
+      :rx "7.78547"}]]))
 
 
 (def page
 (def page
   [:svg.h-5.w-4 {:viewBox "0 0 24 24", :fill "none", :xmlns "http://www.w3.org/2000/svg"}
   [:svg.h-5.w-4 {:viewBox "0 0 24 24", :fill "none", :xmlns "http://www.w3.org/2000/svg"}

+ 1 - 1
src/main/frontend/components/theme.cljs

@@ -27,7 +27,7 @@
           (.add cls "dark")
           (.add cls "dark")
           (.remove cls "dark"))
           (.remove cls "dark"))
         (ui/apply-custom-theme-effect! theme)
         (ui/apply-custom-theme-effect! theme)
-        (plugin-handler/hook-plugin-app :theme-mode-changed {:mode theme} nil))
+        (plugin-handler/hook-plugin-app :theme-mode-changed {:mode theme}))
      [theme])
      [theme])
 
 
     (rum/use-effect!
     (rum/use-effect!

+ 77 - 47
src/main/frontend/components/whiteboard.cljs

@@ -5,6 +5,7 @@
             [frontend.components.page :as page]
             [frontend.components.page :as page]
             [frontend.components.reference :as reference]
             [frontend.components.reference :as reference]
             [frontend.context.i18n :refer [t]]
             [frontend.context.i18n :refer [t]]
+            [frontend.db-mixins :as db-mixins]
             [frontend.db.model :as model]
             [frontend.db.model :as model]
             [frontend.handler.common :as common-handler]
             [frontend.handler.common :as common-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.route :as route-handler]
@@ -46,60 +47,88 @@
     (when generate-preview
     (when generate-preview
       (generate-preview tldr))))
       (generate-preview tldr))))
 
 
+;; TODO: use frontend.ui instead of making a new one
 (rum/defc dropdown
 (rum/defc dropdown
-  [label children show? outside-click-hander]
+  [label children show? outside-click-hander portal?]
   (let [[anchor-ref anchor-rect] (use-bounding-client-rect show?)
   (let [[anchor-ref anchor-rect] (use-bounding-client-rect show?)
         [content-ref content-rect] (use-bounding-client-rect show?)
         [content-ref content-rect] (use-bounding-client-rect show?)
         offset-x (when (and anchor-rect content-rect)
         offset-x (when (and anchor-rect content-rect)
-                   (let [offset-x (+ (* 0.5 (- (.-width anchor-rect) (.-width content-rect)))
-                                     (.-x anchor-rect))
-                         vp-w (.-innerWidth js/window)
-                         right (+ offset-x (.-width content-rect) 16)
-                         offset-x (if (> right vp-w) (- offset-x (- right vp-w)) offset-x)]
-                     offset-x))
+                   (if portal?
+                     (let [offset-x (+ (* 0.5 (- (.-width anchor-rect) (.-width content-rect)))
+                                       (.-x anchor-rect))
+                           vp-w (.-innerWidth js/window)
+                           right (+ offset-x (.-width content-rect) 16)
+                           offset-x (if (> right vp-w) (- offset-x (- right vp-w)) offset-x)]
+                       offset-x)
+                     (* 0.5 (- (.-width anchor-rect) (.-width content-rect)))))
         offset-y (when (and anchor-rect content-rect)
         offset-y (when (and anchor-rect content-rect)
                    (+ (.-y anchor-rect) (.-height anchor-rect) 8))
                    (+ (.-y anchor-rect) (.-height anchor-rect) 8))
         click-outside-ref (use-click-outside outside-click-hander)
         click-outside-ref (use-click-outside outside-click-hander)
         [d-open set-d-open] (rum/use-state false)
         [d-open set-d-open] (rum/use-state false)
         _ (rum/use-effect! (fn [] (js/setTimeout #(set-d-open show?) 100))
         _ (rum/use-effect! (fn [] (js/setTimeout #(set-d-open show?) 100))
                            [show?])]
                            [show?])]
-    [:div.dropdown-anchor {:ref anchor-ref}
+    [:div.inline-block.dropdown-anchor {:ref anchor-ref}
      label
      label
-     (ui/portal
-      [:div.fixed.shadow-lg.color-level.px-2.rounded-lg.transition.md:w-64.lg:w-128.overflow-auto
-       {:ref (juxt content-ref click-outside-ref)
-        :style {:opacity (if d-open 1 0)
-                :pointer-events (if d-open "auto" "none")
-                :transform (str "translateY(" (if d-open 0 10) "px)")
-                :min-height "40px"
-                :max-height "420px"
-                :left offset-x
-                :top offset-y}} children])]))
+     (if portal?
+       ;; FIXME: refactor the following code ...
+       (ui/portal
+        [:div.fixed.shadow-lg.color-level.px-2.rounded-lg.transition.md:w-64.lg:w-128.overflow-auto
+         {:ref (juxt content-ref click-outside-ref)
+          :style {:opacity (if d-open 1 0)
+                  :pointer-events (if d-open "auto" "none")
+                  :transform (str "translateY(" (if d-open 0 10) "px)")
+                  :min-height "40px"
+                  :max-height "420px"
+                  :left offset-x
+                  :top offset-y}}
+         (when d-open children)])
+       [:div.absolute.shadow-lg.color-level.px-2.rounded-lg.transition.md:w-64.lg:w-128.overflow-auto
+        {:ref (juxt content-ref click-outside-ref)
+         :style {:opacity (if d-open 1 0)
+                 :pointer-events (if d-open "auto" "none")
+                 :transform (str "translateY(" (if d-open 0 10) "px)")
+                 :min-height "40px"
+                 :max-height "420px"
+                 :left offset-x}}
+        (when d-open children)])]))
 
 
-(rum/defc page-refs-count < rum/static
-  ([page-name classname]
-   (page-refs-count page-name classname nil))
-  ([page-name classname render-fn]
-   (let [page-entity (model/get-page page-name)
+(rum/defc dropdown-menu
+  [{:keys [label children classname hover? portal?]}]
+  (let [[open-flag set-open-flag] (rum/use-state 0)
+        open? (> open-flag (if hover? 0 1))
+        d-open-flag (rum/use-memo #(util/debounce 200 set-open-flag) [])]
+    (dropdown
+     [:div {:class (str classname (when open? " open"))
+            :on-mouse-enter (fn [] (d-open-flag #(if (= % 0) 1 %)))
+            :on-mouse-leave (fn [] (d-open-flag #(if (= % 2) % 0)))
+            :on-click (fn [e]
+                        (util/stop e)
+                        (d-open-flag (fn [o] (if (not= o 2) 2 0))))}
+      (if (fn? label) (label open?) label)]
+     children open? #(set-open-flag 0) portal?)))
+
+;; TODO: move to frontend.components.reference
+(rum/defc references-count < rum/reactive db-mixins/query
+  "Shows a references count for any block or page.
+   When clicked, a dropdown menu will show the reference details"
+  ([page-name-or-uuid classname]
+   (references-count page-name-or-uuid classname nil))
+  ([page-name-or-uuid classname {:keys [render-fn
+                                        hover?
+                                        portal?]
+                                 :or {portal? true}}]
+   (let [page-entity (model/get-page page-name-or-uuid)
          block-uuid (:block/uuid page-entity)
          block-uuid (:block/uuid page-entity)
-         refs-count (count (:block/_refs page-entity))
-         [open-flag set-open-flag] (rum/use-state 0)
-         open? (not= open-flag 0)
-         d-open-flag (rum/use-memo #(util/debounce 200 set-open-flag) [])]
+         refs-count (model/get-block-references-count block-uuid)]
      (when (> refs-count 0)
      (when (> refs-count 0)
-       (dropdown
-        [:div.flex.items-center.gap-2.whiteboard-page-refs-count
-         {:class (str classname (when open? " open"))
-          :on-mouse-enter (fn [] (d-open-flag #(if (= % 0) 1 %)))
-          :on-mouse-leave (fn [] (d-open-flag #(if (= % 2) % 0)))
-          :on-click (fn [e]
-                      (util/stop e)
-                      (d-open-flag (fn [o] (if (not= o 2) 2 0))))}
-         [:div.open-page-ref-link refs-count]
-         (when render-fn (render-fn open? refs-count))]
-        (reference/block-linked-references block-uuid)
-        open?
-        #(set-open-flag 0))))))
+       (dropdown-menu {:classname classname
+                       :label (fn [open?]
+                                [:div.inline-flex.items-center.gap-2
+                                 [:div.open-page-ref-link refs-count]
+                                 (when render-fn (render-fn open? refs-count))])
+                       :hover? hover?
+                       :portal? portal?
+                       :children (reference/block-linked-references block-uuid)})))))
 
 
 (defn- get-page-display-name
 (defn- get-page-display-name
   [page-name]
   [page-name]
@@ -144,7 +173,7 @@
     [:div.flex.w-full.opacity-50
     [:div.flex.w-full.opacity-50
      [:div (get-page-human-update-time page-name)]
      [:div (get-page-human-update-time page-name)]
      [:div.flex-1]
      [:div.flex-1]
-     (page-refs-count page-name nil)]]
+     (references-count page-name nil {:hover? true})]]
    [:div.p-4.h-64.flex.justify-center
    [:div.p-4.h-64.flex.justify-center
     (tldraw-preview page-name)]])
     (tldraw-preview page-name)]])
 
 
@@ -248,12 +277,13 @@
                         false)]
                         false)]
 
 
       [:div.whiteboard-page-refs
       [:div.whiteboard-page-refs
-       (page-refs-count page-name
-                        "text-md px-3 py-2 cursor-default whiteboard-page-refs-count"
-                        (fn [open? refs-count] [:span.whiteboard-page-refs-count-label
-                                                (if (> refs-count 1) "References" "Reference")
-                                                (ui/icon (if open? "references-hide" "references-show")
-                                                         {:extension? true})]))]]
+       (references-count page-name
+                         "text-md px-3 py-2 cursor-default whiteboard-page-refs-count"
+                         {:hover? true
+                          :render-fn (fn [open? refs-count] [:span.whiteboard-page-refs-count-label
+                                                             (if (> refs-count 1) "References" "Reference")
+                                                             (ui/icon (if open? "references-hide" "references-show")
+                                                                      {:extension? true})])})]]
      (tldraw-app page-name block-id)]))
      (tldraw-app page-name block-id)]))
 
 
 (rum/defc whiteboard-route
 (rum/defc whiteboard-route

+ 36 - 19
src/main/frontend/db/model.cljs

@@ -16,7 +16,7 @@
             [frontend.util :as util :refer [react]]
             [frontend.util :as util :refer [react]]
             [frontend.util.drawer :as drawer]
             [frontend.util.drawer :as drawer]
             [logseq.db.default :as default-db]
             [logseq.db.default :as default-db]
-            [logseq.db.rules :refer [rules]]
+            [logseq.db.rules :as rules]
             [logseq.db.schema :as db-schema]
             [logseq.db.schema :as db-schema]
             [logseq.graph-parser.config :as gp-config]
             [logseq.graph-parser.config :as gp-config]
             [logseq.graph-parser.text :as text]
             [logseq.graph-parser.text :as text]
@@ -300,16 +300,7 @@
             (alias ?page ?e)]
             (alias ?page ?e)]
           (conn/get-db repo-url)
           (conn/get-db repo-url)
           (util/safe-page-name-sanity-lc page)
           (util/safe-page-name-sanity-lc page)
-          '[[(alias ?e2 ?e1)
-             [?e2 :block/alias ?e1]]
-            [(alias ?e2 ?e1)
-             [?e1 :block/alias ?e2]]
-            [(alias ?e1 ?e3)
-             [?e1 :block/alias ?e2]
-             [?e2 :block/alias ?e3]]
-            [(alias ?e3 ?e1)
-             [?e1 :block/alias ?e2]
-             [?e2 :block/alias ?e3]]])
+          (:alias rules/rules))
      db-utils/seq-flatten
      db-utils/seq-flatten
      (set)
      (set)
      (set/union #{page-id}))))
      (set/union #{page-id}))))
@@ -1306,6 +1297,24 @@
                                (sort-by-left-recursive))]
                                (sort-by-left-recursive))]
          (db-utils/group-by-page query-result))))))
          (db-utils/group-by-page query-result))))))
 
 
+(defn get-block-references-count
+  [block-uuid]
+  (when-let [repo (state/get-current-repo)]
+    (when (conn/get-db repo)
+      (let [block (db-utils/entity [:block/uuid block-uuid])
+            query-result (->> (react/q repo [:frontend.db.react/refs
+                                             (:db/id block)]
+                                       {}
+                                       '[:find [(pull ?ref-block ?block-attrs) ...]
+                                         :in $ ?block-uuid ?block-attrs
+                                         :where
+                                         [?block :block/uuid ?block-uuid]
+                                         [?ref-block :block/refs ?block]]
+                                       block-uuid
+                                       block-attrs)
+                              react)]
+        (count query-result)))))
+
 (defn journal-page?
 (defn journal-page?
   "sanitized page-name only"
   "sanitized page-name only"
   [page-name]
   [page-name]
@@ -1585,7 +1594,7 @@
        [?p :block/name ?namespace]
        [?p :block/name ?namespace]
        (namespace ?p ?c)]
        (namespace ?p ?c)]
      (conn/get-db repo)
      (conn/get-db repo)
-     rules
+     (:namespace rules/rules)
      namespace)))
      namespace)))
 
 
 (defn- tree [flat-col root]
 (defn- tree [flat-col root]
@@ -1677,13 +1686,21 @@
    macro-name))
    macro-name))
 
 
 (defn whiteboard-page?
 (defn whiteboard-page?
-  [page-name]
-  (let [page (db-utils/entity [:block/name (util/safe-page-name-sanity-lc page-name)])]
-    (or
-     (= "whiteboard" (:block/type page))
-     (when-let [file (:block/file page)]
-       (when-let [path (:file/path (db-utils/entity (:db/id file)))]
-         (gp-config/whiteboard? path))))))
+  "Given a page name or a page object, check if it is a whiteboard page"
+  [page]
+  (cond
+    (string? page)
+    (let [page (db-utils/entity [:block/name (util/safe-page-name-sanity-lc page)])]
+      (or
+       (= "whiteboard" (:block/type page))
+       (when-let [file (:block/file page)]
+         (when-let [path (:file/path (db-utils/entity (:db/id file)))]
+           (gp-config/whiteboard? path)))))
+
+    (seq page)
+    (= "whiteboard" (:block/type page))
+
+    :else false))
 
 
 (defn get-all-whiteboards
 (defn get-all-whiteboards
   [repo]
   [repo]

+ 54 - 25
src/main/frontend/dicts.cljc

@@ -1472,7 +1472,7 @@
            :help/working-with-lists " (与列表相关)"
            :help/working-with-lists " (与列表相关)"
            :help/select-nfs-browser "请选择支持 nfs 的浏览器来使用 Logseq 本地文件夹功能, 如最新的 Chrome 浏览器."
            :help/select-nfs-browser "请选择支持 nfs 的浏览器来使用 Logseq 本地文件夹功能, 如最新的 Chrome 浏览器."
            :text/image "图片"
            :text/image "图片"
-           :asset/confirm-delete "确定要删除{1}吗?"
+           :asset/confirm-delete "确定要删除{1}吗"
            :asset/physical-delete "同时删除本地文件(目前不可撤销)"
            :asset/physical-delete "同时删除本地文件(目前不可撤销)"
            :undo "撤销"
            :undo "撤销"
            :redo "重做"
            :redo "重做"
@@ -1516,7 +1516,7 @@
            :settings-page/enable-shortcut-tooltip "启用快捷键提示"
            :settings-page/enable-shortcut-tooltip "启用快捷键提示"
            :settings-page/export-theme "导出主题"
            :settings-page/export-theme "导出主题"
            :tutorial/dummy-notes "练习笔记.md"
            :tutorial/dummy-notes "练习笔记.md"
-           :tutorial/text "指南.md(英文)"
+           :tutorial/text "指南.md(英文)"
            :right-side-bar/help "帮助"
            :right-side-bar/help "帮助"
            :right-side-bar/switch-theme "主题模式"
            :right-side-bar/switch-theme "主题模式"
            :right-side-bar/theme "{1}主题"
            :right-side-bar/theme "{1}主题"
@@ -1543,7 +1543,7 @@
            :format/org-mode "Org Mode 格式"
            :format/org-mode "Org Mode 格式"
            :reference/linked "已链接的引用"
            :reference/linked "已链接的引用"
            :reference/unlinked-ref "未链接的引用"
            :reference/unlinked-ref "未链接的引用"
-           :page/edit-properties-placeholder "点击这里编辑当前页面的属性 (标签,别名等)"
+           :page/edit-properties-placeholder "点击这里编辑当前页面的属性(标签,别名等)"
            :page/presentation-mode "打开幻灯片"
            :page/presentation-mode "打开幻灯片"
            :page/delete-success "页面 {1} 删除成功!"
            :page/delete-success "页面 {1} 删除成功!"
            :page/delete-confirmation "您确定要删除此页面和文件吗?"
            :page/delete-confirmation "您确定要删除此页面和文件吗?"
@@ -1567,7 +1567,7 @@
            :page/hide-name "隐藏页面名"
            :page/hide-name "隐藏页面名"
            :block/name "页面名称"
            :block/name "页面名称"
            :page/last-modified "最后更改于"
            :page/last-modified "最后更改于"
-           :page/new-title "请输入新页面的名字:"
+           :page/new-title "请输入新页面的名字"
            :page/earlier "之前"
            :page/earlier "之前"
            :page/no-more-journals "没有更多了"
            :page/no-more-journals "没有更多了"
            :journal/multiple-files-with-different-formats "你似乎在同一个月有多个日记文件(格式不同),请在每个月只保留一份日记文件。"
            :journal/multiple-files-with-different-formats "你似乎在同一个月有多个日记文件(格式不同),请在每个月只保留一份日记文件。"
@@ -1592,7 +1592,7 @@
            :file-rn/otherwise-breaking "否则标题会变为"
            :file-rn/otherwise-breaking "否则标题会变为"
            :file-rn/no-action "好了!无需更多操作"
            :file-rn/no-action "好了!无需更多操作"
            :file-rn/confirm-proceed "更新格式!"
            :file-rn/confirm-proceed "更新格式!"
-           :file-rn/select-confirm-proceed "开发者: 写入格式"
+           :file-rn/select-confirm-proceed "开发者写入格式"
            :file-rn/unreachable-title "警告!在当前文件名格式下,除非手动设置 `title::` 属性,否则,页面名将变为{1}。"
            :file-rn/unreachable-title "警告!在当前文件名格式下,除非手动设置 `title::` 属性,否则,页面名将变为{1}。"
            :file-rn/optional-rename "建议:"
            :file-rn/optional-rename "建议:"
            :file-rn/format-deprecated "你现在正使用着过时的格式。非常建议更新到最新的格式。在进行该操作之前,请先备份好你的数据,并关闭所有的 Logseq 客户端。"
            :file-rn/format-deprecated "你现在正使用着过时的格式。非常建议更新到最新的格式。在进行该操作之前,请先备份好你的数据,并关闭所有的 Logseq 客户端。"
@@ -1606,12 +1606,12 @@
            :page/created-at "创建日期"
            :page/created-at "创建日期"
            :page/updated-at "更新日期"
            :page/updated-at "更新日期"
            :page/backlinks "双向链接"
            :page/backlinks "双向链接"
-           :file/format-not-supported "格式 .{1} 目前不支持."
+           :file/format-not-supported "格式 .{1} 目前不支持"
            :editor/block-search "搜索块"
            :editor/block-search "搜索块"
            :editor/image-uploading "上传中"
            :editor/image-uploading "上传中"
            :draw/invalid-file "无法加载此无效的 excalidraw 文件"
            :draw/invalid-file "无法加载此无效的 excalidraw 文件"
-           :draw/specify-title "请先指定标题!"
-           :draw/rename-success "文件重命名成功!"
+           :draw/specify-title "请先指定标题"
+           :draw/rename-success "文件重命名成功"
            :draw/rename-failure "文件重命名失败,原因是:"
            :draw/rename-failure "文件重命名失败,原因是:"
            :draw/title-placeholder "未命名"
            :draw/title-placeholder "未命名"
            :draw/save "保存"
            :draw/save "保存"
@@ -1620,7 +1620,7 @@
            :draw/list-files "所有文件"
            :draw/list-files "所有文件"
            :draw/delete "删除"
            :draw/delete "删除"
            :draw/more-options "更多选项"
            :draw/more-options "更多选项"
-           :draw/back-to-logseq "返回 logseq"
+           :draw/back-to-logseq "返回 Logseq"
            :content/copy "复制"
            :content/copy "复制"
            :content/cut "剪切"
            :content/cut "剪切"
            :content/make-todos "格式化为 {1}"
            :content/make-todos "格式化为 {1}"
@@ -1629,8 +1629,8 @@
            :content/open-in-sidebar "在侧边栏打开"
            :content/open-in-sidebar "在侧边栏打开"
            :content/copy-as-json "复制为 JSON"
            :content/copy-as-json "复制为 JSON"
            :content/click-to-edit "点击以编辑"
            :content/click-to-edit "点击以编辑"
-           :settings-page/edit-config-edn "编辑 config.edn (当前库)"
-           :settings-page/edit-custom-css "编辑 custom.css (当前库)"
+           :settings-page/edit-config-edn "编辑 config.edn (当前库)"
+           :settings-page/edit-custom-css "编辑 custom.css (当前库)"
            :settings-page/custom-configuration "自定义配置"
            :settings-page/custom-configuration "自定义配置"
            :settings-page/custom-theme "自定义主题"
            :settings-page/custom-theme "自定义主题"
            :settings-page/git-desc "用于页面的版本管理,可以单击顶部右侧菜单来查看页面的历史记录"
            :settings-page/git-desc "用于页面的版本管理,可以单击顶部右侧菜单来查看页面的历史记录"
@@ -1701,7 +1701,7 @@
            :export-roam-json "以 Roam JSON 格式导出"
            :export-roam-json "以 Roam JSON 格式导出"
            :export-markdown "以 Markdown 格式导出"
            :export-markdown "以 Markdown 格式导出"
            :export-opml "以 OPML 格式导出"
            :export-opml "以 OPML 格式导出"
-           :convert-markdown "转换 Markdown 格式(Unordered list 或 Heading)"
+           :convert-markdown "转换 Markdown 格式(Unordered list 或 Heading)"
            :unlink "解除绑定"
            :unlink "解除绑定"
            :search/publishing "搜索"
            :search/publishing "搜索"
            :search "搜索或者创建新页面"
            :search "搜索或者创建新页面"
@@ -1732,13 +1732,13 @@
            :settings-of-plugins "插件设置"
            :settings-of-plugins "插件设置"
            :plugins "插件"
            :plugins "插件"
            :themes "主题"
            :themes "主题"
-           :developer-mode-alert "如果希望插件功能立刻生效, 请重启应用。是否现在重启?"
-           :relaunch-confirm-to-work "如果希望立刻生效, 请重启应用。是否现在重启?"
+           :developer-mode-alert "如果希望插件功能立刻生效, 请重启应用。是否现在重启"
+           :relaunch-confirm-to-work "如果希望立刻生效, 请重启应用。是否现在重启"
            :import "导入"
            :import "导入"
            :importing "导入中"
            :importing "导入中"
            :join-community "加入社区"
            :join-community "加入社区"
-           :sponsor-us "赞助我们!"
-           :discord-title "我们的 Discord 社群!"
+           :sponsor-us "赞助我们"
+           :discord-title "我们的 Discord 社群"
            :help-shortcut-title "点此查看快捷方式和更多有用帮助"
            :help-shortcut-title "点此查看快捷方式和更多有用帮助"
            :loading "加载中"
            :loading "加载中"
            :cloning "Clone 中"
            :cloning "Clone 中"
@@ -1769,8 +1769,8 @@
            :all-whiteboards "所有白板"
            :all-whiteboards "所有白板"
            :go-to-whiteboard "前往白板"
            :go-to-whiteboard "前往白板"
            :not-available-in-mode "在{1}模式下不可用"
            :not-available-in-mode "在{1}模式下不可用"
-           :user/delete-account "删除号"
-           :user/delete-your-account "删除你的号"
+           :user/delete-account "删除号"
+           :user/delete-your-account "删除你的号"
            :user/delete-account-notice "你在 logseq.com 发布的页面(假如有的话)也会被删除。"
            :user/delete-account-notice "你在 logseq.com 发布的页面(假如有的话)也会被删除。"
 
 
            :plugin/installed "已安装"
            :plugin/installed "已安装"
@@ -1782,7 +1782,7 @@
            :plugin/check-update "检查更新"
            :plugin/check-update "检查更新"
            :plugin/check-all-updates "一键检查更新"
            :plugin/check-all-updates "一键检查更新"
            :plugin/refresh-lists "刷新插件列表"
            :plugin/refresh-lists "刷新插件列表"
-           :plugin/delete-alert "确定删除插件 [{1}]?"
+           :plugin/delete-alert "确定删除插件 [{1}]"
            :plugin/enabled "已开启"
            :plugin/enabled "已开启"
            :plugin/disabled "未开启"
            :plugin/disabled "未开启"
            :plugin/update-available "待更新"
            :plugin/update-available "待更新"
@@ -1802,7 +1802,7 @@
            :plugin/marketplace-tips "如果首次从市场安装或更新的插件,遇到非预期工作情况,可以尝试重启应用。"
            :plugin/marketplace-tips "如果首次从市场安装或更新的插件,遇到非预期工作情况,可以尝试重启应用。"
            :plugin/unpacked-tips "用于开发目的或者从本地磁盘载入可信的社区插件。"
            :plugin/unpacked-tips "用于开发目的或者从本地磁盘载入可信的社区插件。"
            :plugin/up-to-date "已经是最新了"
            :plugin/up-to-date "已经是最新了"
-           :plugin/custom-js-alert "发现 custom.js 自定义脚本,是否允许执行? (如果您对该文件的内容不了解 或 来源不可靠,建议不要允许执行)"
+           :plugin/custom-js-alert "发现 custom.js 自定义脚本,是否允许执行?(如果您对该文件的内容不了解 或 来源不可靠,建议不要允许执行)"
 
 
            :pdf/copy-ref "复制引用"
            :pdf/copy-ref "复制引用"
            :pdf/copy-text "复制文本"
            :pdf/copy-text "复制文本"
@@ -2156,7 +2156,7 @@
         :tutorial/dummy-notes #?(:cljs (rc/inline "dummy-notes-es.md")
         :tutorial/dummy-notes #?(:cljs (rc/inline "dummy-notes-es.md")
                                  :default "dummy-notes-es.md")
                                  :default "dummy-notes-es.md")
         :on-boarding/demo-graph "This is a demo graph, changes will not be saved until you open a local folder."
         :on-boarding/demo-graph "This is a demo graph, changes will not be saved until you open a local folder."
-        :on-boarding/add-graph "Add a graph"
+        :on-boarding/add-graph "Añadir gráfico"
         :on-boarding/open-local-dir "Abrir un directorio local"
         :on-boarding/open-local-dir "Abrir un directorio local"
         :on-boarding/new-graph-desc-1 "Logseq soporta tanto Markdown como Org-mode. Puede abrir un directorio existente o crear uno nuevo en su dispositivo, un directorio se conoce simplemente como una carpeta. Sus datos se almacenarán únicamente en este dispositivo."
         :on-boarding/new-graph-desc-1 "Logseq soporta tanto Markdown como Org-mode. Puede abrir un directorio existente o crear uno nuevo en su dispositivo, un directorio se conoce simplemente como una carpeta. Sus datos se almacenarán únicamente en este dispositivo."
         :on-boarding/new-graph-desc-2 "Después que abra un directorio se crearán tres carpetas en ese directorio:"
         :on-boarding/new-graph-desc-2 "Después que abra un directorio se crearán tres carpetas en ese directorio:"
@@ -2203,6 +2203,7 @@
         :highlight "Resaltado"
         :highlight "Resaltado"
         :strikethrough "Tachado"
         :strikethrough "Tachado"
         :code "Código"
         :code "Código"
+        :untitled "Sin título"
         :right-side-bar/help "Ayuda"
         :right-side-bar/help "Ayuda"
         :right-side-bar/switch-theme "Temas"
         :right-side-bar/switch-theme "Temas"
         :right-side-bar/theme "Tema {1}"
         :right-side-bar/theme "Tema {1}"
@@ -2259,6 +2260,33 @@
         :file/last-modified-at "Fecha de modificación"
         :file/last-modified-at "Fecha de modificación"
         :file/no-data "No hay datos"
         :file/no-data "No hay datos"
         :file/format-not-supported "Formato .{1} no soportado."
         :file/format-not-supported "Formato .{1} no soportado."
+        :file-rn/re-index "Se recomienda encarecidamente volver a indexar después de cambiar el nombre de los archivos y en otros dispositivos después de la sincronización."
+        :file-rn/need-action "Se sugieren acciones de cambio de nombre de archivo para que coincidan con el nuevo formato. Cuando se sincronicen los archivos renombrados se requiere volver a indexar en todos los dispositivos."
+        :file-rn/or-select-actions " o cambie el nombre de los archivos a continuación individualmente, luego "
+        :file-rn/or-select-actions-2 ". Estas acciones no estarán disponibles una vez cierres este panel."
+        :file-rn/legend "🟢 Acciones de cambio de nombre opcionales; 🟡 Cambio de nombre obligatorio para evitar el cambio de título; 🔴 Cambio destructor."
+        :file-rn/close-panel "Cerrar el panel"
+        :file-rn/all-action "¡Aplicar todas las acciones!"
+        :file-rn/select-format "(Opción modo desarrollador, ¡peligroso!) Seccione el formato de nombre de archivo"
+        :file-rn/rename "Renombrar \"{1}\" a \"{2}\""
+        :file-rn/rename-sm "Renombrar"
+        :file-rn/apply-rename "Aplicar la operación de cambio de nombre de archivo"
+        :file-rn/affected-pages "Páginas afectadas después del cambio de formato"
+        :file-rn/suggest-rename "Acción necesaria: "
+        :file-rn/otherwise-breaking "O el título se convertirá"
+        :file-rn/no-action "¡Bien hecho! No se necesario realizar más acciones."
+        :file-rn/confirm-proceed "¡Actualizar formato!"
+        :file-rn/select-confirm-proceed "Desarrollo: formato de escritura"
+        :file-rn/unreachable-title "¡Advertencia! El nombre de la página se convertirá en {1} en el formato de nombre de archivo actual, a no ser que la propiedad `title::` se establezca manualmente"
+        :file-rn/optional-rename "Sugerencia: "
+        :file-rn/format-deprecated "Está utilizando un formato obsoleto. Se recomienda actualizar al formato más reciente. Realice una copia de seguridad de sus datos y cierre los clientes de Logseq en otros dispositivos antes de la operación."
+        :file-rn/filename-desc-1 "Esta configuración configura cómo se guarda una página en un archivo. Logseq guarda una página en un archivo con el mismo nombre."
+        :file-rn/filename-desc-2 "Algunos caracteres como \"/\" o \"?\" no son válidos para nombrar un archivo."
+        :file-rn/filename-desc-3 "Logseq reemplaza los caracteres no válidos con su URL codificado equivalente para hacerlos válidos (por ejemplo \"?\" se convierte en \"%3F\")."
+        :file-rn/filename-desc-4 "El separador \"/\" también se reemplaza por \"___\" (triple guión bajo) por consideraciones estéticas."
+        :file-rn/instruct-1 "Actualizar el formato de nombre es un proceso de 2 pasos:"
+        :file-rn/instruct-2 "1. Click "
+        :file-rn/instruct-3 "2. Siga las intrucciones indicadas abajo para renombrar los archivos al nuevo formato: "
         :page/created-at "Creada el"
         :page/created-at "Creada el"
         :page/updated-at "Actualizada el"
         :page/updated-at "Actualizada el"
         :page/backlinks "Back Links"
         :page/backlinks "Back Links"
@@ -2467,8 +2495,7 @@
         :select.graph/empty-placeholder-description "No encontramos un grafo. Querie añadir otro?"
         :select.graph/empty-placeholder-description "No encontramos un grafo. Querie añadir otro?"
         :select.graph/add-graph "Sí, añadir otro grafo"
         :select.graph/add-graph "Sí, añadir otro grafo"
         :file-sync/other-user-graph "El gráfico local actual está unido al gráfico remoto de otro usuario. Así que no se puede empezar a sincronizar"
         :file-sync/other-user-graph "El gráfico local actual está unido al gráfico remoto de otro usuario. Así que no se puede empezar a sincronizar"
-        :file-sync/graph-deleted "El gráfico remoto actual se ha eliminado"
-        }
+        :file-sync/graph-deleted "El gráfico remoto actual se ha eliminado"}
 
 
    :nb-NO {:tutorial/text #?(:cljs (rc/inline "tutorial-no.md")
    :nb-NO {:tutorial/text #?(:cljs (rc/inline "tutorial-no.md")
                              :default "tutorial-no.md")
                              :default "tutorial-no.md")
@@ -4605,8 +4632,8 @@
         :file-rn/format-deprecated "Şu anda güncel olmayan bir biçim kullanıyorsunuz. En son biçime güncellemeniz kesinlikle önerilir. Lütfen işlemden önce verilerinizi yedekleyin ve Logseq istemcilerini diğer cihazlarda kapatın."
         :file-rn/format-deprecated "Şu anda güncel olmayan bir biçim kullanıyorsunuz. En son biçime güncellemeniz kesinlikle önerilir. Lütfen işlemden önce verilerinizi yedekleyin ve Logseq istemcilerini diğer cihazlarda kapatın."
         :file-rn/filename-desc-1 "Bu ayar, bir sayfanın bir dosyaya nasıl saklanacağını yapılandırır. Logseq, aynı ada sahip bir dosyaya bir sayfa depolar."
         :file-rn/filename-desc-1 "Bu ayar, bir sayfanın bir dosyaya nasıl saklanacağını yapılandırır. Logseq, aynı ada sahip bir dosyaya bir sayfa depolar."
         :file-rn/filename-desc-2 "\"/\" vaya \"?\" gibi bazı karakterler bir dosya adı için geçersizdir."
         :file-rn/filename-desc-2 "\"/\" vaya \"?\" gibi bazı karakterler bir dosya adı için geçersizdir."
-        :file-rn/filename-desc-3 "Logseq, geçersiz karakterleri geçerli kılmak için URL kodlu eşdeğerleriyle değiştirir (ör. \"?\", \"%3F\" olur)."
-        :file-rn/filename-desc-4 "Ad boşluğu ayırıcısı \"/\", estetik değerlendirme için \"___\" (üçlü altçizgi) ile de değiştirilir."
+        :file-rn/filename-desc-3 "Logseq, geçersiz karakterleri URL kodlu eşdeğerleriyle değiştirir (ör. \"?\", \"%3F\" olur)."
+        :file-rn/filename-desc-4 "Ad boşluğu ayırıcısı \"/\", estetik değerlendirme için \"___\" (üçlü altçizgi) ile değiştirilir."
         :file-rn/instruct-1 "Dosya adı biçimini güncellemek 2 adımlı bir işlemdir:"
         :file-rn/instruct-1 "Dosya adı biçimini güncellemek 2 adımlı bir işlemdir:"
         :file-rn/instruct-2 "1. Tıklayın "
         :file-rn/instruct-2 "1. Tıklayın "
         :file-rn/instruct-3 "2. Dosyaları yeni biçimde yeniden adlandırmak için aşağıdaki talimatları izleyin:"
         :file-rn/instruct-3 "2. Dosyaları yeni biçimde yeniden adlandırmak için aşağıdaki talimatları izleyin:"
@@ -4671,8 +4698,10 @@
         :settings-page/disable-sentry "Kullanım verilerini ve tanılamayı Logseq'e gönderin"
         :settings-page/disable-sentry "Kullanım verilerini ve tanılamayı Logseq'e gönderin"
         :settings-page/preferred-outdenting "Mantıksal girinti"
         :settings-page/preferred-outdenting "Mantıksal girinti"
         :settings-page/custom-date-format "Tercih edilen tarih biçimi"
         :settings-page/custom-date-format "Tercih edilen tarih biçimi"
+        :settings-page/custom-date-format-warning "Yeniden dizin oluşturma gerekli! Mevcut günlük referansları bozulabilir!"
         :settings-page/preferred-file-format "Tercih edilen dosya biçimi"
         :settings-page/preferred-file-format "Tercih edilen dosya biçimi"
         :settings-page/preferred-workflow "Tercih edilen iş akışı"
         :settings-page/preferred-workflow "Tercih edilen iş akışı"
+        :settings-page/preferred-pasting-file "Dosya yapıştırmayı tercih et"
         :settings-page/enable-shortcut-tooltip "Kısayol araç ipuçlarını etkinleştir"
         :settings-page/enable-shortcut-tooltip "Kısayol araç ipuçlarını etkinleştir"
         :settings-page/enable-timetracking "Zaman takibi"
         :settings-page/enable-timetracking "Zaman takibi"
         :settings-page/enable-tooltip "Araç ipuçları"
         :settings-page/enable-tooltip "Araç ipuçları"

+ 5 - 2
src/main/frontend/extensions/code.cljs

@@ -146,6 +146,9 @@
 (def textarea-ref-name "textarea")
 (def textarea-ref-name "textarea")
 (def codemirror-ref-name "codemirror-instance")
 (def codemirror-ref-name "codemirror-instance")
 
 
+;; export CodeMirror to global scope
+(set! js/window -CodeMirror cm)
+
 (defn- extra-codemirror-options []
 (defn- extra-codemirror-options []
   (get (state/get-config)
   (get (state/get-config)
        :editor/extra-codemirror-options {}))
        :editor/extra-codemirror-options {}))
@@ -187,6 +190,7 @@
 
 
 (defn- save-file-or-block-when-blur-or-esc!
 (defn- save-file-or-block-when-blur-or-esc!
   [editor textarea config state]
   [editor textarea config state]
+  (state/set-state! :editor/skip-saving-current-block? true)
   (state/set-block-component-editing-mode! false)
   (state/set-block-component-editing-mode! false)
   (save-file-or-block! editor textarea config state))
   (save-file-or-block! editor textarea config state))
 
 
@@ -293,8 +297,7 @@
                 state)
                 state)
    :did-update (fn [state]
    :did-update (fn [state]
                  (reset! (:code-options state) (last (:rum/args state)))
                  (reset! (:code-options state) (last (:rum/args state)))
-                 state)
-   }
+                 state)}
   [state _config id attr code _theme _options]
   [state _config id attr code _theme _options]
   [:div.extensions__code
   [:div.extensions__code
    (when-let [mode (:data-lang attr)]
    (when-let [mode (:data-lang attr)]

+ 1 - 3
src/main/frontend/extensions/html_parser.cljs

@@ -170,9 +170,7 @@
 
 
                                    (string? (first children))
                                    (string? (first children))
                                    (let [pattern (config/get-code format)]
                                    (let [pattern (config/get-code format)]
-                                     (str " "
-                                          pattern (map-join children) pattern
-                                          " "))
+                                     (str pattern (map-join children) pattern))
 
 
                                    ;; skip monospace style, since it has more complex children
                                    ;; skip monospace style, since it has more complex children
                                    :else
                                    :else

+ 48 - 19
src/main/frontend/extensions/pdf/assets.cljs

@@ -8,6 +8,10 @@
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.page :as page-handler]
             [frontend.handler.page :as page-handler]
             [frontend.handler.assets :as assets-handler]
             [frontend.handler.assets :as assets-handler]
+            [frontend.handler.notification :as notification]
+            [frontend.ui :as ui]
+            [frontend.context.i18n :refer [t]]
+            [frontend.extensions.lightbox :as lightbox]
             [frontend.util.page-property :as page-property]
             [frontend.util.page-property :as page-property]
             [frontend.state :as state]
             [frontend.state :as state]
             [frontend.util :as util]
             [frontend.util :as util]
@@ -59,11 +63,11 @@
         data))))
         data))))
 
 
 (defn persist-hls-data$
 (defn persist-hls-data$
-  [{:keys [hls-file]} highlights]
+  [{:keys [hls-file]} highlights extra]
   (when hls-file
   (when hls-file
     (let [repo-cur (state/get-current-repo)
     (let [repo-cur (state/get-current-repo)
           repo-dir (config/get-repo-dir repo-cur)
           repo-dir (config/get-repo-dir repo-cur)
-          data (pr-str {:highlights highlights})]
+          data (pr-str {:highlights highlights :extra extra})]
       (fs/write-file! repo-cur repo-dir hls-file data {:skip-compare? true}))))
       (fs/write-file! repo-cur repo-dir hls-file data {:skip-compare? true}))))
 
 
 (defn resolve-hls-data-by-key$
 (defn resolve-hls-data-by-key$
@@ -226,7 +230,7 @@
             (do
             (do
               (state/set-state! :pdf/ref-highlight matched)
               (state/set-state! :pdf/ref-highlight matched)
               ;; open pdf viewer
               ;; open pdf viewer
-              (state/set-state! :pdf/current (inflate-asset file-path)))
+              (state/set-current-pdf! (inflate-asset file-path)))
             (js/console.debug "[Unmatched highlight ref]" block)))))))
             (js/console.debug "[Unmatched highlight ref]" block)))))))
 
 
 (defn goto-block-ref!
 (defn goto-block-ref!
@@ -242,32 +246,57 @@
    (when-let [name (:key current)]
    (when-let [name (:key current)]
      (rfe/push-state :page {:name (str "hls__" name)} (if id {:anchor (str "block-content-" + id)} nil)))))
      (rfe/push-state :page {:name (str "hls__" name)} (if id {:anchor (str "block-content-" + id)} nil)))))
 
 
+(defn open-lightbox
+  [e]
+  (let [images (js/document.querySelectorAll ".hl-area img")
+        images (to-array images)
+        images (if-not (= (count images) 1)
+                 (let [^js image (.closest (.-target e) ".hl-area")
+                       image (. image querySelector "img")]
+                   (->> images
+                        (sort-by (juxt #(.-y %) #(.-x %)))
+                        (split-with (complement #{image}))
+                        reverse
+                        (apply concat)))
+                 images)
+        images (for [^js it images] {:src (.-src it)
+                                     :w (.-naturalWidth it)
+                                     :h (.-naturalHeight it)})]
+
+    (when (seq images)
+      (lightbox/preview-images! images))))
+
 (rum/defc area-display
 (rum/defc area-display
   [block]
   [block]
   (when-let [asset-path' (and block (pdf-utils/get-area-block-asset-url
   (when-let [asset-path' (and block (pdf-utils/get-area-block-asset-url
                                      block (db-utils/pull (:db/id (:block/page block)))))]
                                      block (db-utils/pull (:db/id (:block/page block)))))]
-    (let [asset-path     (editor-handler/make-asset-url asset-path')]
+    (let [asset-path (editor-handler/make-asset-url asset-path')]
       [:span.hl-area
       [:span.hl-area
+       [:span.actions
+        (when-not config/publishing?
+          [:button.asset-action-btn.px-1
+           {:title         (t :asset/copy)
+            :tabIndex      "-1"
+            :on-mouse-down util/stop
+            :on-click      (fn [e]
+                             (util/stop e)
+                             (-> (util/copy-image-to-clipboard (gp-config/remove-asset-protocol asset-path))
+                                 (p/then #(notification/show! "Copied!" :success))))}
+           (ui/icon "copy")])
+
+        [:button.asset-action-btn.px-1
+         {:title         (t :asset/maximize)
+          :tabIndex      "-1"
+          :on-mouse-down util/stop
+          :on-click      open-lightbox}
+
+         (ui/icon "maximize")]]
        [:img {:src asset-path}]])))
        [:img {:src asset-path}]])))
 
 
-(defn fix-local-asset-pagename
-  [filename]
-  (when-not (string/blank? filename)
-    (let [local-asset? (re-find #"[0-9]{13}_\d$" filename)
-          hls?         (re-find #"^hls__" filename)
-          len          (count filename)]
-      (if (or local-asset? hls?)
-        (-> filename
-            (subs 0 (if local-asset? (- len 15) len))
-            (string/replace #"^hls__" "")
-            (string/replace "_" " ")
-            (string/trimr))
-        filename))))
-
 (defn human-page-name
 (defn human-page-name
   [page-name]
   [page-name]
   (cond
   (cond
     (string/starts-with? page-name "hls__")
     (string/starts-with? page-name "hls__")
-    (fix-local-asset-pagename page-name)
+    (pdf-utils/fix-local-asset-pagename page-name)
 
 
     :else (util/trim-safe page-name)))
     :else (util/trim-safe page-name)))

+ 81 - 71
src/main/frontend/extensions/pdf/highlights.cljs

@@ -12,7 +12,6 @@
             [frontend.commands :as commands]
             [frontend.commands :as commands]
             [frontend.rum :refer [use-atom]]
             [frontend.rum :refer [use-atom]]
             [frontend.state :as state]
             [frontend.state :as state]
-            [frontend.storage :as storage]
             [frontend.util :as util]
             [frontend.util :as util]
             [medley.core :as medley]
             [medley.core :as medley]
             [promesa.core :as p]
             [promesa.core :as p]
@@ -44,14 +43,12 @@
   (rum/use-effect!
   (rum/use-effect!
    (fn []
    (fn []
      (when viewer
      (when viewer
-       (when-let [current (:pdf/current @state/state)]
-         (let [active-hl (:pdf/ref-highlight @state/state)
-               page-key  (:filename current)
-               last-page (and page-key
-                              (util/safe-parse-int (storage/get (str "ls-pdf-last-page-" page-key))))]
-
-           (when (and last-page (nil? active-hl))
-             (set! (.-currentPageNumber viewer) last-page))))))
+       (when-let [_ (:pdf/current @state/state)]
+         (let [active-hl (:pdf/ref-highlight @state/state)]
+           (when-not active-hl
+             (.on (.-eventBus viewer) (name :restore-last-page)
+                  (fn [last-page]
+                    (set! (.-currentPageNumber viewer) (util/safe-parse-int last-page)))))))))
    [viewer])
    [viewer])
   nil)
   nil)
 
 
@@ -665,7 +662,7 @@
        })]))
        })]))
 
 
 (rum/defc pdf-viewer
 (rum/defc pdf-viewer
-  [url initial-hls ^js pdf-document ops]
+  [_url initial-hls initial-page ^js pdf-document ops]
 
 
   (let [*el-ref (rum/create-ref)
   (let [*el-ref (rum/create-ref)
         [state, set-state!] (rum/use-state {:viewer nil :bus nil :link nil :el nil})
         [state, set-state!] (rum/use-state {:viewer nil :bus nil :link nil :el nil})
@@ -675,33 +672,50 @@
 
 
     ;; instant pdfjs viewer
     ;; instant pdfjs viewer
     (rum/use-effect!
     (rum/use-effect!
-     (fn [] (let [^js event-bus    (js/pdfjsViewer.EventBus.)
-                  ^js link-service (js/pdfjsViewer.PDFLinkService. #js {:eventBus event-bus :externalLinkTarget 2})
-                  ^js el           (rum/deref *el-ref)
-                  ^js viewer       (js/pdfjsViewer.PDFViewer.
-                                    #js {:container         el
-                                         :eventBus          event-bus
-                                         :linkService       link-service
-                                         :findController    (js/pdfjsViewer.PDFFindController.
-                                                             #js {:linkService link-service :eventBus event-bus})
-                                         :textLayerMode     2
-                                         :annotationMode    2
-                                         :removePageBorders true})]
-              (. link-service setDocument pdf-document)
-              (. link-service setViewer viewer)
-
-              ;; TODO: debug
-              (set! (. js/window -lsPdfViewer) viewer)
-
-              (p/then (. viewer setDocument pdf-document)
-                      #(set-state! {:viewer viewer :bus event-bus :link link-service :el el}))
-
-              ;;TODO: destroy
-              (fn []
-                (when-let [last-page (.-currentPageNumber viewer)]
-                  (storage/set (str "ls-pdf-last-page-" (util/node-path.basename url)) last-page))
-
-                (when pdf-document (.destroy pdf-document)))))
+     (fn []
+       (let [^js event-bus    (js/pdfjsViewer.EventBus.)
+             ^js link-service (js/pdfjsViewer.PDFLinkService. #js {:eventBus event-bus :externalLinkTarget 2})
+             ^js el           (rum/deref *el-ref)
+             ^js viewer       (js/pdfjsViewer.PDFViewer.
+                               #js {:container         el
+                                    :eventBus          event-bus
+                                    :linkService       link-service
+                                    :findController    (js/pdfjsViewer.PDFFindController.
+                                                        #js {:linkService link-service :eventBus event-bus})
+                                    :textLayerMode     2
+                                    :annotationMode    2
+                                    :removePageBorders true})]
+
+         (. link-service setDocument pdf-document)
+         (. link-service setViewer viewer)
+
+         ;; events
+         (doto event-bus
+           ;; it must be initialized before set-up document
+           (.on "pagesinit"
+                (fn []
+                  (set! (. viewer -currentScaleValue) "auto")
+                  (set-page-ready! true)))
+
+           (.on (name :ls-update-extra-state)
+                #(when-let [extra (bean/->clj %)]
+                   (apply (:set-hls-extra! ops) [extra]))))
+
+         (p/then (. viewer setDocument pdf-document)
+                 #(set-state! {:viewer viewer :bus event-bus :link link-service :el el}))
+
+         ;; TODO: debug
+         (set! (. js/window -lsPdfViewer) viewer)
+
+         ;; set initial page
+         (js/setTimeout
+          #(set! (.-currentPageNumber viewer) initial-page) 16)
+
+         ;; destroy
+         (fn []
+           (.destroy pdf-document)
+           (set! (. js/window -lsPdfViewer) nil)
+           (.cleanup viewer))))
      [])
      [])
 
 
     ;; interaction events
     ;; interaction events
@@ -710,20 +724,13 @@
        (when-let [^js viewer (:viewer state)]
        (when-let [^js viewer (:viewer state)]
          (let [fn-textlayer-ready
          (let [fn-textlayer-ready
                (fn [^js p]
                (fn [^js p]
-                 (set-ano-state! {:loaded-pages (conj (:loaded-pages ano-state) (int (.-pageNumber p)))}))
-
-               fn-page-ready
-               (fn []
-                 (set! (. viewer -currentScaleValue) "auto")
-                 (set-page-ready! true))]
+                 (set-ano-state! {:loaded-pages (conj (:loaded-pages ano-state) (int (.-pageNumber p)))}))]
 
 
            (doto (.-eventBus viewer)
            (doto (.-eventBus viewer)
-             (.on "pagesinit" fn-page-ready)
              (.on "textlayerrendered" fn-textlayer-ready))
              (.on "textlayerrendered" fn-textlayer-ready))
 
 
            #(do
            #(do
               (doto (.-eventBus viewer)
               (doto (.-eventBus viewer)
-                (.off "pagesinit" fn-page-ready)
                 (.off "textlayerrendered" fn-textlayer-ready))))))
                 (.off "textlayerrendered" fn-textlayer-ready))))))
 
 
      [(:viewer state)
      [(:viewer state)
@@ -750,23 +757,27 @@
 (rum/defc ^:large-vars/data-var pdf-loader
 (rum/defc ^:large-vars/data-var pdf-loader
   [{:keys [url hls-file] :as pdf-current}]
   [{:keys [url hls-file] :as pdf-current}]
   (let [*doc-ref       (rum/use-ref nil)
   (let [*doc-ref       (rum/use-ref nil)
-        [state, set-state!] (rum/use-state {:error nil :pdf-document nil :status nil})
-        [hls-state, set-hls-state!] (rum/use-state {:initial-hls nil :latest-hls nil})
-        set-dirty-hls! (fn [latest-hls]                     ;; TODO: incremental
-                         (set-hls-state! {:initial-hls [] :latest-hls latest-hls}))]
+        [loader-state, set-loader-state!] (rum/use-state {:error nil :pdf-document nil :status nil})
+        [hls-state, set-hls-state!] (rum/use-state {:initial-hls nil :latest-hls nil :extra nil :loaded false})
+        [initial-page, set-initial-page!] (rum/use-state 0)
+        set-dirty-hls! (fn [latest-hls]  ;; TODO: incremental
+                         (set-hls-state! #(merge % {:initial-hls [] :latest-hls latest-hls})))
+        set-hls-extra! (fn [extra]
+                         (set-hls-state! #(merge % {:extra extra})))]
 
 
     ;; load highlights
     ;; load highlights
     (rum/use-effect!
     (rum/use-effect!
      (fn []
      (fn []
        (p/catch
        (p/catch
-        (p/let [data       (pdf-assets/load-hls-data$ pdf-current)
-                highlights (:highlights data)]
-          (set-hls-state! {:initial-hls highlights}))
+        (p/let [data (pdf-assets/load-hls-data$ pdf-current)
+                {:keys [highlights extra]} data]
+          (set-initial-page! (util/safe-parse-int (:page extra)))
+          (set-hls-state! {:initial-hls highlights :latest-hls highlights :extra extra :loaded true}))
 
 
         ;; error
         ;; error
         (fn [e]
         (fn [e]
           (js/console.error "[load hls error]" e)
           (js/console.error "[load hls error]" e)
-          (set-hls-state! {:initial-hls []})))
+          (set-hls-state! {:initial-hls [] :loaded true})))
 
 
        ;; cancel
        ;; cancel
        #())
        #())
@@ -775,15 +786,16 @@
     ;; cache highlights
     ;; cache highlights
     (rum/use-effect!
     (rum/use-effect!
      (fn []
      (fn []
-       (when-let [hls (:latest-hls hls-state)]
+       (when (= :completed (:status loader-state))
          (p/catch
          (p/catch
-          (pdf-assets/persist-hls-data$ pdf-current hls)
+          (pdf-assets/persist-hls-data$
+           pdf-current (:latest-hls hls-state) (:extra hls-state))
 
 
           ;; write hls file error
           ;; write hls file error
           (fn [e]
           (fn [e]
             (js/console.error "[write hls error]" e)))))
             (js/console.error "[write hls error]" e)))))
 
 
-     [(:latest-hls hls-state)])
+     [(:latest-hls hls-state) (:extra hls-state)])
 
 
     ;; load document
     ;; load document
     (rum/use-effect!
     (rum/use-effect!
@@ -795,20 +807,18 @@
                        ;;:cMapUrl       "https://cdn.jsdelivr.net/npm/[email protected]/cmaps/"
                        ;;:cMapUrl       "https://cdn.jsdelivr.net/npm/[email protected]/cmaps/"
                        :cMapPacked    true}]
                        :cMapPacked    true}]
 
 
-         (set-state! {:status :loading})
+         (set-loader-state! {:status :loading})
 
 
          (-> (get-doc$ (clj->js opts))
          (-> (get-doc$ (clj->js opts))
-             (p/then #(set-state! {:pdf-document %}))
-             (p/catch #(set-state! {:error %}))
-             (p/finally #(set-state! {:status :completed})))
-
+             (p/then #(set-loader-state! {:pdf-document % :status :completed}))
+             (p/catch #(set-loader-state! {:error %})))
          #()))
          #()))
      [url])
      [url])
 
 
     (rum/use-effect!
     (rum/use-effect!
      (fn []
      (fn []
-       (when-let [error (:error state)]
-         (dd "[ERROR loader]" (:error state))
+       (when-let [error (:error loader-state)]
+         (dd "[ERROR loader]" (:error loader-state))
          (case (.-name error)
          (case (.-name error)
            "MissingPDFException"
            "MissingPDFException"
            (do
            (do
@@ -835,24 +845,24 @@
               :error
               :error
               false)
               false)
              (state/set-state! :pdf/current nil)))))
              (state/set-state! :pdf/current nil)))))
-     [(:error state)])
+     [(:error loader-state)])
 
 
     (rum/bind-context
     (rum/bind-context
      [*highlights-ctx* hls-state]
      [*highlights-ctx* hls-state]
      [:div.extensions__pdf-loader {:ref *doc-ref}
      [:div.extensions__pdf-loader {:ref *doc-ref}
-      (let [status-doc  (:status state)
+      (let [status-doc  (:status loader-state)
             initial-hls (:initial-hls hls-state)]
             initial-hls (:initial-hls hls-state)]
 
 
-        (if (or (= status-doc :loading)
-                (nil? initial-hls))
+        (if (= status-doc :loading)
 
 
           [:div.flex.justify-center.items-center.h-screen.text-gray-500.text-lg
           [:div.flex.justify-center.items-center.h-screen.text-gray-500.text-lg
            svg/loading]
            svg/loading]
 
 
-          [(rum/with-key (pdf-viewer
-                          url initial-hls
-                          (:pdf-document state)
-                          {:set-dirty-hls! set-dirty-hls!}) "pdf-viewer")]))])))
+          (when-let [pdf-document (and (:loaded hls-state) (:pdf-document loader-state))]
+            [(rum/with-key (pdf-viewer
+                            url initial-hls initial-page pdf-document
+                            {:set-dirty-hls! set-dirty-hls!
+                             :set-hls-extra! set-hls-extra!}) "pdf-viewer")])))])))
 
 
 (rum/defc pdf-container
 (rum/defc pdf-container
   [{:keys [identity] :as pdf-current}]
   [{:keys [identity] :as pdf-current}]

+ 17 - 1
src/main/frontend/extensions/pdf/pdf.css

@@ -101,15 +101,21 @@ input::-webkit-inner-spin-button {
 
 
         > .nu {
         > .nu {
           padding-right: 4px;
           padding-right: 4px;
-
+          
           input {
           input {
             user-select: inherit;
             user-select: inherit;
             width: 35px;
             width: 35px;
             text-align: right;
             text-align: right;
             padding-right: 4px;
             padding-right: 4px;
+            padding-left: 2px;
             height: 18px;
             height: 18px;
             border: none;
             border: none;
             background: transparent;
             background: transparent;
+            font-size: 15px;
+            
+            &.is-long {
+              font-size: 12px;
+            }
           }
           }
         }
         }
 
 
@@ -841,6 +847,16 @@ input::-webkit-inner-spin-button {
       overflow: hidden;
       overflow: hidden;
       margin-top: 4px;
       margin-top: 4px;
 
 
+      .actions {
+        @apply absolute right-1 top-1 flex opacity-0 transition-opacity;
+      }
+
+      &:hover {
+        .actions {
+          @apply opacity-100;
+        }
+      }
+
       img {
       img {
         margin: 0;
         margin: 0;
         box-shadow: none;
         box-shadow: none;

+ 12 - 2
src/main/frontend/extensions/pdf/toolbar.cljs

@@ -433,6 +433,14 @@
          #(js-delete (. el -dataset) "theme")))
          #(js-delete (. el -dataset) "theme")))
      [viewer-theme])
      [viewer-theme])
 
 
+    ;; export page state
+    (rum/use-effect!
+     (fn []
+       (when viewer
+         (.dispatch (.-eventBus viewer) (name :ls-update-extra-state)
+                    #js {:page current-page-num})))
+     [viewer current-page-num])
+
     ;; pager hooks
     ;; pager hooks
     (rum/use-effect!
     (rum/use-effect!
      (fn []
      (fn []
@@ -511,14 +519,16 @@
         [:span.nu.flex.items-center.opacity-70
         [:span.nu.flex.items-center.opacity-70
          [:input {:ref            *page-ref
          [:input {:ref            *page-ref
                   :type           "number"
                   :type           "number"
+                  :class          (util/classnames [{:is-long (> (util/safe-parse-int current-page-num) 999)}])
                   :default-value  current-page-num
                   :default-value  current-page-num
                   :on-mouse-enter #(.select ^js (.-target %))
                   :on-mouse-enter #(.select ^js (.-target %))
                   :on-key-up      (fn [^js e]
                   :on-key-up      (fn [^js e]
                                     (let [^js input (.-target e)
                                     (let [^js input (.-target e)
                                           value     (util/safe-parse-int (.-value input))]
                                           value     (util/safe-parse-int (.-value input))]
+                                      (set-current-page-num! value)
                                       (when (and (= (.-keyCode e) 13) value (> value 0))
                                       (when (and (= (.-keyCode e) 13) value (> value 0))
-                                        (set! (. viewer -currentPageNumber)
-                                              (if (> value total-page-num) total-page-num value)))))}]
+                                        (->> (if (> value total-page-num) total-page-num value)
+                                             (set! (. viewer -currentPageNumber))))))}]
          [:small "/ " total-page-num]]
          [:small "/ " total-page-num]]
 
 
         [:span.ct.flex.items-center
         [:span.ct.flex.items-center

+ 14 - 0
src/main/frontend/extensions/pdf/utils.cljs

@@ -173,6 +173,20 @@
           (string/replace #"\|#\|([a-zA-Z_])" " $1")
           (string/replace #"\|#\|([a-zA-Z_])" " $1")
           (string/replace sp "")))))
           (string/replace sp "")))))
 
 
+(defn fix-local-asset-pagename
+  [filename]
+  (when-not (string/blank? filename)
+    (let [local-asset? (re-find #"[0-9]{13}_\d$" filename)
+          hls?         (re-find #"^hls__" filename)
+          len          (count filename)]
+      (if (or local-asset? hls?)
+        (-> filename
+            (subs 0 (if local-asset? (- len 15) len))
+            (string/replace #"^hls__" "")
+            (string/replace "_" " ")
+            (string/trimr))
+        filename))))
+
 ;; TODO: which viewer instance?
 ;; TODO: which viewer instance?
 (defn next-page
 (defn next-page
   []
   []

+ 22 - 8
src/main/frontend/extensions/sci.cljs

@@ -1,16 +1,31 @@
 (ns frontend.extensions.sci
 (ns frontend.extensions.sci
+  "Provides a consistent approach to sci evaluation. Used in at least the following places:
+- For :view evaluation
+- For :result-transform evaluation
+- For cljs evaluation in Src blocks
+- For evaluating {{function }} under query tables"
   (:require [sci.core :as sci]
   (:require [sci.core :as sci]
             [frontend.util :as util]
             [frontend.util :as util]
             [goog.dom]
             [goog.dom]
             [goog.object]
             [goog.object]
             [goog.string]))
             [goog.string]))
 
 
-;; Some helpers
-(def sum (partial apply +))
+;; Helper fns for eval-string
+;; ==========================
+(def ^:private sum (partial apply +))
 
 
-(defn average [coll]
+(defn- average [coll]
   (/ (reduce + coll) (count coll)))
   (/ (reduce + coll) (count coll)))
 
 
+(defn- call-api
+  "Given a fn name from logseq.api, invokes it with the given arguments"
+  [fn-name & args]
+  (when-not (aget js/window.logseq "api" fn-name)
+    (throw (ex-info "Api function does not exist" {:fn fn-name})))
+  (apply js-invoke (aget js/window.logseq "api") fn-name args))
+
+;; Public fns
+;; ==========
 (defn eval-string
 (defn eval-string
   "Second arg is a map of options for sci/eval-string"
   "Second arg is a map of options for sci/eval-string"
   ([s]
   ([s]
@@ -23,7 +38,9 @@
                                                 'parseFloat js/parseFloat
                                                 'parseFloat js/parseFloat
                                                 'isNaN js/isNaN
                                                 'isNaN js/isNaN
                                                 'log js/console.log
                                                 'log js/console.log
-                                                'pprint util/pp-str}}
+                                                'pprint util/pp-str
+                                                ;; Provide to all evals as it useful in most contexts
+                                                'call-api call-api}}
                                     options))
                                     options))
      (catch :default e
      (catch :default e
        (println "Query: sci eval failed:")
        (println "Query: sci eval failed:")
@@ -39,10 +56,7 @@
   [:div
   [:div
    [:code "Results:"]
    [:code "Results:"]
    [:div.results.mt-1
    [:div.results.mt-1
-    (let [result (eval-string code {:bindings {'block block}
-                                    :classes {'logseq-api js/logseq.api
-                                              'logseq-gp js/logseq.graph_parser
-                                              :allow :all}})]
+    (let [result (eval-string code {:bindings {'block block}})]
       (if (and (vector? result) (:hiccup (meta result)))
       (if (and (vector? result) (:hiccup (meta result)))
         result
         result
         [:pre.code (str result)]))]])
         [:pre.code (str result)]))]])

+ 39 - 17
src/main/frontend/extensions/tldraw.cljs

@@ -14,11 +14,12 @@
             [goog.object :as gobj]
             [goog.object :as gobj]
             [promesa.core :as p]
             [promesa.core :as p]
             [rum.core :as rum]
             [rum.core :as rum]
-            [frontend.ui :as ui]))
+            [frontend.ui :as ui]
+            [frontend.components.whiteboard :as whiteboard]))
 
 
 (def tldraw (r/adapt-class (gobj/get TldrawLogseq "App")))
 (def tldraw (r/adapt-class (gobj/get TldrawLogseq "App")))
 
 
-(def generate-preview (gobj/get TldrawLogseq "generateJSXFromApp"))
+(def generate-preview (gobj/get TldrawLogseq "generateJSXFromModel"))
 
 
 (rum/defc page-cp
 (rum/defc page-cp
   [props]
   [props]
@@ -30,7 +31,16 @@
 
 
 (rum/defc breadcrumb
 (rum/defc breadcrumb
   [props]
   [props]
-  (block/breadcrumb {:preview? true} (state/get-current-repo) (uuid (gobj/get props "blockId")) {:end-separator? true}))
+  (block/breadcrumb {:preview? true}
+                    (state/get-current-repo)
+                    (uuid (gobj/get props "blockId"))
+                    {:end-separator? (gobj/get props "endSeparator")
+                     :level-limit (gobj/get props "levelLimit" 3)}))
+
+(rum/defc block-reference
+  [props]
+  (println "page-name-linkpage-name-linkpage-name-linkpage-name-link" props)
+  (block/block-reference {} (gobj/get props "blockId") nil))
 
 
 (rum/defc page-name-link
 (rum/defc page-name-link
   [props]
   [props]
@@ -54,12 +64,19 @@
          (when-let [[asset-file-name _ full-file-path] (and (seq res) (first res))]
          (when-let [[asset-file-name _ full-file-path] (and (seq res) (first res))]
            (editor-handler/resolve-relative-path (or full-file-path asset-file-name)))))))
            (editor-handler/resolve-relative-path (or full-file-path asset-file-name)))))))
 
 
+(defn references-count
+  [props]
+  (apply whiteboard/references-count
+         (map (fn [k] (js->clj (gobj/get props k) {:keywordize-keys true})) ["id" "className" "options"])))
+
 (def tldraw-renderers {:Page page-cp
 (def tldraw-renderers {:Page page-cp
                        :Block block-cp
                        :Block block-cp
                        :Breadcrumb breadcrumb
                        :Breadcrumb breadcrumb
-                       :PageNameLink page-name-link})
+                       :PageName page-name-link
+                       :ReferencesCount references-count
+                       :BlockReference block-reference})
 
 
-(defn get-tldraw-handlers [name]
+(defn get-tldraw-handlers [current-whiteboard-name]
   {:search search-handler
   {:search search-handler
    :queryBlockByUUID #(clj->js (model/query-block-by-uuid (parse-uuid %)))
    :queryBlockByUUID #(clj->js (model/query-block-by-uuid (parse-uuid %)))
    :isWhiteboardPage model/whiteboard-page?
    :isWhiteboardPage model/whiteboard-page?
@@ -68,29 +85,34 @@
    :addNewWhiteboard (fn [page-name]
    :addNewWhiteboard (fn [page-name]
                        (whiteboard-handler/create-new-whiteboard-page! page-name))
                        (whiteboard-handler/create-new-whiteboard-page! page-name))
    :addNewBlock (fn [content]
    :addNewBlock (fn [content]
-                  (str (whiteboard-handler/add-new-block! name content)))
+                  (str (whiteboard-handler/add-new-block! current-whiteboard-name content)))
    :sidebarAddBlock (fn [uuid type]
    :sidebarAddBlock (fn [uuid type]
                       (state/sidebar-add-block! (state/get-current-repo)
                       (state/sidebar-add-block! (state/get-current-repo)
                                                 (:db/id (model/get-page uuid))
                                                 (:db/id (model/get-page uuid))
                                                 (keyword type)))
                                                 (keyword type)))
-   :redirectToPage (fn [page-name]
-                     (if (model/whiteboard-page? page-name)
-                       (route-handler/redirect-to-whiteboard! page-name)
-                       (route-handler/redirect-to-page! page-name)))})
+   :redirectToPage (fn [page-name-or-uuid]
+                     (let [page-name (if (util/uuid-string? page-name-or-uuid)
+                                       (:block/name (model/get-block-parent (parse-uuid page-name-or-uuid)))
+                                       page-name-or-uuid)
+                           whiteboard? (model/whiteboard-page? page-name)]
+                       (if whiteboard? (route-handler/redirect-to-whiteboard!
+                                        page-name {:block-id page-name-or-uuid})
+                           (route-handler/redirect-to-page! page-name-or-uuid))))})
 
 
 (rum/defc tldraw-app
 (rum/defc tldraw-app
   [page-name block-id]
   [page-name block-id]
   (let [populate-onboarding?  (whiteboard-handler/should-populate-onboarding-whiteboard? page-name)
   (let [populate-onboarding?  (whiteboard-handler/should-populate-onboarding-whiteboard? page-name)
-        data (whiteboard-handler/page-name->tldr! page-name block-id)
-        [loaded? set-loaded?] (rum/use-state false)
+        data (whiteboard-handler/page-name->tldr! page-name)
+        [loaded-app set-loaded-app] (rum/use-state nil)
         on-mount (fn [tln]
         on-mount (fn [tln]
                    (when-let [^js api (gobj/get tln "api")]
                    (when-let [^js api (gobj/get tln "api")]
                      (p/then (when populate-onboarding?
                      (p/then (when populate-onboarding?
                                (whiteboard-handler/populate-onboarding-whiteboard api))
                                (whiteboard-handler/populate-onboarding-whiteboard api))
-                             #(do (when (and block-id (parse-uuid block-id))
-                                    (. api selectShapes block-id)
-                                    (. api zoomToSelection))
-                                  (set-loaded? true)))))]
+                             #(do (state/focus-whiteboard-shape tln block-id)
+                                  (set-loaded-app tln)))))]
+    (rum/use-effect! (fn [] (when (and loaded-app block-id)
+                              (state/focus-whiteboard-shape loaded-app block-id)) #())
+                     [block-id loaded-app])
 
 
     (when data
     (when data
       [:div.draw.tldraw.whiteboard.relative.w-full.h-full
       [:div.draw.tldraw.whiteboard.relative.w-full.h-full
@@ -102,7 +124,7 @@
         :on-wheel util/stop-propagation}
         :on-wheel util/stop-propagation}
 
 
        (when
        (when
-        (and populate-onboarding? (not loaded?))
+        (and populate-onboarding? (not loaded-app))
          [:div.absolute.inset-0.flex.items-center.justify-center
          [:div.absolute.inset-0.flex.items-center.justify-center
           {:style {:z-index 200}}
           {:style {:z-index 200}}
           (ui/loading "Loading onboarding whiteboard ...")])
           (ui/loading "Loading onboarding whiteboard ...")])

+ 14 - 12
src/main/frontend/fs.cljs

@@ -76,7 +76,8 @@
 (defn write-file!
 (defn write-file!
   [repo dir path content opts]
   [repo dir path content opts]
   (when content
   (when content
-    (let [fs-record (get-fs dir)]
+    (let [path (gp-util/path-normalize path)
+          fs-record (get-fs dir)]
       (->
       (->
        (p/let [opts (assoc opts
        (p/let [opts (assoc opts
                            :error-handler
                            :error-handler
@@ -111,18 +112,19 @@
 
 
 (defn rename!
 (defn rename!
   [repo old-path new-path]
   [repo old-path new-path]
-  (cond
+  (let [new-path (gp-util/path-normalize new-path)]
+    (cond
                                         ; See https://github.com/isomorphic-git/lightning-fs/issues/41
                                         ; See https://github.com/isomorphic-git/lightning-fs/issues/41
-    (= old-path new-path)
-    (p/resolved nil)
-
-    :else
-    (let [[old-path new-path]
-          (map #(if (or (util/electron?) (mobile-util/native-platform?))
-                  %
-                  (str (config/get-repo-dir repo) "/" %))
-               [old-path new-path])]
-      (protocol/rename! (get-fs old-path) repo old-path new-path))))
+     (= old-path new-path)
+     (p/resolved nil)
+
+     :else
+     (let [[old-path new-path]
+           (map #(if (or (util/electron?) (mobile-util/native-platform?))
+                   %
+                   (str (config/get-repo-dir repo) "/" %))
+             [old-path new-path])]
+       (protocol/rename! (get-fs old-path) repo old-path new-path)))))
 
 
 (defn copy!
 (defn copy!
   [repo old-path new-path]
   [repo old-path new-path]

+ 4 - 2
src/main/frontend/fs/capacitor_fs.cljs

@@ -243,14 +243,16 @@
                             (str "file://" (js/encodeURI dir)))
                             (str "file://" (js/encodeURI dir)))
                           dir)
                           dir)
         path            (some-> path (string/replace #"^/+" ""))
         path            (some-> path (string/replace #"^/+" ""))
+        normalize-f     gp-util/path-normalize
+        encodeURI-f     js/encodeURI
         safe-encode-url #(let [encoded-chars?
         safe-encode-url #(let [encoded-chars?
                                (and (string? %) (boolean (re-find #"(?i)%[0-9a-f]{2}" %)))]
                                (and (string? %) (boolean (re-find #"(?i)%[0-9a-f]{2}" %)))]
                            (cond
                            (cond
                              (not encoded-chars?)
                              (not encoded-chars?)
-                             (js/encodeURI %)
+                             (encodeURI-f (normalize-f %))
 
 
                              :else
                              :else
-                             (js/encodeURI (js/decodeURI %))))
+                             (encodeURI-f (normalize-f (js/decodeURI %)))))
         path' (cond
         path' (cond
                 (and path (string/starts-with? path "file:/"))
                 (and path (string/starts-with? path "file:/"))
                 (safe-encode-url path)
                 (safe-encode-url path)

文件差异内容过多而无法显示
+ 325 - 296
src/main/frontend/fs/sync.cljs


+ 1 - 1
src/main/frontend/fs/watcher_handler.cljs

@@ -88,7 +88,7 @@
                          (string/trim (or (state/get-default-journal-template) "")))
                          (string/trim (or (state/get-default-journal-template) "")))
                       (= (string/trim content) "-")
                       (= (string/trim content) "-")
                       (= (string/trim content) "*")))
                       (= (string/trim content) "*")))
-            (handle-add-and-change! repo path content db-content mtime true))
+            (handle-add-and-change! repo path content db-content mtime (not global-dir))) ;; no backup for global dir
 
 
           (and (= "unlink" type)
           (and (= "unlink" type)
                (db/file-exists? repo path))
                (db/file-exists? repo path))

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

@@ -200,6 +200,7 @@
   [render]
   [render]
   (set-global-error-notification!)
   (set-global-error-notification!)
   (register-components-fns!)
   (register-components-fns!)
+  (user-handler/restore-tokens-from-localstorage)
   (state/set-db-restoring! true)
   (state/set-db-restoring! true)
   (render)
   (render)
   (i18n/start)
   (i18n/start)
@@ -219,13 +220,15 @@
 
 
   (-> (p/let [repos (get-repos)
   (-> (p/let [repos (get-repos)
               _ (state/set-repos! repos)
               _ (state/set-repos! repos)
-              _ (restore-and-setup! repos)])
+              _ (restore-and-setup! repos)]
+        (when (mobile-util/native-platform?)
+          (p/do!
+           (mobile-util/hide-splash)
+           (state/restore-mobile-theme!))))
       (p/catch (fn [e]
       (p/catch (fn [e]
                  (js/console.error "Error while restoring repos: " e)))
                  (js/console.error "Error while restoring repos: " e)))
       (p/finally (fn []
       (p/finally (fn []
                    (state/set-db-restoring! false))))
                    (state/set-db-restoring! false))))
-  (when (mobile-util/native-platform?)
-    (mobile-util/hide-splash))
 
 
   (db/run-batch-txs!)
   (db/run-batch-txs!)
   (file/<ratelimit-file-writes!)
   (file/<ratelimit-file-writes!)
@@ -235,7 +238,6 @@
   (when (util/electron?)
   (when (util/electron?)
     (el/listen!))
     (el/listen!))
   (persist-var/load-vars)
   (persist-var/load-vars)
-  (user-handler/restore-tokens-from-localstorage)
   (js/setTimeout instrument! (* 60 1000)))
   (js/setTimeout instrument! (* 60 1000)))
 
 
 (defn stop! []
 (defn stop! []

+ 1 - 1
src/main/frontend/handler/block.cljs

@@ -242,7 +242,7 @@
           (reset! *swipe nil))))))
           (reset! *swipe nil))))))
 
 
 (defn on-touch-cancel
 (defn on-touch-cancel
-  [_event *show-left-menu? *show-right-menu?]
+  [*show-left-menu? *show-right-menu?]
   (reset! *show-left-menu? false)
   (reset! *show-left-menu? false)
   (reset! *show-right-menu? false)
   (reset! *show-right-menu? false)
   (reset! *swipe nil))
   (reset! *swipe nil))

+ 3 - 3
src/main/frontend/handler/config.cljs

@@ -48,6 +48,6 @@
   (let [enable-tooltip? (state/enable-tooltip?)]
   (let [enable-tooltip? (state/enable-tooltip?)]
     (set-config! :ui/enable-tooltip? (not enable-tooltip?))))
     (set-config! :ui/enable-tooltip? (not enable-tooltip?))))
 
 
-(defn toggle-perferred-pasting-file! []
-  (let [perferred-pasting-file? (state/perferred-pasting-file?)]
-    (set-config! :editor/perferred-pasting-file? (not perferred-pasting-file?))))
+(defn toggle-preferred-pasting-file! []
+  (let [preferred-pasting-file? (state/preferred-pasting-file?)]
+    (set-config! :editor/preferred-pasting-file? (not preferred-pasting-file?))))

+ 1 - 1
src/main/frontend/handler/conversion.cljs

@@ -100,7 +100,7 @@
      {:status        :informal | :breaking | :unreachable
      {:status        :informal | :breaking | :unreachable
       :target        the new file name
       :target        the new file name
       :old-title     the old title
       :old-title     the old title
-      :chagned-title the new title} | nil"
+      :changed-title the new title} | nil"
   [page path old-format new-format]
   [page path old-format new-format]
   (let [prop-title (get-in page [:block/properties :title])
   (let [prop-title (get-in page [:block/properties :title])
         file-body  (gp-util/path->file-body path)
         file-body  (gp-util/path->file-body path)

+ 40 - 20
src/main/frontend/handler/editor.cljs

@@ -420,7 +420,8 @@
 
 
 (declare save-current-block!)
 (declare save-current-block!)
 (defn outliner-insert-block!
 (defn outliner-insert-block!
-  [config current-block new-block {:keys [sibling? keep-uuid? replace-empty-target?]}]
+  [config current-block new-block {:keys [sibling? keep-uuid?
+                                          replace-empty-target?]}]
   (let [ref-query-top-block? (and (or (:ref? config)
   (let [ref-query-top-block? (and (or (:ref? config)
                                       (:custom-query? config))
                                       (:custom-query? config))
                                   (not (:ref-query-child? config)))
                                   (not (:ref-query-child? config)))
@@ -439,7 +440,7 @@
                    (not has-children?))]
                    (not has-children?))]
     (outliner-tx/transact!
     (outliner-tx/transact!
      {:outliner-op :insert-blocks}
      {:outliner-op :insert-blocks}
-     (save-current-block! {:current-block current-block})
+      (save-current-block! {:current-block current-block})
      (outliner-core/insert-blocks! [new-block] current-block {:sibling? sibling?
      (outliner-core/insert-blocks! [new-block] current-block {:sibling? sibling?
                                                               :keep-uuid? keep-uuid?
                                                               :keep-uuid? keep-uuid?
                                                               :replace-empty-target? replace-empty-target?}))))
                                                               :replace-empty-target? replace-empty-target?}))))
@@ -1255,7 +1256,8 @@
    (save-current-block! {}))
    (save-current-block! {}))
   ([{:keys [force? skip-properties? current-block] :as opts}]
   ([{:keys [force? skip-properties? current-block] :as opts}]
    ;; non English input method
    ;; non English input method
-   (when-not (state/editor-in-composition?)
+   (when-not (or (state/editor-in-composition?)
+                 (:editor/skip-saving-current-block? @state/state))
      (when (state/get-current-repo)
      (when (state/get-current-repo)
        (when-not (state/get-editor-action)
        (when-not (state/get-editor-action)
          (try
          (try
@@ -1285,7 +1287,8 @@
                             (string/trim value)))
                             (string/trim value)))
                  (save-block-aux! db-block value opts))))
                  (save-block-aux! db-block value opts))))
            (catch :default error
            (catch :default error
-             (log/error :save-block-failed error))))))))
+             (log/error :save-block-failed error))))))
+   (state/set-state! :editor/skip-saving-current-block? false)))
 
 
 (defn- clean-content!
 (defn- clean-content!
   [format content]
   [format content]
@@ -1570,6 +1573,15 @@
           pos (cursor/pos input)]
           pos (cursor/pos input)]
       (text-util/surround-by? value pos before end))))
       (text-util/surround-by? value pos before end))))
 
 
+(defn- autopair-left-paren?
+  [input key]
+  (and (= key "(")
+       (or
+         (surround-by? input :start "")
+         (surround-by? input " " "")
+         (surround-by? input "]" "")
+         (surround-by? input "(" ""))))
+
 (defn wrapped-by?
 (defn wrapped-by?
   [input before end]
   [input before end]
   (when input
   (when input
@@ -1759,7 +1771,7 @@
 
 
   (handle-command-input-close id))
   (handle-command-input-close id))
 
 
-(defn close-autocomplete-if-outside
+(defn- close-autocomplete-if-outside
   [input]
   [input]
   (when (and input
   (when (and input
              (contains? #{:page-search :page-search-hashtag :block-search} (state/get-editor-action))
              (contains? #{:page-search :page-search-hashtag :block-search} (state/get-editor-action))
@@ -1813,7 +1825,9 @@
     ;; TODO: is it cross-browser compatible?
     ;; TODO: is it cross-browser compatible?
     ;; (not= (gobj/get native-e "inputType") "insertFromPaste")
     ;; (not= (gobj/get native-e "inputType") "insertFromPaste")
     (cond
     (cond
-      (= last-input-char (state/get-editor-command-trigger))
+      ;; By default, "/" is also used as namespace separator in Logseq.
+      (and (= last-input-char (state/get-editor-command-trigger))
+           (not (contains? #{:page-search-hashtag} (state/sub :editor/action))))
       (do
       (do
         (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)})
         (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)})
         (commands/reinit-matched-commands!)
         (commands/reinit-matched-commands!)
@@ -1844,6 +1858,15 @@
       (and (= last-input-char commands/colon) (= :property-search (state/get-editor-action)))
       (and (= last-input-char commands/colon) (= :property-search (state/get-editor-action)))
       (state/clear-editor-action!)
       (state/clear-editor-action!)
 
 
+      ;; Open "Search page or New page" auto-complete
+      (and (= last-input-char commands/hashtag)
+           ;; Only trigger at beginning of line or before whitespace
+           (or (= 1 pos) (contains? #{" " "\t"} (get (.-value input) (- pos 2)))))
+      (do
+        (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)})
+        (state/set-editor-last-pos! pos)
+        (state/set-editor-action! :page-search-hashtag))
+
       :else
       :else
       nil)))
       nil)))
 
 
@@ -2409,7 +2432,7 @@
       (scroll-to-block sibling-block)
       (scroll-to-block sibling-block)
       (state/exit-editing-and-set-selected-blocks! [sibling-block]))))
       (state/exit-editing-and-set-selected-blocks! [sibling-block]))))
 
 
-(defn- move-cross-boundrary-up-down
+(defn- move-cross-boundary-up-down
   [direction]
   [direction]
   (let [input (state/get-input)
   (let [input (state/get-input)
         line-pos (util/get-first-or-last-line-pos input)
         line-pos (util/get-first-or-last-line-pos input)
@@ -2448,14 +2471,14 @@
 
 
       (or (and up? (cursor/textarea-cursor-first-row? input))
       (or (and up? (cursor/textarea-cursor-first-row? input))
           (and down? (cursor/textarea-cursor-last-row? input)))
           (and down? (cursor/textarea-cursor-last-row? input)))
-      (move-cross-boundrary-up-down direction)
+      (move-cross-boundary-up-down direction)
 
 
       :else
       :else
       (if up?
       (if up?
         (cursor/move-cursor-up input)
         (cursor/move-cursor-up input)
         (cursor/move-cursor-down input)))))
         (cursor/move-cursor-down input)))))
 
 
-(defn- move-to-block-when-cross-boundrary
+(defn- move-to-block-when-cross-boundary
   [direction]
   [direction]
   (let [up? (= :left direction)
   (let [up? (= :left direction)
         pos (if up? :max 0)
         pos (if up? :max 0)
@@ -2491,7 +2514,7 @@
 
 
         (or (and left? (cursor/start? input))
         (or (and left? (cursor/start? input))
             (and right? (cursor/end? input)))
             (and right? (cursor/end? input)))
-        (move-to-block-when-cross-boundrary direction)
+        (move-to-block-when-cross-boundary direction)
 
 
         :else
         :else
         (if left?
         (if left?
@@ -2659,6 +2682,7 @@
     nil))
     nil))
 
 
 (defn ^:large-vars/cleanup-todo keydown-not-matched-handler
 (defn ^:large-vars/cleanup-todo keydown-not-matched-handler
+  "NOTE: Keydown cannot be used on Android platform"
   [format]
   [format]
   (fn [e _key-code]
   (fn [e _key-code]
     (let [input-id (state/get-edit-input-id)
     (let [input-id (state/get-edit-input-id)
@@ -2727,7 +2751,11 @@
 
 
         ;; If you type `xyz`, the last backtick should close the first and not add another autopair
         ;; If you type `xyz`, the last backtick should close the first and not add another autopair
         ;; If you type several backticks in a row, each one should autopair to accommodate multiline code (```)
         ;; If you type several backticks in a row, each one should autopair to accommodate multiline code (```)
-        (contains? (set (keys autopair-map)) key)
+        (-> (keys autopair-map)
+            set
+            (disj "(")
+            (contains? key)
+            (or (autopair-left-paren? input key)))
         (let [curr (get-current-input-char input)
         (let [curr (get-current-input-char input)
                   prev (util/nth-safe value (dec pos))]
                   prev (util/nth-safe value (dec pos))]
             (util/stop e)
             (util/stop e)
@@ -2735,14 +2763,6 @@
               (cursor/move-cursor-forward input)
               (cursor/move-cursor-forward input)
               (autopair input-id key format nil)))
               (autopair input-id key format nil)))
 
 
-        (and hashtag? (or (zero? pos) (re-matches #"\s" (get value (dec pos)))))
-        (do
-          (commands/handle-step [:editor/search-page-hashtag])
-          (if (= key "#")
-            (state/set-editor-last-pos! (inc (cursor/pos input))) ;; In keydown handler, the `#` is not inserted yet.
-            (state/set-editor-last-pos! (cursor/pos input)))
-          (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)}))
-
         (let [sym "$"]
         (let [sym "$"]
           (and (= key sym)
           (and (= key sym)
                (>= (count value) 1)
                (>= (count value) 1)
@@ -3247,7 +3267,7 @@
         repo (state/get-current-repo)
         repo (state/get-current-repo)
         value (boolean value)]
         value (boolean value)]
     (when repo
     (when repo
-      (save-current-block!) ;; Save the input contents before collapsing 
+      (save-current-block!) ;; Save the input contents before collapsing
       (outliner-tx/transact! ;; Save the new collapsed state as an undo transaction (if it changed)
       (outliner-tx/transact! ;; Save the new collapsed state as an undo transaction (if it changed)
         {:outliner-op :collapse-expand-blocks}
         {:outliner-op :collapse-expand-blocks}
         (doseq [block-id block-ids]
         (doseq [block-id block-ids]

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

@@ -565,12 +565,12 @@
          :warning
          :warning
          false)))))
          false)))))
 
 
-(defmethod handle :file-watcher/changed [[_ ^js event]]
+(defmethod handle :mobile-file-watcher/changed [[_ ^js event]]
   (let [type (.-event event)
   (let [type (.-event event)
         payload (-> event
         payload (-> event
                     (js->clj :keywordize-keys true)
                     (js->clj :keywordize-keys true)
                     (update :path (fn [path]
                     (update :path (fn [path]
-                                    (when (string? path) (capacitor-fs/ios-force-include-private path)))))]
+                                    (when (string? path) (capacitor-fs/normalize-file-protocol-path nil path)))))]
     (fs-watcher/handle-changed! type payload)
     (fs-watcher/handle-changed! type payload)
     (when (file-sync-handler/enable-sync?)
     (when (file-sync-handler/enable-sync?)
      (sync/file-watch-handler type payload))))
      (sync/file-watch-handler type payload))))

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

@@ -93,7 +93,8 @@
                            re-render-root? false
                            re-render-root? false
                            from-disk? false
                            from-disk? false
                            skip-compare? false}}]
                            skip-compare? false}}]
-  (let [original-content (db/get-file repo path)
+  (let [path (gp-util/path-normalize path)
+        original-content (db/get-file repo path)
         write-file! (if from-disk?
         write-file! (if from-disk?
                       #(p/resolved nil)
                       #(p/resolved nil)
                       #(let [path-dir (if (and
                       #(let [path-dir (if (and
@@ -164,7 +165,8 @@
   [repo files {:keys [finish-handler]} file->content]
   [repo files {:keys [finish-handler]} file->content]
   (let [write-file-f (fn [[path content]]
   (let [write-file-f (fn [[path content]]
                        (when path
                        (when path
-                         (let [original-content (get file->content path)]
+                         (let [path (gp-util/path-normalize path)
+                               original-content (get file->content path)]
                           (-> (p/let [_ (or
                           (-> (p/let [_ (or
                                          (util/electron?)
                                          (util/electron?)
                                          (nfs/check-directory-permission! repo))]
                                          (nfs/check-directory-permission! repo))]

+ 19 - 11
src/main/frontend/handler/file_sync.cljs

@@ -15,7 +15,8 @@
             [cljs-time.coerce :as tc]
             [cljs-time.coerce :as tc]
             [cljs-time.core :as t]
             [cljs-time.core :as t]
             [frontend.storage :as storage]
             [frontend.storage :as storage]
-            [logseq.graph-parser.util :as gp-util]))
+            [logseq.graph-parser.util :as gp-util]
+            [lambdaisland.glogi :as log]))
 
 
 (def *beta-unavailable? (volatile! false))
 (def *beta-unavailable? (volatile! false))
 
 
@@ -142,18 +143,25 @@
                                      (#(js->clj % :keywordize-keys true))
                                      (#(js->clj % :keywordize-keys true))
                                      ((juxt :dir :name))
                                      ((juxt :dir :name))
                                      (apply path/join base-path))
                                      (apply path/join base-path))
-            version-file-paths (<! (p->c (fs/readdir version-files-dir :path-only? true)))]
+            version-file-paths (->> (<! (p->c (fs/readdir version-files-dir :path-only? true)))
+                                    (remove #{version-files-dir}))]
         (when-not (instance? ExceptionInfo version-file-paths)
         (when-not (instance? ExceptionInfo version-file-paths)
           (when (seq version-file-paths)
           (when (seq version-file-paths)
-            (mapv
-             (fn [path]
-               (let [create-time
-                     (-> (path/parse path)
-                         (js->clj :keywordize-keys true)
-                         :name
-                         (#(tf/parse (tf/formatter "yyyy-MM-dd'T'HH_mm_ss.SSSZZ") %)))]
-                 {:create-time create-time :path path :relative-path (string/replace-first path base-path "")}))
-             version-file-paths)))))))
+            (->>
+             (mapv
+              (fn [path]
+                (try
+                  (let [create-time
+                       (-> (path/parse path)
+                           (js->clj :keywordize-keys true)
+                           :name
+                           (#(tf/parse (tf/formatter "yyyy-MM-dd'T'HH_mm_ss.SSSZZ") %)))]
+                    {:create-time create-time :path path :relative-path (string/replace-first path base-path "")})
+                  (catch :default e
+                    (log/error :page-history/parse-format-error e)
+                    nil)))
+              version-file-paths)
+             (remove nil?))))))))
 
 
 (defn fetch-page-file-versions [graph-uuid page]
 (defn fetch-page-file-versions [graph-uuid page]
   []
   []

+ 2 - 57
src/main/frontend/handler/mobile/swipe.cljs

@@ -1,58 +1,3 @@
-(ns ^:no-doc frontend.handler.mobile.swipe
-  (:require [cljs-bean.core :as bean]
-            [frontend.state :as state]
-            [frontend.util :as util]
-            [frontend.mobile.util :as mobile-util]))
+(ns ^:no-doc frontend.handler.mobile.swipe)
 
 
-(defn setup-listeners!
-  []
-  (let [container js/document]
-    (.addEventListener
-     container "swiped"
-     (fn [e]
-       (let [detail (some-> (.-detail e)
-                            (bean/->clj))
-             width (.-innerWidth js/window)
-             height (.-innerHeight js/window)
-             xstart (:xStart detail)
-             ystart (:yStart detail)]
-         (case (:dir detail)
-           "left"
-           (cond
-             (and (> xstart (/ width 1.2))
-                  (not (util/sm-breakpoint?)))
-             (when-not (state/sub :ui/sidebar-open?)
-               (state/set-state! :ui/sidebar-open? true))
-
-             (if (util/sm-breakpoint?)
-               (< xstart (/ width 1.25))
-               (< xstart (/ width 2)))
-             (when (state/get-left-sidebar-open?)
-               (state/set-left-sidebar-open! false))
-
-             :else
-             nil)
-
-           "right"
-           (cond
-             (and (mobile-util/native-android?)
-                  (<= ystart (/ height 2)))
-             (when-not (state/get-left-sidebar-open?)
-               (when (util/sm-breakpoint?)
-                 (state/clear-edit!))
-               (state/set-left-sidebar-open! true))
-
-             (> xstart (/ width 2))
-             (when (state/sub :ui/sidebar-open?)
-               (state/set-state! :ui/sidebar-open? false))
-
-             (and (mobile-util/native-ios?)
-                  (<= (:xStart detail) 20))
-             (when-not (state/get-left-sidebar-open?)
-               (when (mobile-util/native-iphone?)
-                 (state/clear-edit!))
-               (state/set-left-sidebar-open! true))
-
-             :else nil)
-
-           nil))))))
+;; TODO

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

@@ -61,7 +61,8 @@
     (gp-util/safe-subs s 0 200)))
     (gp-util/safe-subs s 0 200)))
 
 
 (defn get-page-file-path
 (defn get-page-file-path
-  ([] (get-page-file-path (state/get-current-page)))
+  ([] (get-page-file-path (or (state/get-current-page)
+                              (state/get-current-whiteboard))))
   ([page-name]
   ([page-name]
    (when page-name
    (when page-name
      (let [page-name (util/page-name-sanity-lc page-name)]
      (let [page-name (util/page-name-sanity-lc page-name)]
@@ -99,6 +100,7 @@
 (defn- create-title-property?
 (defn- create-title-property?
   [journal? page-name]
   [journal? page-name]
   (and (not journal?)
   (and (not journal?)
+       (not= (state/get-filename-format) :triple-lowbar)
        (fs-util/create-title-property? page-name)))
        (fs-util/create-title-property? page-name)))
 
 
 (defn- build-page-tx [format properties page journal? whiteboard?]
 (defn- build-page-tx [format properties page journal? whiteboard?]
@@ -475,7 +477,7 @@
 
 
       ;; Redirect to the newly renamed page
       ;; Redirect to the newly renamed page
       (when redirect?
       (when redirect?
-        (route-handler/redirect! {:to          (if (= "whiteboard" (:block/type page)) :whiteboard :page)
+        (route-handler/redirect! {:to          (if (model/whiteboard-page? page) :whiteboard :page)
                                   :push        false
                                   :push        false
                                   :path-params {:name new-page-name}}))
                                   :path-params {:name new-page-name}}))
 
 

+ 4 - 3
src/main/frontend/handler/paste.cljs

@@ -68,8 +68,9 @@
 
 
 (defn- get-whiteboard-tldr-from-text
 (defn- get-whiteboard-tldr-from-text
   [text]
   [text]
-  (when-let [matched-text (util/safe-re-find #"<whiteboard-tldr>(.*)</whiteboard-tldr>" text)]
-    (try-parse-as-json (gp-util/safe-decode-uri-component (second matched-text)))))
+  (when-let [matched-text (util/safe-re-find #"<whiteboard-tldr>(.*)</whiteboard-tldr>"
+                                             (gp-util/safe-decode-uri-component text))]
+    (try-parse-as-json (second matched-text))))
 
 
 (defn- get-whiteboard-shape-refs-text
 (defn- get-whiteboard-shape-refs-text
   [text]
   [text]
@@ -203,5 +204,5 @@
                                           (util/stop e))))]
                                           (util/stop e))))]
            (cond
            (cond
              (and (string/blank? text) (string/blank? html)) (paste-file-if-exist)
              (and (string/blank? text) (string/blank? html)) (paste-file-if-exist)
-             (and (seq files) (state/perferred-pasting-file?)) (paste-file-if-exist)
+             (and (seq files) (state/preferred-pasting-file?)) (paste-file-if-exist)
              :else (paste-text-or-blocks-aux input e text html))))))))
              :else (paste-text-or-blocks-aux input e text html))))))))

+ 16 - 5
src/main/frontend/handler/plugin.cljs

@@ -163,7 +163,7 @@
 
 
 (defn get-enabled-plugins-if-setting-schema
 (defn get-enabled-plugins-if-setting-schema
   []
   []
-  (when-let [plugins (seq (state/get-enabled?-installed-plugins false nil true))]
+  (when-let [plugins (seq (state/get-enabled?-installed-plugins false nil true true))]
     (filter #(has-setting-schema? (:id %)) plugins)))
     (filter #(has-setting-schema? (:id %)) plugins)))
 
 
 (defn setup-install-listener!
 (defn setup-install-listener!
@@ -329,6 +329,16 @@
     (swap! state/state medley/dissoc-in [:plugin/installed-resources pid])
     (swap! state/state medley/dissoc-in [:plugin/installed-resources pid])
     true))
     true))
 
 
+(defn register-plugin-search-service
+  [pid name opts]
+  (when-let [pid (and name (keyword pid))]
+    (state/install-plugin-service pid :search name opts)))
+
+(defn unregister-plugin-search-services
+  [pid]
+  (when-let [pid (keyword pid)]
+    (state/uninstall-plugin-service pid :search)))
+
 (defn unregister-plugin-themes
 (defn unregister-plugin-themes
   ([pid] (unregister-plugin-themes pid true))
   ([pid] (unregister-plugin-themes pid true))
   ([pid effect]
   ([pid effect]
@@ -564,7 +574,7 @@
     (when-not (= text "END")
     (when-not (= text "END")
       [:div.flex.align-items.justify-center.h-screen.w-full.preboot-loading
       [:div.flex.align-items.justify-center.h-screen.w-full.preboot-loading
        [:span.flex.items-center.justify-center.w-60.flex-col
        [:span.flex.items-center.justify-center.w-60.flex-col
-        [:small.scale-250.opacity-70.mb-10.animate-pulse (svg/logo false)]
+        [:small.scale-250.opacity-70.mb-10.animate-pulse (svg/logo)]
         [:small.block.text-sm.relative.opacity-50 {:style {:right "-8px"}} text]]])))
         [:small.block.text-sm.relative.opacity-50 {:style {:right "-8px"}} text]]])))
 
 
 (defn ^:large-vars/cleanup-todo init-plugins!
 (defn ^:large-vars/cleanup-todo init-plugins!
@@ -586,7 +596,8 @@
                                 (invoke-exported-api "unregister_plugin_simple_command" pid)
                                 (invoke-exported-api "unregister_plugin_simple_command" pid)
                                 (invoke-exported-api "uninstall_plugin_hook" pid)
                                 (invoke-exported-api "uninstall_plugin_hook" pid)
                                 (unregister-plugin-ui-items pid)
                                 (unregister-plugin-ui-items pid)
-                                (unregister-plugin-resources pid))
+                                (unregister-plugin-resources pid)
+                                (unregister-plugin-search-services pid))
 
 
               _               (doto js/LSPluginCore
               _               (doto js/LSPluginCore
                                 (.on "registered"
                                 (.on "registered"
@@ -632,8 +643,8 @@
                                                           (when mode
                                                           (when mode
                                                             (state/set-custom-theme! mode theme)
                                                             (state/set-custom-theme! mode theme)
                                                             (state/set-theme-mode! mode))
                                                             (state/set-theme-mode! mode))
-                                                          (hook-plugin-app :theme-changed theme)
-                                                          (state/set-state! :plugin/selected-theme url))))
+                                                          (state/set-state! :plugin/selected-theme url)
+                                                          (hook-plugin-app :theme-changed theme))))
 
 
                                 (.on "reset-custom-theme" (fn [^js themes]
                                 (.on "reset-custom-theme" (fn [^js themes]
                                                             (let [themes       (bean/->clj themes)
                                                             (let [themes       (bean/->clj themes)

+ 1 - 1
src/main/frontend/handler/plugin_config.cljs

@@ -125,7 +125,7 @@ returns map of plugins to install and uninstall"
     (js/window.apis.addListener "lsp-installed" listener)))
     (js/window.apis.addListener "lsp-installed" listener)))
 
 
 (defn start
 (defn start
-  "This component has just one reponsibility on start, to create a plugins.edn
+  "This component has just one responsibility on start, to create a plugins.edn
   if none exists"
   if none exists"
   []
   []
   (create-plugin-config-file-if-not-exists))
   (create-plugin-config-file-if-not-exists))

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

@@ -244,7 +244,7 @@
                                               :file/content)]
                                               :file/content)]
                      (repo-config-handler/read-repo-config repo-url content))
                      (repo-config-handler/read-repo-config repo-url content))
                    (state/get-config repo-url))
                    (state/get-config repo-url))
-        ;; NOTE: Use config while parsing. Make sure it's the corrent journal title format
+        ;; NOTE: Use config while parsing. Make sure it's the current journal title format
         _ (state/set-config! repo-url config)
         _ (state/set-config! repo-url config)
         relate-path-fn (fn [m k]
         relate-path-fn (fn [m k]
                          (some-> (get m k)
                          (some-> (get m k)

+ 1 - 1
src/main/frontend/handler/repo_config.cljs

@@ -57,7 +57,7 @@
    (set-repo-config-state! repo-url config-content)))
    (set-repo-config-state! repo-url config-content)))
 
 
 (defn start
 (defn start
-  "This component only has one reponsibility on start, to manage db and ui state
+  "This component only has one responsibility on start, to manage db and ui state
   from repo config. It does not manage the repo directory, logseq/, as that is
   from repo config. It does not manage the repo directory, logseq/, as that is
   loosely done by repo-handler"
   loosely done by repo-handler"
   [{:keys [repo]}]
   [{:keys [repo]}]

+ 7 - 5
src/main/frontend/handler/route.cljs

@@ -3,12 +3,12 @@
             [frontend.config :as config]
             [frontend.config :as config]
             [frontend.date :as date]
             [frontend.date :as date]
             [frontend.db :as db]
             [frontend.db :as db]
-            [frontend.handler.ui :as ui-handler]
             [frontend.handler.recent :as recent-handler]
             [frontend.handler.recent :as recent-handler]
             [frontend.handler.search :as search-handler]
             [frontend.handler.search :as search-handler]
+            [frontend.handler.ui :as ui-handler]
             [frontend.state :as state]
             [frontend.state :as state]
-            [logseq.graph-parser.text :as text]
             [frontend.util :as util]
             [frontend.util :as util]
+            [logseq.graph-parser.text :as text]
             [reitit.frontend.easy :as rfe]))
             [reitit.frontend.easy :as rfe]))
 
 
 (defn redirect!
 (defn redirect!
@@ -65,9 +65,11 @@
    (redirect-to-whiteboard! name nil))
    (redirect-to-whiteboard! name nil))
   ([name {:keys [block-id]}]
   ([name {:keys [block-id]}]
    (recent-handler/add-page-to-recent! (state/get-current-repo) name false)
    (recent-handler/add-page-to-recent! (state/get-current-repo) name false)
-   (redirect! {:to :whiteboard
-               :path-params {:name (str name)}
-               :query-params (merge {:block-id block-id})})))
+   (if (= name (state/get-current-whiteboard))
+     (state/focus-whiteboard-shape block-id)
+     (redirect! {:to :whiteboard
+                 :path-params {:name (str name)}
+                 :query-params (merge {:block-id block-id})}))))
 
 
 (defn get-title
 (defn get-title
   [name path-params]
   [name path-params]

+ 2 - 1
src/main/frontend/handler/search.cljs

@@ -111,7 +111,8 @@
   ([clear-search-mode?]
   ([clear-search-mode?]
    (let [m {:search/result nil
    (let [m {:search/result nil
             :search/q ""}]
             :search/q ""}]
-     (swap! state/state merge m))
+     (swap! state/state merge m)
+     (when config/lsp-enabled? (state/reset-plugin-search-engines)))
    (when (and clear-search-mode? (not= (state/get-search-mode) :graph))
    (when (and clear-search-mode? (not= (state/get-search-mode) :graph))
      (state/set-search-mode! :global))))
      (state/set-search-mode! :global))))
 
 

+ 10 - 8
src/main/frontend/handler/whiteboard.cljs

@@ -66,10 +66,14 @@
                                       (block-ids (:block/uuid (:block/parent block)))
                                       (block-ids (:block/uuid (:block/parent block)))
                                       (not (gp-whiteboard/shape-block? block)))))
                                       (not (gp-whiteboard/shape-block? block)))))
                                existing-blocks)
                                existing-blocks)
+        ;; always recalcuate refs for now. 
+        ;; todo: optimize in frontend.modules.outliner.pipeline/compute-block-path-refs?
+        refs-tx (mapcat (fn [m] [[:db/retract (:db/id m) :block/path-refs]
+                                 [:db/retract (:db/id m) :block/refs]]) existing-blocks)
         delete-blocks-tx (mapv (fn [s] [:db/retractEntity (:db/id s)]) delete-blocks)
         delete-blocks-tx (mapv (fn [s] [:db/retractEntity (:db/id s)]) delete-blocks)
         page-and-blocks (->> (cons page-block blocks)
         page-and-blocks (->> (cons page-block blocks)
                              (map outliner/block-with-timestamps))]
                              (map outliner/block-with-timestamps))]
-    (concat page-and-blocks delete-blocks-tx)))
+    (concat refs-tx page-and-blocks delete-blocks-tx)))
 
 
 (defn- get-whiteboard-clj [page-name]
 (defn- get-whiteboard-clj [page-name]
   (when (model/page-exists? page-name)
   (when (model/page-exists? page-name)
@@ -78,7 +82,7 @@
           blocks (model/get-page-blocks-no-cache page-name)]
           blocks (model/get-page-blocks-no-cache page-name)]
       [page-block blocks])))
       [page-block blocks])))
 
 
-(defn- whiteboard-clj->tldr [page-block blocks shape-id]
+(defn- whiteboard-clj->tldr [page-block blocks]
   (let [id (str (:block/uuid page-block))
   (let [id (str (:block/uuid page-block))
         shapes (->> blocks
         shapes (->> blocks
                     (filter gp-whiteboard/shape-block?)
                     (filter gp-whiteboard/shape-block?)
@@ -89,7 +93,7 @@
         tldr-page (dissoc tldr-page :assets)]
         tldr-page (dissoc tldr-page :assets)]
     (clj->js {:currentPageId id
     (clj->js {:currentPageId id
               :assets (or assets #js[])
               :assets (or assets #js[])
-              :selectedIds (if (not-empty shape-id) #js[shape-id] #js[])
+              :selectedIds #js[]
               :pages [(merge tldr-page
               :pages [(merge tldr-page
                              {:id id
                              {:id id
                               :name "page"
                               :name "page"
@@ -169,16 +173,14 @@
 
 
 (defn page-name->tldr!
 (defn page-name->tldr!
   ([page-name]
   ([page-name]
-   (page-name->tldr! page-name nil))
-  ([page-name shape-id]
    (if page-name
    (if page-name
      (if-let [[page-block blocks] (get-whiteboard-clj page-name)]
      (if-let [[page-block blocks] (get-whiteboard-clj page-name)]
-       (whiteboard-clj->tldr page-block blocks shape-id)
+       (whiteboard-clj->tldr page-block blocks)
        (create-new-whiteboard-page! page-name))
        (create-new-whiteboard-page! page-name))
      (create-new-whiteboard-page! nil))))
      (create-new-whiteboard-page! nil))))
 
 
 (defn- get-whiteboard-blocks
 (defn- get-whiteboard-blocks
-  "Given a page, return all the logseq blocks (exlude all shapes)"
+  "Given a page, return all the logseq blocks (exclude all shapes)"
   [page-name]
   [page-name]
   (let [blocks (model/get-page-blocks-no-cache page-name)]
   (let [blocks (model/get-page-blocks-no-cache page-name)]
     (remove gp-whiteboard/shape-block? blocks)))
     (remove gp-whiteboard/shape-block? blocks)))
@@ -261,4 +263,4 @@
           (clone-whiteboard-from-edn edn api)
           (clone-whiteboard-from-edn edn api)
           (state/set-onboarding-whiteboard! true))
           (state/set-onboarding-whiteboard! true))
         (p/catch
         (p/catch
-         (fn [e] (js/console.warn "Faield to populate onboarding whiteboard" e))))))
+         (fn [e] (js/console.warn "Failed to populate onboarding whiteboard" e))))))

+ 1 - 1
src/main/frontend/idb.cljs

@@ -13,7 +13,7 @@
 ;; To maintain backward compatibility
 ;; To maintain backward compatibility
 
 
 
 
-(def store (atom nil))
+(defonce store (atom nil))
 
 
 (defn clear-idb!
 (defn clear-idb!
   []
   []

+ 1 - 2
src/main/frontend/mobile/core.cljs

@@ -13,7 +13,6 @@
             [frontend.util :as util]
             [frontend.util :as util]
             [cljs-bean.core :as bean]))
             [cljs-bean.core :as bean]))
 
 
-
 (def *url (atom nil))
 (def *url (atom nil))
 ;; FIXME: `appUrlOpen` are fired twice when receiving a same intent.
 ;; FIXME: `appUrlOpen` are fired twice when receiving a same intent.
 ;; The following two variable atoms are used to compare whether
 ;; The following two variable atoms are used to compare whether
@@ -113,7 +112,7 @@
 
 
   (.addListener mobile-util/fs-watcher "watcher"
   (.addListener mobile-util/fs-watcher "watcher"
                 (fn [event]
                 (fn [event]
-                  (state/pub-event! [:file-watcher/changed event])))
+                  (state/pub-event! [:mobile-file-watcher/changed event])))
 
 
   (.addListener Keyboard "keyboardWillShow"
   (.addListener Keyboard "keyboardWillShow"
                 (fn [^js info]
                 (fn [^js info]

+ 1 - 1
src/main/frontend/mobile/graph_picker.cljs

@@ -84,7 +84,7 @@
 
 
      (when-not onboarding-and-home?
      (when-not onboarding-and-home?
        [:h1.flex.items-center
        [:h1.flex.items-center
-        [:span.scale-75 (svg/logo false)]
+        [:span.scale-75 (svg/logo)]
         [:span.pl-1 "Set up a graph"]])
         [:span.pl-1 "Set up a graph"]])
 
 
      (case step
      (case step

+ 6 - 3
src/main/frontend/mobile/index.css

@@ -9,7 +9,7 @@
   flex: 0 0 auto;
   flex: 0 0 auto;
   white-space: nowrap;
   white-space: nowrap;
   height: 80px;
   height: 80px;
-  align-items: start;
+  align-items: flex-start;
   box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.05);
   box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.05);
 
 
   .bottom-action {
   .bottom-action {
@@ -185,6 +185,10 @@ html.is-native-android {
       }
       }
     }
     }
   }
   }
+
+  .cp__sidebar-left-layout {
+    bottom: 0;
+  }
 }
 }
 
 
 html.is-zoomed-native-ios {
 html.is-zoomed-native-ios {
@@ -199,7 +203,6 @@ html.is-zoomed-native-ios {
   }
   }
 }
 }
 
 
-
 .cp__graph-picker {
 .cp__graph-picker {
   button.toggle-item {
   button.toggle-item {
     opacity: .5;
     opacity: .5;
@@ -231,7 +234,7 @@ html.is-zoomed-native-ios {
 
 
     .cp__graph-picker {
     .cp__graph-picker {
       padding: 58px 20px 20px 20px;
       padding: 58px 20px 20px 20px;
-      background: var(--ls-search-background-color);
+      background: var(--ls-primary-background-color);
 
 
       > h1 {
       > h1 {
         position: absolute;
         position: absolute;

+ 9 - 2
src/main/frontend/mobile/util.cljs

@@ -1,9 +1,10 @@
 (ns frontend.mobile.util
 (ns frontend.mobile.util
-  (:require ["@capacitor/core" :refer [Capacitor registerPlugin]]
+  (:require ["@capacitor/core" :refer [Capacitor registerPlugin ^js Plugins]]
             ["@capacitor/splash-screen" :refer [SplashScreen]]
             ["@capacitor/splash-screen" :refer [SplashScreen]]
             ["@logseq/capacitor-file-sync" :refer [FileSync]]
             ["@logseq/capacitor-file-sync" :refer [FileSync]]
             [clojure.string :as string]
             [clojure.string :as string]
-            [promesa.core :as p]))
+            [promesa.core :as p]
+            [goog.object :as gobj]))
 
 
 (defn platform []
 (defn platform []
   (.getPlatform Capacitor))
   (.getPlatform Capacitor))
@@ -93,3 +94,9 @@
   [path]
   [path]
   (string/includes? path "iCloud~com~logseq~logseq"))
   (string/includes? path "iCloud~com~logseq~logseq"))
 
 
+(defn app-active?
+  "Whether the app is active. This function returns a promise."
+  []
+  (let [app ^js (gobj/get Plugins "App")]
+    (p/let [state (.getState app)]
+      (gobj/get state "isActive"))))

+ 3 - 2
src/main/frontend/modules/file/core.cljs

@@ -8,7 +8,8 @@
             [frontend.state :as state]
             [frontend.state :as state]
             [frontend.util.property :as property]
             [frontend.util.property :as property]
             [frontend.util.fs :as fs-util]
             [frontend.util.fs :as fs-util]
-            [frontend.handler.file :as file-handler]))
+            [frontend.handler.file :as file-handler]
+            [frontend.db.model :as model]))
 
 
 (defn- indented-block-content
 (defn- indented-block-content
   [content spaces-tabs]
   [content spaces-tabs]
@@ -111,7 +112,7 @@
       (let [format (name (get page :block/format
       (let [format (name (get page :block/format
                               (state/get-preferred-format)))
                               (state/get-preferred-format)))
             title (string/capitalize (:block/name page))
             title (string/capitalize (:block/name page))
-            whiteboard-page? (= "whiteboard" (:block/type page))
+            whiteboard-page? (model/whiteboard-page? page)
             format (if whiteboard-page? "edn" format)
             format (if whiteboard-page? "edn" format)
             journal-page? (date/valid-journal-title? title)
             journal-page? (date/valid-journal-title? title)
             journal-title (date/normalize-journal-title title)
             journal-title (date/normalize-journal-title title)

+ 2 - 0
src/main/frontend/modules/outliner/pipeline.cljs

@@ -18,6 +18,8 @@
 ;; and the new path-refs changes, which makes both undo/redo and
 ;; and the new path-refs changes, which makes both undo/redo and
 ;; react-query/refresh! easier.
 ;; react-query/refresh! easier.
 
 
+;; TODO: also need to consider whiteboard transactions
+
 ;; Steps:
 ;; Steps:
 ;; 1. For each changed block, new-refs = its page + :block/refs + parents :block/refs
 ;; 1. For each changed block, new-refs = its page + :block/refs + parents :block/refs
 ;; 2. Its children' block/path-refs might need to be updated too.
 ;; 2. Its children' block/path-refs might need to be updated too.

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