浏览代码

Merge branch 'master' into feat/db

Tienson Qin 2 年之前
父节点
当前提交
fd6b587235
共有 100 个文件被更改,包括 1976 次插入896 次删除
  1. 3 0
      .clj-kondo/config.edn
  2. 4 0
      .github/ISSUE_TEMPLATE/bug_report.yaml
  3. 13 2
      .github/workflows/build-desktop-release.yml
  4. 13 2
      .github/workflows/build.yml
  5. 12 1
      .github/workflows/e2e.yml
  6. 1 0
      CONTRIBUTING.md
  7. 2 2
      android/app/build.gradle
  8. 4 7
      bb.edn
  9. 2 2
      deps.edn
  10. 3 3
      deps/common/bb.edn
  11. 1 1
      deps/common/deps.edn
  12. 2 2
      deps/db/bb.edn
  13. 1 1
      deps/db/deps.edn
  14. 2 2
      deps/graph-parser/bb.edn
  15. 1 1
      deps/graph-parser/deps.edn
  16. 14 1
      deps/graph-parser/src/logseq/graph_parser/mldoc.cljc
  17. 18 0
      deps/graph-parser/test/logseq/graph_parser/mldoc_test.cljs
  18. 2 2
      deps/publishing/bb.edn
  19. 1 1
      deps/publishing/deps.edn
  20. 18 1
      deps/publishing/src/logseq/publishing/db.cljs
  21. 3 1
      deps/publishing/test/logseq/publishing/db_test.cljs
  22. 14 18
      docs/contributing-to-translations.md
  23. 5 2
      docs/dev-practices.md
  24. 3 1
      e2e-tests/code-editing.spec.ts
  25. 2 1
      e2e-tests/editor.spec.ts
  26. 2 1
      e2e-tests/fixtures.ts
  27. 1 0
      e2e-tests/history.spec.ts
  28. 2 1
      e2e-tests/hotkey.spec.ts
  29. 105 0
      e2e-tests/logseq-api.spec.ts
  30. 13 1
      e2e-tests/page-rename.spec.ts
  31. 1 11
      e2e-tests/plugins.spec.ts
  32. 0 1
      e2e-tests/util/page.ts
  33. 32 29
      e2e-tests/util/search-modal.ts
  34. 1 0
      e2e-tests/utils.ts
  35. 97 21
      e2e-tests/whiteboards.spec.ts
  36. 34 0
      e2e-tests/window.spec.ts
  37. 4 4
      ios/App/App.xcodeproj/project.pbxproj
  38. 13 5
      ios/App/App/FsWatcher.swift
  39. 1 1
      libs/src/LSPlugin.core.ts
  40. 24 20
      libs/src/LSPlugin.ts
  41. 2 2
      package.json
  42. 18 2
      playwright.config.ts
  43. 0 0
      resources/css/katex.min.css
  44. 0 0
      resources/js/katex.min.js
  45. 2 2
      resources/package.json
  46. 53 20
      scripts/src/logseq/tasks/lang.clj
  47. 16 1
      src/electron/electron/handler.cljs
  48. 37 14
      src/electron/electron/plugin.cljs
  49. 5 2
      src/electron/electron/window.cljs
  50. 3 10
      src/main/electron/listener.cljs
  51. 14 13
      src/main/frontend/commands.cljs
  52. 6 6
      src/main/frontend/components/assets.cljs
  53. 80 102
      src/main/frontend/components/block.cljs
  54. 1 1
      src/main/frontend/components/block.css
  55. 71 0
      src/main/frontend/components/block/macros.cljs
  56. 24 23
      src/main/frontend/components/bug_report.cljs
  57. 21 10
      src/main/frontend/components/container.cljs
  58. 29 3
      src/main/frontend/components/container.css
  59. 1 1
      src/main/frontend/components/conversion.cljs
  60. 6 4
      src/main/frontend/components/editor.cljs
  61. 1 1
      src/main/frontend/components/file.cljs
  62. 4 4
      src/main/frontend/components/file_sync.cljs
  63. 14 10
      src/main/frontend/components/header.cljs
  64. 5 3
      src/main/frontend/components/header.css
  65. 5 5
      src/main/frontend/components/onboarding.cljs
  66. 24 24
      src/main/frontend/components/onboarding/quick_tour.cljs
  67. 20 20
      src/main/frontend/components/onboarding/setups.cljs
  68. 18 20
      src/main/frontend/components/page.cljs
  69. 100 27
      src/main/frontend/components/plugins.cljs
  70. 65 16
      src/main/frontend/components/plugins.css
  71. 10 13
      src/main/frontend/components/query.cljs
  72. 29 29
      src/main/frontend/components/query/result.cljs
  73. 2 2
      src/main/frontend/components/repo.cljs
  74. 23 17
      src/main/frontend/components/right_sidebar.cljs
  75. 7 7
      src/main/frontend/components/search.cljs
  76. 269 50
      src/main/frontend/components/settings.cljs
  77. 57 40
      src/main/frontend/components/settings.css
  78. 90 44
      src/main/frontend/components/shortcut.cljs
  79. 28 0
      src/main/frontend/components/shortcut.css
  80. 26 0
      src/main/frontend/components/svg.cljs
  81. 0 2
      src/main/frontend/components/theme.css
  82. 2 2
      src/main/frontend/components/whiteboard.cljs
  83. 52 0
      src/main/frontend/components/window_controls.cljs
  84. 14 0
      src/main/frontend/components/window_controls.css
  85. 6 2
      src/main/frontend/config.cljs
  86. 17 5
      src/main/frontend/date.cljs
  87. 1 0
      src/main/frontend/db/model.cljs
  88. 2 2
      src/main/frontend/db/react.cljs
  89. 2 4
      src/main/frontend/extensions/excalidraw.cljs
  90. 2 3
      src/main/frontend/extensions/latex.cljs
  91. 107 95
      src/main/frontend/extensions/pdf/core.cljs
  92. 1 0
      src/main/frontend/extensions/pdf/toolbar.cljs
  93. 27 26
      src/main/frontend/extensions/srs.cljs
  94. 4 2
      src/main/frontend/extensions/tldraw.cljs
  95. 4 1
      src/main/frontend/handler/code.cljs
  96. 1 1
      src/main/frontend/handler/common/config_edn.cljs
  97. 49 36
      src/main/frontend/handler/editor.cljs
  98. 2 2
      src/main/frontend/handler/editor/lifecycle.cljs
  99. 22 15
      src/main/frontend/handler/events.cljs
  100. 1 1
      src/main/frontend/handler/global_config.cljs

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

@@ -15,6 +15,8 @@
   :aliased-namespace-symbol {:level :warning}
   ;; Disable until it doesn't trigger false positives on rum/defcontext
   :earmuffed-var-not-dynamic {:level :off}
+  ;; Disable until we decide to use conj! as recommended in docs
+  :unused-value {:level :off}
   :unresolved-symbol {:exclude [goog.DEBUG
                                 goog.string.unescapeEntities
                                 ;; TODO:lint: Fix when fixing all type hints
@@ -39,6 +41,7 @@
              electron.utils utils
              "/electron/utils" js-utils
              frontend.commands commands
+             frontend.components.block.macros block-macros
              frontend.components.query query
              frontend.components.query.result query-result
              frontend.config config

+ 4 - 0
.github/ISSUE_TEMPLATE/bug_report.yaml

@@ -7,6 +7,10 @@ body:
         Thank you very much for opening a bug report with Logseq.
 
         If you have a feature idea or need help, please go to [our Forum](https://discuss.logseq.com/) or [our Discord](https://discord.com/invite/KpN4eHY).
+
+        Please make sure to provide a descriptive, deterministic, and reproducible report. It saves time for both the developers and users who are looking for solutions. Providing as much information as possible, including screenshots and logs, is highly appreciated. This will help us to better understand the issue and respond more effectively.
+
+        Please DO NOT use this template to ask questions. There are other appropriate channels to ask questions. This template is strictly for reporting bugs.
   - type: checkboxes
     id: confirm-search
     attributes:

+ 13 - 2
.github/workflows/build-desktop-release.yml

@@ -122,7 +122,7 @@ jobs:
         run: |
           sed -i 's/defonce version ".*"/defonce version "${{ steps.ref.outputs.version }}"/g' src/main/frontend/version.cljs
 
-      - name: Set Build Environment Variables (only when workflow_dispath)
+      - name: Set Build Environment Variables (only when workflow_dispatch)
         if: ${{ github.event_name == 'workflow_dispatch' }}
         # if scheduled, use default settings
         run: |
@@ -204,8 +204,19 @@ jobs:
         env:
           PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: true
 
+      - name: Install Fluxbox
+        run: sudo apt-get update && sudo apt-get install -y fluxbox
+
+      # Emulate a virtual framebuffer on machines with no display hardware
+      - name: Run XVFB
+        run: Xvfb :1 -screen 0 1024x768x24 >/dev/null 2>&1 &
+
+      # Start a lightweight window manager to simulate window actions (maximize,restore etc)
+      - name: Start Fluxbox
+        run:  DISPLAY=:1.0 fluxbox >/dev/null 2>&1 &
+
       - name: Run Playwright test
-        run: xvfb-run -- npx playwright test --reporter github --shard=${{ matrix.shard }}/3
+        run: DISPLAY=:1.0 npx playwright test --reporter github --shard=${{ matrix.shard }}/3
         env:
           LOGSEQ_CI: true
           DEBUG: "pw:api"

+ 13 - 2
.github/workflows/build.yml

@@ -182,15 +182,26 @@ jobs:
       - name: Ensure static yarn.lock is up to date
         run: git diff --exit-code static/yarn.lock
 
+      - name: Install Fluxbox
+        run: sudo apt-get update && sudo apt-get install -y fluxbox
+
+      # Emulate a virtual framebuffer on machines with no display hardware
+      - name: Run XVFB
+        run: Xvfb :1 -screen 0 1024x768x24 >/dev/null 2>&1 &
+
+      # Start a lightweight window manager to simulate window actions (maximize,restore etc)
+      - name: Start Fluxbox
+        run:  DISPLAY=:1.0 fluxbox >/dev/null 2>&1 &
+
       - name: Run Playwright test - 1/2
-        run: xvfb-run -- npx playwright test --reporter github --shard=1/2
+        run: DISPLAY=:1.0 npx playwright test --reporter github --shard=1/2
         env:
           LOGSEQ_CI: true
           DEBUG: "pw:api"
           RELEASE: true # skip dev only test
 
       - name: Run Playwright test - 2/2
-        run: xvfb-run -- npx playwright test --reporter github --shard=2/2
+        run: DISPLAY=:1.0 npx playwright test --reporter github --shard=2/2
         env:
           LOGSEQ_CI: true
           DEBUG: "pw:api"

+ 12 - 1
.github/workflows/e2e.yml

@@ -137,8 +137,19 @@ jobs:
       - name: Ensure static yarn.lock is up to date
         run: git diff --exit-code static/yarn.lock
 
+      - name: Install Fluxbox
+        run: sudo apt-get update && sudo apt-get install -y fluxbox
+
+      # Emulate a virtual framebuffer on machines with no display hardware
+      - name: Run XVFB
+        run: Xvfb :1 -screen 0 1024x768x24 >/dev/null 2>&1 &
+
+      # Start a lightweight window manager to simulate window actions (maximize,restore etc)
+      - name: Start Fluxbox
+        run:  DISPLAY=:1.0 fluxbox >/dev/null 2>&1 &
+
       - name: Run Playwright test
-        run: xvfb-run -- npx playwright test --reporter github --shard=${{ matrix.shard }}/3
+        run: DISPLAY=:1.0 npx playwright test --reporter github --shard=${{ matrix.shard }}/3
         env:
           LOGSEQ_CI: true
           DEBUG: "pw:api"

+ 1 - 0
CONTRIBUTING.md

@@ -133,6 +133,7 @@ When submitting a Pull Request (PR) or expecting a subsequent review, please fol
    * Unrelated refactoring or heavy refactoring
    * Code or doc formatting changes including whitespace changes
    * Dependency updates e.g. in package.json
+   * Changes that contain multiple unverified resources. This is risky for our users and is a lot of work to verify. A change with one resource that can be verified is acceptable.
 
 ### PR Additional Links
 

+ 2 - 2
android/app/build.gradle

@@ -6,8 +6,8 @@ android {
         applicationId "com.logseq.app"
         minSdkVersion rootProject.ext.minSdkVersion
         targetSdkVersion rootProject.ext.targetSdkVersion
-        versionCode 61
-        versionName "0.9.8"
+        versionCode 62
+        versionName "0.9.9"
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
         aaptOptions {
              // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.

+ 4 - 7
bb.edn

@@ -5,13 +5,13 @@
   logseq/bb-tasks
   #_{:local/root "../bb-tasks"}
   {:git/url "https://github.com/logseq/bb-tasks"
-   :git/sha "4295d5df0458cc06a09c5d506510ee49b785407d"}
+   :git/sha "70d3edeb287f5cec7192e642549a401f7d6d4263"}
   logseq/graph-parser
   {:local/root "deps/graph-parser"}
   org.clj-commons/digest
   {:mvn/version "1.4.100"}}
  :pods
- {clj-kondo/clj-kondo {:version "2022.10.05"}
+ {clj-kondo/clj-kondo {:version "2023.05.26"}
   org.babashka/fswatcher {:version "0.0.3"}
   org.babashka/go-sqlite3 {:version "0.1.0"}}
  :tasks
@@ -39,8 +39,8 @@
   {:depends [dev:build-publishing]
    :doc "Build publishing spa app given graph and output dirs"
    :task (apply shell {:dir "scripts"}
-           "yarn -s nbb-logseq -cp src -m logseq.tasks.dev.publishing"
-           (into ["static"] *command-line-args*))}
+                "yarn -s nbb-logseq -cp src -m logseq.tasks.dev.publishing"
+                (into ["static"] *command-line-args*))}
 
   dev:npx-cap-run-ios
   logseq.tasks.dev.mobile/npx-cap-run-ios
@@ -113,9 +113,6 @@
   lang:missing
   logseq.tasks.lang/list-missing
 
-  lang:duplicates
-  logseq.tasks.lang/list-duplicates
-
   lang:validate-translations
   logseq.tasks.lang/validate-translations
 

+ 2 - 2
deps.edn

@@ -1,4 +1,4 @@
-{:paths ["src/main" "src/electron" "templates" "src/resources"]
+{:paths ["src/main" "src/electron" "src/resources"]
  :deps
  {org.clojure/clojure                   {:mvn/version "1.11.1"}
   rum/rum                               {:mvn/version "0.12.9"}
@@ -55,5 +55,5 @@
                    :main-opts ["-m" "cljs-test-runner.main" "-d" "src/bench" "-n" "frontend.benchmark-test-runner"]}
 
            ;; Use :replace-deps for tools. See https://github.com/clj-kondo/clj-kondo/issues/1536#issuecomment-1013006889
-           :clj-kondo {:replace-deps {clj-kondo/clj-kondo {:mvn/version "2022.12.08"}}
+           :clj-kondo {:replace-deps {clj-kondo/clj-kondo {:mvn/version "2023.05.26"}}
                        :main-opts  ["-m" "clj-kondo.main"]}}}

+ 3 - 3
deps/common/bb.edn

@@ -3,10 +3,10 @@
  {logseq/bb-tasks
   #_{:local/root "../../../bb-tasks"}
   {:git/url "https://github.com/logseq/bb-tasks"
-   :git/sha "0d49051909bfa0c6b414e86606d82b4ea54f382c"}}
+   :git/sha "70d3edeb287f5cec7192e642549a401f7d6d4263"}}
 
  :pods
- {clj-kondo/clj-kondo {:version "2023.03.17"}}
+ {clj-kondo/clj-kondo {:version "2023.05.26"}}
 
  :tasks
  {test:load-all-namespaces-with-nbb
@@ -23,4 +23,4 @@
 
  :tasks/config
  {:large-vars
-  {:max-lines-count 45}}}
+  {:max-lines-count 45}}}

+ 1 - 1
deps/common/deps.edn

@@ -4,5 +4,5 @@
                        org.clojure/clojurescript {:mvn/version "1.11.54"}}
          :main-opts   ["-m" "cljs-test-runner.main"]}
   :clj-kondo
-  {:replace-deps {clj-kondo/clj-kondo {:mvn/version "2022.12.08"}}
+  {:replace-deps {clj-kondo/clj-kondo {:mvn/version "2023.05.26"}}
    :main-opts  ["-m" "clj-kondo.main"]}}}

+ 2 - 2
deps/db/bb.edn

@@ -4,10 +4,10 @@
  {logseq/bb-tasks
   #_{:local/root "../../../bb-tasks"}
   {:git/url "https://github.com/logseq/bb-tasks"
-   :git/sha "1815db538241082a01e95601e23e4290dd64d0c0"}}
+   :git/sha "70d3edeb287f5cec7192e642549a401f7d6d4263"}}
 
  :pods
- {clj-kondo/clj-kondo {:version "2022.10.05"}}
+ {clj-kondo/clj-kondo {:version "2023.05.26"}}
 
  :tasks
  {test:load-all-namespaces-with-nbb

+ 1 - 1
deps/db/deps.edn

@@ -3,5 +3,5 @@
  {datascript/datascript {:mvn/version "1.3.8"}}
  :aliases
  {:clj-kondo
-  {:replace-deps {clj-kondo/clj-kondo {:mvn/version "2022.12.08"}}
+  {:replace-deps {clj-kondo/clj-kondo {:mvn/version "2023.05.26"}}
    :main-opts  ["-m" "clj-kondo.main"]}}}

+ 2 - 2
deps/graph-parser/bb.edn

@@ -3,10 +3,10 @@
  {logseq/bb-tasks
   #_{:local/root "../../../bb-tasks"}
   {:git/url "https://github.com/logseq/bb-tasks"
-   :git/sha "1815db538241082a01e95601e23e4290dd64d0c0"}}
+   :git/sha "70d3edeb287f5cec7192e642549a401f7d6d4263"}}
  
  :pods
- {clj-kondo/clj-kondo {:version "2022.10.05"}}
+ {clj-kondo/clj-kondo {:version "2023.05.26"}}
 
  :tasks
  {test:load-all-namespaces-with-nbb

+ 1 - 1
deps/graph-parser/deps.edn

@@ -20,5 +20,5 @@
                        org.clojure/clojurescript {:mvn/version "1.11.54"}}
          :main-opts   ["-m" "cljs-test-runner.main"]}
 
-  :clj-kondo {:replace-deps {clj-kondo/clj-kondo {:mvn/version "2022.12.08"}}
+  :clj-kondo {:replace-deps {clj-kondo/clj-kondo {:mvn/version "2023.05.26"}}
               :main-opts    ["-m" "clj-kondo.main"]}}}

+ 14 - 1
deps/graph-parser/src/logseq/graph_parser/mldoc.cljc

@@ -75,13 +75,26 @@
           js/JSON.stringify))))
 
 (defn remove-indentation-spaces
+  "Remove the indentation spaces from the content. Only for markdown.
+   level - ast level + 1 (2 for the first level, 3 for the second level, etc., as the non-first line of multi-line block has 2 more space
+           Ex.
+              - level 1 multiline block first line
+                level 1 multiline block second line
+              \t- level 2 multiline block first line
+              \t  level 2 multiline block second line
+   remove-first-line? - apply the indentation removal to the first line or not"
   [s level remove-first-line?]
   (let [lines (string/split-lines s)
         [f & r] lines
         body (map (fn [line]
+                    ;; Check if the indentation area only contains white spaces
+                    ;; Level = ast level + 1, 1-based indentation level
+                    ;; For markdown in Logseq, the indentation area for the non-first line of multi-line block is (ast level - 1) * "\t" + 2 * "(space)"
                     (if (string/blank? (gp-util/safe-subs line 0 level))
+                      ;; If valid, then remove the indentation area spaces. Keep the rest of the line (might contain leading spaces)
                       (gp-util/safe-subs line level)
-                      line))
+                      ;; Otherwise, trim these invalid spaces
+                      (string/triml line)))
                (if remove-first-line? lines r))
         content (if remove-first-line? body (cons f body))]
     (string/join "\n" content)))

+ 18 - 0
deps/graph-parser/test/logseq/graph_parser/mldoc_test.cljs

@@ -119,6 +119,24 @@ body"
       (is ["@tag" "tag1" "tag2"] (sort (:filetags props)))
       (is ["@tag" "tag1" "tag2" "tag3"] (sort (:tags props))))))
 
