Переглянути джерело

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. BIN
      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.diff diff
              frontend.encrypt encrypt
+             frontend.extensions.sci sci
              frontend.format.mldoc mldoc
              frontend.format.block block
              frontend.fs fs

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

@@ -8,10 +8,12 @@ on:
     branches: [master]
     paths:
       - 'ios/App'
+      - package.json
   pull_request:
     branches: [master]
     paths:
       - 'ios/App'
+      - package.json
 
 env:
   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
 
       - 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:
-            #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
 * 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)
 
+## 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
 
 [![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-status-bar')
     implementation project(':capawesome-capacitor-background-task')
+    implementation project(':hugotomazi-capacitor-navigation-bar')
     implementation project(':logseq-capacitor-file-sync')
     implementation project(':capacitor-voice-recorder')
     implementation project(':send-intent')

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

@@ -39,6 +39,10 @@
 		"pkg": "@capawesome/capacitor-background-task",
 		"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",
 		"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"?>
 <resources>
     <color name="logoPrimary">#002b36</color>
+    <color name="colorPrimary">#ffffff</color>
+    <color name="colorPrimaryDark">#002b36</color>
 </resources>

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

@@ -14,6 +14,8 @@
         <item name="windowNoTitle">true</item>
         <item name="android:background">@null</item>
         <item name="android:windowIsTranslucent">true</item>
+        <item name="android:navigationBarColor">@color/colorPrimary</item>
+        <item name="android:statusBarColor">@color/colorPrimary</item>
     </style>
 
     <!-- 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'
 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'
 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])
    :doc "Lint datalog rules for parsability and unbound variables"
    :task (datalog/lint-rules
-          (into rules/rules
+          (into (mapcat val rules/rules)
                 (-> rules/query-dsl-rules
                     ;; TODO: Update linter to handle false positive on ?str-val
                     (dissoc :property)

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

@@ -2,62 +2,63 @@
   "Datalog rules for use with logseq.db.schema")
 
 (def ^:large-vars/data-var rules
+  "Rules used mainly in frontend.db.model"
   ;; 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
   "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]
-  (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
-  [shape]
+  [shape page-name]
   (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
   "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
               {:block/uuid (uuid (:id shape))}
-              (with-whiteboard-block-refs shape)
+              (with-whiteboard-block-refs shape page-name)
               (with-whiteboard-content shape))
 
              ;; TODO: remove?

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

@@ -11,5 +11,5 @@ test('should not have any automatically detectable accessibility issues', async
         .setLegacyMode()
         .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 }) => {
   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 }) => {
-  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")
 

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

@@ -1,7 +1,7 @@
 import { expect } from '@playwright/test'
 import { test } from './fixtures'
 import {
-  createRandomPage, randomInt, randomInsert, randomEditDelete, randomEditMoveUpDown, IsMac, randomString,
+  createRandomPage, randomInt, IsMac, randomString,
 } from './utils'
 
 /**
@@ -96,7 +96,8 @@ const generateRandomTest = (size: number): RandomTestStep[] => {
   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)
 
   await createRandomPage(page)
@@ -175,7 +176,5 @@ test('Random editor operations', async ({ page, block }) => {
 
     // FIXME: CHECK await block.waitForBlocks(expectedBlocks)
     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()
 })
+
+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 * as process from 'process'
 import { Block } from './types'
-import pathlib from 'path'
+import * as pathlib from 'path'
 
 export const IsMac = process.platform === 'darwin'
 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 { IsMac } from './utils'
 
-test('enable whiteboards', async ({ page }) => {
+test.skip('enable whiteboards', async ({ page }) => {
     await page.evaluate(() => {
         window.localStorage.removeItem('ls-onboarding-whiteboard?')
     })
@@ -16,17 +16,17 @@ test('enable whiteboards', async ({ page }) => {
     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('#tl-create-whiteboard')
     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)
 })
 
-test('cleanup the shapes', async ({ page }) => {
+test.skip('cleanup the shapes', async ({ page }) => {
     if (IsMac) {
         await page.keyboard.press('Meta+a')
     } else {
@@ -36,19 +36,19 @@ test('cleanup the shapes', async ({ page }) => {
     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', {
         button: 'right',
     })
-  
+
     await expect(page.locator('#custom-context-menu')).toBeVisible()
-  
+
     await page.keyboard.press('Escape')
-  
+
     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"
     // Newly created whiteboard should have a default title
     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");
 })
 
-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')
 })
 
-test('draw a rectangle', async ({ page }) => {
+test.skip('draw a rectangle', async ({ page }) => {
     const canvas = await page.waitForSelector('.logseq-tldraw');
     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.down();
@@ -88,35 +88,35 @@ test('draw a rectangle', async ({ page }) => {
     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 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 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 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 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
     await page.click('.nav-header .whiteboard')
     await page.click('#tl-create-whiteboard')
-    
+
     await page.click('.whiteboard-page-title')
     await page.fill('.whiteboard-page-title input', "my-whiteboard-3")
     await page.keyboard.press('Enter')
-    
+
     const canvas = await page.waitForSelector('.logseq-tldraw');
     await canvas.dblclick({
         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()
 })
 
-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 expect(page.locator('.whiteboard-page-title .title')).toContainText("my-whiteboard-2");
-  
+
     const pageRefCount$ = page.locator('.whiteboard-page-refs-count')
     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 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
   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 'CapacitorVoiceRecorder', :path => '../../node_modules/capacitor-voice-recorder'
   pod 'SendIntent', :path => '../../node_modules/send-intent'

+ 1 - 0
libs/.npmignore

@@ -1,3 +1,4 @@
 src/
 webpack.*
 .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",
-  "version": "0.0.10",
+  "version": "0.0.11",
   "description": "Logseq SDK libraries",
   "main": "dist/lsplugin.user.js",
   "typings": "index.d.ts",
@@ -12,7 +12,8 @@
     "dev:core": "npm run build:core -- --mode development --watch",
     "build": "tsc && rm dist/*.js && npm run build:user",
     "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": {
     "csstype": "3.1.0",
@@ -25,12 +26,18 @@
     "snake-case": "3.0.4"
   },
   "devDependencies": {
+    "@babel/core": "^7.20.2",
+    "@babel/preset-env": "^7.20.2",
     "@types/debug": "^4.1.5",
     "@types/dompurify": "2.3.3",
     "@types/lodash-es": "4.17.6",
+    "babel-loader": "^9.1.0",
     "prettier": "^2.6.2",
     "prettier-config-standard": "^5.0.0",
+    "terser-webpack-plugin": "^5.3.6",
     "ts-loader": "9.3.0",
+    "typedoc": "^0.23.17",
+    "typedoc-plugin-lsp-docs": "^0.0.1",
     "typescript": "4.7.3",
     "webpack": "5.73.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)
 
             this._pluginLocal?.emit(type, payload || {})
+            this._pluginLocal?.caller.emit(type, payload || {})
           })
 
           this._call = async (...args: any) => {

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

@@ -139,7 +139,12 @@ class PluginLogger extends EventEmitter<'change'> {
     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) => {
       if (it && it instanceof Error) {
         ac += `${it.message} ${it.stack}`
@@ -150,6 +155,11 @@ class PluginLogger extends EventEmitter<'change'> {
     }, `[${this._tag}][${new Date().toLocaleTimeString()}] `)
 
     this._logs.push([type, msg])
+
+    if (inConsole) {
+      console?.['ERROR' === type ? 'error' : 'debug'](`${type}: ${msg}`)
+    }
+
     this.emit('change')
   }
 
@@ -907,9 +917,9 @@ class PluginLocal extends EventEmitter<'loaded'
 
       this._dispose(cleanInjectedScripts.bind(this))
     } 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._loadErr = e
     } finally {
@@ -1329,7 +1339,6 @@ class LSPluginCore
           }
         }
 
-
         pluginLocal.settings?.on('change', (a) => {
           this.emit('settings-changed', pluginLocal.id, 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 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
  */
@@ -302,6 +330,9 @@ export interface IAppProxy {
   getUserInfo: () => Promise<AppUserInfo | null>
   getUserConfigs: () => Promise<AppUserConfigs>
 
+  // services
+  registerSearchService<T extends IPluginSearchServiceHooks>(s: T): void
+
   // commands
   registerCommand: (
     type: string,
@@ -352,6 +383,7 @@ export interface IAppProxy {
    * @param path
    */
   getStateFromStore: <T = any>(path: string | Array<string>) => Promise<T>
+  setStateFromStore: (path: string | Array<string>, value: any) => Promise<void>
 
   // native
   relaunch: () => Promise<void>
@@ -367,6 +399,9 @@ export interface IAppProxy {
 
   // graph
   getCurrentGraph: () => Promise<AppGraphInfo | null>
+  getCurrentGraphConfigs: () => Promise<any>
+  getCurrentGraphFavorites: () => Promise<Array<string> | null>
+  getCurrentGraphRecent: () => Promise<Array<string> | null>
 
   // router
   pushState: (
@@ -403,6 +438,7 @@ export interface IAppProxy {
   setFullScreen: (flag: boolean | 'toggle') => void
   setLeftSidebarVisible: (flag: boolean | 'toggle') => void
   setRightSidebarVisible: (flag: boolean | 'toggle') => void
+  clearRightSidebarBlocks: (opts?: { close: boolean }) => void
 
   registerUIItem: (
     type: 'toolbar' | 'pagebar',
@@ -592,6 +628,7 @@ export interface IEditorProxy extends Record<string, any> {
       before: boolean
       sibling: boolean
       isPageBlock: boolean
+      focus: boolean
       customUUID: string
       properties: {}
     }>
@@ -804,14 +841,14 @@ export interface IAssetsProxy {
    * @added 0.0.2
    * @param exts
    */
-  listFilesOfCurrentGraph(exts?: string | string[]): Promise<{
+  listFilesOfCurrentGraph(exts?: string | string[]): Promise<Array<{
     path: string
     size: number
     accessTime: number
     modifiedTime: number
     changeTime: number
     birthTime: number
-  }>
+  }>>
 
   /**
    * @example https://github.com/logseq/logseq/pull/6488

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

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

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

@@ -60,7 +60,7 @@ export class LSPluginExperiments {
   }
 
   registerExtensionsEnhancer<T = any>(
-    type: 'katex',
+    type: 'katex' | 'codemirror',
     enhancer: (v: T) => Promise<any>
   ) {
     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 webpack = require('webpack')
 const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
+const TerserPlugin = require('terser-webpack-plugin')
 
 module.exports = {
   entry: './src/LSPlugin.user.ts',
@@ -9,14 +10,27 @@ module.exports = {
     rules: [
       {
         test: /\.tsx?$/,
-        use: 'ts-loader',
+        use: [
+          {
+            loader: 'babel-loader'
+          },
+          {
+            loader: 'ts-loader'
+          }
+        ],
         exclude: /node_modules/,
-      },
+      }
     ],
   },
   resolve: {
     extensions: ['.tsx', '.ts', '.js'],
   },
+  optimization: {
+    minimize: true,
+    minimizer: [
+      new TerserPlugin()
+    ]
+  },
   plugins: [
     new webpack.ProvidePlugin({
       process: 'process/browser',

Різницю між файлами не показано, бо вона завелика
+ 956 - 4
libs/yarn.lock


+ 2 - 1
package.json

@@ -90,8 +90,9 @@
         "@capacitor/status-bar": "^4.0.0",
         "@capawesome/capacitor-background-task": "^2.0.0",
         "@excalidraw/excalidraw": "0.12.0",
+        "@hugotomazi/capacitor-navigation-bar": "^2.0.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",
         "@sentry/react": "^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/tldraw.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>
 </html>

+ 2 - 8
resources/css/common.css

@@ -12,7 +12,7 @@
   --ls-headbar-height: 3rem;
   --ls-headbar-inner-top-padding: 0px;
   --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-error-color: var(--color-red-500);
   --ls-warning-color: var(--color-orange-500);
@@ -48,12 +48,6 @@ html[data-theme='dark'] {
   --ls-block-properties-background-color: #06323e;
   --ls-page-properties-background-color: #02171d;
   --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-secondary-border-color: #126277;
   --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-page-properties-background-color: #f7f7f7;
   --ls-block-ref-link-text-color: #d8e1e8;
-  --ls-search-background-color: var(--ls-primary-background-color);
   --ls-border-color: #ccc;
   --ls-secondary-border-color: #e2e2e2;
   --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-highlight-color: #c0e6fd;
   --ls-selection-background-color: #e4f2ff;
+  --ls-selection-text-color: var(--ls-secondary-text-color);
   --ls-page-checkbox-color: #9dbbd8;
   --ls-page-checkbox-border-color: var(--ls-page-checkbox-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;
 }
 
-.tie-app-feature::before {
+.tie-add-link::before {
   content: "\ea01";
 }
 
-.tie-block::before {
+.tie-app-feature::before {
   content: "\ea02";
 }
 
-.tie-block-search::before {
+.tie-block::before {
   content: "\ea03";
 }
 
-.tie-connector::before {
+.tie-block-search::before {
   content: "\ea04";
 }
 
-.tie-h-auto::before {
+.tie-connector::before {
   content: "\ea05";
 }
 
-.tie-heading-off::before {
+.tie-h-auto::before {
   content: "\ea06";
 }
 
-.tie-new-block::before {
+.tie-heading-off::before {
   content: "\ea07";
 }
 
-.tie-new-page::before {
+.tie-internal-link::before {
   content: "\ea08";
 }
 
-.tie-new-whiteboard::before {
+.tie-link-to-block::before {
   content: "\ea09";
 }
 
-.tie-new-whiteboard-element::before {
+.tie-link-to-page::before {
   content: "\ea0a";
 }
 
-.tie-object-compact::before {
+.tie-link-to-whiteboard::before {
   content: "\ea0b";
 }
 
-.tie-object-expanded::before {
+.tie-move-to-sidebar-right::before {
   content: "\ea0c";
 }
 
-.tie-page::before {
+.tie-new-block::before {
   content: "\ea0d";
 }
 
-.tie-page-search::before {
+.tie-new-page::before {
   content: "\ea0e";
 }
 
-.tie-references-hide::before {
+.tie-new-whiteboard::before {
   content: "\ea0f";
 }
 
-.tie-references-show::before {
+.tie-new-whiteboard-element::before {
   content: "\ea10";
 }
 
-.tie-select-cursor::before {
+.tie-object-compact::before {
   content: "\ea11";
 }
 
-.tie-text::before {
+.tie-object-expanded::before {
   content: "\ea12";
 }
 
-.tie-whiteboard::before {
+.tie-open-as-page::before {
   content: "\ea13";
 }
 
-.tie-whiteboard-element::before {
+.tie-page::before {
   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";
+}

BIN
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 "/" %)
            endpoint (api url-suffix)
            ^js res (fetch endpoint)
+           _ (debug "[Release URL] " endpoint "[Response Status/Text]" (.-status res) "-")
            res (response-transform res)
            res (.json res)
-           _ (debug "[Release URL] " endpoint)
            res (bean/->clj res)
            version (:tag_name 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
   "System-component-like ns that defines listeners by event name to receive ipc
   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!
   []
   ;; only persist current db!
   ;; TODO rename the function and event to persist-db
   (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-error   #(ipc/ipc "persistent-dbs-error")}))
 
+
 (defn listen-persistent-dbs!
   []
   ;; TODO: move "file-watcher" to electron.ipc.channels
   (js/window.apis.on
-   "persistent-dbs"
-   (fn [_req]
-     (persist-dbs!))))
+    "persistent-dbs"
+    (fn [_req]
+      (persist-dbs!))))
+
 
 (defn ^:large-vars/cleanup-todo listen-to-electron!
   []
@@ -92,10 +96,13 @@
                        (let [{:keys [page-name block-id file]} (bean/->clj data)]
                          (cond
                            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
                              ;; 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
                            (if (db-model/get-block-by-uuid block-id)
@@ -121,14 +128,14 @@
                      (fn [data]
                        (let [repo (bean/->clj data)
                              before-f #(notification/show!
-                                        (ui/loading (t :graph/persist))
-                                        :warning)
+                                         (ui/loading (t :graph/persist))
+                                         :warning)
                              after-f #(ipc/ipc "broadcastPersistGraphDone")
                              error-f (fn []
                                        (after-f)
                                        (notification/show!
-                                        (t :graph/persist-error)
-                                        :error))
+                                         (t :graph/persist-error)
+                                         :error))
                              handlers {:before     before-f
                                        :on-success after-f
                                        :on-error   error-f}]
@@ -150,31 +157,42 @@
   (js/window.apis.on "quickCapture"
                      (fn [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)
                              time (date/get-current-time)
                              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)
                                               [:quick-capture-templates :text]
                                               "**{time}** [[quick capture]]: {text} {url}")
                              content (-> template
                                          (string/replace "{time}" time)
                                          (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
                                                                           :edit-block? false
                                                                           :replace-empty-target? true})))))
 
+
   (js/window.apis.on "openNewWindowOfGraph"
                      ;; 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
                      (fn [repo]
                        (ui-handler/open-new-window! repo))))
 
+
 (defn listen!
   []
   (listen-to-electron!)

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

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

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

@@ -61,6 +61,7 @@
             [frontend.util.drawer :as drawer]
             [frontend.util.property :as property]
             [frontend.util.text :as text-util]
+            [frontend.handler.notification :as notification]
             [goog.dom :as gdom]
             [goog.object :as gobj]
             [lambdaisland.glogi :as log]
@@ -264,14 +265,6 @@
     (when (seq 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))
 (rum/defcs resizable-image <
   (rum/local nil ::size)
@@ -353,12 +346,13 @@
             (ui/icon "trash")]
 
            [:button.asset-action-btn
-            {:title (t :asset/copy)
-             :tabIndex "-1"
+            {:title         (t :asset/copy)
+             :tabIndex      "-1"
              :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")]
 
            [:button.asset-action-btn
@@ -687,10 +681,7 @@
           inner (page-inner config
                             page-name-in-block
                             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
         (:breadcrumb? config)
         (or (:block/original-name page)
@@ -1045,7 +1036,7 @@
         [:a.asset-ref.is-pdf
          {:on-mouse-down (fn [_event]
                            (when-let [current (pdf-assets/inflate-asset s)]
-                             (state/set-state! :pdf/current current)))}
+                             (state/set-current-pdf! current)))}
          (or label-text
              (->elem :span (map-inline config label)))]
 
@@ -2540,7 +2531,7 @@
                             (filterv identity)
                             (map (fn [x] (if (vector? 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 "⋯"])))
                             (interpose (breadcrumb-separator)))]
         [: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?))
        :on-touch-end (fn [event]
                        (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]
                         (block-mouse-over e *control-show? block-id doc-mode?))
        :on-mouse-leave (fn [e]
@@ -3609,7 +3601,7 @@
             (when (seq blocks)
               (let [alias? (:block/alias? 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))}
                              (:ref? config)
                              (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)]
       [:.menu-links-wrapper
        (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
 ;; Also, keyboard bindings should only be activated after

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

@@ -181,7 +181,7 @@
      result
      {:on-chosen   chosen-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
                      (let [page (or (:block/original-name page)
                                     (:block/name page))
@@ -191,7 +191,7 @@
                            content (:block/content block)]
                        (when-not (string/blank? content)
                          [:.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
   {:will-unmount (fn [state]
@@ -339,10 +339,14 @@
 
 (rum/defc absolute-modal < rum/static
   [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-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))
                         (let [delta-height (- vw-height (+ (:top rect) top offset-top))]
                           (if (< delta-height max-height)

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

@@ -379,20 +379,19 @@
                                                         (first @graphs-txid)))))
                                            nil
 
-                                           (and synced-file-graph?
-                                                (second @graphs-txid)
+                                           (and (second @graphs-txid)
                                                 (fs-sync/graph-sync-off? (second @graphs-txid))
                                                 (async/<! (fs-sync/<check-remote-graph-exists (second @graphs-txid))))
                                            (fs-sync/<sync-start)
 
                                            ;; 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!
                                                 (map (fn [r]
                                                        (if (= (:url r) current-repo)
                                                          (dissoc r :GraphUUID :GraphName :remote?)
                                                          r))
-                                                     (state/get-repos)))
+                                                  (state/get-repos)))
                                                (create-remote-graph-fn))
 
                                            (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?)
                                (= (state/sub-default-home-page) (state/get-current-page)))
         sync-enabled? (file-sync-handler/enable-sync?)]
-    [:div.cp__header#head
+    [:div.cp__header.drag-region#head
      {:class           (util/classnames [{:electron-mac   electron-mac?
                                           :native-ios     (mobile-util/native-ios?)
                                           :native-android (mobile-util/native-android?)}])
       :on-double-click (fn [^js e]
                          (when-let [target (.-target e)]
                            (when (and (util/electron?)
-                                      (.. target -classList (contains "cp__header")))
+                                      (.. target -classList (contains "drag-region")))
                              (js/window.apis.toggleMaxOrMinActiveWindow))))
       :style           {:fontSize  50}}
-     [:div.l.flex
+     [:div.l.flex.drag-region
       (when-not (mobile-util/native-platform?)
         [left-menu
          (when current-repo ;; this is for the Search button
@@ -196,7 +196,7 @@
              {:title "Go back" :on-click #(js/window.history.back)}
              (ui/icon "chevron-left" {:size 26})])))]
 
-     [:div.r.flex
+     [:div.r.flex.drag-region
       (when (and current-repo
                  (not (config/demo-graph? current-repo))
                  (user-handler/alpha-or-beta-user?))

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

@@ -1,8 +1,6 @@
 .cp__header {
   @apply z-10;
 
-  -webkit-app-region: drag;
-
   padding-top: var(--ls-headbar-inner-top-padding);
   height: calc(var(--ls-headbar-height) + var(--ls-headbar-inner-top-padding));
   display: flex;
@@ -13,7 +11,6 @@
   top: 0;
   left: 0;
   right: 0;
-  user-select: none;
   line-height: 1;
   white-space: nowrap;
 
@@ -351,3 +348,8 @@ html.is-zoomed-native-ios {
     --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]
             [shadow.lazy :as lazy]
             [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
 #_:clj-kondo/ignore
@@ -11,17 +14,28 @@
 
 (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]
   (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?
       (@lazy-editor config id attr code theme options)
       (ui/loading "CodeMirror"))))

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

@@ -16,6 +16,7 @@
             [frontend.db.model :as model]
             [frontend.extensions.graph :as graph]
             [frontend.extensions.pdf.assets :as pdf-assets]
+            [frontend.extensions.pdf.utils :as pdf-utils]
             [frontend.format.block :as block]
             [frontend.handler.common :as common-handler]
             [frontend.handler.config :as config-handler]
@@ -282,7 +283,7 @@
           whiteboard-page? (model/whiteboard-page? page-name)
           untitled? (and whiteboard-page? (parse-uuid page-name)) ;; normal page cannot be untitled right?
           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))
           old-name (or title page-name)]
       [:h1.page-title.flex.cursor-pointer.gap-1.w-full
@@ -369,8 +370,8 @@
           journal? (db/journal-page? page-name)
           fmt-journal? (boolean (date/journal-title->int page-name))
           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
           page (if block?
                  (->> (:db/id (:block/page (db/entity repo [:block/uuid block-id])))
@@ -400,7 +401,7 @@
               {:key path-page-name
                :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.relative
           (when (and (not sidebar?) (not block?))
@@ -1050,6 +1051,7 @@
                                              (swap! *checks update idx not))})]
 
                 [:td.name [:a {:on-click (fn [e]
+                                          (.preventDefault e)
                                            (let [repo (state/get-current-repo)]
                                              (when (gobj/get e "shiftKey")
                                                (state/sidebar-add-block!

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

@@ -113,7 +113,8 @@
             {:title   (t :page/delete)
              :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)
              :options {:on-click (fn []
                                    (state/sidebar-add-block!

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

@@ -13,7 +13,7 @@
             [frontend.db.model :as model]
             [frontend.handler.search :as search-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.state :as state]
             [frontend.mixins :as mixins]
@@ -203,7 +203,7 @@
 (defn- search-item-render
   [search-q {:keys [type data alias]}]
   (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"}
      (case type
        :graph-add-filter
@@ -231,25 +231,33 @@
                            (highlight-exact-query data search-q))
 
        :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)
              repo (state/sub :git/current-repo)
              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}
           (search-result-item {:name "block"
                                :title (t :search-item/block)
                                :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)
                                     (str "Cache is outdated. Please click the 'Re-index' button in the graph's dropdown menu."))))])
 
        nil)]))
 
 (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 [alias (model/get-redirect-page-name page)]
                                       (cond->
@@ -264,6 +272,7 @@
         blocks (map (fn [block] {:type :block :data block}) blocks)
         search-mode (state/sub :search/mode)
         new-page (if (or
+                      (some? engine)
                       (and (seq pages)
                            (= (util/safe-page-name-sanity-lc search-q)
                               (util/safe-page-name-sanity-lc (:data (first pages)))))
@@ -401,14 +410,17 @@
       state
       :on-hide (fn []
                  (search-handler/clear-search!)))))
+  (rum/local nil ::active-engine-tab)
   [state]
   (let [search-result (state/sub :search/result)
         search-q (state/sub :search/q)
         search-mode (state/sub :search/mode)
+        engines (state/sub :search/engines)
+        *active-engine-tab (::active-engine-tab state)
         timeout 300
         in-page-search? (= search-mode :page)]
     [:div.cp__palette.cp__palette-main
-     [:div.ls-search.p-2
+     [:div.ls-search.p-2.md:p-0
       [:div.input-wrap
       [:input.cp__palette-input.w-full
        {:type          "text"
@@ -421,7 +433,12 @@
                          (default-placeholder search-mode))
         :auto-complete (if (util/chrome?) "chrome-off" "off") ; off not working here
         :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
                            (js/clearTimeout @search-timeout))
                          (let [value (util/evalue e)
@@ -443,9 +460,35 @@
                                             (search-handler/search (state/get-current-repo) value)))
                                         timeout))))))}]]
       [: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
   [route]

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

@@ -1,6 +1,9 @@
 #search {
   > .inner {
     width: 100%;
+    max-width: 100%;
+
+    border-radius: 4px;
   }
 
   .search-result {
@@ -8,52 +11,46 @@
   }
 }
 
-.search-ac {
-    background-color: var(--ls-primary-background-color);
-}
-
 #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 {
-    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 {
-    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?
           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"
           (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?]
   (toggle "enable_tooltip"
@@ -572,7 +572,7 @@
         enable-timetracking? (state/enable-timetracking?)
         enable-all-pages-public? (state/all-pages-public?)
         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-shortcut-tooltip? (state/sub :ui/shortcut-tooltip?)
         show-brackets? (state/show-brackets?)
@@ -586,7 +586,7 @@
      (show-brackets-row t show-brackets?)
      (when (util/electron?) (switch-spell-check-row t))
      (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?))
        (shortcut-tooltip-row t enable-shortcut-tooltip?))
      (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]
             [clojure.string :as string]
             [frontend.components.command-palette :as command-palette]
+            [frontend.components.find-in-page :as find-in-page]
             [frontend.components.header :as header]
             [frontend.components.journal :as journal]
             [frontend.components.onboarding :as onboarding]
@@ -12,17 +13,15 @@
             [frontend.components.svg :as svg]
             [frontend.components.theme :as theme]
             [frontend.components.widgets :as widgets]
-            [frontend.components.find-in-page :as find-in-page]
             [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
             [frontend.db :as db]
             [frontend.db-mixins :as db-mixins]
             [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.handler.common :as common-handler]
             [frontend.handler.editor :as editor-handler]
-            [frontend.handler.mobile.swipe :as swipe]
             [frontend.handler.page :as page-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.user :as user-handler]
@@ -89,7 +88,7 @@
               (route-handler/redirect-to-whiteboard! name)
               (route-handler/redirect-to-page! name {:click-from-recent? recent?})))))}
      [: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]
   (let [default-icon (ui/icon "page" {:extension? true})
@@ -226,7 +225,7 @@
 (defn close-sidebar-on-mobile!
   []
   (and (util/sm-breakpoint?)
-    (state/toggle-left-sidebar!)))
+       (state/toggle-left-sidebar!)))
 
 (defn create-dropdown
   []
@@ -256,115 +255,207 @@
                            :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?)
-        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
-     {: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-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
   []
@@ -392,14 +483,16 @@
                             (when-let [id (state/get-edit-input-id)]
                               (let [format (:block/format (state/get-edit-block))]
                                 (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)}
   [{: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?)
         onboarding-and-home? (and (or (nil? (state/get-current-repo)) (config/demo-graph?))
                                   (not config/publishing?)
                                   (= :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
      {:class (util/classnames [{:is-left-sidebar-open left-sidebar-open?}])}
 
@@ -572,7 +665,6 @@
                   :exit 300}}
        (render-custom-context-menu links position)))))
 
-
 (rum/defc new-block-mode < rum/reactive
   []
   (when (state/sub [:document/mode?])
@@ -617,9 +709,6 @@
                                    (:editor/editing? @state/state))))
                           (state/close-modal!)
                           (hide-context-menu-and-clear-selection e)))))))
-  {:did-mount (fn [state]
-                (swipe/setup-listeners!)
-                state)}
   [state route-match main-content]
   (let [{:keys [open-fn]} state
         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] {
   padding: 0 !important;
   position: relative;
+  overflow: auto;
 }
 
 .left-sidebar-inner {
@@ -90,7 +91,7 @@
 
   > .wrap {
     height: calc(100vh - var(--ls-headbar-inner-top-padding) - 50px);
-    margin-top: 40px;
+    margin-top: 30px;
     width: 100%;
 
     > .fake-bar {
@@ -330,36 +331,50 @@
 
 .cp__sidebar-left-layout {
   position: fixed;
-  width: 0;
-  top: var(--ls-headbar-inner-top-padding);
+  top: 0;
+
   left: 0;
   z-index: var(--ls-z-index-level-5);
-  transition: width .3s;
+  width: 10px;
 
   a {
+    @apply opacity-90 hover:opacity-100;
+
     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 {
-    background-color: rgba(0, 0, 0, .5);
+    background-color: rgba(0, 0, 0, .7);
     position: absolute;
     top: 0;
     left: 0;
     bottom: 0;
     right: 0;
-    z-index: -1;
+    z-index: 1;
     opacity: 0;
-    transition: opacity .1s;
+    transition: opacity .4s;
     touch-action: none;
   }
 
-  &.is-open {
+  &.is-touching {
+    width: 100%;
     transition: none;
+
+    .left-sidebar-inner {
+      transition: none !important;
+    }
+
+    > .shade-mask {
+      transition: none !important;
+      z-index: 1;
+    }
+  }
+
+  &.is-open {
     width: 100%;
 
     .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 {
     content: " ";
     height: 3rem;
@@ -387,6 +414,7 @@
   @screen sm {
     width: 0;
     z-index: var(--ls-z-index-level-1);
+    transition: width .3s;
 
     &:before {
       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"
      :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
   [: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")
           (.remove cls "dark"))
         (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])
 
     (rum/use-effect!

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

@@ -5,6 +5,7 @@
             [frontend.components.page :as page]
             [frontend.components.reference :as reference]
             [frontend.context.i18n :refer [t]]
+            [frontend.db-mixins :as db-mixins]
             [frontend.db.model :as model]
             [frontend.handler.common :as common-handler]
             [frontend.handler.route :as route-handler]
@@ -46,60 +47,88 @@
     (when generate-preview
       (generate-preview tldr))))
 
+;; TODO: use frontend.ui instead of making a new one
 (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?)
         [content-ref content-rect] (use-bounding-client-rect show?)
         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)
                    (+ (.-y anchor-rect) (.-height anchor-rect) 8))
         click-outside-ref (use-click-outside outside-click-hander)
         [d-open set-d-open] (rum/use-state false)
         _ (rum/use-effect! (fn [] (js/setTimeout #(set-d-open show?) 100))
                            [show?])]
-    [:div.dropdown-anchor {:ref anchor-ref}
+    [:div.inline-block.dropdown-anchor {:ref anchor-ref}
      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)
-         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)
-       (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
   [page-name]
@@ -144,7 +173,7 @@
     [:div.flex.w-full.opacity-50
      [:div (get-page-human-update-time page-name)]
      [:div.flex-1]
-     (page-refs-count page-name nil)]]
+     (references-count page-name nil {:hover? true})]]
    [:div.p-4.h-64.flex.justify-center
     (tldraw-preview page-name)]])
 
@@ -248,12 +277,13 @@
                         false)]
 
       [: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)]))
 
 (rum/defc whiteboard-route

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

@@ -16,7 +16,7 @@
             [frontend.util :as util :refer [react]]
             [frontend.util.drawer :as drawer]
             [logseq.db.default :as default-db]
-            [logseq.db.rules :refer [rules]]
+            [logseq.db.rules :as rules]
             [logseq.db.schema :as db-schema]
             [logseq.graph-parser.config :as gp-config]
             [logseq.graph-parser.text :as text]
@@ -300,16 +300,7 @@
             (alias ?page ?e)]
           (conn/get-db repo-url)
           (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
      (set)
      (set/union #{page-id}))))
@@ -1306,6 +1297,24 @@
                                (sort-by-left-recursive))]
          (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?
   "sanitized page-name only"
   [page-name]
@@ -1585,7 +1594,7 @@
        [?p :block/name ?namespace]
        (namespace ?p ?c)]
      (conn/get-db repo)
-     rules
+     (:namespace rules/rules)
      namespace)))
 
 (defn- tree [flat-col root]
@@ -1677,13 +1686,21 @@
    macro-name))
 
 (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
   [repo]

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

@@ -1472,7 +1472,7 @@
            :help/working-with-lists " (与列表相关)"
            :help/select-nfs-browser "请选择支持 nfs 的浏览器来使用 Logseq 本地文件夹功能, 如最新的 Chrome 浏览器."
            :text/image "图片"
-           :asset/confirm-delete "确定要删除{1}吗?"
+           :asset/confirm-delete "确定要删除{1}吗"
            :asset/physical-delete "同时删除本地文件(目前不可撤销)"
            :undo "撤销"
            :redo "重做"
@@ -1516,7 +1516,7 @@
            :settings-page/enable-shortcut-tooltip "启用快捷键提示"
            :settings-page/export-theme "导出主题"
            :tutorial/dummy-notes "练习笔记.md"
-           :tutorial/text "指南.md(英文)"
+           :tutorial/text "指南.md(英文)"
            :right-side-bar/help "帮助"
            :right-side-bar/switch-theme "主题模式"
            :right-side-bar/theme "{1}主题"
@@ -1543,7 +1543,7 @@
            :format/org-mode "Org Mode 格式"
            :reference/linked "已链接的引用"
            :reference/unlinked-ref "未链接的引用"
-           :page/edit-properties-placeholder "点击这里编辑当前页面的属性 (标签,别名等)"
+           :page/edit-properties-placeholder "点击这里编辑当前页面的属性(标签,别名等)"
            :page/presentation-mode "打开幻灯片"
            :page/delete-success "页面 {1} 删除成功!"
            :page/delete-confirmation "您确定要删除此页面和文件吗?"
@@ -1567,7 +1567,7 @@
            :page/hide-name "隐藏页面名"
            :block/name "页面名称"
            :page/last-modified "最后更改于"
-           :page/new-title "请输入新页面的名字:"
+           :page/new-title "请输入新页面的名字"
            :page/earlier "之前"
            :page/no-more-journals "没有更多了"
            :journal/multiple-files-with-different-formats "你似乎在同一个月有多个日记文件(格式不同),请在每个月只保留一份日记文件。"
@@ -1592,7 +1592,7 @@
            :file-rn/otherwise-breaking "否则标题会变为"
            :file-rn/no-action "好了!无需更多操作"
            :file-rn/confirm-proceed "更新格式!"
-           :file-rn/select-confirm-proceed "开发者: 写入格式"
+           :file-rn/select-confirm-proceed "开发者写入格式"
            :file-rn/unreachable-title "警告!在当前文件名格式下,除非手动设置 `title::` 属性,否则,页面名将变为{1}。"
            :file-rn/optional-rename "建议:"
            :file-rn/format-deprecated "你现在正使用着过时的格式。非常建议更新到最新的格式。在进行该操作之前,请先备份好你的数据,并关闭所有的 Logseq 客户端。"
@@ -1606,12 +1606,12 @@
            :page/created-at "创建日期"
            :page/updated-at "更新日期"
            :page/backlinks "双向链接"
-           :file/format-not-supported "格式 .{1} 目前不支持."
+           :file/format-not-supported "格式 .{1} 目前不支持"
            :editor/block-search "搜索块"
            :editor/image-uploading "上传中"
            :draw/invalid-file "无法加载此无效的 excalidraw 文件"
-           :draw/specify-title "请先指定标题!"
-           :draw/rename-success "文件重命名成功!"
+           :draw/specify-title "请先指定标题"
+           :draw/rename-success "文件重命名成功"
            :draw/rename-failure "文件重命名失败,原因是:"
            :draw/title-placeholder "未命名"
            :draw/save "保存"
@@ -1620,7 +1620,7 @@
            :draw/list-files "所有文件"
            :draw/delete "删除"
            :draw/more-options "更多选项"
-           :draw/back-to-logseq "返回 logseq"
+           :draw/back-to-logseq "返回 Logseq"
            :content/copy "复制"
            :content/cut "剪切"
            :content/make-todos "格式化为 {1}"
@@ -1629,8 +1629,8 @@
            :content/open-in-sidebar "在侧边栏打开"
            :content/copy-as-json "复制为 JSON"
            :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-theme "自定义主题"
            :settings-page/git-desc "用于页面的版本管理,可以单击顶部右侧菜单来查看页面的历史记录"
@@ -1701,7 +1701,7 @@
            :export-roam-json "以 Roam JSON 格式导出"
            :export-markdown "以 Markdown 格式导出"
            :export-opml "以 OPML 格式导出"
-           :convert-markdown "转换 Markdown 格式(Unordered list 或 Heading)"
+           :convert-markdown "转换 Markdown 格式(Unordered list 或 Heading)"
            :unlink "解除绑定"
            :search/publishing "搜索"
            :search "搜索或者创建新页面"
@@ -1732,13 +1732,13 @@
            :settings-of-plugins "插件设置"
            :plugins "插件"
            :themes "主题"
-           :developer-mode-alert "如果希望插件功能立刻生效, 请重启应用。是否现在重启?"
-           :relaunch-confirm-to-work "如果希望立刻生效, 请重启应用。是否现在重启?"
+           :developer-mode-alert "如果希望插件功能立刻生效, 请重启应用。是否现在重启"
+           :relaunch-confirm-to-work "如果希望立刻生效, 请重启应用。是否现在重启"
            :import "导入"
            :importing "导入中"
            :join-community "加入社区"
-           :sponsor-us "赞助我们!"
-           :discord-title "我们的 Discord 社群!"
+           :sponsor-us "赞助我们"
+           :discord-title "我们的 Discord 社群"
            :help-shortcut-title "点此查看快捷方式和更多有用帮助"
            :loading "加载中"
            :cloning "Clone 中"
@@ -1769,8 +1769,8 @@
            :all-whiteboards "所有白板"
            :go-to-whiteboard "前往白板"
            :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 发布的页面(假如有的话)也会被删除。"
 
            :plugin/installed "已安装"
@@ -1782,7 +1782,7 @@
            :plugin/check-update "检查更新"
            :plugin/check-all-updates "一键检查更新"
            :plugin/refresh-lists "刷新插件列表"
-           :plugin/delete-alert "确定删除插件 [{1}]?"
+           :plugin/delete-alert "确定删除插件 [{1}]"
            :plugin/enabled "已开启"
            :plugin/disabled "未开启"
            :plugin/update-available "待更新"
@@ -1802,7 +1802,7 @@
            :plugin/marketplace-tips "如果首次从市场安装或更新的插件,遇到非预期工作情况,可以尝试重启应用。"
            :plugin/unpacked-tips "用于开发目的或者从本地磁盘载入可信的社区插件。"
            :plugin/up-to-date "已经是最新了"
-           :plugin/custom-js-alert "发现 custom.js 自定义脚本,是否允许执行? (如果您对该文件的内容不了解 或 来源不可靠,建议不要允许执行)"
+           :plugin/custom-js-alert "发现 custom.js 自定义脚本,是否允许执行?(如果您对该文件的内容不了解 或 来源不可靠,建议不要允许执行)"
 
            :pdf/copy-ref "复制引用"
            :pdf/copy-text "复制文本"
@@ -2156,7 +2156,7 @@
         :tutorial/dummy-notes #?(:cljs (rc/inline "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/add-graph "Add a graph"
+        :on-boarding/add-graph "Añadir gráfico"
         :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-2 "Después que abra un directorio se crearán tres carpetas en ese directorio:"
@@ -2203,6 +2203,7 @@
         :highlight "Resaltado"
         :strikethrough "Tachado"
         :code "Código"
+        :untitled "Sin título"
         :right-side-bar/help "Ayuda"
         :right-side-bar/switch-theme "Temas"
         :right-side-bar/theme "Tema {1}"
@@ -2259,6 +2260,33 @@
         :file/last-modified-at "Fecha de modificación"
         :file/no-data "No hay datos"
         :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/updated-at "Actualizada el"
         :page/backlinks "Back Links"
@@ -2467,8 +2495,7 @@
         :select.graph/empty-placeholder-description "No encontramos un grafo. Querie añadir otro?"
         :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/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")
                              :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/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-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-2 "1. Tıklayın "
         :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/preferred-outdenting "Mantıksal girinti"
         :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-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-timetracking "Zaman takibi"
         :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 codemirror-ref-name "codemirror-instance")
 
+;; export CodeMirror to global scope
+(set! js/window -CodeMirror cm)
+
 (defn- extra-codemirror-options []
   (get (state/get-config)
        :editor/extra-codemirror-options {}))
@@ -187,6 +190,7 @@
 
 (defn- save-file-or-block-when-blur-or-esc!
   [editor textarea config state]
+  (state/set-state! :editor/skip-saving-current-block? true)
   (state/set-block-component-editing-mode! false)
   (save-file-or-block! editor textarea config state))
 
@@ -293,8 +297,7 @@
                 state)
    :did-update (fn [state]
                  (reset! (:code-options state) (last (:rum/args state)))
-                 state)
-   }
+                 state)}
   [state _config id attr code _theme _options]
   [:div.extensions__code
    (when-let [mode (:data-lang attr)]

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

@@ -170,9 +170,7 @@
 
                                    (string? (first children))
                                    (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
                                    :else

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

@@ -8,6 +8,10 @@
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.page :as page-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.state :as state]
             [frontend.util :as util]
@@ -59,11 +63,11 @@
         data))))
 
 (defn persist-hls-data$
-  [{:keys [hls-file]} highlights]
+  [{:keys [hls-file]} highlights extra]
   (when hls-file
     (let [repo-cur (state/get-current-repo)
           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}))))
 
 (defn resolve-hls-data-by-key$
@@ -226,7 +230,7 @@
             (do
               (state/set-state! :pdf/ref-highlight matched)
               ;; 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)))))))
 
 (defn goto-block-ref!
@@ -242,32 +246,57 @@
    (when-let [name (:key current)]
      (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
   [block]
   (when-let [asset-path' (and block (pdf-utils/get-area-block-asset-url
                                      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.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}]])))
 
-(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
   [page-name]
   (cond
     (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)))

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

@@ -12,7 +12,6 @@
             [frontend.commands :as commands]
             [frontend.rum :refer [use-atom]]
             [frontend.state :as state]
-            [frontend.storage :as storage]
             [frontend.util :as util]
             [medley.core :as medley]
             [promesa.core :as p]
@@ -44,14 +43,12 @@
   (rum/use-effect!
    (fn []
      (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])
   nil)
 
@@ -665,7 +662,7 @@
        })]))
 
 (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)
         [state, set-state!] (rum/use-state {:viewer nil :bus nil :link nil :el nil})
@@ -675,33 +672,50 @@
 
     ;; instant pdfjs viewer
     (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
@@ -710,20 +724,13 @@
        (when-let [^js viewer (:viewer state)]
          (let [fn-textlayer-ready
                (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)
-             (.on "pagesinit" fn-page-ready)
              (.on "textlayerrendered" fn-textlayer-ready))
 
            #(do
               (doto (.-eventBus viewer)
-                (.off "pagesinit" fn-page-ready)
                 (.off "textlayerrendered" fn-textlayer-ready))))))
 
      [(:viewer state)
@@ -750,23 +757,27 @@
 (rum/defc ^:large-vars/data-var pdf-loader
   [{:keys [url hls-file] :as pdf-current}]
   (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
     (rum/use-effect!
      (fn []
        (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
         (fn [e]
           (js/console.error "[load hls error]" e)
-          (set-hls-state! {:initial-hls []})))
+          (set-hls-state! {:initial-hls [] :loaded true})))
 
        ;; cancel
        #())
@@ -775,15 +786,16 @@
     ;; cache highlights
     (rum/use-effect!
      (fn []
-       (when-let [hls (:latest-hls hls-state)]
+       (when (= :completed (:status loader-state))
          (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
           (fn [e]
             (js/console.error "[write hls error]" e)))))
 
-     [(:latest-hls hls-state)])
+     [(:latest-hls hls-state) (:extra hls-state)])
 
     ;; load document
     (rum/use-effect!
@@ -795,20 +807,18 @@
                        ;;:cMapUrl       "https://cdn.jsdelivr.net/npm/[email protected]/cmaps/"
                        :cMapPacked    true}]
 
-         (set-state! {:status :loading})
+         (set-loader-state! {:status :loading})
 
          (-> (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])
 
     (rum/use-effect!
      (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)
            "MissingPDFException"
            (do
@@ -835,24 +845,24 @@
               :error
               false)
              (state/set-state! :pdf/current nil)))))
-     [(:error state)])
+     [(:error loader-state)])
 
     (rum/bind-context
      [*highlights-ctx* hls-state]
      [:div.extensions__pdf-loader {:ref *doc-ref}
-      (let [status-doc  (:status state)
+      (let [status-doc  (:status loader-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
            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
   [{:keys [identity] :as pdf-current}]

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

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

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

@@ -433,6 +433,14 @@
          #(js-delete (. el -dataset) "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
     (rum/use-effect!
      (fn []
@@ -511,14 +519,16 @@
         [:span.nu.flex.items-center.opacity-70
          [:input {:ref            *page-ref
                   :type           "number"
+                  :class          (util/classnames [{:is-long (> (util/safe-parse-int current-page-num) 999)}])
                   :default-value  current-page-num
                   :on-mouse-enter #(.select ^js (.-target %))
                   :on-key-up      (fn [^js e]
                                     (let [^js input (.-target e)
                                           value     (util/safe-parse-int (.-value input))]
+                                      (set-current-page-num! value)
                                       (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]]
 
         [: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 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?
 (defn next-page
   []

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

@@ -1,16 +1,31 @@
 (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]
             [frontend.util :as util]
             [goog.dom]
             [goog.object]
             [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)))
 
+(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
   "Second arg is a map of options for sci/eval-string"
   ([s]
@@ -23,7 +38,9 @@
                                                 'parseFloat js/parseFloat
                                                 'isNaN js/isNaN
                                                 '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))
      (catch :default e
        (println "Query: sci eval failed:")
@@ -39,10 +56,7 @@
   [:div
    [:code "Results:"]
    [: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)))
         result
         [:pre.code (str result)]))]])

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

@@ -14,11 +14,12 @@
             [goog.object :as gobj]
             [promesa.core :as p]
             [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 generate-preview (gobj/get TldrawLogseq "generateJSXFromApp"))
+(def generate-preview (gobj/get TldrawLogseq "generateJSXFromModel"))
 
 (rum/defc page-cp
   [props]
@@ -30,7 +31,16 @@
 
 (rum/defc breadcrumb
   [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
   [props]
@@ -54,12 +64,19 @@
          (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)))))))
 
+(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
                        :Block block-cp
                        :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
    :queryBlockByUUID #(clj->js (model/query-block-by-uuid (parse-uuid %)))
    :isWhiteboardPage model/whiteboard-page?
@@ -68,29 +85,34 @@
    :addNewWhiteboard (fn [page-name]
                        (whiteboard-handler/create-new-whiteboard-page! page-name))
    :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]
                       (state/sidebar-add-block! (state/get-current-repo)
                                                 (:db/id (model/get-page uuid))
                                                 (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
   [page-name block-id]
   (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]
                    (when-let [^js api (gobj/get tln "api")]
                      (p/then (when populate-onboarding?
                                (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
       [:div.draw.tldraw.whiteboard.relative.w-full.h-full
@@ -102,7 +124,7 @@
         :on-wheel util/stop-propagation}
 
        (when
-        (and populate-onboarding? (not loaded?))
+        (and populate-onboarding? (not loaded-app))
          [:div.absolute.inset-0.flex.items-center.justify-center
           {:style {:z-index 200}}
           (ui/loading "Loading onboarding whiteboard ...")])

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

@@ -76,7 +76,8 @@
 (defn write-file!
   [repo dir path content opts]
   (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
                            :error-handler
@@ -111,18 +112,19 @@
 
 (defn rename!
   [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
-    (= 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!
   [repo old-path new-path]

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

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

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

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

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

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

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

@@ -48,6 +48,6 @@
   (let [enable-tooltip? (state/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
       :target        the new file name
       :old-title     the old title
-      :chagned-title the new title} | nil"
+      :changed-title the new title} | nil"
   [page path old-format new-format]
   (let [prop-title (get-in page [:block/properties :title])
         file-body  (gp-util/path->file-body path)

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

@@ -420,7 +420,8 @@
 
 (declare save-current-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)
                                       (:custom-query? config))
                                   (not (:ref-query-child? config)))
@@ -439,7 +440,7 @@
                    (not has-children?))]
     (outliner-tx/transact!
      {: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?
                                                               :keep-uuid? keep-uuid?
                                                               :replace-empty-target? replace-empty-target?}))))
@@ -1255,7 +1256,8 @@
    (save-current-block! {}))
   ([{:keys [force? skip-properties? current-block] :as opts}]
    ;; 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-not (state/get-editor-action)
          (try
@@ -1285,7 +1287,8 @@
                             (string/trim value)))
                  (save-block-aux! db-block value opts))))
            (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!
   [format content]
@@ -1570,6 +1573,15 @@
           pos (cursor/pos input)]
       (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?
   [input before end]
   (when input
@@ -1759,7 +1771,7 @@
 
   (handle-command-input-close id))
 
-(defn close-autocomplete-if-outside
+(defn- close-autocomplete-if-outside
   [input]
   (when (and input
              (contains? #{:page-search :page-search-hashtag :block-search} (state/get-editor-action))
@@ -1813,7 +1825,9 @@
     ;; TODO: is it cross-browser compatible?
     ;; (not= (gobj/get native-e "inputType") "insertFromPaste")
     (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
         (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)})
         (commands/reinit-matched-commands!)
@@ -1844,6 +1858,15 @@
       (and (= last-input-char commands/colon) (= :property-search (state/get-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
       nil)))
 
@@ -2409,7 +2432,7 @@
       (scroll-to-block sibling-block)
       (state/exit-editing-and-set-selected-blocks! [sibling-block]))))
 
-(defn- move-cross-boundrary-up-down
+(defn- move-cross-boundary-up-down
   [direction]
   (let [input (state/get-input)
         line-pos (util/get-first-or-last-line-pos input)
@@ -2448,14 +2471,14 @@
 
       (or (and up? (cursor/textarea-cursor-first-row? input))
           (and down? (cursor/textarea-cursor-last-row? input)))
-      (move-cross-boundrary-up-down direction)
+      (move-cross-boundary-up-down direction)
 
       :else
       (if up?
         (cursor/move-cursor-up input)
         (cursor/move-cursor-down input)))))
 
-(defn- move-to-block-when-cross-boundrary
+(defn- move-to-block-when-cross-boundary
   [direction]
   (let [up? (= :left direction)
         pos (if up? :max 0)
@@ -2491,7 +2514,7 @@
 
         (or (and left? (cursor/start? input))
             (and right? (cursor/end? input)))
-        (move-to-block-when-cross-boundrary direction)
+        (move-to-block-when-cross-boundary direction)
 
         :else
         (if left?
@@ -2659,6 +2682,7 @@
     nil))
 
 (defn ^:large-vars/cleanup-todo keydown-not-matched-handler
+  "NOTE: Keydown cannot be used on Android platform"
   [format]
   (fn [e _key-code]
     (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 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)
                   prev (util/nth-safe value (dec pos))]
             (util/stop e)
@@ -2735,14 +2763,6 @@
               (cursor/move-cursor-forward input)
               (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 "$"]
           (and (= key sym)
                (>= (count value) 1)
@@ -3247,7 +3267,7 @@
         repo (state/get-current-repo)
         value (boolean value)]
     (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-op :collapse-expand-blocks}
         (doseq [block-id block-ids]

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

@@ -565,12 +565,12 @@
          :warning
          false)))))
 
-(defmethod handle :file-watcher/changed [[_ ^js event]]
+(defmethod handle :mobile-file-watcher/changed [[_ ^js event]]
   (let [type (.-event event)
         payload (-> event
                     (js->clj :keywordize-keys true)
                     (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)
     (when (file-sync-handler/enable-sync?)
      (sync/file-watch-handler type payload))))

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

@@ -93,7 +93,8 @@
                            re-render-root? false
                            from-disk? 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?
                       #(p/resolved nil)
                       #(let [path-dir (if (and
@@ -164,7 +165,8 @@
   [repo files {:keys [finish-handler]} file->content]
   (let [write-file-f (fn [[path content]]
                        (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
                                          (util/electron?)
                                          (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.core :as t]
             [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))
 
@@ -142,18 +143,25 @@
                                      (#(js->clj % :keywordize-keys true))
                                      ((juxt :dir :name))
                                      (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 (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]
   []

+ 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)))
 
 (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]
    (when page-name
      (let [page-name (util/page-name-sanity-lc page-name)]
@@ -99,6 +100,7 @@
 (defn- create-title-property?
   [journal? page-name]
   (and (not journal?)
+       (not= (state/get-filename-format) :triple-lowbar)
        (fs-util/create-title-property? page-name)))
 
 (defn- build-page-tx [format properties page journal? whiteboard?]
@@ -475,7 +477,7 @@
 
       ;; Redirect to the newly renamed page
       (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
                                   :path-params {:name new-page-name}}))
 

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

@@ -68,8 +68,9 @@
 
 (defn- get-whiteboard-tldr-from-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
   [text]
@@ -203,5 +204,5 @@
                                           (util/stop e))))]
            (cond
              (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))))))))

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

@@ -163,7 +163,7 @@
 
 (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)))
 
 (defn setup-install-listener!
@@ -329,6 +329,16 @@
     (swap! state/state medley/dissoc-in [:plugin/installed-resources pid])
     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
   ([pid] (unregister-plugin-themes pid true))
   ([pid effect]
@@ -564,7 +574,7 @@
     (when-not (= text "END")
       [:div.flex.align-items.justify-center.h-screen.w-full.preboot-loading
        [: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]]])))
 
 (defn ^:large-vars/cleanup-todo init-plugins!
@@ -586,7 +596,8 @@
                                 (invoke-exported-api "unregister_plugin_simple_command" pid)
                                 (invoke-exported-api "uninstall_plugin_hook" pid)
                                 (unregister-plugin-ui-items pid)
-                                (unregister-plugin-resources pid))
+                                (unregister-plugin-resources pid)
+                                (unregister-plugin-search-services pid))
 
               _               (doto js/LSPluginCore
                                 (.on "registered"
@@ -632,8 +643,8 @@
                                                           (when mode
                                                             (state/set-custom-theme! mode theme)
                                                             (state/set-theme-mode! mode))
-                                                          (hook-plugin-app :theme-changed theme)
-                                                          (state/set-state! :plugin/selected-theme url))))
+                                                          (state/set-state! :plugin/selected-theme url)
+                                                          (hook-plugin-app :theme-changed theme))))
 
                                 (.on "reset-custom-theme" (fn [^js 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)))
 
 (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"
   []
   (create-plugin-config-file-if-not-exists))

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

@@ -244,7 +244,7 @@
                                               :file/content)]
                      (repo-config-handler/read-repo-config repo-url content))
                    (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)
         relate-path-fn (fn [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)))
 
 (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
   loosely done by repo-handler"
   [{:keys [repo]}]

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

@@ -3,12 +3,12 @@
             [frontend.config :as config]
             [frontend.date :as date]
             [frontend.db :as db]
-            [frontend.handler.ui :as ui-handler]
             [frontend.handler.recent :as recent-handler]
             [frontend.handler.search :as search-handler]
+            [frontend.handler.ui :as ui-handler]
             [frontend.state :as state]
-            [logseq.graph-parser.text :as text]
             [frontend.util :as util]
+            [logseq.graph-parser.text :as text]
             [reitit.frontend.easy :as rfe]))
 
 (defn redirect!
@@ -65,9 +65,11 @@
    (redirect-to-whiteboard! name nil))
   ([name {:keys [block-id]}]
    (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
   [name path-params]

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

@@ -111,7 +111,8 @@
   ([clear-search-mode?]
    (let [m {:search/result nil
             :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))
      (state/set-search-mode! :global))))
 

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

@@ -66,10 +66,14 @@
                                       (block-ids (:block/uuid (:block/parent block)))
                                       (not (gp-whiteboard/shape-block? block)))))
                                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)
         page-and-blocks (->> (cons page-block blocks)
                              (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]
   (when (model/page-exists? page-name)
@@ -78,7 +82,7 @@
           blocks (model/get-page-blocks-no-cache page-name)]
       [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))
         shapes (->> blocks
                     (filter gp-whiteboard/shape-block?)
@@ -89,7 +93,7 @@
         tldr-page (dissoc tldr-page :assets)]
     (clj->js {:currentPageId id
               :assets (or assets #js[])
-              :selectedIds (if (not-empty shape-id) #js[shape-id] #js[])
+              :selectedIds #js[]
               :pages [(merge tldr-page
                              {:id id
                               :name "page"
@@ -169,16 +173,14 @@
 
 (defn page-name->tldr!
   ([page-name]
-   (page-name->tldr! page-name nil))
-  ([page-name shape-id]
    (if 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! nil))))
 
 (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]
   (let [blocks (model/get-page-blocks-no-cache page-name)]
     (remove gp-whiteboard/shape-block? blocks)))
@@ -261,4 +263,4 @@
           (clone-whiteboard-from-edn edn api)
           (state/set-onboarding-whiteboard! true))
         (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
 
 
-(def store (atom nil))
+(defonce store (atom nil))
 
 (defn clear-idb!
   []

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

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

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

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

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

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

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

@@ -1,9 +1,10 @@
 (ns frontend.mobile.util
-  (:require ["@capacitor/core" :refer [Capacitor registerPlugin]]
+  (:require ["@capacitor/core" :refer [Capacitor registerPlugin ^js Plugins]]
             ["@capacitor/splash-screen" :refer [SplashScreen]]
             ["@logseq/capacitor-file-sync" :refer [FileSync]]
             [clojure.string :as string]
-            [promesa.core :as p]))
+            [promesa.core :as p]
+            [goog.object :as gobj]))
 
 (defn platform []
   (.getPlatform Capacitor))
@@ -93,3 +94,9 @@
   [path]
   (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.util.property :as property]
             [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
   [content spaces-tabs]
@@ -111,7 +112,7 @@
       (let [format (name (get page :block/format
                               (state/get-preferred-format)))
             title (string/capitalize (:block/name page))
-            whiteboard-page? (= "whiteboard" (:block/type page))
+            whiteboard-page? (model/whiteboard-page? page)
             format (if whiteboard-page? "edn" format)
             journal-page? (date/valid-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
 ;; react-query/refresh! easier.
 
+;; TODO: also need to consider whiteboard transactions
+
 ;; Steps:
 ;; 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.

Деякі файли не було показано, через те що забагато файлів було змінено