+(deftest remove-indentation-spaces
+  (testing "Remove indentations for every line"
+    (is (=  "block 1.1\n  line 1\n    line 2\nline 3\nline 4"
+            (let [s "block 1.1
+    line 1
+      line 2
+ line 3
+line 4"]
+              (gp-mldoc/remove-indentation-spaces s 2 false)))) 
+    (is (=  "\t- block 1.1\n  line 1\n    line 2\nline 3\nline 4"
+            (let [s "\t- block 1.1
+\t    line 1
+\t      line 2
+\t line 3
+\tline 4"]
+              (gp-mldoc/remove-indentation-spaces s 3 false))))))
+    
+
 (deftest ^:integration test->edn
   (let [graph-dir "test/docs-0.9.2"
         _ (docs-graph-helper/clone-docs-repo-if-not-exists graph-dir "v0.9.2")

+ 2 - 2
deps/publishing/bb.edn

@@ -3,10 +3,10 @@
  {logseq/bb-tasks
   #_{:local/root "../../../bb-tasks"}
   {:git/url "https://github.com/logseq/bb-tasks"
-   :git/sha "0d49051909bfa0c6b414e86606d82b4ea54f382c"}}
+   :git/sha "70d3edeb287f5cec7192e642549a401f7d6d4263"}}
 
  :pods
- {clj-kondo/clj-kondo {:version "2023.03.17"}}
+ {clj-kondo/clj-kondo {:version "2023.05.26"}}
 
  :tasks
  {test:load-all-namespaces-with-nbb

+ 1 - 1
deps/publishing/deps.edn

@@ -3,5 +3,5 @@
  {logseq/db {:local/root "../db"}}
 
  :aliases
- {:clj-kondo {:replace-deps {clj-kondo/clj-kondo {:mvn/version "2023.03.17"}}
+ {:clj-kondo {:replace-deps {clj-kondo/clj-kondo {:mvn/version "2023.05.26"}}
               :main-opts    ["-m" "clj-kondo.main"]}}}

+ 18 - 1
deps/publishing/src/logseq/publishing/db.cljs

@@ -2,6 +2,8 @@
   "Provides db fns and associated util fns for publishing"
   (:require [datascript.core :as d]
             [logseq.db.schema :as db-schema]
+            [logseq.db.rules :as rules]
+            [clojure.set :as set]
             [clojure.string :as string]))
 
 (defn ^:api get-area-block-asset-url
@@ -92,6 +94,20 @@
      flatten
      distinct)))
 
+(defn- get-aliases-for-page-ids
+  [db page-ids]
+  (->> (d/q '[:find ?e
+              :in $ ?pages %
+              :where
+              [?page :block/name]
+              [(contains? ?pages ?page)]
+              (alias ?page ?e)]
+            db
+            (set page-ids)
+            (:alias rules/rules))
+       (map first)
+       set))
+
 (defn clean-export!
   "Prepares a database assuming all pages are public unless a page has a 'public:: false'"
   [db]
@@ -113,7 +129,8 @@
   "Prepares a database assuming all pages are private unless a page has a 'public:: true'"
   [db]
   (when-let [public-pages* (seq (get-public-pages db))]
-    (let [public-pages (set public-pages*)
+    (let [public-pages (set/union (set public-pages*)
+                                  (get-aliases-for-page-ids db public-pages*))
           exported-namespace? #(contains? #{"block" "recent"} %)
           filtered-db (d/filter db
                                 (fn [db datom]

+ 3 - 1
deps/publishing/test/logseq/publishing/db_test.cljs

@@ -36,7 +36,7 @@
 (deftest filter-only-public-pages-and-blocks
   (let [conn (ldb/start-conn)
         _ (graph-parser/parse-file conn "page1.md" "- b11\n- b12\n- ![awesome.png](../assets/awesome_1648822509908_0.png)")
-        _ (graph-parser/parse-file conn "page2.md" "public:: true\n- b21\n- ![thumb-on-fire.PNG](../assets/thumb-on-fire_1648822523866_0.PNG)")
+        _ (graph-parser/parse-file conn "page2.md" "alias:: page2-alias\npublic:: true\n- b21\n- ![thumb-on-fire.PNG](../assets/thumb-on-fire_1648822523866_0.PNG)")
         _ (graph-parser/parse-file conn "page3.md" "public:: true\n- b31")
         [filtered-db assets] (publish-db/filter-only-public-pages-and-blocks @conn)
         exported-pages (->> (d/q '[:find (pull ?b [*])
@@ -56,6 +56,8 @@
         "Contains all pages that have been marked public")
     (is (not (contains? exported-pages "page1"))
         "Doesn't contain private page")
+    (is (seq (d/entity filtered-db [:block/name "page2-alias"]))
+          "Alias of public page is exported")
     (is (= #{"page2" "page3"} exported-block-pages)
         "Only exports blocks from public pages")
     (is (= ["thumb-on-fire_1648822523866_0.PNG"] assets)

+ 14 - 18
docs/contributing-to-translations.md

@@ -80,27 +80,23 @@ $ bb lang:missing es --copy
 
 Almost all translations are small. The only exceptions to this are the keys `:tutorial/text` and `:tutorial/dummy-notes`. These translations are files that are part of the onboarding tutorial and can be found under [src/resources/tutorials/](https://github.com/logseq/logseq/blob/master/src/resources/tutorials/).
 
-## Fix Untranslated
-
-There is a lot to translate and sometimes we forget to translate a string. To see what translation keys are still left for your language use :
-
-```shell
-$ bb lang:duplicates LOCALE
-
-Keys with duplicate values found:
-
-|                  :translation-key | :duplicate-value |
-|-----------------------------------+------------------|
-|                          :general |          General |
-|                           :logseq |           Logseq |
-|                               :no |               No |
-```
+### Editing Tips
 
+* Some translations may include punctuation like `:` or `!`. When translating them, please use the punctuation that makes the most sense for your language as you don't have to follow the English ones.
+* Some translations may include arguments/interpolations e.g. `{1}`. If you see them in a translation, be sure to include them. These arguments are substituted in the string and are usually used something the app needs to calculate e.g. a number. See [these docs](https://github.com/tonsky/tongue#interpolation) for more examples.
 ## Fix Mistakes
 
-Sometimes, we typo a translation key or forget to use it. If this happens,
-the github CI step of `bb lang:validate-translations` will detect these errors
-and tell you what's wrong.
+There is a lint command to catch common translation mistakes - `bb
+lang:validate-translations`. This runs for all contribution pull requests so
+you'll need to ensure it doesn't fail. Mistakes that it catches:
+
+* Adding translation entries for nonexistent entries in English.
+    * Most common mistake is mistyping an entry name
+* Adding English entries for translations that don't exist in the UI.
+* Adding translation entries that are just duplicates of the English entry.
+    * This catches contributors copying entries from English and then forgetting to translate. Sometimes you do want to have the translation be the same. For this case, add an entry to `allowed-duplicates` in
+[lang.clj](https://github.com/logseq/logseq/blob/master/scripts/src/logseq/tasks/lang.clj) for your language
+with a list of duplicated entries e.g. `:nb-NO #{:port ...}`.
 
 ## Add a Language
 

+ 5 - 2
docs/dev-practices.md

@@ -72,11 +72,14 @@ queries and rules. Our queries are linted through clj-kondo and
 [datalog-parser](https://github.com/lambdaforge/datalog-parser). clj-kondo will
 error if it detects an invalid query.
 
-### Invalid translations
+### Translations
 
 Our translations can be configured incorrectly. We can catch some of these
 mistakes [as noted here](./contributing-to-translations.md#fix-mistakes).
 
+Punctuation and delimiting characters (e.g. `:`, `:`, `?`) should be part of the translatable string.
+Those characters and their position may vary depending on the language.
+
 ### Spell Checker
 
 We use [typos](https://github.com/crate-ci/typos) to spell check our source code.
@@ -282,7 +285,7 @@ point out:
 * `dev:validate-repo-config-edn` - Validate a repo config.edn
 
   ```sh
-  bb dev:validate-repo-config-edn templates/config.edn
+  bb dev:validate-repo-config-edn src/resources/templates/config.edn
   ```
 
 * `dev:publishing` - Build a publishing app for a given graph dir. If the

+ 3 - 1
e2e-tests/code-editing.spec.ts

@@ -9,7 +9,9 @@ import { createRandomPage, escapeToCodeEditor, escapeToBlockEditor } from './uti
  * For more information, see: https://codemirror.net/doc/manual.html
  */
 
-test('switch code editing mode', async ({ page }) => {
+// TODO: Fix test that started intermittently failing some time around
+// https://github.com/logseq/logseq/pull/9540
+test.skip('switch code editing mode', async ({ page }) => {
   await createRandomPage(page)
 
   // NOTE: ` will trigger auto-pairing in Logseq

+ 2 - 1
e2e-tests/editor.spec.ts

@@ -152,6 +152,7 @@ test(
     // This test requires dev mode
     test.skip(process.env.RELEASE === 'true', 'not available for release version')
 
+    // @ts-ignore
     for (let [idx, events] of [
       kb_events.win10_pinyin_left_full_square_bracket,
       kb_events.macos_pinyin_left_full_square_bracket
@@ -168,7 +169,7 @@ test(
       expect(await page.inputValue(':nth-match(textarea, 1)')).toBe(check_text + '[[]]')
     };
 
-    // dont trigger RIME #3440
+    // @ts-ignore dont trigger RIME #3440
     for (let [idx, events] of [
       kb_events.macos_pinyin_selecting_candidate_double_left_square_bracket,
       kb_events.win10_RIME_selecting_candidate_double_left_square_bracket

+ 2 - 1
e2e-tests/fixtures.ts

@@ -27,12 +27,13 @@ const consoleLogWatcher = (msg: ConsoleMessage) => {
 
   // List of error messages to ignore
   const ignoreErrors = [
-    /net::ERR_CONNECTION_REFUSED/,
+    /net/,
     /^Error with Permissions-Policy header:/
   ];
 
   // If the text matches any of the ignoreErrors, return early
   if (ignoreErrors.some(error => text.match(error))) {
+    console.log(`WARN:: ${text}\n`)
     return;
   }
 

+ 1 - 0
e2e-tests/history.spec.ts

@@ -43,6 +43,7 @@ test('undo/redo of a renamed page should be preserved', async ({ page, block })
   await page.waitForTimeout(500) // Wait for 500ms autosave period to expire
 
   await renamePage(page, randomString(10))
+  await page.click('.ui__confirm-modal button')
 
   await page.keyboard.press(modKey + '+z')
   await page.waitForTimeout(100)

+ 2 - 1
e2e-tests/hotkey.spec.ts

@@ -1,9 +1,10 @@
 import { expect } from '@playwright/test'
 import { test } from './fixtures'
-import { createRandomPage, enterNextBlock, lastBlock, modKey, IsLinux } from './utils'
+import { createRandomPage, enterNextBlock, lastBlock, modKey, IsLinux, closeSearchBox } from './utils'
 
 test('open search dialog', async ({ page }) => {
   await page.waitForTimeout(200)
+  await closeSearchBox(page)
   await page.keyboard.press(modKey + '+k')
 
   await page.waitForSelector('[placeholder="Search or create page"]')

+ 105 - 0
e2e-tests/logseq-api.spec.ts

@@ -0,0 +1,105 @@
+import { test } from './fixtures'
+import { expect } from '@playwright/test'
+
+test('block related apis',
+  async ({ page }) => {
+    const callAPI = callPageAPI.bind(null, page)
+
+    const bPageName = 'block-test-page'
+    await callAPI('create_page', bPageName, null, { createFirstBlock: false })
+
+    await page.waitForSelector(`span[data-ref="${bPageName}"]`)
+
+    let p = await callAPI('get_current_page')
+    const bp = await callAPI('append_block_in_page', bPageName, 'tests')
+
+    expect(p.name).toBe(bPageName)
+
+    p = await callAPI('get_page', bPageName)
+
+    expect(p.name).toBe(bPageName)
+
+    await callAPI('edit_block', bp.uuid)
+
+    const b = (await callAPI('get_current_block'))
+    expect(Object.keys(b)).toContain('uuid')
+
+    await page.waitForSelector('.block-editor > textarea')
+    await page.locator('.block-editor > textarea').fill('')
+    const content = 'test api'
+    await page.type('.block-editor > textarea', content)
+
+    const editingContent = await callAPI('get_editing_block_content')
+    expect(editingContent).toBe(content)
+
+    // create
+    let b1 = await callAPI('insert_block', b.uuid, content)
+    b1 = await callAPI('get_block', b1.uuid)
+
+    expect(b1.parent.id).toBe(b.id)
+
+    // update
+    const content1 = content + '+ update!'
+    await callAPI('update_block', b1.uuid, content1)
+    await page.waitForTimeout(1000)
+    b1 = await callAPI('get_block', b1.uuid)
+
+    expect(b1.content).toBe(content1)
+
+    // remove
+    await callAPI('remove_block', b1.uuid)
+    b1 = await callAPI('get_block', b1.uuid)
+
+    expect(b1).toBeNull()
+
+    // traverse
+    b1 = await callAPI('insert_block', b.uuid, content1, { sibling: true })
+    const nb = await callAPI('get_next_sibling_block', b.uuid)
+    const pb = await callAPI('get_previous_sibling_block', b1.uuid)
+
+    expect(nb.uuid).toBe(b1.uuid)
+    expect(pb.uuid).toBe(b.uuid)
+
+    // move
+    await callAPI('move_block', b.uuid, b1.uuid)
+    const mb = await callAPI('get_next_sibling_block', b1.uuid)
+
+    expect(mb.uuid).toBe(b.uuid)
+
+    // properties
+    await callAPI('upsert_block_property', b1.uuid, 'a', 1)
+    let prop1 = await callAPI('get_block_property', b1.uuid, 'a')
+
+    expect(prop1).toBe(1)
+
+    await callAPI('upsert_block_property', b1.uuid, 'a', 2)
+    prop1 = await callAPI('get_block_property', b1.uuid, 'a')
+
+    expect(prop1).toBe(2)
+
+    await callAPI('remove_block_property', b1.uuid, 'a')
+    prop1 = await callAPI('get_block_property', b1.uuid, 'a')
+
+    expect(prop1).toBeNull()
+
+    await callAPI('upsert_block_property', b1.uuid, 'a', 1)
+    await callAPI('upsert_block_property', b1.uuid, 'b', 1)
+
+    prop1 = await callAPI('get_block_properties', b1.uuid)
+
+    expect(prop1).toEqual({ a: 1, b: 1 })
+
+    // await page.pause()
+  })
+
+/**
+ * @param page
+ * @param method
+ * @param args
+ */
+export async function callPageAPI(page, method, ...args) {
+  return await page.evaluate(([method, args]) => {
+    // @ts-ignore
+    return window.logseq.api[method]?.(...args)
+  }, [method, args])
+}

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

@@ -1,6 +1,6 @@
 import { expect, Page } from '@playwright/test'
 import { test } from './fixtures'
-import { createPage, randomLowerString, randomString, renamePage } from './utils'
+import { closeSearchBox, createPage, randomLowerString, randomString, renamePage, searchPage } from './utils'
 
 /***
  * Test rename feature
@@ -15,6 +15,7 @@ async function page_rename_test(page: Page, original_page_name: string, new_page
 
   // Rename page in UI
   await renamePage(page, new_name)
+  await page.click('.ui__confirm-modal button')
 
   expect(await page.innerText('.page-title .title')).toBe(new_name)
 
@@ -45,6 +46,7 @@ async function homepage_rename_test(page: Page, original_page_name: string, new_
   expect(await page.locator('.home-nav span.flex-1').innerText()).toBe(original_name);
 
   await renamePage(page, new_name)
+  await page.click('.ui__confirm-modal button')
 
   expect(await page.locator('.home-nav span.flex-1').innerText()).toBe(new_name);
 
@@ -64,6 +66,16 @@ test('page rename test', async ({ page }) => {
   await page_rename_test(page, "abcd", "a.b.c.d")
   await page_rename_test(page, "abcd", "a/b/c/d")
 
+  // The page name in page search are not updated after changing the capitalization of the page name #9577
+  // https://github.com/logseq/logseq/issues/9577
+  // Expect the page name to be updated in the search results
+  await page_rename_test(page, "DcBA_", "dCBA_")
+  const results = await searchPage(page, "DcBA_")
+  // search result 0 is the new page & 1 is the new whiteboard
+  const thirdResultRow = await results[2].innerText()
+  expect(thirdResultRow).toContain("dCBA_");
+  expect(thirdResultRow).not.toContain("DcBA_");
+  await closeSearchBox(page)
 })
 
 // TODO introduce more samples when #4722 is fixed

+ 1 - 11
e2e-tests/plugins.spec.ts

@@ -1,5 +1,6 @@
 import { expect } from '@playwright/test'
 import { test } from './fixtures'
+import { callPageAPI } from './logseq-api.spec'
 
 test.skip('enabled plugin system default', async ({ page }) => {
   const callAPI = callPageAPI.bind(null, page)
@@ -59,14 +60,3 @@ test.skip('play a plugin<logseq-journals-calendar> from the Marketplace', async
   await expect(page.locator('body[data-page="page"]')).toBeVisible()
 })
 
-/**
- * @param page
- * @param method
- * @param args
- */
-async function callPageAPI(page, method, ...args) {
-  return await page.evaluate(([method, args]) => {
-    // @ts-ignore
-    return window.logseq.api[method]?.(...args)
-  }, [method, args])
-}

+ 0 - 1
e2e-tests/util/page.ts

@@ -11,5 +11,4 @@ export async function renamePage(page: Page, new_name: string) {
   await page.fill('input[type="text"]', '')
   await page.type('.title input', new_name)
   await page.keyboard.press('Enter')
-  await page.click('.ui__confirm-modal button')
 }

+ 32 - 29
e2e-tests/util/search-modal.ts

@@ -1,63 +1,66 @@
 import { Page, Locator, ElementHandle } from '@playwright/test'
 import { randomString } from './basic'
 
+export async function closeSearchBox(page: Page): Promise<void> {
+    await page.keyboard.press("Escape") // escape (potential) search box typing
+    await page.waitForTimeout(500)
+    await page.keyboard.press("Escape") // escape modal
+}
+
 export async function createRandomPage(page: Page) {
     const randomTitle = randomString(20)
-  
+    await closeSearchBox(page)
     // Click #search-button
     await page.click('#search-button')
     // Fill [placeholder="Search or create page"]
     await page.fill('[placeholder="Search or create page"]', randomTitle)
     // Click text=/.*New page: "new page".*/
-    await page.click('text=/.*New page: ".*/')
+    await page.click('text=/.*New page:".*/')
     // Wait for h1 to be from our new page
     await page.waitForSelector(`h1 >> text="${randomTitle}"`, { state: 'visible' })
     // wait for textarea of first block
     await page.waitForSelector('textarea >> nth=0', { state: 'visible' })
-  
+
     return randomTitle;
-  }
-  
-  export async function createPage(page: Page, page_name: string) {// Click #search-button
+}
+
+export async function createPage(page: Page, page_name: string) {// Click #search-button
+    await closeSearchBox(page)
     await page.click('#search-button')
     // Fill [placeholder="Search or create page"]
     await page.fill('[placeholder="Search or create page"]', page_name)
     // Click text=/.*New page: "new page".*/
-    await page.click('text=/.*New page: ".*/')
+    await page.click('text=/.*New page:".*/')
     // wait for textarea of first block
     await page.waitForSelector('textarea >> nth=0', { state: 'visible' })
-  
+
     return page_name;
   }
-  
-  export async function searchAndJumpToPage(page: Page, pageTitle: string) {
+
+export async function searchAndJumpToPage(page: Page, pageTitle: string) {
+    await closeSearchBox(page)
     await page.click('#search-button')
     await page.type('[placeholder="Search or create page"]', pageTitle)
     await page.waitForSelector(`[data-page-ref="${pageTitle}"]`, { state: 'visible' })
     page.click(`[data-page-ref="${pageTitle}"]`)
     await page.waitForNavigation()
     return pageTitle;
-  }
-  
-  /**
-   * type a search query into the search box
-   * stop at the point where search box shows up
-   * 
-   * @param page the pw page object
-   * @param query the search query to type into the search box
-   * @returns the HTML element for the search results ui
-   */
-  export async function searchPage(page: Page, query: string): Promise<ElementHandle<SVGElement | HTMLElement>[]>{
+}
+
+/**
+ * type a search query into the search box
+ * stop at the point where search box shows up
+ *
+ * @param page the pw page object
+ * @param query the search query to type into the search box
+ * @returns the HTML element for the search results ui
+ */
+export async function searchPage(page: Page, query: string): Promise<ElementHandle<SVGElement | HTMLElement>[]>{
+    await closeSearchBox(page)
     await page.click('#search-button')
     await page.waitForSelector('[placeholder="Search or create page"]')
     await page.type('[placeholder="Search or create page"]', query, { delay: 10 })
     await page.waitForTimeout(2000) // wait longer for search contents to render
-  
+
     return page.$$('#ui__ac-inner>div');
-  }
-  
-  export async function closeSearchBox(page: Page): Promise<void> {
-    await page.keyboard.press("Escape") // escape (potential) search box typing
-    await page.waitForTimeout(500)
-    await page.keyboard.press("Escape") // escape modal
-  }
+}

+ 1 - 0
e2e-tests/utils.ts

@@ -2,6 +2,7 @@ import { Page, Locator } from 'playwright'
 import { expect, ConsoleMessage } from '@playwright/test'
 import * as pathlib from 'path'
 import { modKey } from './util/basic'
+import { Block } from './types'
 
 // TODO: The file should be a facade of utils in the /util folder
 // No more additional functions should be added to this file

+ 97 - 21
e2e-tests/whiteboards.spec.ts

@@ -1,6 +1,6 @@
 import { expect } from '@playwright/test'
 import { test } from './fixtures'
-import { modKey } from './utils'
+import { modKey, renamePage } from './utils'
 
 test('enable whiteboards', async ({ page }) => {
   if (await page.$('.nav-header .whiteboard') === null) {
@@ -85,10 +85,10 @@ test('draw a rectangle', async ({ page }) => {
 
   await page.keyboard.type('wr')
 
-  await page.mouse.move(bounds.x + 5, bounds.y + 5)
+  await page.mouse.move(bounds.x + 105, bounds.y + 105)
   await page.mouse.down()
 
-  await page.mouse.move(bounds.x + 50, bounds.y + 50 )
+  await page.mouse.move(bounds.x + 150, bounds.y + 150 )
   await page.mouse.up()
   await page.keyboard.press('Escape')
 
@@ -114,12 +114,14 @@ test('clone the rectangle', async ({ page }) => {
   const canvas = await page.waitForSelector('.logseq-tldraw')
   const bounds = (await canvas.boundingBox())!
 
-  await page.mouse.move(bounds.x + 20, bounds.y + 20, {steps: 5})
+  await page.mouse.move(bounds.x + 400, bounds.y + 400)
+
+  await page.mouse.move(bounds.x + 120, bounds.y + 120, {steps: 5})
 
   await page.keyboard.down('Alt')
   await page.mouse.down()
 
-  await page.mouse.move(bounds.x + 100, bounds.y + 100, {steps: 5})
+  await page.mouse.move(bounds.x + 200, bounds.y + 200, {steps: 5})
   await page.mouse.up()
   await page.keyboard.up('Alt')
 
@@ -163,10 +165,10 @@ test('connect rectangles with an arrow', async ({ page }) => {
 
   await page.keyboard.type('wc')
 
-  await page.mouse.move(bounds.x + 20, bounds.y + 20)
+  await page.mouse.move(bounds.x + 120, bounds.y + 120)
   await page.mouse.down()
 
-  await page.mouse.move(bounds.x + 100, bounds.y + 100, {steps: 5}) // will fail without steps
+  await page.mouse.move(bounds.x + 200, bounds.y + 200, {steps: 5}) // will fail without steps
   await page.mouse.up()
   await page.keyboard.press('Escape')
 
@@ -191,10 +193,15 @@ test('undo the delete action', async ({ page }) => {
 })
 
 test('convert the first rectangle to ellipse', async ({ page }) => {
+  const canvas = await page.waitForSelector('.logseq-tldraw')
+  const bounds = (await canvas.boundingBox())!
+
   await page.keyboard.press('Escape')
-  await page.waitForTimeout(1000)
-  await page.click('.logseq-tldraw .tl-box-container:first-of-type')
-  await page.mouse.move(0, 0)  // move mouse to trigger a rerender of the context bar
+  await page.mouse.move(bounds.x + 220, bounds.y + 220)
+  await page.mouse.down()
+  await page.mouse.up()
+  await page.mouse.move(bounds.x + 520, bounds.y + 520)
+
   await page.click('.tl-context-bar .tl-geometry-tools-pane-anchor')
   await page.click('.tl-context-bar .tl-geometry-toolbar [data-tool=ellipse]')
 
@@ -223,9 +230,14 @@ test('undo the shape conversion', async ({ page }) => {
 })
 
 test('locked elements should not be removed', async ({ page }) => {
+  const canvas = await page.waitForSelector('.logseq-tldraw')
+  const bounds = (await canvas.boundingBox())!
+
   await page.keyboard.press('Escape')
-  await page.waitForTimeout(1000)
-  await page.click('.logseq-tldraw .tl-box-container:first-of-type')
+  await page.mouse.move(bounds.x + 220, bounds.y + 220)
+  await page.mouse.down()
+  await page.mouse.up()
+  await page.mouse.move(bounds.x + 520, bounds.y + 520)
   await page.keyboard.press(`${modKey}+l`)
   await page.keyboard.press('Delete')
   await page.keyboard.press(`${modKey}+Shift+l`)
@@ -269,7 +281,7 @@ test('create a block', async ({ page }) => {
   const bounds = (await canvas.boundingBox())!
 
   await page.keyboard.type('ws')
-  await page.mouse.dblclick(bounds.x + 5, bounds.y + 5)
+  await page.mouse.dblclick(bounds.x + 105, bounds.y + 105)
   await page.waitForTimeout(100)
 
   await page.keyboard.type('a')
@@ -279,15 +291,17 @@ test('create a block', async ({ page }) => {
   await expect(page.locator('.logseq-tldraw .tl-logseq-portal-container')).toHaveCount(1)
 })
 
-test('expand the block', async ({ page }) => {
+// TODO: Fix the failing test
+test.skip('expand the block', async ({ page }) => {
   await page.keyboard.press('Escape')
-  await page.click('.logseq-tldraw .tl-context-bar .tie-object-expanded ')
+  await page.keyboard.press(modKey + '+ArrowDown')
   await page.waitForTimeout(100)
 
   await expect(page.locator('.logseq-tldraw .tl-logseq-portal-container .tl-logseq-portal-header')).toHaveCount(1)
 })
 
-test('undo the expand action', async ({ page }) => {
+// TODO: Depends on the previous test
+test.skip('undo the expand action', async ({ page }) => {
   await page.keyboard.press(modKey + '+z')
 
   await expect(page.locator('.logseq-tldraw .tl-logseq-portal-container .tl-logseq-portal-header')).toHaveCount(0)
@@ -304,7 +318,7 @@ test('copy/paste url to create an iFrame shape', async ({ page }) => {
   const bounds = (await canvas.boundingBox())!
 
   await page.keyboard.type('wt')
-  await page.mouse.move(bounds.x + 5, bounds.y + 5)
+  await page.mouse.move(bounds.x + 105, bounds.y + 105)
   await page.mouse.down()
   await page.waitForTimeout(100)
 
@@ -323,7 +337,7 @@ test('copy/paste twitter status url to create a Tweet shape', async ({ page }) =
   const bounds = (await canvas.boundingBox())!
 
   await page.keyboard.type('wt')
-  await page.mouse.move(bounds.x + 5, bounds.y + 5)
+  await page.mouse.move(bounds.x + 105, bounds.y + 105)
   await page.mouse.down()
   await page.waitForTimeout(100)
 
@@ -342,7 +356,7 @@ test('copy/paste youtube video url to create a Youtube shape', async ({ page })
   const bounds = (await canvas.boundingBox())!
 
   await page.keyboard.type('wt')
-  await page.mouse.move(bounds.x + 5, bounds.y + 5)
+  await page.mouse.move(bounds.x + 105, bounds.y + 105)
   await page.mouse.down()
   await page.waitForTimeout(100)
 
@@ -394,8 +408,8 @@ test('quick add another whiteboard', async ({ page }) => {
   const canvas = await page.waitForSelector('.logseq-tldraw')
   await canvas.dblclick({
     position: {
-      x: 100,
-      y: 100,
+      x: 200,
+      y: 200,
     },
   })
 
@@ -425,3 +439,65 @@ test('go to another board and check reference', async ({ page }) => {
   const pageRefCount$ = page.locator('.whiteboard-page-refs-count')
   await expect(pageRefCount$.locator('.open-page-ref-link')).toContainText('1')
 })
+
+test('Create an embedded whiteboard', async ({ page }) => {
+  const canvas = await page.waitForSelector('.logseq-tldraw')
+  await canvas.dblclick({
+    position: {
+      x: 110,
+      y: 110,
+    },
+  })
+
+  const quickAdd$ = page.locator('.tl-quick-search')
+  await expect(quickAdd$).toBeVisible()
+
+  await page.fill('.tl-quick-search input', 'My embedded whiteboard')
+  await quickAdd$
+    .locator('div[data-index="2"] .tl-quick-search-option')
+    .first()
+    .click()
+
+  await expect(quickAdd$).toBeHidden()
+  await expect(page.locator('.tl-logseq-portal-header a')).toContainText('My embedded whiteboard')
+})
+
+test('New whiteboard should have the correct name', async ({ page }) => {
+  page.locator('.tl-logseq-portal-header a').click()
+
+  await expect(page.locator('.whiteboard-page-title')).toContainText('My embedded whiteboard')
+})
+
+test('Create an embedded page', async ({ page }) => {
+  const canvas = await page.waitForSelector('.logseq-tldraw')
+  await canvas.dblclick({
+    position: {
+      x: 150,
+      y: 150,
+    },
+  })
+
+  const quickAdd$ = page.locator('.tl-quick-search')
+  await expect(quickAdd$).toBeVisible()
+
+  await page.fill('.tl-quick-search input', 'My page')
+  await quickAdd$
+    .locator('div[data-index="1"] .tl-quick-search-option')
+    .first()
+    .click()
+
+  await expect(quickAdd$).toBeHidden()
+  await expect(page.locator('.tl-logseq-portal-header a')).toContainText('My page')
+})
+
+test('New page should have the correct name', async ({ page }) => {
+  page.locator('.tl-logseq-portal-header a').click()
+
+  await expect(page.locator('.ls-page-title')).toContainText('My page')
+})
+
+test('Renaming a page to an existing whiteboard name should be prohibited', async ({ page }) => {
+  await renamePage(page, "My embedded whiteboard")
+
+  await expect(page.locator('.page-title input')).toHaveValue('My page')
+})

+ 34 - 0
e2e-tests/window.spec.ts

@@ -0,0 +1,34 @@
+import { expect } from '@playwright/test'
+import { test } from './fixtures'
+import { IsMac } from './utils';
+
+if (!IsMac) {
+    test('window should not be maximized on first launch', async ({ page, app }) => {
+        await expect(page.locator('.window-controls .maximize-toggle.maximize')).toHaveCount(1)
+    })
+
+    test('window should be maximized and icon should change on maximize-toggle click', async ({ page }) => {
+        await page.click('.window-controls .maximize-toggle.maximize')
+
+        await expect(page.locator('.window-controls .maximize-toggle.restore')).toHaveCount(1)
+    })
+
+    test('window should be restored and icon should change on maximize-toggle click', async ({ page }) => {
+        await page.click('.window-controls .maximize-toggle.restore')
+
+        await expect(page.locator('.window-controls .maximize-toggle.maximize')).toHaveCount(1)
+    })
+
+    test('window controls should be hidden on fullscreen mode', async ({ page }) => {
+        // Keyboard press F11 won't work, probably because it's a chromium shortcut (not a document event)
+        await page.evaluate(`window.document.body.requestFullscreen()`)
+
+        await expect(page.locator('.window-controls .maximize-toggle')).toHaveCount(0)
+    })
+
+    test('window controls should be visible when we exit fullscreen mode', async ({ page }) => {
+        await page.click('.window-controls .fullscreen-toggle')
+
+        await expect(page.locator('.window-controls')).toHaveCount(1)
+    })
+}

+ 4 - 4
ios/App/App.xcodeproj/project.pbxproj

@@ -519,7 +519,7 @@
 				INFOPLIST_FILE = App/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 14.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
-				MARKETING_VERSION = 0.9.8;
+				MARKETING_VERSION = 0.9.9;
 				OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq;
 				PRODUCT_NAME = "$(TARGET_NAME)";
@@ -546,7 +546,7 @@
 				INFOPLIST_FILE = App/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 14.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
-				MARKETING_VERSION = 0.9.8;
+				MARKETING_VERSION = 0.9.9;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
@@ -571,7 +571,7 @@
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				IPHONEOS_DEPLOYMENT_TARGET = 14.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
-				MARKETING_VERSION = 0.9.8;
+				MARKETING_VERSION = 0.9.9;
 				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
 				MTL_FAST_MATH = YES;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
@@ -598,7 +598,7 @@
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				IPHONEOS_DEPLOYMENT_TARGET = 14.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
-				MARKETING_VERSION = 0.9.8;
+				MARKETING_VERSION = 0.9.9;
 				MTL_FAST_MATH = YES;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
 				PRODUCT_NAME = "$(TARGET_NAME)";

+ 13 - 5
ios/App/App/FsWatcher.swift

@@ -56,7 +56,7 @@ public class FsWatcher: CAPPlugin, PollingWatcherDelegate {
             DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
                 self.notifyListeners("watcher", data: ["event": "unlink",
                                                        "dir": baseUrl.description as Any,
-                                                       "path": url.description
+                                                       "path": url.relativePath(from: baseUrl)?.precomposedStringWithCanonicalMapping as Any
                 ])
             }
         case .Add, .Change:
@@ -173,11 +173,11 @@ public class PollingWatcher {
     public init?(at: URL) {
         url = at
     }
-    
+
     public func start() {
-        
+
         self.tick(notify: false)
-        
+
         let queue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".timer")
         timer = DispatchSource.makeTimerSource(queue: queue)
         timer!.setEventHandler(qos: .background, flags: []) { [weak self] in
@@ -278,10 +278,18 @@ extension URL {
             return nil
         }
 
+        // NOTE: standardizedFileURL will remove `/private` prefix
+        // If the file is not exist, it won't remove the prefix.
+
         // Remove/replace "." and "..", make paths absolute:
-        let destComponents = self.standardizedFileURL.pathComponents
+        var destComponents = self.standardizedFileURL.pathComponents
         let baseComponents = base.standardizedFileURL.pathComponents
 
+        // replace "private" when needed
+        if destComponents.count > 1 && destComponents[1] == "private" && baseComponents.count > 1 && baseComponents[1] != "private" {
+            destComponents.remove(at: 1)
+        }
+
         // Find number of common path components:
         var i = 0
         while i < destComponents.count && i < baseComponents.count

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

@@ -233,7 +233,7 @@ function initMainUIHandlers(pluginLocal: PluginLocal) {
   pluginLocal.on(_('attrs'), (attrs: Partial<UIContainerAttrs>) => {
     const el = pluginLocal.getMainUIContainer()
     Object.entries(attrs).forEach(([k, v]) => {
-      el?.setAttribute(k, v)
+      el?.setAttribute(k, String(v))
       if (k === 'draggable' && v) {
         pluginLocal._dispose(
           pluginLocal._setupDraggableContainer(el, {

+ 24 - 20
libs/src/LSPlugin.ts

@@ -33,8 +33,6 @@ export type StyleOptions = {
 export type UIContainerAttrs = {
   draggable: boolean
   resizable: boolean
-
-  [key: string]: any
 }
 
 export type UIBaseOptions = {
@@ -75,20 +73,34 @@ export interface LSPluginPkgConfig {
   mode: 'shadow' | 'iframe'
   themes: Theme[]
   icon: string
-
-  [key: string]: any
+  /**
+   * Alternative entrypoint for development.
+   */
+  devEntry: unknown
+  /**
+   * For legacy themes, do not use.
+   */
+  theme: unknown
 }
 
 export interface LSPluginBaseInfo {
-  id: string // should be unique
+  /**
+   * Must be unique.
+   */
+  id: string
   mode: 'shadow' | 'iframe'
-
   settings: {
     disabled: boolean
-    [key: string]: any
-  }
-
-  [key: string]: any
+  } & Record<string, unknown>
+  effect: boolean
+  /**
+   * For internal use only. Indicates if plugin is installed in dot root.
+   */
+  iir: boolean
+  /**
+   * For internal use only.
+   */
+  lsr: string
 }
 
 export type IHookEvent = {
@@ -146,8 +158,6 @@ export interface AppUserConfigs {
   showBracket: boolean
   enabledFlashcards: boolean
   enabledJournals: boolean
-
-  [key: string]: any
 }
 
 /**
@@ -157,8 +167,6 @@ export interface AppGraphInfo {
   name: string
   url: string
   path: string
-
-  [key: string]: any
 }
 
 /**
@@ -184,8 +192,6 @@ export interface BlockEntity {
   level?: number
   meta?: { timestamps: any; properties: any; startPos: number; endPos: number }
   title?: Array<any>
-
-  [key: string]: any
 }
 
 /**
@@ -205,8 +211,6 @@ export interface PageEntity {
   format?: 'markdown' | 'org'
   journalDay?: number
   updatedAt?: number
-
-  [key: string]: any
 }
 
 export type BlockIdentity = BlockUUID | Pick<BlockEntity, 'uuid'>
@@ -1078,8 +1082,8 @@ export interface ILSPluginUser extends EventEmitter<LSPluginUserEvents> {
 
   resolveResourceFullUrl(filePath: string): string
 
-  App: IAppProxy & Record<string, any>
-  Editor: IEditorProxy & Record<string, any>
+  App: IAppProxy
+  Editor: IEditorProxy
   DB: IDBProxy
   Git: IGitProxy
   UI: IUIProxy

+ 2 - 2
package.json

@@ -117,7 +117,7 @@
         "highlight.js": "10.4.1",
         "ignore": "5.1.8",
         "jszip": "3.8.0",
-        "mldoc": "^1.5.1",
+        "mldoc": "^1.5.5",
         "path": "0.12.7",
         "path-complete-extname": "1.0.0",
         "pixi-graph-fork": "0.2.0",
@@ -158,4 +158,4 @@
         "pixi-graph-fork/@pixi/mixin-get-child-by-name": "6.2.0",
         "pixi-graph-fork/@pixi/math": "6.2.0"
     }
-}
+}

+ 18 - 2
playwright.config.ts

@@ -1,12 +1,28 @@
 import { PlaywrightTestConfig } from '@playwright/test'
 
 const config: PlaywrightTestConfig = {
+  // The directory where the tests are located
+  // The order of the tests is determined by the file names alphabetically.
   testDir: './e2e-tests',
+
+  // The number of retries before marking a test as failed.
   maxFailures: 1,
-  workers: 1, // NOTE: must be 1 for now, otherwise tests will fail.
+
+  // The number of Logseq instances to run in parallel.
+  // NOTE: must be 1 for now, otherwise tests will fail.
+  workers: 1,
+
+  // 'github' for GitHub Actions CI to generate annotations, plus a concise 'dot'.
+  // default 'list' when running locally.
+  reporter: process.env.CI ? 'github' : 'list',
+
+  // Fail the build on CI if test.only is present.
+  forbidOnly: !!process.env.CI,
+
   use: {
+    // SCapture screenshot after each test failure.
     screenshot: 'only-on-failure',
-  }
+  },
 }
 
 export default config

文件差异内容过多而无法显示
+ 0 - 0
resources/css/katex.min.css


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


+ 2 - 2
resources/package.json

@@ -1,7 +1,7 @@
 {
   "name": "Logseq",
   "productName": "Logseq",
-  "version": "0.9.8",
+  "version": "0.9.9",
   "main": "electron.js",
   "author": "Logseq",
   "license": "AGPL-3.0",
@@ -22,7 +22,7 @@
   "dependencies": {
     "better-sqlite3": "8.0.1",
     "chokidar": "^3.5.1",
-    "dugite": "1.108.0",
+    "dugite": "2.5.0",
     "electron-dl": "3.3.0",
     "electron-log": "4.3.1",
     "electron-squirrel-startup": "1.0.0",

+ 53 - 20
scripts/src/logseq/tasks/lang.clj

@@ -115,6 +115,14 @@
    "(t title" []
    "(t subtitle" [:asset/physical-delete]})
 
+(defn- whiteboard-dicts
+  []
+  (->> (shell {:out :string}
+              "grep -E -oh" "\\bt\\('[^ ']+" "-r" "tldraw/apps/tldraw-logseq/src/components")
+       :out
+       string/split-lines
+       (map #(keyword (subs % 3)))))
+
 (defn- validate-ui-translations-are-used
   "This validation checks to see that translations done by (t ...) are equal to
   the ones defined for the default :en lang. This catches translations that have
@@ -129,6 +137,7 @@
                           string/split-lines
                           (map #(keyword (subs % 4)))
                           (concat (mapcat val manual-ui-dicts))
+                          (concat (whiteboard-dicts))
                           set)
         expected-dicts (set (remove #(re-find #"^(command|shortcut)\." (str (namespace %)))
                                     (keys (:en (get-dicts)))))
@@ -145,31 +154,55 @@
           (task-util/print-table (map #(hash-map :invalid-key %) expected-only)))
         (System/exit 1)))))
 
-(defn validate-translations
-  "Runs multiple translation validations that fail fast if one of them is invalid"
-  []
-  (validate-non-default-languages)
-  (validate-ui-translations-are-used))
+(def allowed-duplicates
+  "Allows certain keys in a language to have the same translation
+   as English. Happens more in romance languages but pretty rare otherwise"
+  {:fr #{:port :type :help/docs :search-item/page :shortcut.category/navigating :text/image
+         :settings-of-plugins}
+   :de #{:graph :host :plugins :port :right-side-bar/whiteboards :search-item/block
+         :settings-of-plugins :search-item/whiteboard :shortcut.category/navigating
+         :settings-page/enable-tooltip :settings-page/enable-whiteboards :settings-page/plugin-system}
+   :es #{:settings-page/tab-general :settings-page/tab-editor :whiteboard/color}
+   :it #{:plugins}
+   :nl #{:plugins :type :left-side-bar/nav-recent-pages :plugin/update}
+   :pl #{:port}
+   :pt-BR #{:plugins :right-side-bar/flashcards :settings-page/enable-flashcards :page/backlinks
+            :host :settings-page/tab-editor :shortcut.category/plugins :whiteboard/link}
+   :pt-PT #{:plugins :settings-of-plugins :plugin/downloads :right-side-bar/flashcards
+            :settings-page/enable-flashcards :settings-page/plugin-system}
+   :nb-NO #{:port :type :whiteboard :right-side-bar/flashcards :right-side-bar/whiteboards 
+            :search-item/whiteboard :settings-page/enable-flashcards :settings-page/enable-whiteboards 
+            :settings-page/tab-editor :shortcut.category/whiteboard :whiteboard/medium 
+            :whiteboard/twitter-url :whiteboard/youtube-url}
+   :tr #{:help/awesome-logseq}
+   })
 
-(defn list-duplicates
-  "Lists translations that are the same as the one in English"
-  [& args]
+(defn- validate-languages-dont-have-duplicates
+  "Looks up duplicates for all languages"
+  []
   (let [dicts (get-dicts)
         en-dicts (dicts :en)
-        lang (or (keyword (first args))
-                 (task-util/print-usage "LOCALE"))
-        lang-dicts (dicts lang)
         invalid-dicts
-        (sort-by
-         :translation-key
-         (keep
-          #(when (= (en-dicts %) (lang-dicts %))
-             {:translation-key %
-              :duplicate-value (shorten (lang-dicts %) 70)})
-          (keys lang-dicts)))]
+        (->> (dissoc dicts :en)
+             (mapcat
+              (fn [[lang lang-dicts]]
+                (keep
+                 #(when (= (en-dicts %) (lang-dicts %))
+                    {:translation-key %
+                     :lang lang
+                     :duplicate-value (shorten (lang-dicts %) 70)})
+                 (keys (apply dissoc lang-dicts (allowed-duplicates lang))))))
+             (sort-by (juxt :lang :translation-key)))]
     (if (empty? invalid-dicts)
-      (println "No duplicated keys found!")
+      (println "All languages have no duplicate English values!")
       (do
-        (println "Keys with duplicate values found:")
+        (println "These translations keys are invalid because they are just copying the English value:")
         (task-util/print-table invalid-dicts)
         (System/exit 1)))))
+
+(defn validate-translations
+  "Runs multiple translation validations that fail fast if one of them is invalid"
+  []
+  (validate-non-default-languages)
+  (validate-ui-translations-are-used)
+  (validate-languages-dont-have-duplicates))

+ 16 - 1
src/electron/electron/handler.cljs

@@ -502,7 +502,8 @@
   js/__dirname)
 
 (defmethod handle :getAppBaseInfo [^js win [_ _opts]]
-  {:isFullScreen (.isFullScreen win)})
+  {:isFullScreen (.isFullScreen win)
+   :isMaximized (.isMaximized win)})
 
 (defmethod handle :getAssetsFiles [^js win [_ {:keys [exts]}]]
   (when-let [graph-path (state/get-window-graph-path win)]
@@ -679,6 +680,20 @@
   (when-let [web-content (.-webContents win)]
     (.reload web-content)))
 
+(defmethod handle :window-minimize [^js win]
+  (.minimize win))
+
+(defmethod handle :window-toggle-maximized [^js win]
+  (if (.isMaximized win)
+    (.unmaximize win)
+    (.maximize win)))
+
+(defmethod handle :window-toggle-fullscreen [^js win]
+  (.setFullScreen win (not (.isFullScreen win))))
+
+(defmethod handle :window-close [^js win]
+  (.close win))
+
 ;;;;;;;;;;;;;;;;;;;;;;;
 ;; file-sync-rs-apis ;;
 ;;;;;;;;;;;;;;;;;;;;;;;

+ 37 - 14
src/electron/electron/plugin.cljs

@@ -19,6 +19,23 @@
               (.. win -webContents
                   (send (name type) (bean/->js payload))))))
 
+(defonce github-api-0 "https://api.github.com")
+(defonce github-api-1 "https://plugins.logseq.io/github/api")
+(defonce *github-api (atom github-api-0))
+(defonce *last-valid-github-api (atom nil))
+
+(defn valid-github-api!
+  []
+  (when (or (nil? @*last-valid-github-api)
+            (> (- (js/Date.now) @*last-valid-github-api) (* 1000 60)))
+    (let [target github-api-1]
+      (-> (fetch (str target "/rate_limit") {:timeout 2000})
+          (p/then #(when (not= (.-status %) 200) (throw (js/Error. (.-statusText %)))))
+          (p/then #(do (reset! *github-api target) (debug "INFO: use github api - " target)))
+          (p/catch #(do (reset! *github-api github-api-0) (debug "ERR: valid github api - " %)))
+          (p/finally #(reset! *last-valid-github-api (js/Date.now)))))))
+
+
 (defn dotdir-file?
   [file]
   (and file (string/starts-with? (node-path/normalize file) cfgs/dot-root)))
@@ -34,13 +51,14 @@
 (defn- fetch-release-asset
   [{:keys [repo theme]} url-suffix {:keys [response-transform]
                                     :or   {response-transform identity}}]
-  (-> (p/let [repo         (some-> repo (string/trim) (string/replace #"^/+(.+?)/+$" "$1"))
-              api          #(str "https://api.github.com/repos/" repo "/" %)
+  (-> (p/let [_            (valid-github-api!)
+              repo         (some-> repo (string/trim) (string/replace #"^/+(.+?)/+$" "$1"))
+              api          #(str @*github-api "/repos/" repo "/" %)
               endpoint     (api url-suffix)
-              ^js res      (fetch endpoint)
+              ^js res      (fetch endpoint {:timeout (* 1000 5)})
               illegal-text (when-not (= 200 (.-status res)) (.text res))
               _            (when-not (string/blank? illegal-text) (throw (js/Error. (str "Github API Failed(" (.-status res) ") " illegal-text))))
-              _            (debug "[Release URL] " endpoint "[Status/Text]" (.-status res))
+              _            (debug "Release latest:" endpoint ":status" (.-status res))
               res          (response-transform res)
               res          (.json res)
               res          (bean/->clj res)
@@ -83,7 +101,7 @@
                             ;; cases. Previous logseq versions did not store the
                             ;; plugin's git tag required to correctly install it
                             (let [repo' (some-> repo (string/trim) (string/replace #"^/+(.+?)/+$" "$1"))
-                                  api   #(str "https://api.github.com/repos/" repo' "/" %)]
+                                  api   #(str @*github-api "/repos/" repo' "/" %)]
                               (fetch (api "releases/latest")))
                             res))}))
 
@@ -174,10 +192,10 @@
           updating?       (and version (. semver valid coerced-version)
                                (not= action :install))]
 
-      (debug (if updating? "Updating:" "Installing:") repo)
+      (debug "===" (if updating? "Updating:" "Installing:") repo "===")
 
       (-> (p/create
-            (fn [resolve _reject]
+            (fn [resolve reject]
               ;;(reset! *installing-or-updating item)
               ;; get releases
               (-> (p/let [[asset latest-version notes]
@@ -185,18 +203,19 @@
                             (fetch-specific-release-asset item)
                             (fetch-latest-release-asset item))
 
-                          _      (debug "[Release Asset] #" latest-version " =>" (:url asset))
+                          _      (debug "Release latest:" latest-version " from" (:url asset))
 
                           ;; compare latest version
                           _      (when-let [coerced-latest-version
                                             (and updating? latest-version
                                                  (. semver coerce latest-version))]
 
-                                   (debug "[Updating Latest?] " version " > " latest-version)
+                                   (debug "Release compare:" version "(current) > " latest-version "(latest)")
 
                                    (if (. semver lt coerced-version coerced-latest-version)
-                                     (debug "[Updating Latest] " latest-version)
-                                     (throw (js/Error. :no-new-version))))
+                                     (debug "Updating latest:" latest-version)
+                                     (do (debug "Update skip: no new version")
+                                         (throw (js/Error. :no-new-version)))))
 
                           dl-url (if-not (string? asset)
                                    (:browser_download_url asset) asset)
@@ -207,7 +226,7 @@
 
                           dest   (.join node-path cfgs/dot-root "plugins" (:id item))
                           _      (when-not only-check (download-asset-zip item dl-url latest-version dest))
-                          _      (debug (str "[" (if only-check "Checked" "Updated") "DONE]") latest-version)]
+                          _      (debug (str "[" (if only-check "Checked" "Updated") " DONE]") latest-version)]
 
                     (emit :lsp-updates
                           {:status     :completed
@@ -224,8 +243,12 @@
                             {:status     :error
                              :only-check only-check
                              :payload    (assoc item :error-code (.-message e))})
-                      (debug e))
-                    (resolve nil)))))
+                      (reject e))))))
+
+          (p/catch
+            (fn [^js e]
+              (when-not (contains? #{":no-new-version"} (.-message e))
+                (debug e))))
 
           (p/finally
             (fn []))))

+ 5 - 2
src/electron/electron/window.cljs

@@ -26,10 +26,11 @@
    (create-main-window! url nil))
   ([url opts]
    (let [win-state (windowStateKeeper (clj->js {:defaultWidth 980 :defaultHeight 700}))
+         native-titlebar? (cfgs/get-item :window/native-titlebar?)
          win-opts  (cond->
                      {:width                (.-width win-state)
                       :height               (.-height win-state)
-                      :frame                true
+                      :frame                (or mac? native-titlebar?)
                       :titleBarStyle        "hiddenInset"
                       :trafficLightPosition {:x 16 :y 16}
                       :autoHideMenuBar      (not mac?)
@@ -187,7 +188,9 @@
 
       (doto win
         (.on "enter-full-screen" #(.send web-contents "full-screen" "enter"))
-        (.on "leave-full-screen" #(.send web-contents "full-screen" "leave")))
+        (.on "leave-full-screen" #(.send web-contents "full-screen" "leave"))
+        (.on "maximize" #(.send web-contents "maximize" true))
+        (.on "unmaximize" #(.send web-contents "maximize" false)))
 
       ;; clear
       (fn []

+ 3 - 10
src/main/electron/listener.cljs

@@ -5,7 +5,6 @@
             [datascript.core :as d]
             [dommy.core :as dom]
             [electron.ipc :as ipc]
-            [frontend.context.i18n :refer [t]]
             [frontend.db :as db]
             [frontend.db.model :as db-model]
             [frontend.fs.sync :as sync]
@@ -35,9 +34,7 @@
   []
   ;; 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)
+  (repo-handler/persist-db! {:before     #(ui/notify-graph-persist!)
                              :on-success #(ipc/ipc "persistent-dbs-saved")
                              :on-error   #(ipc/ipc "persistent-dbs-error")}))
 
@@ -139,15 +136,11 @@
                  ;; fire back "broadcastPersistGraphDone" on done
                  (fn [data]
                    (let [repo (bean/->clj data)
-                         before-f #(notification/show!
-                                    (ui/loading (t :graph/persist))
-                                    :warning)
+                         before-f #(ui/notify-graph-persist!)
                          after-f #(ipc/ipc "broadcastPersistGraphDone")
                          error-f (fn []
                                    (after-f)
-                                   (notification/show!
-                                    (t :graph/persist-error)
-                                    :error))
+                                   (ui/notify-graph-persist-error!))
                          handlers {:before     before-f
                                    :on-success after-f
                                    :on-error   error-f}]

+ 14 - 13
src/main/frontend/commands.cljs

@@ -30,6 +30,7 @@
 (defonce angle-bracket "<")
 (defonce hashtag "#")
 (defonce colon ":")
+(defonce command-trigger "/")
 (defonce *current-command (atom nil))
 
 (def query-doc
@@ -52,7 +53,7 @@
     "."]])
 
 (defn link-steps []
-  [[:editor/input (str (state/get-editor-command-trigger) "link")]
+  [[:editor/input (str command-trigger "link")]
    [:editor/show-input [{:command :link
                          :id :link
                          :placeholder "Link"
@@ -62,7 +63,7 @@
                          :placeholder "Label"}]]])
 
 (defn image-link-steps []
-  [[:editor/input (str (state/get-editor-command-trigger) "link")]
+  [[:editor/input (str command-trigger "link")]
    [:editor/show-input [{:command :image-link
                          :id :link
                          :placeholder "Link"
@@ -72,7 +73,7 @@
                          :placeholder "Label"}]]])
 
 (defn zotero-steps []
-  [[:editor/input (str (state/get-editor-command-trigger) "zotero")]
+  [[:editor/input (str command-trigger "zotero")]
    [:editor/show-zotero]])
 
 (def *extend-slash-commands (atom []))
@@ -96,19 +97,19 @@
   [type]
   (let [template (util/format "@@%s: @@"
                               type)]
-    [[:editor/input template {:last-pattern (state/get-editor-command-trigger)
+    [[:editor/input template {:last-pattern command-trigger
                               :backward-pos 2}]]))
 
 (defn embed-page
   []
   (conj
-   [[:editor/input "{{embed [[]]}}" {:last-pattern (state/get-editor-command-trigger)
+   [[:editor/input "{{embed [[]]}}" {:last-pattern command-trigger
                                      :backward-pos 4}]]
    [:editor/search-page :embed]))
 
 (defn embed-block
   []
-  [[:editor/input "{{embed (())}}" {:last-pattern (state/get-editor-command-trigger)
+  [[:editor/input "{{embed (())}}" {:last-pattern command-trigger
                                     :backward-pos 4}]
    [:editor/search-block :embed]])
 
@@ -229,9 +230,9 @@
      ["Image link" (image-link-steps) "Create a HTTP link to a image"]
      (when (state/markdown?)
        ["Underline" [[:editor/input "<ins></ins>"
-                      {:last-pattern (state/get-editor-command-trigger)
+                      {:last-pattern command-trigger
                        :backward-pos 6}]] "Create a underline text decoration"])
-     ["Template" [[:editor/input (state/get-editor-command-trigger) nil]
+     ["Template" [[:editor/input command-trigger nil]
                   [:editor/search-template]] "Insert a created template here"]
      (cond
        (and (util/electron?) (config/local-file-based-graph? (state/get-current-repo)))
@@ -277,7 +278,7 @@
     [["Query" [[:editor/input "{{query }}" {:backward-pos 2}]
                [:editor/exit]] query-doc]
      ["Zotero" (zotero-steps) "Import Zotero journal article"]
-     ["Query table function" [[:editor/input "{{function }}" {:backward-pos 2}]] "Create a query table function"]
+     ["Query function" [[:editor/input "{{function }}" {:backward-pos 2}]] "Create a query function"]
      ["Calculator" [[:editor/input "```calc\n\n```" {:type "block"
                                                      :backward-pos 4}]
                     [:codemirror/focus]] "Insert a calculator"]
@@ -290,12 +291,12 @@
                  text)) "Draw a graph with Excalidraw"]
      ["Embed HTML " (->inline "html")]
 
-     ["Embed Video URL" [[:editor/input "{{video }}" {:last-pattern (state/get-editor-command-trigger)
+     ["Embed Video URL" [[:editor/input "{{video }}" {:last-pattern command-trigger
                                                       :backward-pos 2}]]]
 
      ["Embed Youtube timestamp" [[:youtube/insert-timestamp]]]
 
-     ["Embed Twitter tweet" [[:editor/input "{{tweet }}" {:last-pattern (state/get-editor-command-trigger)
+     ["Embed Twitter tweet" [[:editor/input "{{tweet }}" {:last-pattern command-trigger
                                                           :backward-pos 2}]]]]
 
     @*extend-slash-commands
@@ -335,7 +336,7 @@
   (when-let [input (gdom/getElement id)]
     (let [last-pattern (when-not (= last-pattern :skip-check)
                          (when-not backward-truncate-number
-                          (or last-pattern (state/get-editor-command-trigger))))
+                           (or last-pattern command-trigger)))
           edit-content (gobj/get input "value")
           current-pos (cursor/pos input)
           current-pos (or
@@ -527,7 +528,7 @@
       (let [edit-content (gobj/get current-input "value")
             current-pos (cursor/pos current-input)
             prefix (subs edit-content 0 current-pos)
-            prefix (util/replace-last (state/get-editor-command-trigger) prefix "" (boolean space?))
+            prefix (util/replace-last command-trigger prefix "" (boolean space?))
             new-value (str prefix
                            (subs edit-content current-pos))]
         (state/set-block-content-and-last-pos! input-id

+ 6 - 6
src/main/frontend/components/assets.cljs

@@ -88,11 +88,11 @@
        :disabled (string/blank? val)
        :on-click on-submit)]]))
 
-(rum/defc restart-button [active?]
-  (when active?
-    (ui/button (t :plugin/restart)
-               :on-click #(js/logseq.api.relaunch)
-               :small? true :intent "logseq")))
+(rum/defc restart-button
+  []
+  (ui/button (t :plugin/restart)
+             :on-click #(js/logseq.api.relaunch)
+             :small? true :intent "logseq"))
 
 (rum/defcs ^:large-vars/data-var alias-directories
   < rum/reactive
@@ -215,7 +215,7 @@
              #(state/set-assets-alias-enabled! (not alias-enabled?))
              true)]
       [:span
-       (restart-button alias-enabled-changed?)]]
+       (when alias-enabled-changed? (restart-button))]]
 
      (when alias-enabled?
        [:div.pt-4

+ 80 - 102
src/main/frontend/components/block.cljs

@@ -6,13 +6,13 @@
             [cljs-bean.core :as bean]
             [cljs.core.match :refer [match]]
             [cljs.reader :as reader]
-            [clojure.set :as set]
             [clojure.string :as string]
             [clojure.walk :as walk]
             [datascript.core :as d]
             [datascript.impl.entity :as de]
             [dommy.core :as dom]
             [frontend.commands :as commands]
+            [frontend.components.block.macros :as block-macros]
             [frontend.components.datetime :as datetime-comp]
             [frontend.components.lazy-editor :as lazy-editor]
             [frontend.components.macro :as macro]
@@ -39,13 +39,11 @@
             [frontend.fs :as fs]
             [frontend.handler.assets :as assets-handler]
             [frontend.handler.block :as block-handler]
-            [frontend.handler.common :as common-handler]
             [frontend.handler.dnd :as dnd]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.file-sync :as file-sync]
             [frontend.handler.notification :as notification]
             [frontend.handler.plugin :as plugin-handler]
-            [frontend.handler.query :as query-handler]
             [frontend.handler.repeated :as repeated]
             [frontend.handler.route :as route-handler]
             [frontend.handler.ui :as ui-handler]
@@ -63,6 +61,7 @@
             [frontend.util.clock :as clock]
             [frontend.util.drawer :as drawer]
             [frontend.util.property-edit :as property-edit]
+            [frontend.util.property :as property]
             [frontend.util.text :as text-util]
             [goog.dom :as gdom]
             [goog.object :as gobj]
@@ -70,7 +69,6 @@
             [logseq.graph-parser.block :as gp-block]
             [logseq.graph-parser.config :as gp-config]
             [logseq.graph-parser.mldoc :as gp-mldoc]
-            [logseq.graph-parser.property :as gp-property]
             [logseq.graph-parser.text :as text]
             [logseq.graph-parser.util :as gp-util]
             [logseq.graph-parser.util.block-ref :as block-ref]
@@ -554,7 +552,7 @@
                untitled? (str " opacity-50"))
       :data-ref page-name
       :draggable true
-      :on-drag-start (fn [e] (editor-handler/block->data-transfer! page-name e))
+      :on-drag-start (fn [e] (editor-handler/block->data-transfer! page-name-in-block e))
       :on-mouse-down (fn [_e] (reset! *mouse-down? true))
       :on-mouse-up (fn [e]
                      (when @*mouse-down?
@@ -1253,17 +1251,7 @@
 (defn- macro-function-cp
   [config arguments]
   (or
-   (when (:query-result config)
-     (when-let [query-result (rum/react (:query-result config))]
-       (let [fn-string (-> (util/format "(fn [result] %s)" (first arguments))
-                           (common-handler/safe-read-string "failed to parse function")
-                           (query-handler/normalize-query-function query-result)
-                           (str))
-             f (sci/eval-string fn-string)]
-         (when (fn? f)
-           (try (f query-result)
-                (catch :default e
-                  (js/console.error e)))))))
+   (some-> (:query-result config) rum/react (block-macros/function-macro arguments))
    [:span.warning
     (util/format "{{function %s}}" (first arguments))]))
 
@@ -1696,7 +1684,7 @@
        :block)
       (util/stop e))
 
-    (whiteboard-handler/inside-portal? (.-target e))
+    (and (util/meta-key? e) (whiteboard-handler/inside-portal? (.-target e)))
     (do (whiteboard-handler/add-new-block-portal-shape!
          uuid
          (whiteboard-handler/closest-shape (.-target e)))
@@ -1967,11 +1955,12 @@
                  (not= "nil" marker))
         {:class (str (string/lower-case marker))})
       (when bg-color
-        {:style {:background-color (if (some #{bg-color} ui/block-background-colors)
-                                     (str "var(--ls-highlight-color-" bg-color ")")
-                                     bg-color)
-                 :color (when-not (some #{bg-color} ui/block-background-colors) "white")}
-         :class "px-1 with-bg-color"}))
+        (let [built-in-color? (ui/built-in-color? bg-color)]
+          {:style {:background-color (if built-in-color?
+                                       (str "var(--ls-highlight-color-" bg-color ")")
+                                       bg-color)
+                   :color (when-not built-in-color? "white")}
+           :class "px-1 with-bg-color"})))
 
      ;; children
      (let [area?  (= :area (keyword (:hl-type properties)))
@@ -2067,66 +2056,25 @@
         :else
         (inline-text config (:block/format block) (str v)))]]))
 
-(def hidden-editable-page-properties
-  "Properties that are hidden in the pre-block (page property)"
-  #{:title :filters :icon})
-
-(assert (set/subset? hidden-editable-page-properties (gp-property/editable-built-in-properties))
-        "Hidden editable page properties must be valid editable properties")
-
-(def hidden-editable-block-properties
-  "Properties that are hidden in a block (block property)"
-  (into #{:logseq.query/nlp-date}
-        gp-property/editable-view-and-table-properties))
-
-(assert (set/subset? hidden-editable-block-properties (gp-property/editable-built-in-properties))
-        "Hidden editable page properties must be valid editable properties")
-
-(defn- add-aliases-to-properties
-  [properties block]
-  (let [repo (state/get-current-repo)
-        aliases (db/get-page-alias-names repo
-                                         (:block/name (db/pull (:db/id (:block/page block)))))]
-    (if (seq aliases)
-      (if (:alias properties)
-        (update properties :alias (fn [c]
-                                    (util/distinct-by string/lower-case (concat c aliases))))
-        (assoc properties :alias aliases))
-      properties)))
-
 (rum/defc properties-cp
   [config {:block/keys [pre-block?] :as block}]
-  (let [dissoc-keys (fn [m keys] (apply dissoc m keys))
-        properties (cond-> (update-keys (:block/properties block) keyword)
-                           true
-                           (dissoc-keys (property-edit/hidden-properties))
-                           pre-block?
-                           (dissoc-keys hidden-editable-page-properties)
-                           (not pre-block?)
-                           (dissoc-keys hidden-editable-block-properties)
-                           pre-block?
-                           (add-aliases-to-properties block))]
+  (let [ordered-properties
+        (property/get-visible-ordered-properties (:block/properties block)
+                                                 (:block/properties-order block)
+                                                 {:pre-block? pre-block?
+                                                  :page-id (:db/id (:block/page block))})]
     (cond
-      (seq properties)
-      (let [properties-order (cond->> (:block/properties-order block)
-                                      true
-                                      (remove (property-edit/hidden-properties))
-                                      pre-block?
-                                      (remove hidden-editable-page-properties))
-            properties-order (distinct properties-order)
-            ordered-properties (if (seq properties-order)
-                                 (map (fn [k] [k (get properties k)]) properties-order)
-                                 properties)]
-        [:div.block-properties
-         {:class (when pre-block? "page-properties")
-          :title (if pre-block?
-                   "Click to edit this page's properties"
-                   "Click to edit this block's properties")}
-         (for [[k v] ordered-properties]
-           (rum/with-key (property-cp config block k v)
-             (str (:block/uuid block) "-" k)))])
-
-      (and pre-block? properties)
+      (seq ordered-properties)
+      [:div.block-properties
+       {:class (when pre-block? "page-properties")
+        :title (if pre-block?
+                 "Click to edit this page's properties"
+                 "Click to edit this block's properties")}
+       (for [[k v] ordered-properties]
+         (rum/with-key (property-cp config block k v)
+           (str (:block/uuid block) "-" k)))]
+
+      (and pre-block? ordered-properties)
       [:span.opacity-50 "Properties"]
 
       :else
@@ -2764,27 +2712,49 @@
        (= (:id config)
           (str (:block/uuid block)))))
 
+(defn- build-config [config block {:keys [navigating-block navigated?]}]
+  (cond-> config
+    navigated?
+    (assoc :id (str navigating-block))
+
+    true
+    (assoc :block block)
+
+    ;; Each block might have multiple queries, but we store only the first query's result.
+    ;; This :query-result atom is used by the query function feature to share results between
+    ;; the parent's query block and the children blocks. This works because config is shared
+    ;; between parent and children blocks
+    (nil? (:query-result config))
+    (assoc :query-result (atom nil))
+
+    (:ref? config)
+    (block-handler/attach-order-list-state block)))
+
+(defn- build-block [repo config block* {:keys [navigating-block navigated?]}]
+  (let [block (if (or (and (:custom-query? config)
+                           (empty? (:block/_parent block*))
+                           (not (and (:dsl-query? config)
+                                     (string/includes? (:query config) "not"))))
+                      navigated?)
+                (db/entity [:block/uuid navigating-block])
+                block*)]
+    (merge (db/sub-block (:db/id block))
+           (select-keys block [:block/level]))))
+
 (rum/defc ^:large-vars/cleanup-todo block-container-inner < rum/reactive db-mixins/query
-  [state repo config block {:keys [edit? edit-input-id]}]
-  (let [ref? (:ref? config)
-        custom-query? (boolean (:custom-query? config))
+  [state repo config* block* {:keys [edit? edit-input-id]}]
+  (let [ref? (:ref? config*)
+        custom-query? (boolean (:custom-query? config*))
         ref-or-custom-query? (or ref? custom-query?)
         *navigating-block (get state ::navigating-block)
         navigating-block (rum/react *navigating-block)
-        navigated? (and (not= (:block/uuid block) navigating-block) navigating-block)
-        block (merge (db/sub-block (:db/id block))
-                     (select-keys block [:block/level]))
-        {:block/keys [uuid pre-block? refs level format content properties]} block
+        navigated? (and (not= (:block/uuid block*) navigating-block) navigating-block)
+        block (build-block repo config* block* {:navigating-block navigating-block :navigated? navigated?})
+        {:block/keys [uuid pre-block? refs level content properties]} block
         children (db/sort-by-left (:block/_parent block) block)
         {:block.temp/keys [top?]} block
-        config (if navigated? (assoc config :id (str navigating-block)) config)
+        config (build-config config* block {:navigated? navigated? :navigating-block navigating-block})
         blocks-container-id (:blocks-container-id config)
-        config (assoc config :block block)
-        ;; Each block might have multiple queries, but we store only the first query's result
-        config (if (nil? (:query-result config))
-                 (assoc config :query-result (atom nil))
-                 config)
-        config (if ref? (block-handler/attach-order-list-state config block) config)
         heading? (:heading properties)
         *control-show? (get state ::control-show?)
         db-collapsed? (util/collapsed? block)
@@ -2810,6 +2780,8 @@
         data-refs-self (build-refs-data-value refs)
         card? (string/includes? data-refs-self "\"card\"")
         review-cards? (:review-cards? config)
+        own-number-list? (:own-order-number-list? config)
+        order-list? (boolean own-number-list?)
         selected? (when-not slide?
                     (state/sub-block-selected? blocks-container-id uuid))]
     [:div.ls-block
@@ -2822,6 +2794,7 @@
                    (when pre-block? " pre-block")
                    (when (and card? (not review-cards?)) " shadow-md")
                    (when selected? " selected noselect")
+                   (when order-list? " is-order-list")
                    (when (string/blank? content) " is-blank"))
        :blockid (str uuid)
        :haschild (str (boolean has-child?))}
@@ -2850,7 +2823,7 @@
      (when top?
        (dnd-separator-wrapper block block-id slide? true false))
 
-     [:div.flex.flex-row.pr-2
+     [:div.block-main-container.flex.flex-row.pr-2
       {:class (if (and heading? (seq (:block/title block))) "items-baseline" "")
        :on-touch-start (fn [event uuid] (block-handler/on-touch-start event uuid))
        :on-touch-move (fn [event]
@@ -2872,7 +2845,7 @@
       (if whiteboard-block?
         (block-reference {} (str uuid) nil)
         ;; Not embed self
-        (let [block (merge block (block/parse-title-and-body uuid format pre-block? content))
+        (let [block (merge block (block/parse-title-and-body uuid (:block/format block) pre-block? content))
               hide-block-refs-count? (and (:embed? config)
                                           (= (:block/uuid block) (:embed-id config)))]
           (block-content-or-editor config block edit-input-id block-id edit? hide-block-refs-count?)))
@@ -3140,14 +3113,15 @@
 
         :else
         (let [language (if (contains? #{"edn" "clj" "cljc" "cljs"} language) "clojure" language)]
-          [:div {:ref (fn [el]
-                        (set-inside-portal? (and el (whiteboard-handler/inside-portal? el))))}
+          [:div.ui-fenced-code-editor
+           {:ref (fn [el]
+                   (set-inside-portal? (and el (whiteboard-handler/inside-portal? el))))}
            (cond
              (nil? inside-portal?) nil
 
              (or (:slide? config) inside-portal?)
              (highlight/highlight (str (random-uuid))
-                                  {:class (str "language-" language)
+                                  {:class     (str "language-" language)
                                    :data-lang language}
                                   code)
 
@@ -3316,10 +3290,14 @@
             [:sup.fn (str name "↩︎")]])]])
 
       ["Src" options]
-      [:div.cp__fenced-code-block
-       (if-let [opts (plugin-handler/hook-fenced-code-by-type (util/safe-lower-case (:language options)))]
-         (plugins/hook-ui-fenced-code (string/join "" (:lines options)) opts)
-         (src-cp config options html-export?))]
+      (let [lang (util/safe-lower-case (:language options))]
+        [:div.cp__fenced-code-block
+         {:data-lang lang}
+         (if-let [opts (plugin-handler/hook-fenced-code-by-type lang)]
+           [:div.ui-fenced-code-wrap
+            (src-cp config options html-export?)
+            (plugins/hook-ui-fenced-code (:block config) (string/join "" (:lines options)) opts)]
+           (src-cp config options html-export?))])
 
       :else
       "")

+ 1 - 1
src/main/frontend/components/block.css

@@ -1,7 +1,7 @@
 .block-content-wrapper {
   /* 38px is the width of block-control */
   width: calc(100% - 22px);
-
+  user-select: text;
   @screen sm {
     width: calc(100% - 33px);
     overflow-x: visible;

+ 71 - 0
src/main/frontend/components/block/macros.cljs

@@ -0,0 +1,71 @@
+(ns frontend.components.block.macros
+  "Logseq macros that render and evaluate in blocks"
+  (:require [clojure.walk :as walk]
+            [frontend.extensions.sci :as sci]
+            [frontend.handler.common :as common-handler]
+            [goog.string :as gstring]
+            [goog.string.format]))
+
+(defn- normalize-query-function
+  [ast result]
+  (let [ast (walk/prewalk
+             (fn [f]
+               (if (and (list? f)
+                        (keyword? (second f))
+                        (contains? #{'sum 'average 'count 'min 'max} (first f)))
+                 (if (contains? #{'min 'max} (first f))
+                   (list
+                    'apply
+                    (first f)
+                    (list 'map (second f) 'result))
+                   (list
+                    (first f)
+                    (list 'map (second f) 'result)))
+                 f))
+             ast)]
+    (walk/postwalk
+     (fn [f]
+       (cond
+         (keyword? f)
+         ;; These keyword aliases should be the same as those used in the query-table for sorting
+         (case f
+           :block
+           :block/content
+
+           :page
+           :block/name
+
+           :created-at
+           :block/created-at
+
+           :updated-at
+           :block/updated-at
+
+           (let [vals (map #(get-in % [:block/properties f]) result)
+                 int? (some integer? vals)]
+             `(~'fn [~'b]
+                    (~'let [~'result-str (~'get-in ~'b [:block/properties ~f])
+                            ~'result-num (~'parseFloat ~'result-str)
+                            ~'result (if (~'isNaN ~'result-num) ~'result-str ~'result-num)]
+                           (~'or ~'result (~'when ~int? 0))))))
+
+         :else
+         f))
+     ast)))
+
+(defn function-macro
+  "Provides functionality for {{function}}"
+  [query-result* arguments]
+  (let [query-result (if (map? query-result*)
+                       ;; Ungroup results grouped by page in page view
+                       (mapcat val query-result*)
+                       query-result*)
+        fn-string (-> (gstring/format "(fn [result] %s)" (first arguments))
+                      (common-handler/safe-read-string "failed to parse function")
+                      (normalize-query-function query-result)
+                      (str))
+        f (sci/eval-string fn-string)]
+    (when (fn? f)
+      (try (f query-result)
+           (catch :default e
+             (js/console.error e))))))

+ 24 - 23
src/main/frontend/components/bug_report.cljs

@@ -5,7 +5,8 @@
             [frontend.util :as util]
             [reitit.frontend.easy :as rfe]
             [clojure.string :as string]
-            [frontend.handler.notification :as notification]))
+            [frontend.handler.notification :as notification]
+            [frontend.context.i18n :refer [t]]))
 
 (defn parse-clipboard-data-transfer
   "parse dataTransfer
@@ -42,7 +43,7 @@
 
         copy-result-to-clipboard! (fn [result]
                                     (util/copy-to-clipboard! result)
-                                    (notification/show! "Copied to clipboard!"))
+                                    (notification/show! (t :bug-report/inspector-page-copy-notif)))
 
         reset-step! (fn []
                       (set-step! 0)
@@ -56,26 +57,26 @@
 
     [:div.flex.flex-col
      (when (= step 0)
-       (list [:div.mx-auto "Press Ctrl+V / ⌘+V to inspect your clipboard data"]
-             [:div.mx-auto "or click here to paste if you are using the mobile version"]
+       (list [:div.mx-auto (t :bug-report/inspector-page-desc-1)]
+             [:div.mx-auto (t :bug-report/inspector-page-desc-2)]
              ;; for mobile
-             [:input.form-input.is-large.transition.duration-150.ease-in-out {:type "text" :placeholder "Long press here to paste if you are on mobile"}]
+             [:input.form-input.is-large.transition.duration-150.ease-in-out {:type "text" :placeholder (t :bug-report/inspector-page-placeholder)}]
              [:div.flex.justify-between.items-center.mt-2
-              [:div "Something wrong? No problem, click to go back to the previous step."]
-              (ui/button "Go back" :on-click #(util/open-url (rfe/href :bug-report)))]))
+              [:div (t :bug-report/inspector-page-tip)]
+              (ui/button (t :bug-report/inspector-page-btn-back) :on-click #(util/open-url (rfe/href :bug-report)))]))
 
      (when (= step 1)
        (list
-        [:div "Here is the data read from clipboard."]
+        [:div (t :bug-report/inspector-page-desc-clipboard)]
         [:div.flex.justify-between.items-center.mt-2
-         [:div "If this is okay to share, click the copy button."]
-         (ui/button "Copy the result" :on-click #(copy-result-to-clipboard! (js/JSON.stringify (clj->js result) nil 2)))]
+         [:div (t :bug-report/inspector-page-desc-copy)]
+         (ui/button (t :bug-report/inspector-page-btn-copy) :on-click #(copy-result-to-clipboard! (js/JSON.stringify (clj->js result) nil 2)))]
         [:div.flex.justify-between.items-center.mt-2
-         [:div "Now you can report the result pasted to your clipboard. Please paste the result in the 'Additional Context' section and state where you copied the original content from. Thanks!"]
-         (ui/button "Create an issue" :href header/bug-report-url)]
+         [:div (t :bug-report/inspector-page-desc-create-issue)]
+         (ui/button (t :bug-report/inspector-page-btn-create-issue) :href header/bug-report-url)]
         [:div.flex.justify-between.items-center.mt-2
-         [:div "Something wrong? No problem, click to go back to the previous step."]
-         (ui/button "Go back" :on-click reset-step!)]
+         [:div (t :bug-report/inspector-page-tip)]
+         (ui/button (t :bug-report/inspector-page-btn-back) :on-click reset-step!)]
 
         [:pre.whitespace-pre-wrap [:code (js/JSON.stringify (clj->js result) nil 2)]]))]))
 
@@ -102,17 +103,17 @@
    [:div.flex.flex-col.items-center
     [:div.flex.items-center.mb-2
      (ui/icon "bug")
-     [:h1.text-3xl.ml-2 "Bug report"]]
-    [:div.opacity-60 "Can you help us out by submitting a bug report? We'll get it sorted out as soon as we can."]]
+     [:h1.text-3xl.ml-2 (t :bug-report/main-title)]]
+    [:div.opacity-60 (t :bug-report/main-desc)]]
    [:div.cp__bug-report-reporter.rounded-lg.p-8.mt-8
-    [:h1.text-2xl "Is the bug you encountered related to these features?"]
-    [:div.opacity-60 "You can use these handy tools to give us additional information."]
-    (report-item-button "Clipboard helper"
-                 "Inspect and collect clipboard data"
+    [:h1.text-2xl (t :bug-report/section-clipboard-title)]
+    [:div.opacity-60 (t :bug-report/section-clipboard-desc)]
+    (report-item-button (t :bug-report/section-clipboard-btn-title)
+                 (t :bug-report/section-clipboard-btn-desc)
                  "clipboard"
                  {:on-click #(util/open-url (rfe/href :bug-report-tools {:tool "clipboard-data-inspector"}))})
     [:div.py-2] ;; divider
     [:div.flex.flex-col
-     [:h1.text-2xl "Or..."]
-     [:div.opacity-60 "If there are no tools available for you to gather additional information, please report the bug directly."]
-     (report-item-button "Submit a bug report" "Help Make Logseq Better!" "message-report" {:on-click #(util/open-url header/bug-report-url)})]]])
+     [:h1.text-2xl (t :bug-report/section-issues-title)]
+     [:div.opacity-60 (t :bug-report/section-issues-desc)]
+     (report-item-button (t :bug-report/section-issues-btn-title) (t :bug-report/section-issues-btn-desc) "message-report" {:on-click #(util/open-url header/bug-report-url)})]]])

+ 21 - 10
src/main/frontend/components/container.cljs

@@ -35,12 +35,13 @@
             [frontend.ui :as ui]
             [frontend.util :as util]
             [frontend.util.cursor :as cursor]
+            [frontend.components.window-controls :as window-controls]
             [goog.dom :as gdom]
             [goog.object :as gobj]
+            [logseq.common.path :as path]
             [react-draggable]
             [reitit.frontend.easy :as rfe]
-            [rum.core :as rum]
-            [logseq.common.path :as path]))
+            [rum.core :as rum]))
 
 (rum/defc nav-content-item < rum/reactive
   [name {:keys [class]} child]
@@ -284,8 +285,8 @@
                               (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)))]
+                          (some->> (:width el-rect)
+                                   (/ touching-x-offset)))]
 
     (rum/use-effect!
      #(js/setTimeout
@@ -481,7 +482,7 @@
 
 (rum/defc main <
   {:did-mount (fn [state]
-                (when-let [element (gdom/getElement "main-content-container")]
+                (when-let [element (gdom/getElement "app-container")]
                   (dnd/subscribe!
                    element
                    :upload-files
@@ -492,19 +493,23 @@
                   (common-handler/listen-to-scroll! element)
                   (when (:margin-less-pages? (first (:rum/args state))) ;; makes sure full screen pages displaying without scrollbar
                     (set! (.. element -scrollTop) 0)))
-                state)}
+                state)
+   :will-unmount (fn [state]
+                   (when-let [el (gdom/getElement "app-container")]
+                     (dnd/unsubscribe! el :upload-files))
+                   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?)
+  (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 (and (mobile-util/native-platform?) 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?}])}
 
      ;; desktop left sidebar layout
      (left-sidebar {:left-sidebar-open? left-sidebar-open?
-                    :route-match route-match})
+                    :route-match        route-match})
 
      [:div#main-content-container.scrollbar-spacing.w-full.flex.justify-center.flex-row.outline-none.relative
 
@@ -536,7 +541,7 @@
          db-restoring?
          [:div.mt-20
           [:div.ls-center
-           (ui/loading (t :loading))]]
+           (ui/loading)]]
 
          :else
          [:div
@@ -730,6 +735,8 @@
         indexeddb-support? (state/sub :indexeddb/support?)
         page? (= :page route-name)
         home? (= :home route-name)
+        native-titlebar? (state/sub [:electron/user-cfgs :window/native-titlebar?])
+        window-controls? (and (util/electron?) (not util/mac?) (not native-titlebar?))
         edit? (:editor/editing? @state/state)
         default-home (get-default-home-if-valid)
         logged? (user-handler/logged-in?)
@@ -759,6 +766,7 @@
       {:class (util/classnames [{:ls-left-sidebar-open    left-sidebar-open?
                                  :ls-right-sidebar-open   sidebar-open?
                                  :ls-wide-mode            wide-mode?
+                                 :ls-window-controls      window-controls?
                                  :ls-fold-button-on-right fold-button-on-right?
                                  :ls-hl-colored           ls-block-hl-colored?}])}
 
@@ -795,6 +803,9 @@
                :show-action-bar?    show-action-bar?
                :show-recording-bar? show-recording-bar?})]
 
+       (when window-controls?
+         (window-controls/container))
+
        (right-sidebar/sidebar)
 
        [:div#app-single-container]]

+ 29 - 3
src/main/frontend/components/container.css

@@ -93,6 +93,7 @@
     height: calc(100vh - var(--ls-headbar-inner-top-padding) - 50px);
     margin-top: 30px;
     width: 100%;
+    padding-top: var(--ls-win32-title-bar-height);
 
     > .fake-bar {
       @apply w-full px-5 pt-1 sm:hidden;
@@ -450,6 +451,28 @@
   }
 }
 
+.ls-window-controls {
+  &.ls-right-sidebar-open {
+    .cp__right-sidebar-topbar {
+      margin-right: 144px;
+
+      .is-fullscreen & {
+        margin-right: 48px;
+      }
+    }
+  }
+
+  &:not(.ls-right-sidebar-open) {
+    .cp__header > .r {
+      margin-right: 144px;
+
+      .is-fullscreen & {
+        margin-right: 48px;
+      }
+    }
+  }
+}
+
 .ls-wide-mode {
   .cp__sidebar-main-content {
     max-width: var(--ls-main-content-max-width-wide);
@@ -465,7 +488,8 @@ html[data-theme='dark'] {
 }
 
 .settings-modal {
-  margin: -15px;
+  @apply -m-8 rounded-lg;
+  /* box-shadow: inset 0 0 0 1px var(--ls-border-color); */
 }
 
 .cp__sidebar-main-layout {
@@ -513,6 +537,7 @@ html[data-theme='dark'] {
 
   .resizer {
     @apply absolute top-0 bottom-0;
+    touch-action: none;
     left: 0;
     width: 4px;
     user-select: none;
@@ -537,7 +562,6 @@ html[data-theme='dark'] {
   }
 
   &.open {
-    width: var(--ls-right-sidebar-width);
     max-width: 60vw;
   }
 
@@ -573,7 +597,9 @@ html[data-theme='dark'] {
     user-select: none;
     -webkit-app-region: drag;
 
-    a, svg {
+    a,
+    svg,
+    button {
       -webkit-app-region: no-drag;
     }
   }

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

@@ -149,7 +149,7 @@
              [:p (t :file-rn/need-action)])
            [:p
             (ui/button
-             (str (t :file-rn/all-action) " (" (count rename-items) ")")
+             (t :file-rn/all-action (count rename-items))
              :on-click <rename-all
              :class "text-md p-2 mr-1")
             (t :file-rn/or-select-actions)

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

@@ -131,7 +131,7 @@
                                 nil
 
                                 (empty? matched-pages)
-                                (cons (str (t :new-page) ": " q) matched-pages)
+                                (cons q matched-pages)
 
                                ;; reorder, shortest and starts-with first.
                                 :else
@@ -142,8 +142,8 @@
                                                      matched-pages)]
                                   (if (gstring/caseInsensitiveStartsWith (first matched-pages) q)
                                     (cons (first matched-pages)
-                                          (cons  (str (t :new-page) ": " q) (rest matched-pages)))
-                                    (cons (str (t :new-page) ": " q) matched-pages))))]
+                                          (cons q (rest matched-pages)))
+                                    (cons q matched-pages))))]
             (ui/auto-complete
              matched-pages
              {:on-chosen   (page-handler/on-chosen-handler input id q pos format)
@@ -154,7 +154,9 @@
                                {:children
                                 [:div.flex
                                  (when (db-model/whiteboard-page? page-name) [:span.mr-1 (ui/icon "whiteboard" {:extension? true})])
-                                 (search/highlight-exact-query page-name q)]
+                                 [:div.flex.space-x-1
+                                  [:div (when-not (db/page-exists? page-name) (t :new-page))]
+                                  (search/highlight-exact-query page-name q)]]
                                 :open?           chosen?
                                 :manual?         true
                                 :fixed-position? true

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

@@ -152,7 +152,7 @@
        ;; wait for content load
        (and format
             (contains? (gp-config/text-formats) format))
-       (ui/loading "Loading ...")
+       (ui/loading)
 
        :else
        [:div (t :file/format-not-supported (name format))])]))

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

@@ -598,7 +598,7 @@
 
     [:div.version-list
      (if loading?
-       [:div.p-4 (ui/loading "Loading...")]
+       [:div.p-4 (ui/loading)]
        (for [version version-files]
          (let [version-uuid (get-version-key version)
                local?       (some? (:relative-path version))]
@@ -704,7 +704,7 @@
 
      ;; ready loading
      [:div.flex.items-center.h-full.justify-center.w-full.absolute.ready-loading
-      (ui/loading "Loading...")]]))
+      (ui/loading)]]))
 
 (defn pick-page-histories-panel [graph-uuid page-name]
   (fn []
@@ -789,7 +789,7 @@
    [:div.cloud-tip.rounded-md.mt-6.py-4
     [:div.items-center.opacity-90.flex.justify-center
      [:span.pr-2.flex (ui/icon "bell-ringing" {:class "font-semibold"})]
-     [:strong "Logseq Sync is still in Beta and we're working on a Pro plan!"]]
+     [:strong "Logseq Sync is still in Beta and we're working on a Pro plan!"]]]
 
     ;; [:ul.flex.py-6.px-4
     ;;  [:li.it
@@ -802,7 +802,7 @@
     ;;  [:li.it
     ;;   [:h1.dark:text-white "50G"]
     ;;   [:h2 "Total Storage"]]]
-    ]
+    
 
    [:div.pt-6.flex.justify-end.space-x-2
     (ui/button "Done" :on-click close-fn)]])

+ 14 - 10
src/main/frontend/components/header.cljs

@@ -29,7 +29,7 @@
   []
   (ui/with-shortcut :go/home "left"
     [:button.button.icon.inline
-     {:title "Home"
+     {:title (t :home)
       :on-click #(do
                    (when (mobile-util/native-iphone?)
                      (state/set-left-sidebar-open! false))
@@ -58,7 +58,7 @@
   [{:keys [on-click]}]
   (ui/with-shortcut :ui/toggle-left-sidebar "bottom"
     [:button.#left-menu.cp__header-left-menu.button.icon
-     {:title "Toggle left menu"
+     {:title (t :header/toggle-left-sidebar)
       :on-click on-click}
      (ui/icon "menu-2" {:size ui/icon-size})]))
 
@@ -86,7 +86,7 @@
      (fn [{:keys [toggle-fn]}]
        [:button.button.icon.toolbar-dots-btn
         {:on-click toggle-fn
-         :title "More"}
+         :title (t :header/more)}
         (ui/icon "dots" {:size ui/icon-size})])
      (->>
       [(when (state/enable-editing?)
@@ -128,8 +128,13 @@
           :options {:href (rfe/href :bug-report)}
           :icon (ui/icon "bug")})
 
+       (when config/publishing?
+         {:title (t :toggle-theme)
+          :options {:on-click #(state/toggle-theme!)}
+          :icon (ui/icon "bulb")})
+
        (when (and (state/sub :auth/id-token) (user-handler/logged-in?))
-         {:title (str (t :logout) " (" (user-handler/email) ")")
+         {:title (t :logout-user (user-handler/email))
           :options {:on-click #(user-handler/logout)}
           :icon  (ui/icon "logout")})]
       (concat page-menu-and-hr)
@@ -143,12 +148,12 @@
 
    (ui/with-shortcut :go/backward "bottom"
      [:button.it.navigation.nav-left.button.icon
-      {:title "Go back" :on-click #(js/window.history.back)}
+      {:title (t :header/go-back) :on-click #(js/window.history.back)}
       (ui/icon "arrow-left" {:size ui/icon-size})])
 
    (ui/with-shortcut :go/forward "bottom"
      [:button.it.navigation.nav-right.button.icon
-      {:title "Go forward" :on-click #(js/window.history.forward)}
+      {:title (t :header/go-forward) :on-click #(js/window.history.forward)}
       (ui/icon "arrow-right" {:size ui/icon-size})])])
 
 (rum/defc updater-tips-new-version
@@ -213,13 +218,13 @@
          (when-not (or (state/home?) custom-home-page? (state/whiteboard-dashboard?))
            (ui/with-shortcut :go/backward "bottom"
              [:button.it.navigation.nav-left.button.icon.opacity-70
-              {:title "Go back" :on-click #(js/window.history.back)}
+              {:title (t :header/go-back) :on-click #(js/window.history.back)}
               (ui/icon "chevron-left" {:size 26})]))
          ;; search button for non-mobile
          (when current-repo
            (ui/with-shortcut :go/search "right"
              [:button.button.icon#search-button
-              {:title "Search"
+              {:title (t :header/search)
                :on-click #(do (when (or (mobile-util/native-android?)
                                         (mobile-util/native-iphone?))
                                 (state/set-left-sidebar-open! false))
@@ -269,7 +274,6 @@
                       :current-repo current-repo
                       :default-home default-home})
 
-      (when (not (state/sub :ui/sidebar-open?))
-        (sidebar/toggle))
+      (sidebar/toggle)
 
       (updater-tips-new-version t)]]))

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

@@ -1,7 +1,9 @@
 .cp__header {
-  @apply z-10;
-
-  padding-top: var(--ls-headbar-inner-top-padding);
+  @apply shadow z-10;
+  -webkit-app-region: drag;
+  
+  padding-top: calc(var(--ls-headbar-inner-top-padding));
+  margin-top: var(--ls-win32-title-bar-height);
   height: calc(var(--ls-headbar-height) + var(--ls-headbar-inner-top-padding));
   display: flex;
   align-items: center;

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

@@ -16,7 +16,7 @@
                               [:span.mr-1 (t :help/forum-community)]
                               (ui/icon "message-circle" {:style {:font-size 20}})]
          list
-         [{:title "Usage"
+         [{:title (t :help/title-usage)
            :children [[[:a
                         {:on-click (fn [] (state/sidebar-add-block! (state/get-current-repo) "shortcut-settings" :shortcut-settings))}
                         [:div.flex-row.inline-flex.items-center
@@ -26,21 +26,21 @@
                       [(t :help/start) "https://docs.logseq.com/#/page/tutorial"]
                       ["FAQ" "https://docs.logseq.com/#/page/faq"]]}
 
-          {:title "Community"
+          {:title (t :help/title-community)
            :children [[(t :help/awesome-logseq) "https://github.com/logseq/awesome-logseq"]
                       [(t :help/blog) "https://blog.logseq.com"]
                       [discourse-with-icon "https://discuss.logseq.com"]]}
 
-          {:title "Development"
+          {:title (t :help/title-development)
            :children [[(t :help/roadmap) "https://trello.com/b/8txSM12G/roadmap"]
                       [(t :help/bug) "https://github.com/logseq/logseq/issues/new?labels=from:in-app&template=bug_report.yaml"]
                       [(t :help/feature) "https://discuss.logseq.com/c/feature-requests/"]
                       [(t :help/changelog) "https://docs.logseq.com/#/page/changelog"]]}
 
-          {:title "About"
+          {:title (t :help/title-about)
            :children [[(t :help/about) "https://blog.logseq.com/about/"]]}
 
-          {:title "Terms"
+          {:title (t :help/title-terms)
            :children [[(t :help/privacy) "https://blog.logseq.com/privacy-policy/"]
                       [(t :help/terms) "https://blog.logseq.com/terms/"]]}]]
 

+ 24 - 24
src/main/frontend/components/onboarding/quick_tour.cljs

@@ -18,7 +18,7 @@
   [^js jsTour]
   (let [^js el (js/document.createElement "button")]
     (.add (.-classList el) "cp__onboarding-skip-quick-tour")
-    (set! (.-innerHTML el) (h/render-html [:span [:i.ti.ti-player-skip-forward] "Skip Quick Tour"]))
+    (set! (.-innerHTML el) (h/render-html [:span [:i.ti.ti-player-skip-forward] (t :on-boarding/quick-tour-btn-skip)]))
     (.addEventListener el "click" #(.cancel jsTour))
     [#(.appendChild js/document.body el)
      #(.removeChild js/document.body el)]))
@@ -36,21 +36,21 @@
 
   (h/render-html
    [:div.steps
-    [:strong (str "STEP " current)]
+    [:strong (str (t :on-boarding/quick-tour-steps) current)]
     [:ul (for [i (range total)] [:li {:class (when (= current (inc i)) "active")} i])]]))
 
 (defn- create-steps! [^js jsTour]
   [
    ;; step 1
    {:id                "nav-help"
-    :text              (h/render-html [:section [:h2 "❓ Help"]
-                                       [:p "You can always click here for help and other information about Logseq."]])
+    :text              (h/render-html [:section [:h2 (t :on-boarding/quick-tour-help-title)]
+                                       [:p (t :on-boarding/quick-tour-help-desc)]])
     :attachTo          {:element ".cp__sidebar-help-btn" :on "top"}
     :beforeShowPromise #(if (state/sub :ui/sidebar-open?)
                           (wait-target state/hide-right-sidebar! 700)
                           (p/resolved true))
     :canClickTarget    true
-    :buttons           [{:text "Next" :action (.-next jsTour)}]
+    :buttons           [{:text (t :on-boarding/quick-tour-btn-next) :action (.-next jsTour)}]
     :popperOptions     {:modifiers [{:name    "preventOverflow"
                                      :options {:padding 20}}
                                     {:name    "offset"
@@ -58,11 +58,11 @@
 
    ;; step 2
    {:id                "nav-journal-page"
-    :text              (h/render-html [:section [:h2 "📆 Daily Journal Page"]
+    :text              (h/render-html [:section [:h2 (t :on-boarding/quick-tour-journal-page-title)]
                                        [:p
-                                        [:span "This is today’s daily journal page. Here you can dump your thoughts, learnings and ideas. Don’t worry about organizing. Just write and"]
-                                        [:a "[[link]]"]
-                                        [:span "your thoughts."]]])
+                                        [:span (t :on-boarding/quick-tour-journal-page-desc-1)]
+                                        [:a (t :on-boarding/quick-tour-journal-page-desc-2)]
+                                        [:span (t :on-boarding/quick-tour-journal-page-desc-3)]]])
 
     :attachTo          {:element ".page.is-journals .page-title" :on "top-end"}
     :beforeShowPromise #(if-not (= (util/safe-lower-case (state/get-current-page))
@@ -71,8 +71,8 @@
                                          (route-handler/redirect-to-page! (date/today))
                                          (util/scroll-to-top)) 200)
                           (p/resolved true))
-    :buttons           [{:text "Back" :classes "back" :action (.-back jsTour)}
-                        {:text "Next" :action (.-next jsTour)}]
+    :buttons           [{:text (t :on-boarding/quick-tour-btn-back) :classes "back" :action (.-back jsTour)}
+                        {:text (t :on-boarding/quick-tour-btn-next) :action (.-next jsTour)}]
     :popperOptions     {:modifiers [{:name    "preventOverflow"
                                      :options {:padding 63}}
                                     {:name    "offset"
@@ -80,13 +80,13 @@
 
    ;; step 3
    {:id                "nav-left-sidebar"
-    :text              (h/render-html [:section [:h2 "👀 Left Sidebar"]
-                                       [:p [:span "Open the left sidebar to explore important menu items in Logseq."]]])
+    :text              (h/render-html [:section [:h2 (t :on-boarding/quick-tour-left-sidebar-title)]
+                                       [:p [:span (t :on-boarding/quick-tour-left-sidebar-desc)]]])
 
     :attachTo          {:element "#left-menu" :on "top"}
     :beforeShowPromise #(p/resolved true)
-    :buttons           [{:text "Back" :classes "back" :action (.-back jsTour)}
-                        {:text "Next" :action (.-next jsTour)}]
+    :buttons           [{:text (t :on-boarding/quick-tour-btn-back) :classes "back" :action (.-back jsTour)}
+                        {:text (t :on-boarding/quick-tour-btn-next) :action (.-next jsTour)}]
     :popperOptions     {:modifiers [{:name    "preventOverflow"
                                      :options {:padding 20}}
                                     {:name    "offset"
@@ -94,15 +94,15 @@
 
    ;; step 4
    {:id                "nav-favorites"
-    :text              (h/render-html [:section [:h2 "⭐️ Favorites"]
-                                       [:p "Pin your favorite pages via the `... `menu on any page."]
-                                       [:p "We’ve also added some template pages here to help you get started. You can remove these once you start writing your own notes."]])
+    :text              (h/render-html [:section [:h2 (t :on-boarding/quick-tour-favorites-title)]
+                                       [:p (t :on-boarding/quick-tour-favorites-desc-1)]
+                                       [:p (t :on-boarding/quick-tour-favorites-desc-2)]])
     :beforeShowPromise #(if-not (state/sub :ui/left-sidebar-open?)
                           (wait-target state/toggle-left-sidebar! 500)
                           (p/resolved true))
     :attachTo          {:element ".nav-content-item.favorites" :on "right"}
-    :buttons           [{:text "Back" :classes "back" :action (.-back jsTour)}
-                        {:text "Finish" :action (.-complete jsTour)}]}
+    :buttons           [{:text (t :on-boarding/quick-tour-btn-back) :classes "back" :action (.-back jsTour)}
+                        {:text (t :on-boarding/quick-tour-btn-finish) :action (.-complete jsTour)}]}
    ])
 
 (defn- create-steps-file-sync! [^js jsTour]
@@ -164,7 +164,7 @@
                          (wait-target ".nav-header .whiteboard" 500)
                          (util/scroll-to-top))
     :canClickTarget    true
-    :buttons           [{:text "Next" :action (.-next jsTour)}]
+    :buttons           [{:text (t :on-boarding/tour-whiteboard-btn-next) :action (.-next jsTour)}]
     :popperOptions     {:modifiers [{:name    "preventOverflow"
                                      :options {:padding 20}}
                                     {:name    "offset"
@@ -178,8 +178,8 @@
                          (route-handler/redirect-to-whiteboard-dashboard!)
                          (wait-target ".dashboard-create-card" 500))
     :attachTo          {:element ".dashboard-create-card" :on "bottom"}
-    :buttons           [{:text "Back" :classes "back" :action (.-back jsTour)}
-                        {:text "Finish" :action (.-complete jsTour)}]
+    :buttons           [{:text (t :on-boarding/tour-whiteboard-btn-back) :classes "back" :action (.-back jsTour)}
+                        {:text (t :on-boarding/tour-whiteboard-btn-finish) :action (.-complete jsTour)}]
     :popperOptions     {:modifiers [{:name    "preventOverflow"
                                      :options {:padding 20}}
                                     {:name    "offset"
@@ -277,7 +277,7 @@
 
 (defn init []
   (command-palette/register {:id     :document/quick-tour
-                             :desc   "Quick tour for onboarding"
+                             :desc   (t :on-boarding/command-palette-quick-tour)
                              :action #(ready start)})
 
   ;; TODO: fix logic

+ 20 - 20
src/main/frontend/components/onboarding/setups.cljs

@@ -30,13 +30,13 @@
 
       [:h1.text-xl
        (if picker?
-         [:span [:strong (ui/icon "heart")] "Welcome to " [:strong "Logseq!"]]
-         [:span [:strong (ui/icon "file-import")] "Import existing notes"])]
+         [:span [:strong (ui/icon "heart")] (t :on-boarding/main-title) [:strong "Logseq!"]]
+         [:span [:strong (ui/icon "file-import")] (t :on-boarding/importing-main-title)])]
 
       [:h2
        (if picker?
-         "First you need to choose a folder where Logseq will store your thoughts, ideas, notes."
-         "You can also do this later in the app.")]
+         (t :on-boarding/main-desc)
+         (t :on-boarding/importing-main-desc))]
 
       content])])
 
@@ -93,8 +93,8 @@
 
               (if parsing?
                 (ui/loading "")
-                [[:strong "Choose a folder"]
-                 [:small "Open existing directory or Create a new one"]])]]]
+                [[:strong (t :on-boarding/section-btn-title)]
+                 [:small (t :on-boarding/section-btn-desc)]])]]]
            [:div.px-5
             (ui/admonition :warning
                            (widgets/native-fs-api-alert))]))]
@@ -102,22 +102,22 @@
        [:p.flex
         [:i.as-flex-center (ui/icon "zoom-question" {:style {:fontSize "22px"}})]
         [:span.flex-1.flex.flex-col
-         [:strong "How Logseq saves your work"]
-         [:small.opacity-60 "Inside the directory you choose, Logseq will create 4 folders."]]]
+         [:strong (t :on-boarding/section-title)]
+         [:small.opacity-60 (t :on-boarding/section-desc)]]]
 
        [:p.text-sm.pt-5.tracking-wide
-        [:span (str "Each page is a file stored only on your " DEVICE ".")]
+        [:span (str (t :on-boarding/section-tip-1) DEVICE ".")]
         [:br]
-        [:span "You may choose to sync it later."]]
+        [:span (t :on-boarding/section-tip-2)]]
 
        [:ul
         (for [[title label icon]
-              [["Graphics & Documents" "/assets" "whiteboard"]
-               ["Daily notes" "/journals" "calendar-plus"]
-               ["PAGES" "/pages" "page"]
+              [[(t :on-boarding/section-assets) "/assets" "whiteboard"]
+               [(t :on-boarding/section-journals) "/journals" "calendar-plus"]
+               [(t :on-boarding/section-pages) "/pages" "page"]
                []
-               ["APP Internal" "/logseq" "tool"]
-               ["Config File" "/logseq/config.edn"]]]
+               [(t :on-boarding/section-app) "/logseq" "tool"]
+               [(t :on-boarding/section-config) "/logseq/config.edn"]]]
           (if-not title
             [:li.hr]
             [:li
@@ -221,14 +221,14 @@
      :importer
      [:article.flex.flex-col.items-center.importer.py-16.px-8
       [:section.c.text-center
-       [:h1 "Do you already have notes that you want to import?"]
-       [:h2 "If they are in a JSON, EDN or Markdown format Logseq can work with them."]]
+       [:h1 (t :on-boarding/importing-title)]
+       [:h2 (t :on-boarding/importing-desc)]]
       [:section.d.md:flex
        [:label.action-input.flex.items-center.mx-2.my-2
         [:span.as-flex-center [:i (svg/roam-research 28)]]
         [:div.flex.flex-col
          [[:strong "RoamResearch"]
-          [:small "Import a JSON Export of your Roam graph"]]]
+          [:small (t :on-boarding/importing-roam-desc)]]]
         [:input.absolute.hidden
          {:id        "import-roam"
           :type      "file"
@@ -238,7 +238,7 @@
         [:span.as-flex-center [:i (svg/logo 28)]]
         [:span.flex.flex-col
          [[:strong "EDN / JSON"]
-          [:small "Import an EDN or a JSON Export of your Logseq graph"]]]
+          [:small (t :on-boarding/importing-lsq-desc)]]]
         [:input.absolute.hidden
          {:id        "import-lsq"
           :type      "file"
@@ -248,7 +248,7 @@
         [:span.as-flex-center (ui/icon "sitemap" {:style {:fontSize "26px"}})]
         [:span.flex.flex-col
          [[:strong "OPML"]
-          [:small " Import OPML files"]]]
+          [:small (t :on-boarding/importing-opml-desc)]]]
 
         [:input.absolute.hidden
          {:id        "import-opml"

+ 18 - 20
src/main/frontend/components/page.cljs

@@ -202,8 +202,7 @@
          [:ul.mt-2
           (for [[original-name name] (sort-by last pages)]
             [:li {:key (str "tagged-page-" name)}
-             [:a {:href (rfe/href :page {:name name})}
-              original-name]])]
+             (component-block/page-cp {} {:block/name name :block/original-name original-name})])]
          {:default-collapsed? false})]])))
 
 (rum/defc page-title-editor < rum/reactive
@@ -213,6 +212,11 @@
                              (util/page-name-sanity-lc @*title-value))
                        (db/page-exists? page-name)
                        (db/page-exists? @*title-value))
+        rollback-fn #(do
+                       (reset! *title-value old-name)
+                       (gobj/set (rum/deref input-ref) "value" old-name)
+                       (reset! *edit? true)
+                       (.focus (rum/deref input-ref)))
         confirm-fn (fn []
                      (let [new-page-name (string/trim @*title-value)]
                        (ui/make-confirm-modal
@@ -223,16 +227,7 @@
                                           (close-fn)
                                           (page-handler/rename! (or title page-name) @*title-value)
                                           (reset! *edit? false))
-                         :on-cancel     (fn []
-                                          (reset! *title-value old-name)
-                                          (gobj/set (rum/deref input-ref) "value" old-name)
-                                          (reset! *edit? true)
-                                          (.focus (rum/deref input-ref)))})))
-        rollback-fn #(do
-                       (reset! *title-value old-name)
-                       (gobj/set (rum/deref input-ref) "value" old-name)
-                       (reset! *edit? false)
-                       (when-not untitled? (notification/show! "Illegal page name, can not rename!" :warning)))
+                         :on-cancel     rollback-fn})))
         blur-fn (fn [e]
                   (when (gp-util/wrapped-by-quotes? @*title-value)
                     (swap! *title-value gp-util/unquote-string)
@@ -242,13 +237,16 @@
                     (reset! *edit? false)
 
                     (string/blank? @*title-value)
-                    (rollback-fn)
+                    (do (when-not untitled? (notification/show! (t :page/illegal-page-name) :warning))
+                        (rollback-fn))
 
-                    (and (collide?) whiteboard-page?)
-                    (notification/show! (str "Page “" @*title-value "” already exists!") :error)
+                    (and (collide?) (or whiteboard-page? (model/whiteboard-page? @*title-value)))
+                    (do (notification/show! (t :page/page-already-exists @*title-value) :error)
+                        (rollback-fn))
 
                     (and (date/valid-journal-title? @*title-value) whiteboard-page?)
-                    (notification/show! (str "Whiteboard page cannot be renamed with journal titles!") :error)
+                    (do (notification/show! (t :page/whiteboard-to-journal-error) :error)
+                        (rollback-fn))
 
                     untitled?
                     (page-handler/rename! (or title page-name) @*title-value)
@@ -815,7 +813,7 @@
       [:div.mt-3.text-center.sm:mt-0.sm:ml-4.sm:text-left
        [:h3#modal-headline.text-lg.leading-6.font-medium
         (if orphaned-pages?
-          (str (t :remove-orphaned-pages) "?")
+          (t :remove-orphaned-pages)
           (t :page/delete-confirmation))]]]
 
      [:table.table-auto.cp__all_pages_table.mt-4
@@ -851,7 +849,7 @@
                     (close-fn)
                     (doseq [page-name (map :block/name pages)]
                       (page-handler/delete! page-name #()))
-                    (notification/show! (str (t :tips/all-done) "!") :success)
+                    (notification/show! (t :tips/all-done) :success)
                     (js/setTimeout #(refresh-fn) 200)))]]))
 
 (rum/defc pagination
@@ -1036,7 +1034,7 @@
          [:div.r.flex.items-center.justify-between
           [:div
            (ui/tippy
-            {:html  [:small (str (t :page/show-whiteboards) " ?")]
+            {:html  [:small (t :page/show-whiteboards)]
              :arrow true}
             [:a.button.whiteboard
              {:class    (util/classnames [{:active (boolean @*whiteboard?)}])
@@ -1044,7 +1042,7 @@
              (ui/icon "whiteboard" {:extension? true :style {:fontSize ui/icon-size}})])]
           [:div
            (ui/tippy
-            {:html  [:small (str (t :page/show-journals) " ?")]
+            {:html  [:small (t :page/show-journals)]
              :arrow true}
             [:a.button.journal
              {:class    (util/classnames [{:active (boolean @*journal?)}])

+ 100 - 27
src/main/frontend/components/plugins.cljs

@@ -5,6 +5,7 @@
             [frontend.context.i18n :refer [t]]
             [frontend.ui :as ui]
             [frontend.handler.ui :as ui-handler]
+            [frontend.handler.editor :as editor-handler]
             [frontend.handler.plugin-config :as plugin-config-handler]
             [frontend.handler.common.plugin :as plugin-common-handler]
             [frontend.search :as search]
@@ -191,10 +192,7 @@
   (ui/admonition
     :warning
     [:p.text-md
-     "Plugins can access your graph and your local files, issue network requests.
-       They can also cause data corruption or loss. We're working on proper access rules for your graphs.
-       Meanwhile, make sure you have regular backups of your graphs and only install the plugins when you can read and
-       understand the source code."]))
+     (t :plugin/security-warning)]))
 
 (rum/defc card-ctls-of-market < rum/static
   [item stat installed? installing-or-updating?]
@@ -273,7 +271,7 @@
         (if installing-or-updating?
           (t :plugin/updating)
           (if new-version
-            (str (t :plugin/update) " 👉 " new-version)
+            [:span (t :plugin/update) " 👉 " new-version]
             (t :plugin/check-update)))]])
 
     (ui/toggle (not disabled?)
@@ -365,7 +363,7 @@
                     (.focus target))}
       (ui/icon "x")])
    [:input.form-input.is-small
-    {:placeholder "Search plugins"
+    {:placeholder (t :plugin/search-plugin)
      :ref         *search-ref
      :auto-focus  true
      :on-key-down (fn [^js e]
@@ -459,6 +457,22 @@
                                 (state/set-state! [:electron/user-cfgs :settings/agent] opts)
                                 (state/close-sub-modal! :https-proxy-panel))))]]]))
 
+(rum/defc auto-check-for-updates-control
+  []
+  (let [[enabled, set-enabled!] (rum/use-state (plugin-handler/get-enabled-auto-check-for-updates?))
+        text (t :plugin/auto-check-for-updates)]
+
+    [:div.flex.items-center.justify-between.px-4.py-2
+     {:on-click (fn []
+                  (let [t (not enabled)]
+                    (set-enabled! t)
+                    (plugin-handler/set-enabled-auto-check-for-updates t)
+                    (notification/show!
+                      [:span text [:strong.pl-1 (if t "ON" "OFF")] "!"]
+                      (if t :success :info))))}
+     [:span.pr-3.opacity-80 text]
+     (ui/toggle enabled #() true)]))
+
 (rum/defc ^:large-vars/cleanup-todo panel-control-tabs < rum/static
   [search-key *search-key category *category
    sort-by *sort-by filter-by *filter-by total-nums
@@ -557,7 +571,7 @@
               :options {:on-click #(reset! *sort-by :stars)}
               :icon    (ui/icon (aim-icon :stars))}
 
-             {:title   (str (t :plugin/title) " (A - Z)")
+             {:title   (t :plugin/title "A - Z")
               :options {:on-click #(reset! *sort-by :letters)}
               :icon    (ui/icon (aim-icon :letters))}])
           {}))
@@ -585,14 +599,18 @@
 
                 (when (state/developer-mode?)
                   [{:hr true}
-                   {:title   [:span.flex.items-center (ui/icon "file-code") "Open Preferences"]
+                   {:title   [:span.flex.items-center (ui/icon "file-code") (t :plugin/open-preferences)]
                     :options {:on-click
                               #(p/let [root (plugin-handler/get-ls-dotdir-root)]
                                  (js/apis.openPath (str root "/preferences.json")))}}
-                   {:title   [:span.flex.items-center (ui/icon "bug") "Open " [:code " ~/.logseq"]]
+                   {:title   [:span.flex.items-center.whitespace-nowrap.space-x-1 (ui/icon "bug") (t :plugin/open-logseq-dir) [:code "~/.logseq"]]
                     :options {:on-click
                               #(p/let [root (plugin-handler/get-ls-dotdir-root)]
-                                 (js/apis.openPath root))}}]))
+                                 (js/apis.openPath root))}}])
+
+                [{:hr true :key "dropdown-more"}
+                 {:title   (auto-check-for-updates-control)
+                  :options {:no-padding? true}}])
         {})
 
       ;; developer
@@ -730,7 +748,7 @@
        [:p.flex.justify-center.py-20 svg/loading]
 
        @*error
-       [:p.flex.justify-center.pt-20.opacity-50 "Remote error: " (.-message @*error)]
+       [:p.flex.justify-center.pt-20.opacity-50 (t :plugin/remote-error) (.-message @*error)]
 
        :else
        [:div.cp__plugins-marketplace-cnt
@@ -895,7 +913,7 @@
                 [:span.opacity-30.hover:opacity-80 (ui/icon "info-circle")]))]])]
 
        ;; all done
-       [:div.py-4 [:strong.text-4xl "\uD83C\uDF89 All updated!"]])
+       [:div.py-4 [:strong.text-4xl (str "\uD83C\uDF89 " (t :plugin/all-updated))]])
 
      ;; actions
      (when (seq updates)
@@ -1047,7 +1065,11 @@
                       [:span (t :plugin/found-updates)] (ui/point "bg-red-600" 5 {:style {:margin-top 2}})]
             :options {:on-click #(open-waiting-updates-modal!)
                       :class    "extra-item"}
-            :icon    (ui/icon "download")})])
+            :icon    (ui/icon "download")})]
+
+        [{:hr true :key "dropdown-more"}
+         {:title (auto-check-for-updates-control)
+          :options {:no-padding? true}}])
       {:trigger-class "toolbar-plugins-manager-trigger"})))
 
 (rum/defc header-ui-items-list-wrap
@@ -1116,14 +1138,61 @@
             (let [updates-coming (state/sub :plugin/updates-coming)]
               (toolbar-plugins-manager-list updates-coming items)))]]))))
 
-(rum/defcs hook-ui-fenced-code < rum/reactive
-  [_state content {:keys [render edit] :as _opts}]
+(rum/defc hook-ui-fenced-code
+  [block content {:keys [render edit] :as _opts}]
 
-  [:div
-   {:on-mouse-down (fn [e] (when (false? edit) (util/stop e)))
-    :class         (util/classnames [{:not-edit (false? edit)}])}
-   (when (fn? render)
-     (js/React.createElement render #js {:content content}))])
+  (let [[content1 set-content1!] (rum/use-state content)
+        [editor-active? set-editor-active!] (rum/use-state (string/blank? content))
+        *cm (rum/use-ref nil)
+        *el (rum/use-ref nil)]
+
+    (rum/use-effect!
+      #(set-content1! content)
+      [content])
+
+    (rum/use-effect!
+      (fn []
+        (some-> (rum/deref *el)
+                (.closest ".ui-fenced-code-wrap")
+                (.-classList)
+                (#(if editor-active?
+                    (.add % "is-active")
+                    (.remove % "is-active"))))
+        (when-let [cm (rum/deref *cm)]
+          (.refresh cm)
+          (.focus cm)
+          (.setCursor cm (.lineCount cm) (count (.getLine cm (.lastLine cm))))))
+      [editor-active?])
+
+    (rum/use-effect!
+      (fn []
+        (let [t (js/setTimeout
+                  #(when-let [^js cm (some-> (rum/deref *el)
+                                             (.closest ".ui-fenced-code-wrap")
+                                             (.querySelector ".CodeMirror")
+                                             (.-CodeMirror))]
+                     (rum/set-ref! *cm cm)
+                     (doto cm
+                       (.on "change" (fn []
+                                       (some-> cm (.getDoc) (.getValue) (set-content1!))))))
+                  ;; wait for the cm loaded
+                  1000)]
+          #(js/clearTimeout t)))
+      [])
+
+    [:div.ui-fenced-code-result
+     {:on-mouse-down (fn [e] (when (false? edit) (util/stop e)))
+      :class         (util/classnames [{:not-edit (false? edit)}])
+      :ref           *el}
+     [:<>
+      [:span.actions
+       {:on-mouse-down #(util/stop %)}
+       (ui/button (ui/icon "square-toggle-horizontal" {:size 14})
+                  :on-click #(set-editor-active! (not editor-active?)))
+       (ui/button (ui/icon "source-code" {:size 14})
+                  :on-click #(editor-handler/edit-block! block (count content1) (:block/uuid block)))]
+      (when (fn? render)
+        (js/React.createElement render #js {:content content1}))]]))
 
 (rum/defc plugins-page
   []
@@ -1183,7 +1252,7 @@
         [sub-content, _set-sub-content!] (rum-utils/use-atom *updates-sub-content)
         notify! (fn [content status]
                   (if auto-checking?
-                    (println "Plugin Updates: " content)
+                    (println (t :plugin/list-of-updates) content)
                     (let [cb #(plugin-handler/cancel-user-checking!)]
                       (try
                         (set-uid (notification/show! content status false uid nil cb))
@@ -1195,7 +1264,7 @@
         (if check-pending?
           (notify!
             [:div
-             [:div (str "Checking for plugin updates ...")]
+             [:div (t :plugin/checking-for-updates)]
              (when sub-content [:p.opacity-60 sub-content])]
             (ui/loading ""))
           (when uid (notification/clear! uid))))
@@ -1206,9 +1275,11 @@
       (fn []
         (when online?
           (let [last-updates (storage/get :lsp-last-auto-updates)]
-            (when (or (not (number? last-updates))
-                      ;; interval 12 hours
-                      (> (- (js/Date.now) last-updates) (* 60 60 12 1000)))
+            (when (and (not (false? last-updates))
+                       (or (true? last-updates)
+                           (not (number? last-updates))
+                           ;; interval 12 hours
+                           (> (- (js/Date.now) last-updates) (* 60 60 12 1000))))
               (js/setTimeout
                 (fn []
                   (plugin-handler/auto-check-enabled-for-updates!)
@@ -1237,7 +1308,8 @@
 
     [:div.cp__plugins-settings.cp__settings-main
      [:header
-      [:h1.title (ui/icon "puzzle") (str " " (or title (t :settings-of-plugins)))]]
+      [:h1.title (ui/icon "puzzle" {:size 22})
+       [:strong (or title (t :settings-of-plugins))]]]
 
      [:div.cp__settings-inner.md:flex
       {:class (util/classnames [{:no-aside (not nav?)}])}
@@ -1247,7 +1319,7 @@
            [:ul.settings-plugin-list
             (for [{:keys [id name title icon]} plugins]
               [:li
-               {:class (util/classnames [{:active (= id focused)}])}
+               {:key id :class (util/classnames [{:active (= id focused)}])}
                [:a.flex.items-center.settings-plugin-item
                 {:data-id  id
                  :on-click #(do (state/set-state! :plugin/focused-settings id))}
@@ -1332,4 +1404,5 @@
       [:div.settings-modal.of-plugins
        (focused-settings-content title)])
     {:center? false
+     :label   "plugin-settings-modal"
      :id      "ls-focused-settings-modal"}))

+ 65 - 16
src/main/frontend/components/plugins.css

@@ -482,6 +482,15 @@
   }
 
   &-settings {
+    > header {
+      padding: 8px 12px;
+      border-bottom: 1px solid var(--ls-quaternary-background-color);
+
+      h1 {
+        @apply flex items-center text-[22px] m-0 space-x-1;
+      }
+    }
+
     &-inner {
       position: relative;
       padding: 10px 0 20px;
@@ -598,25 +607,23 @@
       }
     }
 
-    aside {
-      max-height: 70vh;
-      overflow: auto;
-      margin-bottom: -17px;
+    .cp__settings-inner {
+      aside {
+        @apply max-h-[70vh] overflow-auto mb-[-17px] p-3;
 
-      ul {
-        img.icon {
-          height: 24px;
-          width: 24px;
-        }
+        ul {
+          @apply list-none p-0 m-0;
 
-        li {
-          strong {
-            font-weight: 400;
-            overflow: hidden;
-            height: 22px;
+          img.icon {
+            @apply w-[24px] h-[24px];
+          }
 
-            text-overflow: ellipsis;
-            white-space: nowrap;
+          li {
+            @apply p-1.5 rounded;
+
+            strong {
+              @apply overflow-hidden text-ellipsis whitespace-nowrap;
+            }
           }
         }
       }
@@ -898,6 +905,42 @@
   }
 }
 
+.ui-fenced-code {
+  &-wrap {
+    &:not(.is-active) {
+      .ui-fenced-code-editor {
+        display: none !important;
+      }
+    }
+
+    &.is-active {
+      .ui-fenced-code-result {
+        @apply pt-2;
+      }
+    }
+  }
+
+  &-result {
+    @apply relative;
+
+    > .actions {
+      @apply absolute top-2 right-1 z-10 opacity-0 transition-opacity;
+
+      .ui__button {
+        font-size: 13px;
+        padding: 4px;
+        margin-left: 6px;
+      }
+    }
+
+    &:hover {
+      > .actions {
+        @apply opacity-100;
+      }
+    }
+  }
+}
+
 .lsp-frame-readme {
   margin: -2rem;
   min-height: 75vh;
@@ -939,6 +982,12 @@ html[data-theme='dark'] {
   }
 }
 
+.ui__modal[label=plugin-settings-modal] {
+  .ui__modal-close-wrap {
+    padding-top: 14px;
+  }
+}
+
 .ui__modal[label=plugins-dashboard] {
   .panel-content {
     overflow-y: auto;

+ 10 - 13
src/main/frontend/components/query.cljs

@@ -59,7 +59,6 @@
                                      (:block/name (ffirst result))
                                      (:block/uuid (first (second (first result))))
                                      true)]
-    (println "this should be a function" inline)
     (if @*query-error
       (do
         (log/error :exception @*query-error)
@@ -142,10 +141,8 @@
              (when-not (or built-in? dsl-query?)
                (when collapsed?
                  (editor-handler/collapse-block! current-block-uuid))))
-           state)}
-  (rum/local nil ::query-result)
-  {:init (fn [state] (assoc state :query-error (atom nil)))}
-  [state config {:keys [title builder query view collapsed? table-view?] :as q} *query-triggered?]
+           (assoc state :query-error (atom nil)))}
+  [state config {:keys [title builder query view collapsed? table-view?] :as q}]
   (let [*query-error (:query-error state)
         built-in? (built-in-custom-query? title)
         dsl-query? (:dsl-query? config)
@@ -166,11 +163,12 @@
         view-f (and view-fn (sci/eval-string (pr-str view-fn)))
         dsl-page-query? (and dsl-query?
                              (false? (:blocks? (query-dsl/parse-query query))))
+        ;; FIXME: This isn't getting set for full-text searches
         full-text-search? (and dsl-query?
                                (util/electron?)
                                (symbol? (gp-util/safe-read-string query)))
         result (when (or built-in-collapsed? (not collapsed?'))
-                 (query-result/get-query-result state config *query-error *query-triggered? current-block-uuid q {:table? table?}))
+                 (query-result/get-query-result config q *query-error current-block-uuid {:table? table?}))
         query-time (:query-time (meta result))
         page-list? (and (seq result)
                         (some? (:block/name (first result))))
@@ -209,13 +207,13 @@
                  (if table?
                    [:a.flex.ml-1.fade-link {:title "Switch to list view"
                                             :on-click (fn [] (editor-property/set-block-property! current-block-uuid
-                                                                                                 "query-table"
-                                                                                                 false))}
+                                                                                                  "query-table"
+                                                                                                  false))}
                     (ui/icon "list" {:style {:font-size 20}})]
                    [:a.flex.ml-1.fade-link {:title "Switch to table view"
                                             :on-click (fn [] (editor-property/set-block-property! current-block-uuid
-                                                                                                 "query-table"
-                                                                                                 true))}
+                                                                                                  "query-table"
+                                                                                                  true))}
                     (ui/icon "table" {:style {:font-size 20}})]))
 
                [:a.flex.ml-1.fade-link
@@ -231,7 +229,7 @@
                   (query-refresh-button query-time {:full-text-search? full-text-search?
                                                     :on-mouse-down (fn [e]
                                                                      (util/stop e)
-                                                                     (query-result/trigger-custom-query! state *query-error *query-triggered?))}))]])])
+                                                                     (query-result/trigger-custom-query! config q *query-error))}))]])])
 
          (when dsl-query? builder)
 
@@ -248,11 +246,10 @@
               (custom-query-inner config q opts))])]))))
 
 (rum/defcs custom-query < rum/static
-  (rum/local false ::query-triggered?)
   [state config q]
   (ui/catch-error
    (ui/block-error "Query Error:" {:content (:query q)})
    (ui/lazy-visible
     (fn []
-      (custom-query* config q (::query-triggered? state)))
+      (custom-query* config q))
     {:debug-id q})))

+ 29 - 29
src/main/frontend/components/query/result.cljs

@@ -13,9 +13,8 @@
             [frontend.modules.outliner.tree :as tree]))
 
 (defn trigger-custom-query!
-  [state *query-error *query-triggered?]
-  (let [[config query] (:rum/args state)
-        repo (state/get-current-repo)
+  [config query *query-error]
+  (let [repo (state/get-current-repo)
         result-atom (atom nil)
         current-block-uuid (or (:block/uuid (:block config))
                                (:block/uuid config))
@@ -45,40 +44,41 @@
                      (catch :default e
                        (reset! *query-error e)
                        (atom nil)))]
-    (when *query-triggered?
-      (reset! *query-triggered? true))
     (if (instance? Atom query-atom)
       query-atom
       result-atom)))
 
-(defn get-group-by-page [{:keys [result-transform query] :as q}
+(defn get-group-by-page [{:keys [result-transform query] :as query-m}
                          {:keys [table?]}]
   (if table?
     false ;; Immediately return false as table view can't handle grouping
-    (get q :group-by-page?
+    (get query-m :group-by-page?
          (and (not result-transform)
               (not (and (string? query) (string/includes? query "(by-page false)")))))))
 
 (defn get-query-result
-  [state config *query-error *query-triggered? current-block-uuid q options]
-  (or (when-let [*result (:query-result config)] @*result)
-      (let [query-atom (trigger-custom-query! state *query-error *query-triggered?)
-            query-result (and query-atom (rum/react query-atom))
-            ;; exclude the current one, otherwise it'll loop forever
-            remove-blocks (if current-block-uuid [current-block-uuid] nil)
-            transformed-query-result (when query-result
-                                       (let [result (db/custom-query-result-transform query-result remove-blocks q)]
-                                         (if (and query-result (coll? result) (:block/uuid (first result)))
-                                           (cond-> result
-                                            (get q :remove-block-children? true)
-                                            tree/filter-top-level-blocks)
-                                           result)))
-            group-by-page? (get-group-by-page q options)
-            result (if (and group-by-page? (:block/uuid (first transformed-query-result)))
-                     (let [result (db-utils/group-by-page transformed-query-result)]
-                       (if (map? result)
-                         (dissoc result nil)
-                         result))
-                     transformed-query-result)]
-        (when query-atom
-          (util/safe-with-meta result (meta @query-atom))))))
+  "Fetches a query's result, transforms it as needed and saves the result into
+  an atom that is passed in as an argument"
+  [config query-m *query-error current-block-uuid options]
+  (let [query-atom (trigger-custom-query! config query-m *query-error)
+        query-result (and query-atom (rum/react query-atom))
+        ;; exclude the current one, otherwise it'll loop forever
+        remove-blocks (if current-block-uuid [current-block-uuid] nil)
+        transformed-query-result (when query-result
+                                   (let [result (db/custom-query-result-transform query-result remove-blocks query-m)]
+                                     (if (and query-result (coll? result) (:block/uuid (first result)))
+                                       (cond-> result
+                                         (get query-m :remove-block-children? true)
+                                         tree/filter-top-level-blocks)
+                                       result)))
+        group-by-page? (get-group-by-page query-m options)
+        result (if (and group-by-page? (:block/uuid (first transformed-query-result)))
+                 (let [result (db-utils/group-by-page transformed-query-result)]
+                   (if (map? result)
+                     (dissoc result nil)
+                     result))
+                 transformed-query-result)]
+    (when-let [query-result (:query-result config)]
+      (reset! query-result result))
+    (when query-atom
+      (util/safe-with-meta result (meta @query-atom)))))

+ 2 - 2
src/main/frontend/components/repo.cljs

@@ -107,7 +107,7 @@
        [:div.pl-1.content.mt-3
 
         [:div
-         [:h2.text-lg.font-medium.my-4 (str (t :graph/local-graphs) ":")]
+         [:h2.text-lg.font-medium.my-4 (t :graph/local-graphs)]
          (when (seq local-graphs)
            (repos-inner local-graphs))
 
@@ -123,7 +123,7 @@
           [:div
            [:hr]
            [:div.flex.align-items.justify-between
-            [:h2.text-lg.font-medium.my-4 (str (t :graph/remote-graphs) ":")]
+            [:h2.text-lg.font-medium.my-4 (t :graph/remote-graphs)]
             [:div
              (ui/button
               [:span.flex.items-center "Refresh"

+ 23 - 17
src/main/frontend/components/right_sidebar.cljs

@@ -29,7 +29,7 @@
   (when-not (util/sm-breakpoint?)
     (ui/with-shortcut :ui/toggle-right-sidebar "left"
       [:button.button.icon.toggle-right-sidebar
-       {:title "Toggle right sidebar"
+       {:title (t :right-side-bar/toggle-right-sidebar)
         :on-click ui-handler/toggle-right-sidebar!}
        (ui/icon "layout-sidebar-right" {:size 20})])))
 
@@ -56,7 +56,7 @@
 (rum/defc shortcut-settings
   []
   [:div.contents.flex-col.flex.ml-3
-   (shortcut/shortcut {:show-title? false})])
+   (shortcut/shortcut-page {:show-title? false})])
 
 (defn- block-with-breadcrumb
   [repo block idx sidebar-key ref?]
@@ -73,6 +73,9 @@
   (when v [:.ml-4 (ui/foldable
                    [:div (str k)]
                    [:.ml-4 (case k
+                             :tx-id
+                             [:.my-1 [:pre.code.pre-wrap-white-space.bg-base-4 (str v)]]
+
                              :blocks
                              (map (fn [block]
                                     [:.my-1 [:pre.code.pre-wrap-white-space.bg-base-4 (str block)]]) v)
@@ -111,7 +114,7 @@
 
 (defn build-sidebar-item
   [repo idx db-id block-type]
-  (case block-type
+  (case (keyword block-type)
     :contents
     [(t :right-side-bar/contents)
      (contents)]
@@ -120,11 +123,11 @@
     [(t :right-side-bar/help) (onboarding/help)]
 
     :page-graph
-    [(str (t :right-side-bar/page-graph))
+    [(t :right-side-bar/page-graph)
      (page/page-graph)]
 
     :history
-    [(str (t :right-side-bar/history))
+    [(t :right-side-bar/history)
      (history)]
 
     :block-ref
@@ -220,6 +223,7 @@
 (rum/defc sidebar-resizer
   [sidebar-open? sidebar-id handler-position]
   (let [el-ref (rum/use-ref nil)
+        min-px-width 144 ; Custom window controls width
         min-ratio 0.1
         max-ratio 0.7
         keyboard-step 5
@@ -227,12 +231,12 @@
         remove-resizing-class (fn []
                                 (.. js/document.documentElement -classList (remove "is-resizing-buf"))
                                 (reset! ui-handler/*right-sidebar-resized-at (js/Date.now)))
-        set-width! (fn [ratio element]
-                     (when (and el-ref element)
-                       (let [width (str (* ratio 100) "%")]
-                         (#(.setProperty (.-style element) "width" width)
-                          (.setAttribute (rum/deref el-ref) "aria-valuenow" ratio)
-                          (ui-handler/persist-right-sidebar-width!)))))]
+        set-width! (fn [ratio]
+                     (when el-ref
+                       (let [value (* ratio 100)
+                             width (str value "%")]
+                         (.setAttribute (rum/deref el-ref) "aria-valuenow" value)
+                         (ui-handler/persist-right-sidebar-width! width))))]
     (rum/use-effect!
      (fn []
        (when-let [el (and (fn? js/window.interact) (rum/deref el-ref))]
@@ -243,6 +247,7 @@
                 {:move
                  (fn [^js/MouseEvent e]
                    (let [width js/document.documentElement.clientWidth
+                         min-ratio (max min-ratio (/ min-px-width width))
                          sidebar-el (js/document.getElementById sidebar-id)
                          offset (.-pageX e)
                          ratio (.toFixed (/ offset width) 6)
@@ -259,7 +264,7 @@
                          (and (< ratio max-ratio) sidebar-el)
                          (when sidebar-el
                            (#(.. js/document.documentElement -classList (remove cursor-class))
-                            (set-width! ratio sidebar-el)))
+                            (set-width! ratio)))
                          :else
                          #(.. js/document.documentElement -classList (remove cursor-class)))
                        (when (> ratio (/ min-ratio 2)) (state/open-right-sidebar!)))))}}))
@@ -269,6 +274,7 @@
              (.on "keydown" (fn [e]
                               (when-let [sidebar-el (js/document.getElementById sidebar-id)]
                                 (let [width js/document.documentElement.clientWidth
+                                      min-ratio (max min-ratio (/ min-px-width width))
                                       keyboard-step (case (.-code e)
                                                       "ArrowLeft" (- keyboard-step)
                                                       "ArrowRight" keyboard-step
@@ -278,7 +284,7 @@
                                       ratio (if (= handler-position :west) (- 1 ratio) ratio)]
                                   (when (and (> ratio min-ratio) (< ratio max-ratio) (not (zero? keyboard-step)))
                                     ((add-resizing-class)
-                                     (set-width! ratio sidebar-el)))))))
+                                     (set-width! ratio)))))))
              (.on "keyup" remove-resizing-class)))
        #())
      [])
@@ -333,9 +339,7 @@
         (when config/dev? [:div.text-sm
                            [:button.button.cp__right-sidebar-settings-btn {:on-click (fn [_e]
                                                                                        (state/sidebar-add-block! repo "history" :history))}
-                            (t :right-side-bar/history)]])]
-
-       (toggle)]
+                            (t :right-side-bar/history)]])]]
 
       [:.sidebar-item-list.flex-1.scrollbar-spacing.flex.flex-col.gap-2
        (if @*anim-finished?
@@ -353,9 +357,11 @@
                  [[(state/get-current-repo) "contents" :contents nil]]
                  blocks)
         sidebar-open? (state/sub :ui/sidebar-open?)
+        width (state/sub :ui/sidebar-width)
         repo (state/sub :git/current-repo)]
     [:div#right-sidebar.cp__right-sidebar.h-screen
-     {:class (if sidebar-open? "open" "closed")}
+     {:class (if sidebar-open? "open" "closed")
+      :style {:width width}}
      (sidebar-resizer sidebar-open? "right-sidebar" :west)
      (when sidebar-open?
        (sidebar-inner repo t blocks))]))

+ 7 - 7
src/main/frontend/components/search.cljs

@@ -257,8 +257,8 @@
    {:name icon
     :class "highlight"
     :extension? true}
-   [:div.text.font-bold (str label ": ")
-    [:span.ml-1 name]]))
+   [:div.text.font-bold label
+    [:span.ml-2 name]]))
 
 (defn- search-item-render
   [search-q {:keys [type data alias]}]
@@ -312,7 +312,7 @@
 
                                 :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."))))])
+                                    (t :search/cache-outdated))))])
 
        :page-content
        (let [{:block/keys [snippet uuid]} data  ;; content here is normalized
@@ -327,7 +327,7 @@
                                 (if page
                                   (page-content-search-result-item repo uuid format snippet search-q search-mode)
                                   (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."))))]))
+                                      (t :search/cache-outdated))))]))
 
        nil)]))
 
@@ -396,11 +396,11 @@
   [in-page-search?]
   [:div.recent-search
    [:div.wrap.px-4.pb-2.text-sm.opacity-70.flex.flex-row.justify-between.align-items.mx-1.sm:mx-0
-    [:div "Recent search:"]
+    [:div (t :search/recent)]
     [:div.hidden.md:flex
      (ui/with-shortcut :go/search-in-page "bottom"
        [:div.flex-row.flex.align-items
-        [:div.mr-3.flex "Search blocks in page:"]
+        [:div.mr-3.flex (t :search/blocks-in-page)]
         [:div.flex.items-center
          (ui/toggle in-page-search?
                     (fn [_value]
@@ -408,7 +408,7 @@
                     true)]
         (ui/tippy {:html [:div
                           ;; TODO: fetch from config
-                          "Tip: " [:code (util/->platform-shortcut "Ctrl + Shift + p")] " to open the commands palette"]
+                          (t :search/command-palette-tip-1) [:code (util/->platform-shortcut "Ctrl + Shift + p")] (t :search/command-palette-tip-2)]
                    :interactive     true
                    :arrow           true
                    :theme       "monospace"}

+ 269 - 50
src/main/frontend/components/settings.cljs

@@ -1,37 +1,39 @@
 (ns frontend.components.settings
   (:require [clojure.string :as string]
-            [frontend.components.svg :as svg]
-            [frontend.components.plugins :as plugins]
+            [electron.ipc :as ipc]
             [frontend.components.assets :as assets]
+            [frontend.components.conversion :as conversion-component]
+            [frontend.components.file-sync :as fs]
+            [frontend.components.plugins :as plugins]
+            [frontend.components.svg :as svg]
             [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
-            [frontend.storage :as storage]
-            [frontend.spec.storage :as storage-spec]
             [frontend.date :as date]
+            [frontend.db :as db]
             [frontend.dicts :as dicts]
             [frontend.handler :as handler]
             [frontend.handler.config :as config-handler]
+            [frontend.handler.file-sync :as file-sync-handler]
+            [frontend.handler.global-config :as global-config-handler]
             [frontend.handler.notification :as notification]
+            [frontend.handler.plugin :as plugin-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.ui :as ui-handler]
             [frontend.handler.user :as user-handler]
-            [frontend.handler.plugin :as plugin-handler]
-            [frontend.handler.file-sync :as file-sync-handler]
-            [frontend.handler.global-config :as global-config-handler]
+            [frontend.mobile.util :as mobile-util]
             [frontend.modules.instrumentation.core :as instrument]
             [frontend.modules.shortcut.data-helper :as shortcut-helper]
+            [frontend.spec.storage :as storage-spec]
             [frontend.state :as state]
+            [frontend.storage :as storage]
             [frontend.ui :as ui]
-            [electron.ipc :as ipc]
-            [promesa.core :as p]
             [frontend.util :refer [classnames web-platform?] :as util]
             [frontend.version :refer [version]]
             [goog.object :as gobj]
+            [goog.string :as gstring]
+            [promesa.core :as p]
             [reitit.frontend.easy :as rfe]
-            [rum.core :as rum]
-            [frontend.mobile.util :as mobile-util]
-            [frontend.db :as db]
-            [frontend.components.conversion :as conversion-component]))
+            [rum.core :as rum]))
 
 (defn toggle
   [label-for name state on-toggle & [detail-text]]
@@ -57,19 +59,19 @@
        [:div (cond
                (mobile-util/native-android?)
                (ui/button
-                "Check for updates"
+                (t :settings-page/check-for-updates)
                 :class "text-sm p-1 mr-1"
-                :href "https://github.com/logseq/logseq/releases" )
+                :href "https://github.com/logseq/logseq/releases")
 
                (mobile-util/native-ios?)
                (ui/button
-                "Check for updates"
+                (t :settings-page/check-for-updates)
                 :class "text-sm p-1 mr-1"
-                :href "https://apps.apple.com/app/logseq/id1601013908" )
+                :href "https://apps.apple.com/app/logseq/id1601013908")
 
                (util/electron?)
                (ui/button
-                (if update-pending? "Checking ..." "Check for updates")
+                (if update-pending? (t :settings-page/checking) (t :settings-page/check-for-updates))
                 :class "text-sm p-1 mr-1"
                 :disabled update-pending?
                 :on-click #(js/window.apis.checkForUpdates false))
@@ -78,7 +80,7 @@
                nil)]
 
        [:div.text-sm.cursor
-        {:title (str "Revision: " config/revision)
+        {:title (str (t :settings-page/revision) config/revision)
          :on-click (fn []
                      (notification/show! [:div "Current Revision: "
                                           [:a {:target "_blank"
@@ -91,18 +93,18 @@
        [:a.text-sm.fade-link.underline.inline
         {:target "_blank"
          :href "https://docs.logseq.com/#/page/changelog"}
-        "What's new?"]]]
+        (t :settings-page/changelog)]]]
 
      (when-not (or update-pending?
                    (string/blank? type))
        [:div.update-state.text-sm
         (case type
           "update-not-available"
-          [:p "Your app is up-to-date 🎉"]
+          [:p (t :settings-page/app-updated)]
 
           "update-available"
           (let [{:keys [name url]} payload]
-            [:p (str "Found new release ")
+            [:p (str (t :settings-page/update-available))
              [:a.link
               {:on-click
                (fn [e]
@@ -111,7 +113,7 @@
               svg/external-link name " 🎉"]])
 
           "error"
-          [:p "⚠️ Oops, Something Went Wrong!" [:br] " Please check out the "
+          [:p (t :settings-page/update-error-1) [:br] (t :settings-page/update-error-2)
            [:a.link
             {:on-click
              (fn [e]
@@ -125,10 +127,10 @@
    {:style {:box-shadow "0 4px 20px 4px rgba(0, 20, 60, .1), 0 4px 80px -8px rgba(0, 20, 60, .2)"}}
    [:div {:style {:margin "12px" :max-width "500px"}}
     [:p.text-sm
-     "The left side shows outdenting with the default setting, and the right shows outdenting with logical outdenting enabled. "
+     (t :settings-page/preferred-outdenting-tip)
      [:a.text-sm
       {:target "_blank" :href "https://discuss.logseq.com/t/whats-your-preferred-outdent-behavior-the-direct-one-or-the-logical-one/978"}
-      "→ Learn more"]]
+      (t :settings-page/preferred-outdenting-tip-more)]]
     [:img {:src    "https://discuss.logseq.com/uploads/default/original/1X/e8ea82f63a5e01f6d21b5da827927f538f3277b9.gif"
            :width  500
            :height 500}]]])
@@ -139,7 +141,7 @@
    {:style {:box-shadow "0 4px 20px 4px rgba(0, 20, 60, .1), 0 4px 80px -8px rgba(0, 20, 60, .2)"}}
    [:div {:style {:margin "12px" :max-width "500px"}}
     [:p.text-sm
-     "This option controls whether to expand the block references automatically when zoom-in."]
+     (t :settings-page/auto-expand-block-refs-tip)]
     [:img {:src    "https://user-images.githubusercontent.com/28241963/225818326-118deda9-9d1e-477d-b0ce-771ca0bcd976.gif"
            :width  500
            :height 500}]]])
@@ -293,12 +295,12 @@
 
 (defn theme-modes-row [t switch-theme system-theme? dark?]
   (let [pick-theme [:ul.theme-modes-options
-                    [:li {:on-click (partial state/use-theme-mode! "light")
-                          :class    (classnames [{:active (and (not system-theme?) (not dark?))}])} [:i.mode-light] [:strong "light"]]
-                    [:li {:on-click (partial state/use-theme-mode! "dark")
-                          :class    (classnames [{:active (and (not system-theme?) dark?)}])} [:i.mode-dark] [:strong "dark"]]
-                    [:li {:on-click (partial state/use-theme-mode! "system")
-                          :class    (classnames [{:active system-theme?}])} [:i.mode-system] [:strong "system"]]]]
+                    [:li {:on-click (partial state/use-theme-mode! (t :settings-page/theme-light))
+                          :class    (classnames [{:active (and (not system-theme?) (not dark?))}])} [:i.mode-light] [:strong (t :settings-page/theme-light)]]
+                    [:li {:on-click (partial state/use-theme-mode! (t :settings-page/theme-dark))
+                          :class    (classnames [{:active (and (not system-theme?) dark?)}])} [:i.mode-dark] [:strong (t :settings-page/theme-dark)]]
+                    [:li {:on-click (partial state/use-theme-mode! (t :settings-page/theme-system))
+                          :class    (classnames [{:active system-theme?}])} [:i.mode-system] [:strong (t :settings-page/theme-system)]]]]
     (row-with-button-action {:left-label (t :right-side-bar/switch-theme (string/capitalize switch-theme))
                              :-for       "toggle_theme"
                              :action     pick-theme
@@ -340,7 +342,7 @@
                       (when-not (string/blank? format)
                         (config-handler/set-config! :journal/page-title-format format)
                         (notification/show!
-                          [:div "You must re-index your graph for this change to take effect"]
+                          [:div (t :settings-page/custom-date-format-notification)]
                           :warning false)
                         (state/close-modal!)
                         (route-handler/redirect! {:to :repos}))))}
@@ -385,8 +387,13 @@
 
 (defn preferred-pasting-file [t preferred-pasting-file?]
   (toggle "preferred_pasting_file"
-          (t :settings-page/preferred-pasting-file)
-          preferred-pasting-file?
+          [(t :settings-page/preferred-pasting-file)
+           (ui/tippy {:html        (t :settings-page/preferred-pasting-file-hint)
+                      :class       "tippy-hover ml-2"
+                      :interactive true
+                      :disabled    false}
+                     (svg/info))]
+          preferred-pasting-file? 
           config-handler/toggle-preferred-pasting-file!))
 
 (defn auto-expand-row [t auto-expand-block-refs?]
@@ -610,6 +617,19 @@
                 (fn [_] (conversion-component/files-breaking-changed))
                 {:id :filename-format-panel :center? true})}))
 
+(rum/defcs native-titlebar-row < rum/reactive
+  [state t]
+  (let [enabled? (state/sub [:electron/user-cfgs :window/native-titlebar?])]
+    (toggle
+     "native-titlebar"
+     (t :settings-page/native-titlebar)
+     enabled?
+     #(when (js/confirm (t :relaunch-confirm-to-work))
+        (state/set-state! [:electron/user-cfgs :window/native-titlebar?] (not enabled?))
+        (ipc/ipc :userAppCfgs :window/native-titlebar? (not enabled?))
+        (js/logseq.api.relaunch))
+     [:span.text-sm.opacity-50 (t :settings-page/native-titlebar-desc)])))
+
 (rum/defcs settings-general < rum/reactive
   [_state current-repo]
   (let [preferred-language (state/sub [:preferred-language])
@@ -621,6 +641,7 @@
      (version-row t version)
      (language-row t preferred-language)
      (theme-modes-row t switch-theme system-theme? dark?)
+     (when (and (util/electron?) (not util/mac?)) (native-titlebar-row t))
      (when (config/global-config-enabled?) (edit-global-config-edn))
      (when current-repo (edit-config-edn))
      (when current-repo (edit-custom-css))
@@ -669,18 +690,16 @@
    [:div.text-sm.my-4
     (ui/admonition
      :tip
-     [:p "If you have Logseq Sync enabled, you can view a page's edit history directly. This section is for tech-savvy only."])
+     [:p (t :settings-page/git-tip)])
     [:span.text-sm.opacity-50.my-4 
-     "To view page's edit history, click the three horizontal dots in the top-right corner and select \"View page history\"."]
+     (t :settings-page/git-desc-1)]
     [:br][:br]
     [:span.text-sm.opacity-50.my-4
-     "For professional users, Logseq also supports using "]
+     (t :settings-page/git-desc-2)]
     [:a {:href "https://git-scm.com/" :target "_blank"}
      "Git"]
     [:span.text-sm.opacity-50.my-4
-     " for version control."]
-    [:span.text-sm.opacity-50.my-4
-     "Use Git at your own risk as general Git issues are not supported by the Logseq team"]]
+     (t :settings-page/git-desc-3)]]
    [:br]
    (switch-git-auto-commit-row t)
    (git-auto-commit-seconds t)
@@ -731,6 +750,196 @@
    {:left-label (t :settings-page/enable-whiteboards)
     :action (whiteboards-enabled-switcher enabled?)}))
 
+(rum/defc settings-account-usage-description [pro-account? graph-usage]
+  (let [count-usage (count graph-usage)
+        count-limit (if pro-account? 10 1)
+        count-percent (js/Math.round (/ count-usage count-limit 0.01))
+        storage-usage (->> (map :used-gbs graph-usage)
+                           (reduce + 0)) 
+        storage-usage-formatted (cond 
+                                  (zero? storage-usage) "0.0"
+                                  (< storage-usage 0.01) "Less than 0.01"
+                                  :else (gstring/format "%.2f" storage-usage))
+        ;; TODO: check logic on this. What are the rules around storage limits?  
+        ;; do we, and should we be able to, give individual users more storage?
+        ;; should that be on a per graph or per user basis?
+        default-storage-limit (if pro-account? 10 0.05)
+        storage-limit (->> (range 0 count-limit)
+                           (map #(get-in graph-usage [% :limit-gbs] default-storage-limit))
+                           (reduce + 0))
+        storage-percent (/ storage-usage storage-limit 0.01)
+        storage-percent-formatted (gstring/format "%.1f" storage-percent)]
+    [:div.text-sm
+     (when pro-account?
+       [:<>
+        (gstring/format "%s of %s synced graphs " count-usage count-limit)
+        [:strong.text-white (gstring/format "(%s%%)" count-percent)]
+        ", "]) 
+     (gstring/format "%sGB of %sGB total storage " storage-usage-formatted storage-limit)
+     [:strong.text-white (gstring/format "(%s%%)" storage-percent-formatted)]]))
+     ; storage-usage-formatted "GB of " storage-limit "GB total storage"
+     ; [:strong.text-white " (" storage-percent-formatted "%)"]]))
+
+
+(rum/defc settings-account-usage-graphs [_pro-account? graph-usage]
+  (when (< 0 (count graph-usage))
+   [:div.grid.gap-3 {:style {:grid-template-columns (str "repeat(" (count graph-usage) ", 1fr)")}}
+    (for [{:keys [name used-percent]} graph-usage
+          :let [color (if (<= 100 used-percent) "bg-red-500" "bg-blue-500")]]
+     [:div.rounded-full.w-full.h-2 {:class "bg-black/50" 
+                                    :tooltip name}
+      [:div.rounded-full.h-2 {:class color
+                              :style {:width (str used-percent "%") 
+                                      :min-width "0.5rem" 
+                                      :max-width "100%"}}]])]))
+  
+(rum/defc ^:large-vars/cleanup-todo settings-account < rum/reactive
+  []
+  (let [current-graph-uuid (state/sub-current-file-sync-graph-uuid)
+        graph-usage (state/get-remote-graph-usage)
+        current-graph-is-remote? ((set (map :uuid graph-usage)) current-graph-uuid)
+        logged-in? (user-handler/logged-in?)
+        user-info (state/get-user-info)
+        paid-user? (#{"active" "on_trial" "cancelled"} (:LemonStatus user-info))
+        gift-user? (some #{"pro"} (:UserGroups user-info))
+        pro-account? (or paid-user? gift-user?)
+        expiration-date (some-> user-info :LemonEndsAt date/parse-iso)
+        renewal-date (some-> user-info :LemonRenewsAt date/parse-iso)
+        has-subscribed? (some? (:LemonStatus user-info))]
+    [:div.panel-wrap.is-features.mb-8
+     [:div.mt-1.sm:mt-0.sm:col-span-2
+      (cond
+        logged-in?
+        [:div.grid.grid-cols-3.gap-8.pt-2
+         [:div "Current plan"]
+         [:div.col-span-2 
+          [:div {:class "w-full bg-gray-500/10 rounded-lg p-4 flex flex-col gap-4"}
+           [:div.flex.gap-4.items-center
+            (if pro-account?
+              [:div.flex-1 "Pro"]
+              [:div.flex-1 "Free"])
+            (cond 
+              has-subscribed?
+              (ui/button "Manage plan" {:class "p-1 h-8 justify-center"
+                                        :disabled true
+                                        :icon "upload"})
+                                         ; :on-click user-handler/upgrade})
+              (not pro-account?)
+              (ui/button "Upgrade plan" {:class "p-1 h-8 justify-center"
+                                         :icon "upload"
+                                         :on-click user-handler/upgrade})
+              :else nil)]
+           (settings-account-usage-graphs pro-account? graph-usage)
+           (settings-account-usage-description pro-account? graph-usage)
+           (if current-graph-is-remote?
+             (ui/button "Deactivate syncing" {:class "p-1 h-8 justify-center"
+                                              :disabled true
+                                              :background "gray"
+                                              :icon "cloud-off"})
+             (ui/button "Activate syncing" {:class "p-1 h-8 justify-center"
+                                            :background "blue"
+                                            :icon "cloud"
+                                            :on-click #(fs/maybe-onboarding-show :sync-initiate)}))]]
+         (when has-subscribed?
+          [:<>
+           [:div "Billing"]
+           [:div.col-span-2.flex.flex-col.gap-4
+            (cond 
+              ;; If there is no expiration date, print the renewal date
+              (and renewal-date (nil? expiration-date)) 
+              [:div 
+               [:strong.font-semibold "Next billing date: " 
+                (date/get-locale-string renewal-date)]]
+              ;; If the expiration date is in the future, word it as such
+              (< (js/Date.) expiration-date) 
+              [:div
+               [:strong.font-semibold "Pro plan expires on: " 
+                (date/get-locale-string expiration-date)]]
+              ;; Otherwise, ind
+              :else 
+              [:div 
+               [:strong.font-semibold "Pro plan expired on: " 
+                (date/get-locale-string expiration-date)]])
+                               
+            [:div (ui/button "Open invoices" {:class "w-full h-8 p-1 justify-center" 
+                                              :disabled true 
+                                              :background "gray" 
+                                              :icon "receipt"})]]])
+         [:div "Profile"]
+         [:div.col-span-2.grid.grid-cols-2.gap-4
+          [:div.flex.flex-col.gap-2.box-border {:class "basis-1/2"}
+           [:label.text-sm.font-semibold "First name"]
+           [:input.rounded.border.px-2.py-1.box-border {:class "border-blue-500 bg-black/25 w-full"}]]
+          [:div.flex.flex-col.gap-2 {:class "basis-1/2"}
+           [:label.text-sm.font-semibold "Last name"]
+           [:input.rounded.border.px-2.py-1.box-border {:class "border-blue-500 bg-black/25 w-full"}]]
+          [:div.flex-1.flex.flex-col.gap-2.col-span-2
+           [:label.text-sm.font-semibold "Username"]
+           [:input.rounded.border.px-2.py-1.box-border {:class "border-blue-500 bg-black/25" 
+                                                        :value (user-handler/email)}]]]
+         [:div "Authentication"]
+         [:div.col-span-2
+          [:div.grid.grid-cols-2.gap-4
+           [:div (ui/button (t :logout) {:class "p-1 h-8 justify-center w-full"
+                                         :background "gray"
+                                         :icon "logout"
+                                         :on-click user-handler/logout})]
+           [:div (ui/button "Reset password" {:class "p-1 h-8 justify-center w-full"
+                                              :disabled true
+                                              :background "gray"
+                                              :icon "key"
+                                              :on-click user-handler/logout})]
+           [:div.col-span-2 (ui/button "Delete Account" {:class "p-1 h-8 justify-center w-full" 
+                                                         :disabled true
+                                                         :background "red"})]]]] 
+                                            
+        (not logged-in?)
+        [:div.grid.grid-cols-3.gap-8.pt-2
+         [:div "Authentication"]
+         [:div.col-span-2.flex.flex-wrap.gap-4
+          [:div.w-full.text-white "With a Logseq account, you can access cloud-based services like Logseq Sync and alpha/beta features."]
+          [:div.flex-1 (ui/button "Sign up" {:class "h-8 w-full text-center justify-center"
+                                             :on-click (fn []
+                                                         (state/close-settings!)
+                                                         (state/pub-event! [:user/login]))})]
+          [:div.flex-1 (ui/button (t :login) {:icon "login" 
+                                              :class "h-8 w-full text-center justify-center" 
+                                              :background "gray"
+                                              :on-click (fn []
+                                                          (state/close-settings!)
+                                                          (state/pub-event! [:user/login]))})]]
+         [:div.col-span-3.flex.flex-col.gap-4 {:class "bg-black/20 p-4 rounded-lg"}
+          [:div.flex.w-full.items-center
+           [:div {:class "w-1/2 text-lg"} 
+            "Discover the power of " 
+            [:strong {:class "text-white/80"} "Logseq Sync"]]
+           [:div {:class "w-1/2 bg-gradient-to-r from-white/10 to-transparent p-3 rounded-lg flex items-center gap-2 px-5 ml-5"} 
+            [:div.w-3.h-3.rounded-full.bg-green-500]
+            "Synced"]]
+          [:div.flex.w-full.gap-4
+           [:div {:class "w-1/2 bg-black/50 rounded-lg p-4 pt-10 relative flex flex-col gap-4"}
+            [:div.absolute.top-0.left-4.bg-gray-700.uppercase.px-2.py-1.rounded-b-lg.font-bold.text-xs "Free"]
+            [:div
+             [:strong.text-white.text-xl.font-normal "$0"]] 
+            [:div.text-white.font-bold {:class "h-[2.5rem] "} "Get started with basic syncing"]
+            [:ul.text-xs.list-none.m-0.flex.flex-col.gap-0.5
+             [:li "Unlimited unsynced graphs"]
+             [:li "1 synced graph (up to 50MB, notes only)"]
+             [:li "No asset syncing"]
+             [:li "Access to core Logseq features"]]]
+           [:div {:class "w-1/2 bg-black/50 rounded-lg p-4 pt-10 relative flex flex-col gap-4"}
+            [:div.absolute.top-0.left-4.bg-blue-700.uppercase.px-2.py-1.rounded-b-lg.font-bold.text-xs "Pro"]
+            [:div
+             [:strong.text-white.text-xl.font-normal "$10"] 
+             [:span.text-xs.font-base {:class "ml-0.5"} "/ month"]]
+            [:div.text-white.font-bold {:class "h-[2.5rem]"} "Unlock advanced syncing and more"]
+            [:ul.text-xs.list-none.m-0.flex.flex-col.gap-0.5
+             [:li "Unlimited unsynced graphs"]
+             [:li "10 synced graphs (up to 5GB each)"]
+             [:li "Sync assets up to 100MB per file"]
+             [:li "Early access to alpha/beta features"]
+             [:li "Upcoming cloud-based features, including Logseq Publish"]]]]]])]]))
+
 (rum/defc settings-features < rum/reactive
   []
   (let [current-repo (state/get-current-repo)
@@ -786,11 +995,11 @@
          {:class (when-not user-handler/alpha-or-beta-user? "opacity-50 pointer-events-none cursor-not-allowed")}
          (sync-switcher-row enable-sync?)
          [:div.text-sm
-          "Click"
+          (t :settings-page/sync-desc-1)
           [:a.mx-1 {:href "https://blog.logseq.com/how-to-setup-and-use-logseq-sync/"
                     :target "_blank"}
-           "here"]
-          "for instructions on how to set up and use Sync."]]])
+           (t :settings-page/sync-desc-2)]
+          (t :settings-page/sync-desc-3)]]])]))
 
      ;; (when-not web-platform?
      ;;   [:<>
@@ -801,10 +1010,12 @@
      ;;     {:class (when-not user-handler/alpha-user? "opacity-50 pointer-events-none cursor-not-allowed")}
      ;;     ;; features
      ;;     ]])
-     ]))
+     
+
+(def DEFAULT-ACTIVE-TAB-STATE (if config/ENABLE-SETTINGS-ACCOUNT-TAB [:account :account] [:general :general]))
 
 (rum/defcs settings
-  < (rum/local [:general :general] ::active)
+  < (rum/local DEFAULT-ACTIVE-TAB-STATE ::active)
     {:will-mount
      (fn [state]
        (state/load-app-user-cfgs)
@@ -822,15 +1033,18 @@
         *active (::active state)]
 
     [:div#settings.cp__settings-main
-     [:header
-      [:h1.title (t :settings)]]
 
      [:div.cp__settings-inner
 
       [:aside.md:w-64 {:style {:min-width "10rem"}}
+       [:header.cp__settings-header
+        (ui/icon "settings")
+        [:h1.cp__settings-modal-title (t :settings)]]
        [:ul.settings-menu
         (for [[label id text icon]
-              [[:general "general" (t :settings-page/tab-general) (ui/icon "adjustments")]
+              [(when config/ENABLE-SETTINGS-ACCOUNT-TAB
+                [:account "account" (t :settings-page/tab-account) (ui/icon "user-circle")])
+               [:general "general" (t :settings-page/tab-general) (ui/icon "adjustments")]
                [:editor "editor" (t :settings-page/tab-editor) (ui/icon "writing")]
 
                (when (util/electron?)
@@ -852,11 +1066,13 @@
               :on-click #(reset! *active [label (first @*active)])}
 
              [:a.flex.items-center.settings-menu-link
-             {:data-id id}
+              {:data-id id}
               icon
               [:strong text]]]))]]
 
       [:article
+       [:header.cp__settings-header
+        [:h1.cp__settings-category-title (name (first @*active))]]
 
        (case (first @*active)
 
@@ -866,6 +1082,9 @@
            (reset! *active [label label])
            nil)
 
+         :account 
+         (settings-account)
+
          :general
          (settings-general current-repo)
 

+ 57 - 40
src/main/frontend/components/settings.css

@@ -1,44 +1,76 @@
 .cp__settings {
-
   &-main {
-    > header {
-      padding: 10px;
-      padding-top: 0;
-      border-bottom: 1px solid var(--ls-quaternary-background-color);
+    aside {
+      @apply bg-gray-400/5 p-4;
+    }
 
-      h1 {
-        font-size: 22px;
-        margin: 0;
+    article {
+      @apply p-4 flex-1 min-h-[12rem] w-auto overflow-y-auto;
+      @apply md:max-h-[70vh] md:w-[40rem];
+      /* margin-right: -17px; */
+      /* margin-bottom: -17px; */
+
+      @screen md {
+        /* max-height: 70vh; */
+        /* width: 680px; */
       }
     }
+
+    aside > .cp__settings-header,
+    article > .cp__settings-header {
+      @apply h-10 py-2 flex flex-row items-center justify-start gap-2;
+    }
+
+    aside > .cp__settings-header {
+      @apply px-2;
+    }
+
+    aside > .cp__settings-header > .ui__icon {
+      @apply h-8 w-8 bg-gray-700/10 rounded grid place-items-center;
+    }
+
+    aside > .cp__settings-header > .ui__icon > svg {
+      @apply h-6 w-6;
+    }
+
+    h1.cp__settings-modal-title {
+      @apply text-2xl font-semibold lowercase;
+    }
+
+    h1.cp__settings-category-title {
+      @apply text-xl lowercase;
+    }
+
+    h1.cp__settings-modal-title:first-letter, 
+    h1.cp__settings-category-title:first-letter {
+      @apply uppercase;
+    }
+
+    .settings-menu {
+      @apply p-0 m-0 mt-4 pr-3; 
+    }
+
+    .settings-menu-item {
+      @apply list-none p-0 my-1.5 rounded;
+      @apply hover:bg-black/10;
+    }
+
+    .settings-menu-link {
+      @apply px-2 py-1.5 select-none; 
+      color: var(--ls-primary-text-color);
+    }
   }
 
   &-inner {
     @apply flex flex-col md:flex-row;
 
     > aside {
-      border-right: 0 solid var(--ls-quaternary-background-color);
-      border-bottom: 1px solid var(--ls-quaternary-background-color);
-
-      @screen md {
-        border-right: 1px solid var(--ls-quaternary-background-color);
-        border-bottom: 0 solid var(--ls-quaternary-background-color);
-      }
 
       ul {
-        padding: 12px 12px 12px 5px;
-        margin: 0;
 
         > li {
-          list-style: none;
-          padding: 0;
-          margin: 5px 0;
-          border-radius: 4px;
 
           > a {
-            padding: 10px;
-            user-select: none;
-            color: var(--ls-primary-text-color);
 
             > i {
               overflow: hidden;
@@ -65,21 +97,6 @@
       }
     }
 
-    > article {
-      flex: 1;
-      padding: 0 12px 12px;
-      min-height: 380px;
-      width: auto;
-      overflow: auto;
-      margin-right: -17px;
-      margin-bottom: -17px;
-
-      @screen md {
-        max-height: 70vh;
-        width: 680px;
-      }
-    }
-
     &.no-aside {
       > article {
         padding-left: 0;
@@ -87,7 +104,7 @@
     }
 
     .panel-wrap {
-      padding: 12px;
+      @apply p-1;
 
       @screen sm {
         width: 600px;

+ 90 - 44
src/main/frontend/components/shortcut.cljs

@@ -13,12 +13,15 @@
 
 (rum/defcs customize-shortcut-dialog-inner <
   (rum/local "")
+  (rum/local nil :rum/action)
   (shortcut/record!)
   [state k action-name current-binding]
-  (let [keypress (:rum/local state)
-        keyboard-shortcut (if (= "" @keypress) current-binding @keypress)]
-    [:div
-     [:div
+  (let [*keypress         (:rum/local state)
+        *action           (:rum/action state)
+        keypressed?       (not= "" @*keypress)
+        keyboard-shortcut (if-not keypressed? current-binding @*keypress)]
+    [:<>
+     [:div.sm:w-lsm
       [:p.mb-4 "Press any sequence of keys to set the shortcut for the " [:b action-name] " action."]
       [:p.mb-4.mt-4
        (ui/render-keyboard-shortcut (-> keyboard-shortcut
@@ -26,26 +29,30 @@
                                         (str/lower-case)
                                         (str/split  #" |\+")))
        " "
-       [:a.text-sm
-        {:style {:margin-left "12px"}
-         :on-click (fn []
-                     (dh/remove-shortcut k)
-                     (shortcut/refresh!)
-                     (swap! keypress (fn [] "")) ;; Clear local state
-                     )}
-        "Reset"]]]
+       (when keypressed?
+         [:a.text-sm
+          {:style    {:margin-left "12px"}
+           :on-click (fn []
+                       (dh/remove-shortcut k)
+                       (shortcut/refresh!)
+                       (swap! *keypress (constantly ""))          ;; Clear local state
+                       )}
+          "Reset"])]]
      [:div.cancel-save-buttons.text-right.mt-4
-      (ui/button "Save" :on-click state/close-modal!)
+      (ui/button "Save" :on-click (fn []
+                                    (reset! *action :save)
+                                    (state/close-modal!)))
       [:a.ml-4
        {:on-click (fn []
-                    (reset! keypress (dh/binding-for-storage current-binding))
+                    (reset! *keypress (dh/binding-for-storage current-binding))
+                    (reset! *action :cancel)
                     (state/close-modal!))} "Cancel"]]]))
 
 (defn customize-shortcut-dialog [k action-name displayed-binding]
   (fn [_]
     (customize-shortcut-dialog-inner k action-name displayed-binding)))
 
-(rum/defc shortcut-col [k binding configurable? action-name]
+(rum/defc shortcut-col [_category k binding configurable? action-name]
   (let [conflict?         (dh/potential-conflict? k)
         displayed-binding (dh/binding-for-display k binding)
         disabled?         (str/includes? displayed-binding "system default")]
@@ -61,28 +68,48 @@
                  (if disabled? "Cannot override system default" "Click to modify"))
         :background (if conflict? "pink" (when disabled? "gray"))
         :on-click (when-not disabled?
-                    #(state/set-modal! (customize-shortcut-dialog k action-name displayed-binding))))])))
-
-(rum/defc shortcut-table < rum/reactive
-  ([name]
-   (shortcut-table name false))
-  ([name configurable?]
-   (let [shortcut-config (rum/cursor-in
-                          state/state
-                          [:config (state/get-current-repo) :shortcuts])
-         _ (rum/react shortcut-config)]
-     [:div
-      [:table
-       [:thead
-        [:tr
-         [:th.text-left [:b (t name)]]
-         [:th.text-right]]]
-       [:tbody
-        (map (fn [[k {:keys [binding]}]]
-               [:tr {:key (str k)}
-                [:td.text-left (t (dh/decorate-namespace k))]
-                (shortcut-col k binding configurable? (t (dh/decorate-namespace k)))])
-          (dh/binding-by-category name))]]])))
+                    #(state/set-sub-modal!
+                       (customize-shortcut-dialog k action-name displayed-binding)
+                       {:center? true})))])))
+
+(rum/defcs shortcut-table
+  < rum/reactive
+    (rum/local true ::folded?)
+    {:will-mount (fn [state]
+                   (let [name (first (:rum/args state))]
+                     (cond-> state
+                             (contains? #{:shortcut.category/basics}
+                                        name)
+                             (-> ::folded? (reset! false) (do state)))))}
+  [state category configurable?]
+  (let [*folded? (::folded? state)
+        plugin?  (= category :shortcut.category/plugins)
+        _        (state/sub [:config (state/get-current-repo) :shortcuts])]
+    [:div.cp__shortcut-table-wrap
+     [:a.fold
+      {:on-click #(reset! *folded? (not @*folded?))}
+      (ui/icon (if @*folded? "chevron-left" "chevron-down"))]
+     [:table
+      [:thead
+       [:tr
+        [:th.text-left [:b (t category)]]
+        [:th.text-right]]]
+      (when-not @*folded?
+        [:tbody
+         (map (fn [[k {:keys [binding]}]]
+                (let [cmd   (dh/shortcut-cmd k)
+                      label (cond
+                              (string? (:desc cmd))
+                              [:<>
+                               [:code.text-xs (namespace k)]
+                               [:small.pl-1 (:desc cmd)]]
+
+                              (not plugin?) (-> k (dh/decorate-namespace) (t))
+                              :else (str k))]
+                  [:tr {:key (str k)}
+                   [:td.text-left.flex.items-center label]
+                   (shortcut-col category k binding configurable? label)]))
+              (dh/binding-by-category category))])]]))
 
 (rum/defc trigger-table []
   [:table
@@ -167,13 +194,9 @@
               [:td.text-right (get rendered name)]])
         list)]]))
 
-(rum/defc shortcut
-  [{:keys [show-title?]
-    :or {show-title? true}}]
-  [:div
-   (when show-title? [:h1.title (t :help/shortcut-page-title)])
-   (trigger-table)
-   (markdown-and-orgmode-syntax)
+(rum/defc keymap-tables
+  []
+  [:div.cp__keymap-tables
    (shortcut-table :shortcut.category/basics true)
    (shortcut-table :shortcut.category/navigating true)
    (shortcut-table :shortcut.category/block-editing true)
@@ -182,4 +205,27 @@
    (shortcut-table :shortcut.category/formatting true)
    (shortcut-table :shortcut.category/toggle true)
    (when (state/enable-whiteboards?) (shortcut-table :shortcut.category/whiteboard true))
+   (shortcut-table :shortcut.category/plugins true)
    (shortcut-table :shortcut.category/others true)])
+
+(rum/defc keymap-pane
+  []
+  (let [[ready?, set-ready!] (rum/use-state false)]
+    (rum/use-effect!
+      (fn [] (js/setTimeout #(set-ready! true) 32))
+      [])
+
+    [:div.cp__keymap-pane
+     [:h1.pb-2.text-3xl.pt-2 "Keymap"]
+     (if ready?
+       (keymap-tables)
+       [:p.flex.justify-center.py-20 (ui/loading "")])]))
+
+(rum/defc shortcut-page
+  [{:keys [show-title?]
+    :or {show-title? true}}]
+  [:div.cp__shortcut-page
+   (when show-title? [:h1.title (t :help/shortcut-page-title)])
+   (trigger-table)
+   (markdown-and-orgmode-syntax)
+   (keymap-tables)])

+ 28 - 0
src/main/frontend/components/shortcut.css

@@ -0,0 +1,28 @@
+.ui__modal {
+  &[label="keymap-manager"] {
+    .panel-content {
+      @apply m-[-16px];
+    }
+
+    @screen lg {
+      .panel-content {
+        width: 980px;
+      }
+    }
+  }
+}
+
+.cp__shortcut {
+  &-table-wrap {
+    @apply relative;
+
+    a.fold {
+      @apply absolute right-0 top-0 w-full pt-3 pr-3
+      pb-3 flex items-center justify-end select-none;
+
+      &:active {
+        @apply bg-white/50 opacity-60;
+      }
+    }
+  }
+}

+ 26 - 0
src/main/frontend/components/svg.cljs

@@ -386,3 +386,29 @@
    [:path
     {:d
      "M256 0C114.6 0 0 114.6 0 256c0 141.4 114.6 256 256 256s256-114.6 256-256C512 114.6 397.4 0 256 0zM352 328c0 13.2-10.8 24-24 24h-144C170.8 352 160 341.2 160 328v-144C160 170.8 170.8 160 184 160h144C341.2 160 352 170.8 352 184V328z"}]])
+
+;; Titlebar icons from https://github.com/microsoft/vscode-codicons
+(defn window-minimize
+  ([] (window-minimize 16))
+  ([size]
+   [:svg.icon {:width size :height size :viewBox "0 0 16 16" :fill "currentColor"}
+    [:path {:d "M14 8v1H3V8h11z"}]]))
+
+(defn window-maximize
+  ([] (window-maximize 16))
+  ([size]
+   [:svg.icon {:width size :height size :viewBox "0 0 16 16" :fill "currentColor"}
+    [:path {:d "M3 3v10h10V3H3zm9 9H4V4h8v8z"}]]))
+
+(defn window-restore
+  ([] (window-restore 16))
+  ([size]
+   [:svg.icon {:width size :height size :viewBox "0 0 16 16" :fill "currentColor"}
+    [:path {:d "M3 5v9h9V5H3zm8 8H4V6h7v7z"}]
+    [:path {:fill-rule "evenodd" :clip-rule "evenodd" :d "M5 5h1V4h7v7h-1v1h2V3H5v2z"}]]))
+
+(defn window-close
+  ([] (window-close 16))
+  ([size]
+   [:svg.icon {:width size :height size :viewBox "0 0 16 16" :fill "currentColor"}
+    [:path {:fill-rule "evenodd" :clip-rule "evenodd" :d "M7.116 8l-4.558 4.558.884.884L8 8.884l4.558 4.558.884-.884L8.884 8l4.558-4.558-.884-.884L8 7.116 3.442 2.558l-.884.884L7.116 8z"}]]))

+ 0 - 2
src/main/frontend/components/theme.css

@@ -8,8 +8,6 @@
   --ls-z-index-level-3: 999;
   --ls-z-index-level-4: 9999;
   --ls-z-index-level-5: 99999;
-
-  --ls-right-sidebar-width: 40%;
 }
 
 html {

+ 2 - 2
src/main/frontend/components/whiteboard.cljs

@@ -146,7 +146,7 @@
   [page-name]
   (let [page-entity (model/get-page page-name)
         {:block/keys [updated-at created-at]} page-entity]
-    (str (if (= created-at updated-at) "Created " "Edited ")
+    (str (if (= created-at updated-at) (t :whiteboard/dashboard-card-created) (t :whiteboard/dashboard-card-edited))
          (util/time-ago (js/Date. updated-at)))))
 
 (rum/defc dashboard-preview-card
@@ -190,7 +190,7 @@
       (whiteboard-handler/create-new-whiteboard-and-redirect!))}
    (ui/icon "plus")
    [:span.dashboard-create-card-caption.select-none
-    "New whiteboard"]])
+    (t :whiteboard/dashboard-card-new-whiteboard)]])
 
 (rum/defc whiteboard-dashboard
   []

+ 52 - 0
src/main/frontend/components/window_controls.cljs

@@ -0,0 +1,52 @@
+(ns frontend.components.window-controls
+  (:require [electron.ipc :as ipc]
+            [frontend.components.svg :as svg]
+            [frontend.context.i18n :refer [t]]
+            [frontend.state :as state]
+            [frontend.ui :as ui]
+            [rum.core :as rum]))
+
+(defn minimize
+  []
+  (ipc/ipc "window-minimize"))
+
+(defn toggle-maximized
+  []
+  (ipc/ipc "window-toggle-maximized"))
+
+(defn close
+  []
+  (ipc/ipc "window-close"))
+
+(defn toggle-fullscreen
+  []
+  (ipc/ipc "window-toggle-fullscreen"))
+
+(rum/defc container < rum/reactive
+  []
+  (let [maximized?  (state/sub :electron/window-maximized?)
+        fullscreen? (state/sub :electron/window-fullscreen?)]
+    [:div.window-controls.flex
+     (if fullscreen?
+       [:button.button.icon.fullscreen-toggle
+        {:title (t :window/exit-fullscreen)
+         :on-click toggle-fullscreen}
+        (ui/icon "arrows-minimize")]
+       [:<>
+        [:button.button.icon.minimize
+         {:title (t :window/minimize)
+          :on-click minimize}
+         (svg/window-minimize)]
+
+        [:button.button.icon.maximize-toggle
+         {:title (if maximized? (t :window/restore) (t :window/maximize))
+          :class (if maximized? "restore" "maximize")
+          :on-click toggle-maximized}
+         (if maximized?
+           (svg/window-restore)
+           (svg/window-maximize))]
+
+        [:button.button.icon.close
+         {:title (t :window/close)
+          :on-click close}
+         (svg/window-close)]])]))

+ 14 - 0
src/main/frontend/components/window_controls.css

@@ -0,0 +1,14 @@
+.window-controls {
+  position: fixed;
+  top: 0;
+  right: 0;
+  z-index: 10;
+
+  .button {
+    -webkit-app-region: no-drag;
+    background: transparent;
+    border-radius: 0;
+    width: 48px;
+    height: 48px;
+  }
+}

+ 6 - 2
src/main/frontend/config.cljs

@@ -29,6 +29,10 @@
 
 (goog-define ENABLE-FILE-SYNC-PRODUCTION false)
 
+;; this is a feature flag to enable the account tab
+;; when it launches (when pro plan launches) it should be removed
+(def ENABLE-SETTINGS-ACCOUNT-TAB false)
+
 (if ENABLE-FILE-SYNC-PRODUCTION
   (do (def FILE-SYNC-PROD? true)
       (def LOGIN-URL
@@ -339,7 +343,7 @@
 (def custom-css-file "custom.css")
 (def export-css-file "export.css")
 (def custom-js-file "custom.js")
-(def config-default-content (rc/inline "config.edn"))
+(def config-default-content (rc/inline "templates/config.edn"))
 (def config-default-content-md5 (let [md5 (new crypt/Md5)]
                                   (.update md5 (crypt/stringToUtf8ByteArray config-default-content))
                                   (crypt/byteArrayToHex (.digest md5))))
@@ -482,7 +486,7 @@
 (defn get-current-repo-assets-root
   []
   (when-let [repo-dir (and (local-file-based-graph? (state/get-current-repo))
-                            (get-repo-dir (state/get-current-repo)))]
+                           (get-repo-dir (state/get-current-repo)))]
     (path/path-join repo-dir "assets")))
 
 (defn get-custom-js-path

+ 17 - 5
src/main/frontend/date.cljs

@@ -62,11 +62,13 @@
    (tf/unparse custom-formatter date-time)))
 
 (defn get-locale-string
-  [s]
+  "Accepts a :date-time-no-ms string representation, or a cljs-time date object"
+  [input]
   (try
-    (->> (tf/parse (tf/formatters :date-time-no-ms) s)
-        (t/to-default-time-zone)
-        (tf/unparse (tf/formatter "MMM do, yyyy")))
+    (->> (cond->> input
+          (string? input) (tf/parse (tf/formatters :date-time-no-ms)))
+         (t/to-default-time-zone)
+         (tf/unparse (tf/formatter "MMM do, yyyy")))
     (catch :default _e
       nil)))
 
@@ -209,12 +211,22 @@
    (tf/formatter "yyyy-MM-dd HH:mm")
    (t/to-default-time-zone (tc/from-long n))))
 
+(def iso-parser (tf/formatter "yyyy-MM-dd'T'HH:mm:ss.SSSS'Z'"))
+(defn parse-iso [string]
+  (tf/parse iso-parser string))
+
 (comment
   (def default-formatter (tf/formatter "MMM do, yyyy"))
   (def zh-formatter (tf/formatter "YYYY年MM月dd日"))
 
-  (tf/show-formatters))
+  (tf/show-formatters)
 
   ;; :date 2020-05-31
   ;; :rfc822 Sun, 31 May 2020 03:00:57 Z
 
+  (let [info {:ExpireTime 1680781356,
+              :UserGroups [],
+              :LemonRenewsAt "2024-04-11T07:28:00.000000Z",
+              :LemonEndsAt nil,
+              :LemonStatus "active"}]
+    (->> info :LemonRenewsAt (tf/parse iso-parser) (< (js/Date.))))) 

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

@@ -50,6 +50,7 @@
     :block/marker
     :block/priority
     :block/properties
+    :block/properties-order
     :block/properties-text-values
     :block/pre-block?
     :block/scheduled

+ 2 - 2
src/main/frontend/db/react.cljs

@@ -48,10 +48,10 @@
 (defonce query-state (atom {}))
 
 ;; Current dynamic component
-(def ^:dynamic *query-component*)
+(def ^:dynamic *query-component* nil)
 
 ;; Which reactive queries are triggered by the current component
-(def ^:dynamic *reactive-queries*)
+(def ^:dynamic *reactive-queries* nil)
 
 ;; component -> query-key
 (defonce query-components (atom {}))

+ 2 - 4
src/main/frontend/extensions/excalidraw.cljs

@@ -17,8 +17,7 @@
             [goog.object :as gobj]
             [goog.functions :refer [debounce]]
             [rum.core :as rum]
-            [frontend.mobile.util :as mobile-util]
-            [frontend.context.i18n :refer [t]]))
+            [frontend.mobile.util :as mobile-util]))
 
 (def excalidraw (r/adapt-class Excalidraw))
 
@@ -148,8 +147,7 @@
     (when (:file option)
       (cond
         db-restoring?
-        [:div.ls-center
-         (ui/loading (t :loading))]
+        [:div.ls-center (ui/loading)]
 
         (false? loading?)
         (draw-inner data option)

+ 2 - 3
src/main/frontend/extensions/latex.cljs

@@ -6,8 +6,7 @@
             [frontend.util :as util]
             [frontend.handler.plugin :refer [hook-extensions-enhancer-by-type] :as plugin-handler]
             [promesa.core :as p]
-            [goog.dom :as gdom]
-            [frontend.context.i18n :refer [t]]))
+            [goog.dom :as gdom]))
 
 ;; TODO: extracted to a rum mixin
 (defn loaded? []
@@ -62,7 +61,7 @@
   [id s block? _display?]
   (let [loading? (rum/react *loading?)]
     (if loading?
-      (ui/loading (t :loading))
+      (ui/loading)
       (let [element (if block?
                       :div.latex
                       :span.latex-inline)]

+ 107 - 95
src/main/frontend/extensions/pdf/core.cljs

@@ -347,9 +347,8 @@
                                                     (.setAttribute target "data-x" ax)
                                                     (.setAttribute target "data-y" ay))
                                                   ))}
-                           :modifiers [;; minimum
-                                       (js/interact.modifiers.restrictSize
-                                        (bean/->js {:min {:width 60 :height 25}}))]
+                           :modifiers [(js/interact.modifiers.restrict
+                                         (bean/->js {:restriction (.closest el ".page")}))]
                            :inertia   true})
                          ))]
          ;; destroy
@@ -376,10 +375,10 @@
    (for [hl page-hls]
      (let [vw-hl (update-in hl [:position] #(pdf-utils/scaled-to-vw-pos viewer %))]
        (rum/with-key
-        (if (get-in hl [:content :image])
-          (pdf-highlight-area-region viewer vw-hl hl ops)
-          (pdf-highlights-text-region viewer vw-hl hl ops))
-        (:id hl))
+         (if (get-in hl [:content :image])
+           (pdf-highlight-area-region viewer vw-hl hl ops)
+           (pdf-highlights-text-region viewer vw-hl hl ops))
+         (:id hl))
        ))])
 
 (rum/defc ^:large-vars/cleanup-todo pdf-highlight-area-selection
@@ -388,41 +387,59 @@
   (let [^js viewer-clt          (.. viewer -viewer -classList)
         ^js cnt-el              (.-container viewer)
         *el                     (rum/use-ref nil)
-        *sta-el                 (rum/use-ref nil)
+        *start-el               (rum/use-ref nil)
         *cnt-rect               (rum/use-ref nil)
+        *page-el                (rum/use-ref nil)
+        *page-rect              (rum/use-ref nil)
+        *start-xy               (rum/use-ref nil)
 
-        [start-coord, set-start-coord!] (rum/use-state nil)
-        [end-coord, set-end-coord!] (rum/use-state nil)
+        [start, set-start!] (rum/use-state nil)
+        [end, set-end!] (rum/use-state nil)
         [_ set-area-mode!] (use-atom *area-mode?)
 
         should-start            (fn [^js e]
                                   (let [^js target (.-target e)]
-                                    (when (and (not
-                                                (.contains (.-classList target) "extensions__pdf-hls-area-region"))
+                                    (when (and (not (.contains (.-classList target) "extensions__pdf-hls-area-region"))
                                                (.closest target ".page"))
                                       (and e (or (.-metaKey e)
                                                  (and util/win32? (.-shiftKey e))
                                                  @*area-mode?)))))
 
-        reset-coords            #(do
-                                   (set-start-coord! nil)
-                                   (set-end-coord! nil)
-                                   (rum/set-ref! *sta-el nil))
+        reset-coords!           #(do
+                                   (set-start! nil)
+                                   (set-end! nil)
+                                   (rum/set-ref! *start-xy nil)
+                                   (rum/set-ref! *start-el nil)
+                                   (rum/set-ref! *cnt-rect nil)
+                                   (rum/set-ref! *page-el nil)
+                                   (rum/set-ref! *page-rect nil))
 
-        calc-coords             (fn [page-x page-y]
+        calc-coords!            (fn [page-x page-y]
                                   (when cnt-el
-                                    (let [cnt-rect (rum/deref *cnt-rect)
-                                          cnt-rect (or cnt-rect (bean/->clj (.toJSON (.getBoundingClientRect cnt-el))))
-                                          _        (rum/set-ref! *cnt-rect cnt-rect)]
+                                    (let [cnt-rect    (rum/deref *cnt-rect)
+                                          cnt-rect    (or cnt-rect (bean/->clj (.toJSON (.getBoundingClientRect cnt-el))))
+                                          page-rect   (rum/deref *page-rect)
+                                          [start-x, start-y] (rum/deref *start-xy)
+                                          dx-left?    (> start-x page-x)
+                                          dy-top?     (> start-y page-y)
+                                          page-left   (:left page-rect)
+                                          page-right  (:right page-rect)
+                                          page-top    (:top page-rect)
+                                          page-bottom (:bottom page-rect)
+                                          _           (rum/set-ref! *cnt-rect cnt-rect)]
 
                                       {:x (-> page-x
-                                              (- (:left cnt-rect))
+                                              (#(if dx-left?
+                                                  (if (< % page-left) page-left %)
+                                                  (if (> % page-right) page-right %)))
                                               (+ (.-scrollLeft cnt-el)))
                                        :y (-> page-y
-                                              (- (:top cnt-rect))
+                                              (#(if dy-top?
+                                                  (if (< % page-top) page-top %)
+                                                  (if (> % page-bottom) page-bottom %)))
                                               (+ (.-scrollTop cnt-el)))})))
 
-        calc-pos                (fn [start end]
+        calc-rect               (fn [start end]
                                   {:left   (min (:x start) (:x end))
                                    :top    (min (:y start) (:y end))
                                    :width  (js/Math.abs (- (:x end) (:x start)))
@@ -431,81 +448,78 @@
         disable-text-selection! #(js-invoke viewer-clt (if % "add" "remove") "disabled-text-selection")
 
         fn-move                 (rum/use-callback
-                                 (fn [^js/MouseEvent e]
-                                   (set-end-coord! (calc-coords (.-pageX e) (.-pageY e))))
-                                 [])]
+                                  (fn [^js/MouseEvent e]
+                                    (set-end! (calc-coords! (.-pageX e) (.-pageY e))))
+                                  [])]
 
     (rum/use-effect!
-     (fn []
-       (when-let [^js/HTMLElement root cnt-el]
-         (let [fn-start (fn [^js/MouseEvent e]
-                          (if (should-start e)
-                            (do
-                              (rum/set-ref! *sta-el (.-target e))
-                              (set-start-coord! (calc-coords (.-pageX e) (.-pageY e)))
-                              (disable-text-selection! true)
-
-                              (.addEventListener root "mousemove" fn-move))
-
-                            ;; reset
-                            (reset-coords)))
-
-               fn-end   (fn [^js/MouseEvent e]
-                          (when-let [start-el (rum/deref *sta-el)]
-                            (let [end (calc-coords (.-pageX e) (.-pageY e))
-                                  pos (calc-pos start-coord end)]
-
-                              (if (and (> (:width pos) 10)
-                                       (> (:height pos) 10))
-
-                                (when-let [^js page-el (.closest start-el ".page")]
-                                  (let [page-number (int (.-pageNumber (.-dataset page-el)))
-                                        page-pos    (merge pos {:top  (- (:top pos) (.-offsetTop page-el))
-                                                                :left (- (:left pos) (.-offsetLeft page-el))})
-                                        vw-pos      {:bounding page-pos :rects [] :page page-number}
-                                        sc-pos      (pdf-utils/vw-to-scaled-pos viewer vw-pos)
-
-                                        point       {:x (.-clientX e) :y (.-clientY e)}
-                                        hl          {:id         nil
-                                                     :page       page-number
-                                                     :position   sc-pos
-                                                     :content    {:text "[:span]" :image (js/Date.now)}
-                                                     :properties {}}]
-
-                                    ;; ctx tips
-                                    (show-ctx-menu! viewer hl point {:reset-fn #(reset-coords)})
-
-                                    ;; export area highlight
-                                    ;;(dd "[selection end] :start"
-                                    ;;    start-coord ":end" end ":pos" pos
-                                    ;;    ":page" page-number
-                                    ;;    ":offset" page-pos
-                                    ;;    ":vw-pos" vw-pos
-                                    ;;    ":sc-pos" sc-pos)
-                                    )
-
-                                  (set-area-mode! false))
-
-                                ;; reset
-                                (reset-coords)))
-
-                            (disable-text-selection! false)
-                            (.removeEventListener root "mousemove" fn-move)))]
-
-           (doto root
-             (.addEventListener "mousedown" fn-start)
-             (.addEventListener "mouseup" fn-end #js {:once true}))
-
-           ;; destroy
-           #(doto root
-              (.removeEventListener "mousedown" fn-start)
-              (.removeEventListener "mouseup" fn-end)))))
-     [start-coord])
+      (fn []
+        (when-let [^js/HTMLElement root cnt-el]
+          (let [fn-start (fn [^js/MouseEvent e]
+                           (if (should-start e)
+                             (let [target (.-target e)
+                                   page-el (.closest target ".page")
+                                   [x y] [(.-pageX e) (.-pageY e)]]
+                               (rum/set-ref! *start-el target)
+                               (rum/set-ref! *start-xy [x y])
+                               (rum/set-ref! *page-el page-el)
+                               (rum/set-ref! *page-rect (some-> page-el (.getBoundingClientRect) (.toJSON) (bean/->clj)))
+                               (set-start! (calc-coords! x y))
+                               (disable-text-selection! true)
+
+                               (.addEventListener root "mousemove" fn-move))
+
+                             ;; reset
+                             (do (reset-coords!)
+                                 (disable-text-selection! false))))
+
+                fn-end   (fn [^js/MouseEvent e]
+                           (when-let [start-el (rum/deref *start-el)]
+                             (let [end  (calc-coords! (.-pageX e) (.-pageY e))
+                                   rect (calc-rect start end)]
+
+                               (if (and (> (:width rect) 10)
+                                        (> (:height rect) 10))
+
+                                 (when-let [^js page-el (.closest start-el ".page")]
+                                   (let [page-number (int (.-pageNumber (.-dataset page-el)))
+                                         page-pos    (merge rect {:top  (- (:top rect) (.-offsetTop page-el))
+                                                                  :left (- (:left rect) (.-offsetLeft page-el))})
+                                         vw-pos      {:bounding page-pos :rects [] :page page-number}
+                                         sc-pos      (pdf-utils/vw-to-scaled-pos viewer vw-pos)
+
+                                         point       {:x (.-clientX e) :y (.-clientY e)}
+                                         hl          {:id         nil
+                                                      :page       page-number
+                                                      :position   sc-pos
+                                                      :content    {:text "[:span]" :image (js/Date.now)}
+                                                      :properties {}}]
+
+                                     ;; ctx tips
+                                     (show-ctx-menu! viewer hl point {:reset-fn #(reset-coords!)}))
+
+                                   (set-area-mode! false))
+
+                                 ;; reset
+                                 (reset-coords!)))
+
+                             (disable-text-selection! false)
+                             (.removeEventListener root "mousemove" fn-move)))]
+
+            (doto root
+              (.addEventListener "mousedown" fn-start)
+              (.addEventListener "mouseup" fn-end #js {:once true}))
+
+            ;; destroy
+            #(doto root
+               (.removeEventListener "mousedown" fn-start)
+               (.removeEventListener "mouseup" fn-end)))))
+      [start])
 
     [:div.extensions__pdf-area-selection
      {:ref *el}
-     (when (and start-coord end-coord)
-       [:div.shadow-rect {:style (calc-pos start-coord end-coord)}])]))
+     (when (and start end)
+       [:div.shadow-rect {:style (calc-rect start end)}])]))
 
 (rum/defc ^:large-vars/cleanup-todo pdf-highlights
   [^js el ^js viewer initial-hls loaded-pages {:keys [set-dirty-hls!]}]
@@ -641,8 +655,6 @@
     ;; render hls
     (rum/use-effect!
      (fn []
-       ;;(dd "=== rebuild highlights ===" (count highlights))
-
        (when-let [grouped-hls (and (sequential? highlights) (group-by :page highlights))]
          (doseq [page loaded-pages]
            (when-let [^js/HTMLDivElement hls-layer (pdf-utils/resolve-hls-layer! viewer page)]

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

@@ -21,6 +21,7 @@
 (def *area-dashed? (atom ((fnil identity false) (storage/get (str "ls-pdf-area-is-dashed")))))
 (def *area-mode? (atom false))
 (def *highlight-mode? (atom false))
+#_:clj-kondo/ignore
 (rum/defcontext *highlights-ctx*)
 
 (rum/defc pdf-settings

+ 27 - 26
src/main/frontend/extensions/srs.cljs

@@ -28,7 +28,8 @@
             [clojure.string :as string]
             [rum.core :as rum]
             [frontend.modules.shortcut.core :as shortcut]
-            [medley.core :as medley]))
+            [medley.core :as medley]
+            [frontend.context.i18n :refer [t]]))
 
 ;;; ================================================================
 ;;; Commentary
@@ -409,7 +410,7 @@
     (reset! *phase 1)))
 
 (def review-finished
-  [:p.p-2 "Congrats, you've reviewed all the cards for this query, see you next time! 💯"])
+  [:p.p-2 (t :flashcards/modal-finished)])
 
 (defn- btn-with-shortcut [{:keys [shortcut id btn-text background on-click class]}]
   (ui/button
@@ -457,9 +458,9 @@
            [:div.flex.my-4.justify-between
             (when-not (and (not preview?) (= next-phase 1))
               (btn-with-shortcut {:btn-text (case next-phase
-                                              1 "Hide answers"
-                                              2 "Show answers"
-                                              3 "Show clozes")
+                                              1 (t :flashcards/modal-btn-hide-answers)
+                                              2 (t :flashcards/modal-btn-show-answers)
+                                              3 (t :flashcards/modal-btn-show-clozes))
                                   :shortcut  "s"
                                   :id "card-answers"
                                   :class "mr-2"
@@ -467,7 +468,7 @@
             (when (and (not= @card-index (count blocks))
                        cards?
                        preview?)
-              (btn-with-shortcut {:btn-text "Next"
+              (btn-with-shortcut {:btn-text (t :flashcards/modal-btn-next-card)
                                   :shortcut "n"
                                   :id       "card-next"
                                   :class    "mr-2"
@@ -477,7 +478,7 @@
 
             (when (and (not preview?) (= 1 next-phase))
               [:<>
-               (btn-with-shortcut {:btn-text   "Forgotten"
+               (btn-with-shortcut {:btn-text   (t :flashcards/modal-btn-forgotten)
                                    :shortcut   "f"
                                    :id         "card-forgotten"
                                    :background "red"
@@ -486,12 +487,12 @@
                                                  (let [tomorrow (tc/to-string (t/plus (t/today) (t/days 1)))]
                                                    (editor-property/set-block-property! root-block-id card-next-schedule-property tomorrow)))})
 
-               (btn-with-shortcut {:btn-text (if (util/mobile?) "Hard" "Took a while to recall")
+               (btn-with-shortcut {:btn-text (if (util/mobile?) "Hard" (t :flashcards/modal-btn-recall))
                                    :shortcut "t"
                                    :id       "card-recall"
                                    :on-click #(score-and-next-card 3 card card-index finished? phase review-records cb)})
 
-               (btn-with-shortcut {:btn-text   "Remembered"
+               (btn-with-shortcut {:btn-text   (t :flashcards/modal-btn-remembered)
                                    :shortcut   "r"
                                    :id         "card-remembered"
                                    :background "green"
@@ -499,10 +500,10 @@
 
             (when preview?
               (ui/tippy {:html [:div.text-sm
-                                "Reset this card so that you can review it immediately."]
+                                (t :flashcards/modal-btn-reset-tip)]
                          :class "tippy-hover"
                          :interactive true}
-                        (ui/button [:span "Reset"]
+                        (ui/button [:span (t :flashcards/modal-btn-reset)]
                                    :id "card-reset"
                                    :class (util/hiccup->class "opacity-60.hover:opacity-100.card-reset")
                                    :on-click (fn [e]
@@ -589,11 +590,11 @@
   (let [cards (db-model/get-macro-blocks (state/get-current-repo) "cards")
         items (->> (map (comp :logseq.macro-arguments :block/properties) cards)
                    (map (fn [col] (string/join " " col))))
-        items (concat items ["All"])]
+        items (concat items [(t :flashcards/modal-select-all)])]
     (component-select/select {:items items
                               :on-chosen on-chosen
                               :close-modal? false
-                              :input-default-placeholder "Switch to"
+                              :input-default-placeholder (t :flashcards/modal-select-switch)
                               :extract-fn nil})))
 
 ;;; register cards macro
@@ -626,12 +627,12 @@
                {:on-mouse-down (fn [e]
                                  (util/stop e)
                                  (toggle-fn))}
-               [:span.flex (if (string/blank? query-string) "All" query-string)
+               [:span.flex (if (string/blank? query-string) (t :flashcards/modal-select-all) query-string)
                 [:span {:style {:margin-top 2}}
                  (svg/caret-down)]]])
             (fn [{:keys [toggle-fn]}]
               (cards-select {:on-chosen (fn [query]
-                                          (let [query' (if (= query "All") "" query)]
+                                          (let [query' (if (= query (t :flashcards/modal-select-all)) "" query)]
                                             (reset! query-atom query')
                                             (toggle-fn)))}))
             {:modal-class (util/hiccup->class
@@ -641,13 +642,13 @@
 
            ;; FIXME: CSS issue
            (if @*preview-mode?
-             (ui/tippy {:html [:div.text-sm "current/total"]
+             (ui/tippy {:html [:div.text-sm (t :flashcards/modal-current-total)]
                         :interactive true}
                        [:div.opacity-60.text-sm.mr-3
                         @*card-index
                         [:span "/"]
                         total])
-             (ui/tippy {:html [:div.text-sm "overdue/total"]
+             (ui/tippy {:html [:div.text-sm (t :flashcards/modal-overdue-total)]
                         ;; :class "tippy-hover"
                         :interactive true}
                        [:div.opacity-60.text-sm.mr-3
@@ -656,7 +657,7 @@
                         total]))
 
            (ui/tippy
-            {:html [:div.text-sm "Toggle preview mode"]
+            {:html [:div.text-sm (t :flashcards/modal-toggle-preview-mode)]
              :delay [1000, 100]
              :class "tippy-hover"
              :interactive true
@@ -671,7 +672,7 @@
              "A"])
 
            (ui/tippy
-            {:html [:div.text-sm "Toggle random mode"]
+            {:html [:div.text-sm (t :flashcards/modal-toggle-random-mode)]
              :delay [1000, 100]
              :class "tippy-hover"
              :interactive true}
@@ -701,15 +702,15 @@
                      *card-index))]])
       (if (:global? config)
         [:div.ls-card.content
-         [:h1.title "Time to create a card!"]
+         [:h1.title (t :flashcards/modal-welcome-title)]
 
          [:div
-          [:p "You can add \"#card\" to any block to turn it into a card or trigger \"/cloze\" to add some clozes."]
+          [:p (t :flashcards/modal-welcome-desc-1)]
           [:img.my-4 {:src "https://docs.logseq.com/assets/2021-07-22_22.28.02_1626964258528_0.gif"}]
-          [:p "You can "
-           [:a {:href "https://docs.logseq.com/#/page/cards" :target "_blank"}
-            "click this link"]
-           " to check the documentation."]]]
+          [:p (t :flashcards/modal-welcome-desc-2)
+           [:a {:href "https://docs.logseq.com/#/page/Flashcards" :target "_blank"}
+            (t :flashcards/modal-welcome-desc-3)]
+           (t :flashcards/modal-welcome-desc-4)]]]
         [:div.opacity-60.custom-query-title.ls-card.content
          [:div.w-full.flex-1
           [:code.p-1 (str "Cards: " query-string)]]
@@ -809,4 +810,4 @@
       (when (nil? @*due-cards-interval)
         ;; refresh every hour
         (let [interval (js/setInterval f (* 3600 1000))]
-          (reset! *due-cards-interval interval))))))
+          (reset! *due-cards-interval interval))))))

+ 4 - 2
src/main/frontend/extensions/tldraw.cljs

@@ -5,6 +5,7 @@
             [frontend.components.export :as export]
             [frontend.components.page :as page]
             [frontend.config :as config]
+            [frontend.context.i18n :refer [t]]
             [frontend.db.model :as model]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.route :as route-handler]
@@ -93,7 +94,8 @@
 (def undo (fn [] (history/undo! nil)))
 (def redo (fn [] (history/redo! nil)))
 (defn get-tldraw-handlers [current-whiteboard-name]
-  {:search search-handler
+  {:t (fn [key] (t (keyword key)))
+   :search search-handler
    :queryBlockByUUID (fn [block-uuid]
                        (clj->js
                         (model/query-block-by-uuid (parse-uuid block-uuid))))
@@ -128,7 +130,7 @@
 
 (rum/defc tldraw-app
   [page-name block-id]
-  (let [populate-onboarding?  (whiteboard-handler/should-populate-onboarding-whiteboard? page-name)
+  (let [populate-onboarding? (whiteboard-handler/should-populate-onboarding-whiteboard? page-name)
         data (whiteboard-handler/page-name->tldr! page-name)
         [loaded-app set-loaded-app] (rum/use-state nil)
         on-mount (fn [^js tln]

+ 4 - 1
src/main/frontend/handler/code.cljs

@@ -18,9 +18,12 @@
     (when editor
       (.save editor)
       (let [textarea (.getTextArea editor)
+            ds (.-dataset textarea)
             value (gobj/get textarea "value")
-            default-value (gobj/get textarea "defaultValue")]
+            default-value (or (.-v ds) (gobj/get textarea "defaultValue"))]
         (when (not= value default-value)
+          ;; update default value for the editor initial state
+          (set! ds -v value)
           (cond
             ;; save block content
             (:block/uuid config)

+ 1 - 1
src/main/frontend/handler/common/config_edn.cljs

@@ -92,7 +92,7 @@ nested keys or positional errors e.g. tuples"
   (let [body (try (edn/read-string content)
                (catch :default _ ::failed-to-detect))
         warnings {:editor/command-trigger
-                  "Will no longer be supported soon. Please use '/' and report bugs on it."}]
+                  "is no longer supported. Please use '/' and report bugs on it."}]
     (cond
       (= body ::failed-to-detect)
       (log/info :msg "Skip deprecation check since config is not valid edn")

+ 49 - 36
src/main/frontend/handler/editor.cljs

@@ -169,7 +169,6 @@
   ([text]
    (when-let [m (get-selection-and-format)]
      (let [{:keys [selection-start selection-end format selection value edit-id input]} m
-           cur-pos (cursor/pos input)
            empty-selection? (= selection-start selection-end)
            selection-link? (and selection (gp-mldoc/mldoc-link? format selection))
            [content forward-pos] (cond
@@ -191,7 +190,7 @@
                       (subs value 0 selection-start)
                       content
                       (subs value selection-end))
-           cur-pos (or selection-start cur-pos)]
+           cur-pos (or selection-start (cursor/pos input))]
        (state/set-edit-content! edit-id new-value)
        (cursor/move-cursor-to input (+ cur-pos forward-pos))))))
 
@@ -301,10 +300,10 @@
            (save-block-inner! block value opts)))))))
 
 (defn- compute-fst-snd-block-text
-  [value pos]
+  [value selection-start selection-end]
   (when (string? value)
-    (let [fst-block-text (subs value 0 pos)
-          snd-block-text (string/triml (subs value pos))]
+    (let [fst-block-text (subs value 0 selection-start)
+          snd-block-text (string/triml (subs value selection-end))]
       [fst-block-text snd-block-text])))
 
 (declare save-current-block!)
@@ -342,13 +341,21 @@
     (= uuid block-id)))
 
 (defn insert-new-block-before-block-aux!
-  [config block _value {:keys [ok-handler]}]
-  (let [new-m {:block/uuid (db/new-block-id)
+  [config block value {:keys [ok-handler]}]
+  (let [edit-input-id (state/get-edit-input-id)
+        input (gdom/getElement edit-input-id)
+        input-text-selected? (util/input-text-selected? input)
+        new-m {:block/uuid (db/new-block-id)
                :block/content ""}
         prev-block (-> (merge (select-keys block [:block/parent :block/left :block/format
                                                   :block/page :block/journal?]) new-m)
                        (editor-impl/wrap-parse-block))
         left-block (db/pull (:db/id (:block/left block)))]
+    (when input-text-selected?
+      (let [selection-start (util/get-selection-start input)
+            selection-end (util/get-selection-end input)
+            [_ new-content] (compute-fst-snd-block-text value selection-start selection-end)]
+        (state/set-edit-content! edit-input-id new-content)))
     (profile
      "outliner insert block"
      (let [sibling? (not= (:db/id left-block) (:db/id (:block/parent block)))]
@@ -365,8 +372,9 @@
     :as _opts}]
   (let [block-self? (block-self-alone-when-insert? config uuid)
         input (gdom/getElement (state/get-edit-input-id))
-        pos (cursor/pos input)
-        [fst-block-text snd-block-text] (compute-fst-snd-block-text value pos)
+        selection-start (util/get-selection-start input)
+        selection-end (util/get-selection-end input)
+        [fst-block-text snd-block-text] (compute-fst-snd-block-text value selection-start selection-end)
         current-block (assoc block :block/content fst-block-text)
         current-block (apply dissoc current-block db-schema/retract-attributes)
         new-m {:block/uuid (db/new-block-id)
@@ -419,8 +427,9 @@
                        block)
              block-self? (block-self-alone-when-insert? config block-id)
              input (gdom/getElement (state/get-edit-input-id))
-             pos (cursor/pos input)
-             [fst-block-text snd-block-text] (compute-fst-snd-block-text value pos)
+             selection-start (util/get-selection-start input)
+             selection-end (util/get-selection-end input)
+             [fst-block-text snd-block-text] (compute-fst-snd-block-text value selection-start selection-end)
              insert-fn (cond
                          block-self?
                          insert-new-block-aux!
@@ -691,7 +700,7 @@
    (delete-block! repo true))
   ([repo delete-children?]
    (state/set-editor-op! :delete)
-   (let [{:keys [id block-id block-parent-id value format]} (get-state)]
+   (let [{:keys [id block-id block-parent-id value format config]} (get-state)]
      (when block-id
        (let [page-id (:db/id (:block/page (db/entity [:block/uuid block-id])))
              page-blocks-count (and page-id (db/get-page-blocks-count repo page-id))]
@@ -707,24 +716,28 @@
              (when-not (and has-children? left-has-children?)
                (when block-parent-id
                  (let [block-parent (gdom/getElement block-parent-id)
-                       sibling-block (util/get-prev-block-non-collapsed-non-embed block-parent)
+                       sibling-block (if (:embed? config)
+                                       (util/get-prev-block-non-collapsed
+                                        block-parent
+                                        {:container (util/rec-get-blocks-container block-parent)})
+                                       (util/get-prev-block-non-collapsed-non-embed block-parent))
                        {:keys [prev-block new-content move-fn]} (move-to-prev-block repo sibling-block format id value false)
                        concat-prev-block? (boolean (and prev-block new-content))
                        transact-opts (cond->
-                                       {:outliner-op :delete-blocks}
+                                      {:outliner-op :delete-blocks}
                                        concat-prev-block?
                                        (assoc :concat-data
                                               {:last-edit-block (:block/uuid block)}))]
                    (outliner-tx/transact! transact-opts
-                     (if concat-prev-block?
-                       (let [prev-block' (if (seq (:block/_refs block-e))
-                                           (assoc prev-block
-                                                  :block/uuid (:block/uuid block)
-                                                  :block.temp/additional-properties (:block/properties block))
-                                           prev-block)]
-                         (delete-block-aux! block delete-children?)
-                         (save-block! repo prev-block' new-content {:editor/op :delete}))
-                       (delete-block-aux! block delete-children?)))
+                                          (if concat-prev-block?
+                                            (let [prev-block' (if (seq (:block/_refs block-e))
+                                                                (assoc prev-block
+                                                                       :block/uuid (:block/uuid block)
+                                                                       :block.temp/additional-properties (:block/properties block))
+                                                                prev-block)]
+                                              (delete-block-aux! block delete-children?)
+                                              (save-block! repo prev-block' new-content {:editor/op :delete}))
+                                            (delete-block-aux! block delete-children?)))
                    (move-fn)))))))))
    (state/set-editor-op! nil)))
 
@@ -1416,7 +1429,7 @@
                                        (if file-obj (.-name file-obj) (if image? "image" "asset"))
                                        image?)
                   format
-                  {:last-pattern (if drop-or-paste? "" (state/get-editor-command-trigger))
+                  {:last-pattern (if drop-or-paste? "" commands/command-trigger)
                    :restore?     true
                    :command      :insert-asset})))))
           (p/finally
@@ -1555,7 +1568,7 @@
           last-command (and last-slash-caret-pos (subs edit-content last-slash-caret-pos pos))]
       (when (> pos 0)
         (or
-         (and (= (state/get-editor-command-trigger) (util/nth-safe edit-content (dec pos)))
+         (and (= commands/command-trigger (util/nth-safe edit-content (dec pos)))
               @commands/*initial-commands)
          (and last-command
               (commands/get-matched-commands last-command)))))
@@ -1673,7 +1686,7 @@
                id
                (get-link format link label)
                format
-               {:last-pattern (str (state/get-editor-command-trigger) "link")
+               {:last-pattern (str commands/command-trigger "link")
                 :command :link})))
 
     :image-link (let [{:keys [link label]} m]
@@ -1682,7 +1695,7 @@
                      id
                      (get-image-link format link label)
                      format
-                     {:last-pattern (str (state/get-editor-command-trigger) "link")
+                     {:last-pattern (str commands/command-trigger "link")
                       :command :image-link})))
 
     nil)
@@ -1748,12 +1761,11 @@
     (cond
       (and (= content "1. ") (= last-input-char " ") input-id edit-block
            (not (own-order-number-list? edit-block)))
-      (do
-        (state/set-edit-content! input-id "")
-        (-> (p/delay 10)
-            (p/then #(state/pub-event! [:editor/toggle-own-number-list edit-block]))))
+      (p/do!
+       (state/pub-event! [:editor/toggle-own-number-list edit-block])
+       (state/set-edit-content! input-id ""))
 
-      (and (= last-input-char (state/get-editor-command-trigger))
+      (and (= last-input-char commands/command-trigger)
            (or (re-find #"(?m)^/" (str (.-value input))) (start-of-new-word? input pos)))
       (do
         (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)})
@@ -1935,6 +1947,7 @@
                                                                          :keep-uuid? keep-uuid?})]
          (edit-last-block-after-inserted! result))))))
 
+
 (defn- block-tree->blocks
   "keep-uuid? - maintain the existing :uuid in tree vec"
   [repo tree-vec format keep-uuid? page-name]
@@ -2605,7 +2618,7 @@
             (delete-block! repo false))))
 
       (and (> current-pos 1)
-           (= (util/nth-safe value (dec current-pos)) (state/get-editor-command-trigger)))
+           (= (util/nth-safe value (dec current-pos)) commands/command-trigger))
       (do
         (util/stop e)
         (commands/restore-state)
@@ -2891,8 +2904,8 @@
                (util/event-is-composing? e true)])]
         (cond
           ;; When you type something after /
-          (and (= :commands (state/get-editor-action)) (not= k (state/get-editor-command-trigger)))
-          (if (= (state/get-editor-command-trigger) (second (re-find #"(\S+)\s+$" value)))
+          (and (= :commands (state/get-editor-action)) (not= k commands/command-trigger))
+          (if (= commands/command-trigger (second (re-find #"(\S+)\s+$" value)))
             (state/clear-editor-action!)
             (let [matched-commands (get-matched-commands input)]
               (if (seq matched-commands)
@@ -3495,7 +3508,7 @@
         edit-block (state/get-edit-block)
         target-element (.-nodeName (.-target e))]
     (cond
-      (whiteboard?)
+      (and (whiteboard?) (not edit-input))
       (do
         (util/stop e)
         (.selectAll (.-api ^js (state/active-tldraw-app))))

+ 2 - 2
src/main/frontend/handler/editor/lifecycle.cljs

@@ -27,7 +27,7 @@
       (js/setTimeout #(util/scroll-editor-cursor element) 50)))
   state)
 
-(defn did-remount!
+(defn will-remount!
   [_old-state state]
   (keyboards-handler/esc-save! state)
   state)
@@ -45,5 +45,5 @@
 
 (def lifecycle
   {:did-mount did-mount!
-   :did-remount did-remount!
+   :will-remount will-remount!
    :will-unmount will-unmount})

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

@@ -23,6 +23,7 @@
             [frontend.components.shell :as shell]
             [frontend.components.whiteboard :as whiteboard]
             [frontend.components.user.login :as login]
+            [frontend.components.shortcut :as shortcut]
             [frontend.components.repo :as repo]
             [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
@@ -167,12 +168,8 @@
 
 ;; Parameters for the `persist-db` function, to show the notification messages
 (def persist-db-noti-m
-  {:before     #(notification/show!
-                 (ui/loading (t :graph/persist))
-                 :warning)
-   :on-error   #(notification/show!
-                 (t :graph/persist-error)
-                 :error)})
+  {:before     #(ui/notify-graph-persist!)
+   :on-error   #(ui/notify-graph-persist-error!)})
 
 (defn- graph-switch-on-persisted
   "Logic for keeping db sync when switching graphs
@@ -949,6 +946,11 @@
 (defmethod handle :editor/quick-capture [[_ args]]
   (quick-capture/quick-capture args))
 
+(defmethod handle :modal/keymap-manager [[_]]
+  (state/set-modal!
+    #(shortcut/keymap-pane)
+    {:label "keymap-manager"}))
+
 (defmethod handle :editor/toggle-own-number-list [[_ blocks]]
   (let [batch? (sequential? blocks)
         blocks (cond->> blocks
@@ -973,15 +975,20 @@
   []
   (let [chan (state/get-events-chan)]
     (async/go-loop []
-      (let [payload (async/<! chan)]
-        (try
-          (handle payload)
-          (catch :default error
-            (let [type :handle-system-events/failed]
-              (js/console.error (str type) (clj->js payload) "\n" error)
-              (state/pub-event! [:capture-error {:error error
-                                                 :payload {:type type
-                                                           :payload payload}}])))))
+      (let [[payload d] (async/<! chan)]
+        (->
+         (try
+           (p/resolved (handle payload))
+           (catch :default error
+             (p/rejected error)))
+         (p/then (fn [result]
+                   (p/resolve! d result)))
+         (p/catch (fn [error]
+                    (let [type :handle-system-events/failed]
+                      (state/pub-event! [:capture-error {:error error
+                                                         :payload {:type type
+                                                                   :payload payload}}])
+                      (p/reject! d error))))))
       (recur))
     chan))
 

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

@@ -41,7 +41,7 @@
     (state/set-global-config! config)
     config))
 
-(def default-content (rc/inline "global-config.edn"))
+(def default-content (rc/inline "templates/global-config.edn"))
 
 (defn- create-global-config-file-if-not-exists
   [repo-url]

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