Browse Source

Merge branch 'master' into feat/db

Gabriel Horner 2 years ago
parent
commit
41bd76a704
100 changed files with 3856 additions and 2612 deletions
  1. 1 1
      .github/workflows/build.yml
  2. 2 2
      android/app/build.gradle
  3. 1 0
      capacitor.config.ts
  4. 1 1
      deps/common/src/logseq/common/path.cljs
  5. 2 2
      deps/shui/src/logseq/shui/context.cljs
  6. 1 1
      docs/Build LogSeq Desktop for windows on Ubuntu.md
  7. 3 3
      docs/accessibility.md
  8. 3 3
      docs/contributing-to-translations.md
  9. 3 3
      docs/develop-logseq-on-mobile.md
  10. 15 10
      docs/develop-logseq-on-windows.md
  11. 4 4
      ios/App/App.xcodeproj/project.pbxproj
  12. 1 0
      libs/package.json
  13. 10 5
      libs/src/LSPlugin.caller.ts
  14. 41 32
      libs/src/LSPlugin.core.ts
  15. 56 33
      libs/src/LSPlugin.ts
  16. 115 100
      libs/src/LSPlugin.user.ts
  17. 47 39
      libs/src/helpers.ts
  18. 2 1
      libs/src/modules/LSPlugin.Experiments.ts
  19. 34 36
      libs/src/modules/LSPlugin.Request.ts
  20. 48 51
      libs/src/modules/LSPlugin.Search.ts
  21. 1 1
      libs/src/modules/LSPlugin.Storage.ts
  22. 20 3
      libs/src/postmate/index.ts
  23. 5 0
      libs/yarn.lock
  24. 2 2
      package.json
  25. 1 1
      resources/forge.config.js
  26. 0 0
      resources/js/lsplugin.core.js
  27. 2 2
      resources/package.json
  28. 1 1
      scripts/get-pkg-version.js
  29. 5 5
      scripts/src/logseq/tasks/lang.clj
  30. 9 2
      src/electron/electron/core.cljs
  31. 1 2
      src/main/frontend/common.css
  32. 1 1
      src/main/frontend/components/block.cljs
  33. 2 2
      src/main/frontend/components/command_palette.cljs
  34. 1 18
      src/main/frontend/components/container.css
  35. 4 4
      src/main/frontend/components/conversion.cljs
  36. 2 0
      src/main/frontend/components/plugins.cljs
  37. 2 2
      src/main/frontend/components/plugins.css
  38. 1 1
      src/main/frontend/components/plugins_settings.cljs
  39. 3 2
      src/main/frontend/components/query_table.cljs
  40. 12 2
      src/main/frontend/components/reference.cljs
  41. 32 15
      src/main/frontend/components/settings.cljs
  42. 40 52
      src/main/frontend/components/settings.css
  43. 4 15
      src/main/frontend/components/shortcut.cljs
  44. 165 0
      src/main/frontend/components/shortcut.css
  45. 476 0
      src/main/frontend/components/shortcut2.cljs
  46. 1 1
      src/main/frontend/components/theme.cljs
  47. 1 1
      src/main/frontend/components/whiteboard.cljs
  48. 5 2
      src/main/frontend/config.cljs
  49. 0 12
      src/main/frontend/db/model.cljs
  50. 11 0
      src/main/frontend/db/persist.cljs
  51. 4 2
      src/main/frontend/dicts.cljc
  52. 5 2
      src/main/frontend/extensions/excalidraw.cljs
  53. 0 801
      src/main/frontend/extensions/pdf/_viewer.css
  54. 11 6
      src/main/frontend/extensions/pdf/core.cljs
  55. 10 5
      src/main/frontend/extensions/pdf/pdf.css
  56. 1 1
      src/main/frontend/extensions/srs.cljs
  57. 17 16
      src/main/frontend/fs/capacitor_fs.cljs
  58. 308 238
      src/main/frontend/fs/sync.cljs
  59. 106 24
      src/main/frontend/fs/watcher_handler.cljs
  60. 3 2
      src/main/frontend/handler/command_palette.cljs
  61. 3 0
      src/main/frontend/handler/config.cljs
  62. 20 5
      src/main/frontend/handler/editor.cljs
  63. 32 36
      src/main/frontend/handler/events.cljs
  64. 8 3
      src/main/frontend/handler/file_sync.cljs
  65. 18 1
      src/main/frontend/handler/global_config.cljs
  66. 5 3
      src/main/frontend/handler/notification.cljs
  67. 24 24
      src/main/frontend/handler/page.cljs
  68. 12 3
      src/main/frontend/handler/plugin.cljs
  69. 2 1
      src/main/frontend/handler/repo.cljs
  70. 5 3
      src/main/frontend/handler/route.cljs
  71. 11 5
      src/main/frontend/handler/user.cljs
  72. 28 0
      src/main/frontend/handler/web/nfs.cljs
  73. 8 0
      src/main/frontend/idb.cljs
  74. 26 24
      src/main/frontend/mixins.cljs
  75. 11 10
      src/main/frontend/mobile/core.cljs
  76. 672 695
      src/main/frontend/modules/shortcut/config.cljs
  77. 158 74
      src/main/frontend/modules/shortcut/core.cljs
  78. 173 73
      src/main/frontend/modules/shortcut/data_helper.cljs
  79. 58 0
      src/main/frontend/modules/shortcut/utils.cljs
  80. 1 6
      src/main/frontend/routes.cljs
  81. 0 2
      src/main/frontend/search/agency.cljs
  82. 41 12
      src/main/frontend/state.cljs
  83. 8 7
      src/main/frontend/ui.cljs
  84. 1 1
      src/main/frontend/ui/date_picker.cljs
  85. 0 13
      src/main/frontend/util.cljc
  86. 1 1
      src/main/frontend/util/keycode.cljs
  87. 1 1
      src/main/frontend/version.cljs
  88. 24 14
      src/main/logseq/api.cljs
  89. 1 1
      src/main/logseq/api/block.cljs
  90. 0 2
      src/resources/dicts/de.edn
  91. 18 3
      src/resources/dicts/en.edn
  92. 0 2
      src/resources/dicts/es.edn
  93. 0 2
      src/resources/dicts/fr.edn
  94. 821 0
      src/resources/dicts/id.edn
  95. 0 2
      src/resources/dicts/it.edn
  96. 0 2
      src/resources/dicts/ja.edn
  97. 0 2
      src/resources/dicts/ko.edn
  98. 0 2
      src/resources/dicts/nb-no.edn
  99. 0 2
      src/resources/dicts/nl.edn
  100. 0 2
      src/resources/dicts/pl.edn

+ 1 - 1
.github/workflows/build.yml

@@ -25,7 +25,7 @@ jobs:
       - name: Checkout Actions Repository
         uses: actions/checkout@v3
       - name: Check spelling with custom config file
-        uses: crate-ci/[email protected]3.10
+        uses: crate-ci/[email protected]6.8
         with:
           config: ./typos.toml
 

+ 2 - 2
android/app/build.gradle

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

+ 1 - 0
capacitor.config.ts

@@ -8,6 +8,7 @@ const config: CapacitorConfig = {
   appName: 'Logseq',
   bundledWebRuntime: false,
   webDir: 'public',
+  loggingBehavior: 'debug',
   plugins: {
     SplashScreen: {
       launchShowDuration: 500,

+ 1 - 1
deps/common/src/logseq/common/path.cljs

@@ -294,7 +294,7 @@
 ;; compat
 (defn basename
   [path]
-  (let [path (string/replace path #"/$" "")]
+  (let [path (string/replace path #"/+$" "")]
     (filename path)))
 
 (defn dirname

+ 2 - 2
deps/shui/src/logseq/shui/context.cljs

@@ -16,8 +16,8 @@
    ;; Wrap the old inline function to allow for interception, but fallback to the old inline function
    :inline-block (inline->inline-block inline block-config)
    :map-inline-block (inline->map-inline-block inline block-config)
-   ;; Currently frontend component are provided an object map containin at least the following keys:
-   ;; These will be passed through in a whitelisted fashion so as to be able to track the dependencies
+   ;; Currently frontend component are provided an object map containing at least the following keys:
+   ;; These will be passed through in a whitelisted fashion so as to be able to track the dependencies  
    ;; back to the core application
    ;; TODO: document the following
    :block (:block block-config)  ;; the db entity of the current block

+ 1 - 1
docs/Build LogSeq Desktop for windows on Ubuntu.md

@@ -1,7 +1,7 @@
 # Building Logseq Desktop app for Windows on Ubuntu
 ## Intro
 My Logseq dev machine is on Ubuntu 18.x and my production machine is running Windows 10, I needed a way to compile the Logseq desktop APP for Windows.
-I tired & failed to make the "build" run on my windows machine but I did, however, succeed in letting my Ubuntu machine make Windows x64 files
+I tried & failed to make the "build" run on my windows machine but I did, however, succeed in letting my Ubuntu machine make Windows x64 files
 ## Pre-requisites
 These are the steps I took to make it work on my Ubuntu machine, sharing them hoping it helps someone else. I assume you have all the basic pre-requisites for Logseq, if not you can find them at https://github.com/logseq/logseq#1-requirements
 1. clone Logseq repo if you haven't already

+ 3 - 3
docs/accessibility.md

@@ -1,4 +1,4 @@
-- Accessibility is a vague term, which is why it is usually misunderstood. It is not just about people with with specific disabilities. You can read more about [what is accessibility](https://developer.mozilla.org/en-US/docs/Learn/Accessibility/What_is_accessibility#so_what_is_accessibility) and [diverse abilities and barriers](https://www.w3.org/WAI/people-use-web/abilities-barriers/).
+- Accessibility is a vague term, which is why it is usually misunderstood. It is not just about people with specific disabilities. You can read more about [what is accessibility](https://developer.mozilla.org/en-US/docs/Learn/Accessibility/What_is_accessibility#so_what_is_accessibility) and [diverse abilities and barriers](https://www.w3.org/WAI/people-use-web/abilities-barriers/).
 - ## Web Content Accessibility Guidelines
 	- [WCAG](https://www.w3.org/WAI/standards-guidelines/wcag/) (Web Content Accessibility Guidelines) is the international standard for web content accessibility, developed by [W3C](https://www.w3.org/). Logseq is a web application, so conforming with WCAG should be our first priority. In general, there is no simple way to determine if a website is accessible or not, but WCAG can help us make the tool usable by as many people as possible.
 - ## Levels of conformance
@@ -6,7 +6,7 @@
 		- Level **A** is the minimum level.
 		- Level **AA** includes all Level A and AA requirements.
 		- Level **AAA** includes all Level A, AA, and AAA requirements.
-	- Many organizations strive to meet Level AA. The reason behind this decision, is that in some cases AAA standard is too strict. That does't mean that triple-A issues should be disregarded. On the contrary, all of them should be handled if possible.
+	- Many organizations strive to meet Level AA. The reason behind this decision, is that in some cases AAA standard is too strict. That doesn't mean that triple-A issues should be disregarded. On the contrary, all of them should be handled if possible.
 	- We can also provide alternative options in order to conform with AAA standards. For instance, our default themes can aim for AA, but we can provide a high-contrast theme that aims for AAA. Providing [alternative versions](https://www.w3.org/WAI/GL/2007/05/alternate-versions.html) with different levels of conformance is permitted according to WCAG, if there is an accessible way to reach those alternatives.
 - ## Simple development guidelines
 	- Use semantically correct markup whenever possible. Every time you are about to decide which html tag you are going to use, choose the one that behaves the way you want it. For example, let's say you want to create an element that looks like plain text, but triggers an action on click. Usually, the best approach would be to create a `<button>` and make it look like a `<span>` using css. If you use a `span`, you will also have to override other html attributes like `tabindex` and `role` to make the element behave like a button. This is almost always a bad sign, and should be avoided. If you use the appropriate html element, the browser will be able to properly handle it.
@@ -18,7 +18,7 @@
 	- There is a [huge list of tools](https://www.w3.org/WAI/ER/tools/) that can help us test our application. Most of them use [axe-core](https://github.com/dequelabs/axe-core) internally. There are [browser extensions](https://www.deque.com/axe/browser-extensions/) based on axe, a [VSCode Linter Plugin](https://marketplace.visualstudio.com/items?itemName=deque-systems.vscode-axe-linter) and also [multiple community projects](https://github.com/dequelabs/axe-core/blob/develop/doc/projects.md#community-projects).
 	- Basic accessibility testing could be integrated into our CI, by using the appropriate axe package (e.g. [@axe-core/playwright](https://github.com/dequelabs/axe-core-npm/blob/develop/packages/playwright/README.md))
 - ## Manual testing
-	- In theory, all of the cases described by WCAG should be testable. In practice, there are issues that can't be replicated by code. Also, manual accessibility testing would help us have a better understanding of the difficulties that certain people might encounter. Even if the all the individual cases pass the tests, the overall navigation might be nonsensical.
+	- In theory, all of the cases described by WCAG should be testable. In practice, there are issues that can't be replicated by code. Also, manual accessibility testing would help us have a better understanding of the difficulties that certain people might encounter. Even if all the individual cases pass the tests, the overall navigation might be nonsensical.
 	- ### Manual accessibility testing musts
 		- Keyboard-only navigation
 		- Screen reader testing and compatibility

+ 3 - 3
docs/contributing-to-translations.md

@@ -14,7 +14,7 @@ In order to run the commands in this doc, you will need to install
 ## Where to Contribute
 
 Language translations are under,
-[src/resources/dicts/](https://github.com/logseq/logseq/blob/master/src/resources/dicts/) with each language having it's own file. For example, the es locale is in `es.edn`.
+[src/resources/dicts/](https://github.com/logseq/logseq/blob/master/src/resources/dicts/) with each language having its own file. For example, the es locale is in `es.edn`.
 
 ## Language Overview
 
@@ -83,7 +83,7 @@ Almost all translations are small. The only exceptions to this are the keys `:tu
 ### 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.
+* 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 for something the app needs to calculate e.g. a number. See [these docs](https://github.com/tonsky/tongue#interpolation) for more examples.
 * Rarely, a translation may need to translate formatted text by returning [hiccup-style HTML](https://github.com/weavejester/hiccup#syntax). In this case, a Clojure function is the recommended approach. For example, a function translation would look like `(fn [] [:div "FOO"])`. See `:on-boarding/main-title` for an example.
 ## Fix Mistakes
 
@@ -105,4 +105,4 @@ To add a new language:
 * Add an entry to `frontend.dicts/languages`
 * Create a new file under `src/resources/dicts/` and name the file the same as the locale e.g. zz.edn for a hypothetical zz locale.
 * Add an entry in `frontend.dicts/dicts` referencing the file you created.
-* Then start translating for your language and adding entries in your language's EDN file using the `bb lang:missing` workflow.
+* Then start translating for your language and adding entries in your language's EDN file using the `bb lang:missing` workflow.

+ 3 - 3
docs/develop-logseq-on-mobile.md

@@ -24,7 +24,7 @@
     ```
 - Working directory: Logseq root directory
 - Run `yarn && yarn app-watch` from the logseq project root directory in terminal.
-- Run `npx cap sync ios` in another termimal to copy web assets from public to *ios/App/App/public*, and create *capacitor.config.json* in *ios/App/App*, and update iOS plugins.
+- Run `npx cap sync ios` in another terminal to copy web assets from public to *ios/App/App/public*, and create *capacitor.config.json* in *ios/App/App*, and update iOS plugins.
 - Connect your iOS device to MacBook.
 - Run `npx cap open ios` to open Logseq project in Xcode, and build the app there.
 
@@ -70,13 +70,13 @@ or, you can run `bb release:ios-app` to do those steps with one command.
         } 
     ```
 - Run `yarn && yarn app-watch` from the logseq project root directory in terminal.
-- Run `npx cap sync android` in another termimal.
+- Run `npx cap sync android` in another terminal.
 - Run `npx cap run android` to install app into your device.
 
 or, you can run `bb dev:android-app` to do those steps with one command if you are on macOS.
 
 Then,
-- In Android Studio, open **Tools** -> **AVD Manager** to create Android Virtual Device (AVD), and lanuch it in the emulator.
+- In Android Studio, open **Tools** -> **AVD Manager** to create Android Virtual Device (AVD), and launch it in the emulator.
 - In Android Studio, open **Run** -> **Run** to run Logseq.
 - After logseq startup in Android virtual device, repl should be able to connect
 - For browser console print and devtool remote debug, open chrome, type url chrome://inspect/#devices, you should see your device there, click inspect

+ 15 - 10
docs/develop-logseq-on-windows.md

@@ -2,6 +2,17 @@
 
 This is a guide on setting up Logseq development dependencies on Windows.  Once these dependencies are installed, you can follow the  [develop-logseq](develop-logseq.md) docs for build instructions.
 
+## [scoop](https://scoop.sh/)
+
+Scoop provides a `clojure.exe` shim which works in Command Prompt and Powershell windows.
+
+```
+scoop bucket add scoop-clojure https://github.com/littleli/scoop-clojure
+scoop bucket add extras
+scoop bucket add java
+scoop install java/openjdk clj-deps babashka leiningen nodejs-lts
+```
+
 ## Winget
 
 Winget is a package manager installed by default on windows.
@@ -19,6 +30,10 @@ An installer for clojure is available from [casselc/clj-msi](https://github.com/
 
 ## [chocolatey](https://chocolatey.org/)
 
+Chocolatey installs Clojure as a PowerShell module and alias, and does not provide `clojure` for `cmd.exe`.
+
+[@andelf has written a wrapper utility](https://github.com/andelf/clojure-cli) which you can install with `cargo install --git https://github.com/andelf/clojure-cli.git` instead.
+
 ```
 choco install nvm
 nvm install 18
@@ -29,16 +44,6 @@ choco install javaruntime
 choco install clojure
 ```
 
-
-## [scoop](https://scoop.sh/)
-
-```
-scoop bucket add scoop-clojure https://github.com/littleli/scoop-clojure
-scoop bucket add extras
-scoop bucket add java
-scoop install java/openjdk clojure clj-deps babashka leiningen nodejs-lts
-```
-
 ## Troubleshooting
 
 ### Configuring a proxy for internet access

+ 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.14;
+				MARKETING_VERSION = 0.9.17;
 				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.14;
+				MARKETING_VERSION = 0.9.17;
 				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.14;
+				MARKETING_VERSION = 0.9.17;
 				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.14;
+				MARKETING_VERSION = 0.9.17;
 				MTL_FAST_MATH = YES;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
 				PRODUCT_NAME = "$(TARGET_NAME)";

+ 1 - 0
libs/package.json

@@ -18,6 +18,7 @@
   "dependencies": {
     "csstype": "3.1.0",
     "debug": "4.3.4",
+    "deepmerge": "4.3.1",
     "dompurify": "2.3.8",
     "eventemitter3": "4.0.7",
     "fast-deep-equal": "3.1.3",

+ 10 - 5
libs/src/LSPlugin.caller.ts

@@ -243,18 +243,23 @@ class LSPluginCaller extends EventEmitter {
         let { width, height, left, top, vw, vh } = mainLayoutInfo
 
         left = Math.max(left, 0)
-        left = (typeof vw === 'number') ?
-          `${Math.min(left * 100 / vw, 99)}%` : `${left}px`
+        left =
+          typeof vw === 'number'
+            ? `${Math.min((left * 100) / vw, 99)}%`
+            : `${left}px`
 
         // 45 is height of headbar
         top = Math.max(top, 45)
-        top = (typeof vh === 'number') ?
-          `${Math.min(top * 100 / vh, 99)}%` : `${top}px`
+        top =
+          typeof vh === 'number'
+            ? `${Math.min((top * 100) / vh, 99)}%`
+            : `${top}px`
 
         Object.assign(cnt.style, {
           width: width + 'px',
           height: height + 'px',
-          left, top
+          left,
+          top,
         })
       }
     } catch (e) {

+ 41 - 32
libs/src/LSPlugin.core.ts

@@ -91,7 +91,7 @@ class PluginSettings extends EventEmitter<'change' | 'reset'> {
       if (this._settings[k] == v) return
       this._settings[k] = v
     } else if (isObject(k)) {
-      deepMerge(this._settings, k)
+      this._settings = deepMerge(this._settings, k)
     } else {
       return
     }
@@ -395,11 +395,9 @@ class ExistedImportedPluginPackageError extends Error {
 /**
  * Host plugin for local
  */
-class PluginLocal extends EventEmitter<'loaded'
-  | 'unloaded'
-  | 'beforeunload'
-  | 'error'
-  | string> {
+class PluginLocal extends EventEmitter<
+  'loaded' | 'unloaded' | 'beforeunload' | 'error' | string
+> {
   private _sdk: Partial<PluginLocalSDKMetadata> = {}
   private _disposes: Array<() => Promise<any>> = []
   private _id: PluginLocalIdentity
@@ -534,7 +532,7 @@ class PluginLocal extends EventEmitter<'loaded'
     const localRoot = (this._localRoot = safetyPathNormalize(url))
     const logseq: Partial<LSPluginPkgConfig> = pkg.logseq || {}
 
-      // Pick legal attrs
+    // Pick legal attrs
     ;[
       'name',
       'author',
@@ -594,7 +592,9 @@ class PluginLocal extends EventEmitter<'loaded'
     // Validate id
     const { registeredPlugins, isRegistering } = this._ctx
     if (isRegistering && registeredPlugins.has(this.id)) {
-      throw new ExistedImportedPluginPackageError('Registered plugin package Error')
+      throw new ExistedImportedPluginPackageError(
+        'Registered plugin package Error'
+      )
     }
 
     return async () => {
@@ -642,10 +642,10 @@ class PluginLocal extends EventEmitter<'loaded'
     <meta charset="UTF-8">
     <title>logseq plugin entry</title>
     ${
-        IS_DEV
-          ? `<script src="${sdkPathRoot}/lsplugin.user.js?v=${tag}"></script>`
-          : `<script src="https://cdn.jsdelivr.net/npm/@logseq/libs/dist/lsplugin.user.min.js?v=${tag}"></script>`
-      }
+      IS_DEV
+        ? `<script src="${sdkPathRoot}/lsplugin.user.js?v=${tag}"></script>`
+        : `<script src="https://cdn.jsdelivr.net/npm/@logseq/libs/dist/lsplugin.user.min.js?v=${tag}"></script>`
+    }
     
   </head>
   <body>
@@ -924,7 +924,7 @@ class PluginLocal extends EventEmitter<'loaded'
           )
           this.emit('beforeunload', eventBeforeUnload)
         } catch (e) {
-          this.logger.error('[beforeunload Error]', e)
+          this.logger.error('[beforeunload]', e)
         }
 
         await this.dispose()
@@ -1103,7 +1103,8 @@ class LSPluginCore
     | 'beforereload'
     | 'reloaded'
   >
-  implements ILSPluginThemeManager {
+  implements ILSPluginThemeManager
+{
   private _isRegistering = false
   private _readyIndicator?: DeferredActor
   private readonly _hostMountedActor: DeferredActor = deferred()
@@ -1117,8 +1118,10 @@ class LSPluginCore
     externals: [],
   }
   private readonly _registeredThemes = new Map<PluginLocalIdentity, Theme[]>()
-  private readonly _registeredPlugins = new Map<PluginLocalIdentity,
-    PluginLocal>()
+  private readonly _registeredPlugins = new Map<
+    PluginLocalIdentity,
+    PluginLocal
+  >()
   private _currentTheme: {
     pid: PluginLocalIdentity
     opt: Theme | LegacyTheme
@@ -1194,14 +1197,18 @@ class LSPluginCore
       return
     }
 
-    const perfTable = new Map<string,
-      { o: PluginLocal; s: number; e: number }>()
+    const perfTable = new Map<
+      string,
+      { o: PluginLocal; s: number; e: number }
+    >()
     const debugPerfInfo = () => {
       const data: any = Array.from(perfTable.values()).reduce((ac, it) => {
         const { id, options, status, disabled } = it.o
 
-        if (disabled !== true &&
-          (options.entry || (!options.name && !options.entry))) {
+        if (
+          disabled !== true &&
+          (options.entry || (!options.name && !options.entry))
+        ) {
           ac[id] = {
             name: options.name,
             entry: options.entry,
@@ -1234,17 +1241,19 @@ class LSPluginCore
       // valid externals
       if (externals?.size) {
         try {
-          const validatedExternals: Record<string, boolean> = await invokeHostExportedApi(
-            'validate_external_plugins', [...externals]
-          )
+          const validatedExternals: Record<string, boolean> =
+            await invokeHostExportedApi('validate_external_plugins', [
+              ...externals,
+            ])
 
-          externals = new Set([...Object.entries(validatedExternals)].reduce(
-            (a, [k, v]) => {
+          externals = new Set(
+            [...Object.entries(validatedExternals)].reduce((a, [k, v]) => {
               if (v) {
                 a.push(k)
               }
               return a
-            }, []))
+            }, [])
+          )
         } catch (e) {
           console.error('[validatedExternals Error]', e)
         }
@@ -1557,12 +1566,12 @@ class LSPluginCore
       await this.saveUserPreferences(
         theme.mode
           ? {
-            themes: {
-              ...this._userPreferences.themes,
-              mode: theme.mode,
-              [theme.mode]: theme,
-            },
-          }
+              themes: {
+                ...this._userPreferences.themes,
+                mode: theme.mode,
+                [theme.mode]: theme,
+              },
+            }
           : { theme: theme }
       )
     }

+ 56 - 33
libs/src/LSPlugin.ts

@@ -6,7 +6,8 @@ import { LSPluginExperiments } from './modules/LSPlugin.Experiments'
 import { IAsyncStorage, LSPluginFileStorage } from './modules/LSPlugin.Storage'
 import { LSPluginRequest } from './modules/LSPlugin.Request'
 
-export type WithOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
+export type WithOptional<T, K extends keyof T> = Omit<T, K> &
+  Partial<Pick<T, K>>
 
 export type PluginLocalIdentity = string
 
@@ -297,7 +298,12 @@ export type ExternalCommandType =
 export type UserProxyTags = 'app' | 'editor' | 'db' | 'git' | 'ui' | 'assets'
 
 export type SearchIndiceInitStatus = boolean
-export type SearchBlockItem = { id: EntityID, uuid: BlockIdentity, content: string, page: EntityID }
+export type SearchBlockItem = {
+  id: EntityID
+  uuid: BlockIdentity
+  content: string
+  page: EntityID
+}
 export type SearchPageItem = string
 export type SearchFileItem = string
 
@@ -309,21 +315,23 @@ export interface IPluginSearchServiceHooks {
     graph: string,
     key: string,
     opts: Partial<{ limit: number }>
-  ) =>
-    Promise<{
-      graph: string,
-      key: string,
-      blocks?: Array<Partial<SearchBlockItem>>,
-      pages?: Array<SearchPageItem>,
-      files?: Array<SearchFileItem>
-    }>
+  ) => Promise<{
+    graph: string
+    key: string
+    blocks?: Array<Partial<SearchBlockItem>>
+    pages?: Array<SearchPageItem>
+    files?: Array<SearchFileItem>
+  }>
 
   onIndiceInit: (graph: string) => Promise<SearchIndiceInitStatus>
   onIndiceReset: (graph: string) => Promise<void>
-  onBlocksChanged: (graph: string, changes: {
-    added: Array<SearchBlockItem>,
-    removed: Array<EntityID>
-  }) => Promise<void>
+  onBlocksChanged: (
+    graph: string,
+    changes: {
+      added: Array<SearchBlockItem>
+      removed: Array<EntityID>
+    }
+  ) => Promise<void>
   onGraphRemoved: (graph: string, opts?: {}) => Promise<any>
 }
 
@@ -372,8 +380,14 @@ export interface IAppProxy {
    * @param action
    */
   registerCommandShortcut: (
-    keybinding: SimpleCommandKeybinding,
-    action: SimpleCommandCallback
+    keybinding: SimpleCommandKeybinding | string,
+    action: SimpleCommandCallback,
+    opts?: Partial<{
+      key: string
+      label: string
+      desc: string
+      extras: Record<string, any>
+    }>
   ) => void
 
   /**
@@ -392,10 +406,7 @@ export interface IAppProxy {
    * @param type `xx-plugin-id.commands.xx-key`, `xx-plugin-id.models.xx-key`
    * @param args
    */
-  invokeExternalPlugin: (
-    type: string,
-    ...args: Array<any>
-  ) => Promise<unknown>
+  invokeExternalPlugin: (type: string, ...args: Array<any>) => Promise<unknown>
 
   /**
    * @added 0.0.13
@@ -452,7 +463,11 @@ export interface IAppProxy {
   // templates
   getTemplate: (name: string) => Promise<BlockEntity | null>
   existTemplate: (name: string) => Promise<Boolean>
-  createTemplate: (target: BlockUUID, name: string, opts?: { overwrite: boolean }) => Promise<any>
+  createTemplate: (
+    target: BlockUUID,
+    name: string,
+    opts?: { overwrite: boolean }
+  ) => Promise<any>
   removeTemplate: (name: string) => Promise<any>
   insertTemplate: (target: BlockUUID, name: string) => Promise<any>
 
@@ -495,15 +510,21 @@ export interface IAppProxy {
   onCurrentGraphChanged: IUserHook
   onGraphAfterIndexed: IUserHook<{ repo: string }>
   onThemeModeChanged: IUserHook<{ mode: 'dark' | 'light' }>
-  onThemeChanged: IUserHook<Partial<{ name: string, mode: string, pid: string, url: string }>>
+  onThemeChanged: IUserHook<
+    Partial<{ name: string; mode: string; pid: string; url: string }>>
   onTodayJournalCreated: IUserHook<{ title: string }>
+  onBeforeCommandInvoked: (condition: ExternalCommandType | string, callback: (e: IHookEvent) => void) => IUserOffHook
+  onAfterCommandInvoked: (condition: ExternalCommandType | string, callback: (e: IHookEvent) => void) => IUserOffHook
 
   /**
    * provide ui slot to specific block with UUID
    *
    * @added 0.0.13
    */
-  onBlockRendererSlotted: IUserConditionSlotHook<BlockUUID, Omit<BlockEntity, 'children' | 'page'>>
+  onBlockRendererSlotted: IUserConditionSlotHook<
+    BlockUUID,
+    Omit<BlockEntity, 'children' | 'page'>
+  >
 
   /**
    * provide ui slot to block `renderer` macro for `{{renderer arg1, arg2}}`
@@ -690,7 +711,7 @@ export interface IEditorProxy extends Record<string, any> {
   insertBatchBlock: (
     srcBlock: BlockIdentity,
     batch: IBatchBlock | Array<IBatchBlock>,
-    opts?: Partial<{ before: boolean; sibling: boolean, keepUUID: boolean }>
+    opts?: Partial<{ before: boolean; sibling: boolean; keepUUID: boolean }>
   ) => Promise<Array<BlockEntity> | null>
 
   updateBlock: (
@@ -790,7 +811,7 @@ export interface IEditorProxy extends Record<string, any> {
     opts?: { replaceState: boolean }
   ) => void
 
-  openInRightSidebar: (uuid: BlockUUID) => void
+  openInRightSidebar: (id: BlockUUID | EntityID) => void
 
   /**
    * @example https://github.com/logseq/logseq-plugin-samples/tree/master/logseq-a-translator
@@ -896,14 +917,16 @@ export interface IAssetsProxy {
    * @added 0.0.2
    * @param exts
    */
-  listFilesOfCurrentGraph(exts?: string | string[]): Promise<Array<{
-    path: string
-    size: number
-    accessTime: number
-    modifiedTime: number
-    changeTime: number
-    birthTime: number
-  }>>
+  listFilesOfCurrentGraph(exts?: string | string[]): Promise<
+    Array<{
+      path: string
+      size: number
+      accessTime: number
+      modifiedTime: number
+      changeTime: number
+      birthTime: number
+    }>
+  >
 
   /**
    * @example https://github.com/logseq/logseq/pull/6488

+ 115 - 100
libs/src/LSPlugin.user.ts

@@ -35,7 +35,8 @@ import {
   BlockEntity,
   IDatom,
   IAssetsProxy,
-  AppInfo, IPluginSearchServiceHooks,
+  AppInfo,
+  IPluginSearchServiceHooks,
 } from './LSPlugin'
 import Debug from 'debug'
 import * as CSS from 'csstype'
@@ -52,8 +53,7 @@ declare global {
   }
 }
 
-type callableMethods =
-  keyof typeof callableAPIs | string // host exported SDK apis & host platform related apis
+type callableMethods = keyof typeof callableAPIs | string // host exported SDK apis & host platform related apis
 
 const PROXY_CONTINUE = Symbol.for('proxy-continue')
 const debug = Debug('LSPlugin:user')
@@ -64,7 +64,7 @@ const logger = new PluginLogger('', { console: true })
  * @param opts
  * @param action
  */
-function registerSimpleCommand(
+function registerSimpleCommand (
   this: LSPluginUser,
   type: string,
   opts: {
@@ -91,13 +91,16 @@ function registerSimpleCommand(
     args: [
       this.baseInfo.id,
       // [cmd, action]
-      [{ key, label, type, desc, keybinding, extras }, ['editor/hook', eventKey]],
+      [
+        { key, label, type, desc, keybinding, extras },
+        ['editor/hook', eventKey],
+      ],
       palette,
     ],
   })
 }
 
-function shouldValidUUID(uuid: string) {
+function shouldValidUUID (uuid: string) {
   if (!isValidUUID(uuid)) {
     logger.error(`#${uuid} is not a valid UUID string.`)
     return false
@@ -106,7 +109,7 @@ function shouldValidUUID(uuid: string) {
   return true
 }
 
-function checkEffect(p: LSPluginUser) {
+function checkEffect (p: LSPluginUser) {
   return p && (p.baseInfo?.effect || !p.baseInfo?.iir)
 }
 
@@ -114,10 +117,7 @@ let _appBaseInfo: AppInfo = null
 let _searchServices: Map<string, LSPluginSearchService> = new Map()
 
 const app: Partial<IAppProxy> = {
-  async getInfo(
-    this: LSPluginUser,
-    key
-  ) {
+  async getInfo (this: LSPluginUser, key) {
     if (!_appBaseInfo) {
       _appBaseInfo = await this._execCallableAPIAsync('get-app-info')
     }
@@ -126,7 +126,7 @@ const app: Partial<IAppProxy> = {
 
   registerCommand: registerSimpleCommand,
 
-  registerSearchService<T extends IPluginSearchServiceHooks>(
+  registerSearchService<T extends IPluginSearchServiceHooks> (
     this: LSPluginUser,
     s: T
   ) {
@@ -137,7 +137,7 @@ const app: Partial<IAppProxy> = {
     _searchServices.set(s.name, new LSPluginSearchService(this, s))
   },
 
-  registerCommandPalette(
+  registerCommandPalette (
     opts: { key: string; label: string; keybinding?: SimpleCommandKeybinding },
     action: SimpleCommandCallback
   ) {
@@ -152,10 +152,23 @@ const app: Partial<IAppProxy> = {
     )
   },
 
-  registerCommandShortcut(
-    keybinding: SimpleCommandKeybinding,
-    action: SimpleCommandCallback
+  registerCommandShortcut (
+    keybinding: SimpleCommandKeybinding | string,
+    action: SimpleCommandCallback,
+    opts: Partial<{
+      key: string
+      label: string
+      desc: string
+      extras: Record<string, any>
+    }> = {}
   ) {
+    if (typeof keybinding == 'string') {
+      keybinding = {
+        mode: 'global',
+        binding: keybinding,
+      }
+    }
+
     const { binding } = keybinding
     const group = '$shortcut$'
     const key = group + safeSnakeCase(binding)
@@ -163,12 +176,12 @@ const app: Partial<IAppProxy> = {
     return registerSimpleCommand.call(
       this,
       group,
-      { key, palette: false, keybinding },
+      { ...opts, key, palette: false, keybinding },
       action
     )
   },
 
-  registerUIItem(
+  registerUIItem (
     type: 'toolbar' | 'pagebar',
     opts: { key: string; template: string }
   ) {
@@ -181,7 +194,7 @@ const app: Partial<IAppProxy> = {
     })
   },
 
-  registerPageMenuItem(
+  registerPageMenuItem (
     this: LSPluginUser,
     tag: string,
     action: (e: IHookEvent & { page: string }) => void
@@ -205,9 +218,7 @@ const app: Partial<IAppProxy> = {
     )
   },
 
-  onBlockRendererSlotted(
-    uuid,
-    callback: (payload: any) => void) {
+  onBlockRendererSlotted (uuid, callback: (payload: any) => void) {
     if (!shouldValidUUID(uuid)) return
 
     const pid = this.baseInfo.id
@@ -222,11 +233,7 @@ const app: Partial<IAppProxy> = {
     }
   },
 
-  invokeExternalPlugin(
-    this: LSPluginUser,
-    type: string,
-    ...args: Array<any>
-  ) {
+  invokeExternalPlugin (this: LSPluginUser, type: string, ...args: Array<any>) {
     type = type?.trim()
     if (!type) return
     let [pid, group] = type.split('.')
@@ -240,11 +247,14 @@ const app: Partial<IAppProxy> = {
     }
     return this._execCallableAPIAsync(
       'invoke_external_plugin_cmd',
-      pid, group.toLowerCase(), key, args
+      pid,
+      group.toLowerCase(),
+      key,
+      args
     )
   },
 
-  setFullScreen(flag) {
+  setFullScreen (flag) {
     const sf = (...args) => this._callWin('setFullScreen', ...args)
 
     if (flag === 'toggle') {
@@ -254,17 +264,17 @@ const app: Partial<IAppProxy> = {
     } else {
       flag ? sf(true) : sf()
     }
-  }
+  },
 }
 
 let registeredCmdUid = 0
 
 const editor: Partial<IEditorProxy> = {
-  newBlockUUID(this: LSPluginUser): Promise<string> {
+  newBlockUUID (this: LSPluginUser): Promise<string> {
     return this._execCallableAPIAsync('new_block_uuid')
   },
 
-  registerSlashCommand(
+  registerSlashCommand (
     this: LSPluginUser,
     tag: string,
     actions: BlockCommandCallback | Array<SlashCommandAction>
@@ -312,7 +322,7 @@ const editor: Partial<IEditorProxy> = {
     })
   },
 
-  registerBlockContextMenuItem(
+  registerBlockContextMenuItem (
     this: LSPluginUser,
     label: string,
     action: BlockCommandCallback
@@ -335,11 +345,12 @@ const editor: Partial<IEditorProxy> = {
     )
   },
 
-  registerHighlightContextMenuItem(
+  registerHighlightContextMenuItem (
     this: LSPluginUser,
     label: string,
     action: SimpleCommandCallback,
-    opts?: { clearSelection: boolean }) {
+    opts?: { clearSelection: boolean }
+  ) {
     if (typeof action !== 'function') {
       return false
     }
@@ -353,13 +364,13 @@ const editor: Partial<IEditorProxy> = {
       {
         key,
         label,
-        extras: opts
+        extras: opts,
       },
       action
     )
   },
 
-  scrollToBlockInPage(
+  scrollToBlockInPage (
     this: LSPluginUser,
     pageName: BlockPageName,
     blockId: BlockIdentity,
@@ -371,11 +382,11 @@ const editor: Partial<IEditorProxy> = {
     } else {
       this.App.pushState('page', { name: pageName }, { anchor })
     }
-  }
+  },
 }
 
 const db: Partial<IDBProxy> = {
-  onBlockChanged(
+  onBlockChanged (
     this: LSPluginUser,
     uuid: BlockUUID,
     callback: (
@@ -405,7 +416,7 @@ const db: Partial<IDBProxy> = {
     }
   },
 
-  datascriptQuery<T = any>(
+  datascriptQuery<T = any> (
     this: LSPluginUser,
     query: string,
     ...inputs: Array<any>
@@ -413,16 +424,13 @@ const db: Partial<IDBProxy> = {
     // force remove proxy ns flag `db`
     inputs.pop()
 
-    if (inputs?.some(it => (typeof it === 'function'))) {
+    if (inputs?.some((it) => typeof it === 'function')) {
       const host = this.Experiments.ensureHostScope()
       return host.logseq.api.datascript_query(query, ...inputs)
     }
 
-    return this._execCallableAPIAsync(
-      `datascript_query`,
-      ...[query, ...inputs]
-    )
-  }
+    return this._execCallableAPIAsync(`datascript_query`, ...[query, ...inputs])
+  },
 }
 
 const git: Partial<IGitProxy> = {}
@@ -430,13 +438,9 @@ const git: Partial<IGitProxy> = {}
 const ui: Partial<IUIProxy> = {}
 
 const assets: Partial<IAssetsProxy> = {
-  makeSandboxStorage(
-    this: LSPluginUser
-  ): IAsyncStorage {
-    return new LSPluginFileStorage(
-      this, { assets: true }
-    )
-  }
+  makeSandboxStorage (this: LSPluginUser): IAsyncStorage {
+    return new LSPluginFileStorage(this, { assets: true })
+  },
 }
 
 type uiState = {
@@ -483,7 +487,7 @@ export class LSPluginUser
    * @param _baseInfo
    * @param _caller
    */
-  constructor(
+  constructor (
     private _baseInfo: LSPluginBaseInfo,
     private _caller: LSPluginCaller
   ) {
@@ -509,14 +513,14 @@ export class LSPluginUser
         cb && (await cb(rest))
         actor?.resolve(null)
       } catch (e) {
-        console.debug(`${_caller.debugTag} [beforeunload] `, e)
+        this.logger.error(`[beforeunload] `, e)
         actor?.reject(e)
       }
     })
   }
 
   // Life related
-  async ready(model?: any, callback?: any) {
+  async ready (model?: any, callback?: any) {
     if (this._connected) return
 
     try {
@@ -530,6 +534,7 @@ export class LSPluginUser
       this._connected = true
 
       baseInfo = deepMerge(this._baseInfo, baseInfo)
+      this._baseInfo = baseInfo
 
       if (baseInfo?.id) {
         this._debugTag =
@@ -562,39 +567,39 @@ export class LSPluginUser
     }
   }
 
-  ensureConnected() {
+  ensureConnected () {
     if (!this._connected) {
       throw new Error('not connected')
     }
   }
 
-  beforeunload(callback: (e: any) => Promise<void>): void {
+  beforeunload (callback: (e: any) => Promise<void>): void {
     if (typeof callback !== 'function') return
     this._beforeunloadCallback = callback
   }
 
-  provideModel(model: Record<string, any>) {
+  provideModel (model: Record<string, any>) {
     this.caller._extendUserModel(model)
     return this
   }
 
-  provideTheme(theme: Theme) {
+  provideTheme (theme: Theme) {
     this.caller.call('provider:theme', theme)
     return this
   }
 
-  provideStyle(style: StyleString) {
+  provideStyle (style: StyleString) {
     this.caller.call('provider:style', style)
     return this
   }
 
-  provideUI(ui: UIOptions) {
+  provideUI (ui: UIOptions) {
     this.caller.call('provider:ui', ui)
     return this
   }
 
   // Settings related
-  useSettingsSchema(schema: Array<SettingSchemaDesc>) {
+  useSettingsSchema (schema: Array<SettingSchemaDesc>) {
     if (this.connected) {
       this.caller.call('settings:schema', {
         schema,
@@ -606,35 +611,35 @@ export class LSPluginUser
     return this
   }
 
-  updateSettings(attrs: Record<string, any>) {
+  updateSettings (attrs: Record<string, any>) {
     this.caller.call('settings:update', attrs)
     // TODO: update associated baseInfo settings
   }
 
-  onSettingsChanged<T = any>(cb: (a: T, b: T) => void): IUserOffHook {
+  onSettingsChanged<T = any> (cb: (a: T, b: T) => void): IUserOffHook {
     const type = 'settings:changed'
     this.on(type, cb)
     return () => this.off(type, cb)
   }
 
-  showSettingsUI() {
+  showSettingsUI () {
     this.caller.call('settings:visible:changed', { visible: true })
   }
 
-  hideSettingsUI() {
+  hideSettingsUI () {
     this.caller.call('settings:visible:changed', { visible: false })
   }
 
   // UI related
-  setMainUIAttrs(attrs: Partial<UIContainerAttrs>): void {
+  setMainUIAttrs (attrs: Partial<UIContainerAttrs>): void {
     this.caller.call('main-ui:attrs', attrs)
   }
 
-  setMainUIInlineStyle(style: CSS.Properties): void {
+  setMainUIInlineStyle (style: CSS.Properties): void {
     this.caller.call('main-ui:style', style)
   }
 
-  hideMainUI(opts?: { restoreEditingCursor: boolean }): void {
+  hideMainUI (opts?: { restoreEditingCursor: boolean }): void {
     const payload = {
       key: KEY_MAIN_UI,
       visible: false,
@@ -645,7 +650,7 @@ export class LSPluginUser
     this._ui.set(payload.key, payload)
   }
 
-  showMainUI(opts?: { autoFocus: boolean }): void {
+  showMainUI (opts?: { autoFocus: boolean }): void {
     const payload = {
       key: KEY_MAIN_UI,
       visible: true,
@@ -656,7 +661,7 @@ export class LSPluginUser
     this._ui.set(payload.key, payload)
   }
 
-  toggleMainUI(): void {
+  toggleMainUI (): void {
     const payload = { key: KEY_MAIN_UI, toggle: true }
     const state = this._ui.get(payload.key)
     if (state && state.visible) {
@@ -667,40 +672,40 @@ export class LSPluginUser
   }
 
   // Getters
-  get version(): string {
+  get version (): string {
     return this._version
   }
 
-  get isMainUIVisible(): boolean {
+  get isMainUIVisible (): boolean {
     const state = this._ui.get(KEY_MAIN_UI)
     return Boolean(state && state.visible)
   }
 
-  get connected(): boolean {
+  get connected (): boolean {
     return this._connected
   }
 
-  get baseInfo(): LSPluginBaseInfo {
+  get baseInfo (): LSPluginBaseInfo {
     return this._baseInfo
   }
 
-  get effect(): Boolean {
+  get effect (): Boolean {
     return checkEffect(this)
   }
 
-  get logger() {
+  get logger () {
     return logger
   }
 
-  get settings() {
+  get settings () {
     return this.baseInfo?.settings
   }
 
-  get caller(): LSPluginCaller {
+  get caller (): LSPluginCaller {
     return this._caller
   }
 
-  resolveResourceFullUrl(filePath: string) {
+  resolveResourceFullUrl (filePath: string) {
     this.ensureConnected()
     if (!filePath) return
     filePath = filePath.replace(/^[.\\/]+/, '')
@@ -710,12 +715,12 @@ export class LSPluginUser
   /**
    * @internal
    */
-  _makeUserProxy(target: any, tag?: UserProxyTags) {
+  _makeUserProxy (target: any, tag?: UserProxyTags) {
     const that = this
     const caller = this.caller
 
     return new Proxy(target, {
-      get(target: any, propKey, receiver) {
+      get (target: any, propKey, receiver) {
         const origMethod = target[propKey]
 
         return function (this: any, ...args: any) {
@@ -731,13 +736,23 @@ export class LSPluginUser
             if (hookMatcher != null) {
               const f = hookMatcher[0].toLowerCase()
               const s = hookMatcher.input!
-              const e = s.slice(f.length)
               const isOff = f === 'off'
               const pid = that.baseInfo.id
 
-              const type = `hook:${tag}:${safeSnakeCase(e)}`
-              const handler = args[0]
-              const opts = args[1]
+              let type = s.slice(f.length)
+              let handler = args[0]
+              let opts = args[1]
+
+              // condition mode
+              if (typeof handler === 'string' && typeof opts === 'function') {
+                handler = handler.replace(/^logseq./, ':')
+                type = `${type}${handler}`
+                handler = opts
+                opts = args[2]
+              }
+
+              type = `hook:${tag}:${safeSnakeCase(type)}`
+
               caller[f](type, handler)
 
               const unlisten = () => {
@@ -775,64 +790,64 @@ export class LSPluginUser
     })
   }
 
-  _execCallableAPIAsync(method: callableMethods, ...args) {
+  _execCallableAPIAsync (method: callableMethods, ...args) {
     return this._caller.callAsync(`api:call`, {
       method,
       args,
     })
   }
 
-  _execCallableAPI(method: callableMethods, ...args) {
+  _execCallableAPI (method: callableMethods, ...args) {
     this._caller.call(`api:call`, {
       method,
       args,
     })
   }
 
-  _callWin(...args) {
+  _callWin (...args) {
     return this._execCallableAPIAsync(`_callMainWin`, ...args)
   }
 
   /**
    * The interface methods of {@link IAppProxy}
    */
-  get App(): IAppProxy {
+  get App (): IAppProxy {
     return this._makeUserProxy(app, 'app')
   }
 
-  get Editor(): IEditorProxy {
+  get Editor (): IEditorProxy {
     return this._makeUserProxy(editor, 'editor')
   }
 
-  get DB(): IDBProxy {
+  get DB (): IDBProxy {
     return this._makeUserProxy(db, 'db')
   }
 
-  get Git(): IGitProxy {
+  get Git (): IGitProxy {
     return this._makeUserProxy(git, 'git')
   }
 
-  get UI(): IUIProxy {
+  get UI (): IUIProxy {
     return this._makeUserProxy(ui, 'ui')
   }
 
-  get Assets(): IAssetsProxy {
+  get Assets (): IAssetsProxy {
     return this._makeUserProxy(assets, 'assets')
   }
 
-  get FileStorage(): LSPluginFileStorage {
+  get FileStorage (): LSPluginFileStorage {
     let m = this._mFileStorage
     if (!m) m = this._mFileStorage = new LSPluginFileStorage(this)
     return m
   }
 
-  get Request(): LSPluginRequest {
+  get Request (): LSPluginRequest {
     let m = this._mRequest
     if (!m) m = this._mRequest = new LSPluginRequest(this)
     return m
   }
 
-  get Experiments(): LSPluginExperiments {
+  get Experiments (): LSPluginExperiments {
     let m = this._mExperiments
     if (!m) m = this._mExperiments = new LSPluginExperiments(this)
     return m
@@ -844,7 +859,7 @@ export * from './LSPlugin'
 /**
  * @internal
  */
-export function setupPluginUserInstance(
+export function setupPluginUserInstance (
   pluginBaseInfo: LSPluginBaseInfo,
   pluginCaller: LSPluginCaller
 ) {

+ 47 - 39
libs/src/helpers.ts

@@ -2,7 +2,7 @@ import { SettingSchemaDesc, StyleString, UIOptions } from './LSPlugin'
 import { PluginLocal } from './LSPlugin.core'
 import * as nodePath from 'path'
 import DOMPurify from 'dompurify'
-import { merge } from 'lodash-es'
+import merge from 'deepmerge';
 import { snakeCase } from 'snake-case'
 import * as callables from './callable.apis'
 import EventEmitter from 'eventemitter3'
@@ -52,7 +52,10 @@ export function isObject(item: any) {
   return item === Object(item) && !Array.isArray(item)
 }
 
-export const deepMerge = merge
+export function deepMerge<T>(a: Partial<T>, b: Partial<T>): T {
+  const overwriteArrayMerge = (destinationArray, sourceArray) => sourceArray
+  return merge(a, b, { arrayMerge: overwriteArrayMerge })
+}
 
 export class PluginLogger extends EventEmitter<'change'> {
   private _logs: Array<[type: string, payload: any]> = []
@@ -67,7 +70,7 @@ export class PluginLogger extends EventEmitter<'change'> {
   }
 
   write(type: string, payload: any[], inConsole?: boolean) {
-    if (payload?.length && (true === payload[payload.length - 1])) {
+    if (payload?.length && true === payload[payload.length - 1]) {
       inConsole = true
       payload.pop()
     }
@@ -117,9 +120,13 @@ export class PluginLogger extends EventEmitter<'change'> {
 }
 
 export function isValidUUID(s: string) {
-  return (typeof s === 'string' &&
-    (s.length === 36) &&
-    (/^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/gi).test(s))
+  return (
+    typeof s === 'string' &&
+    s.length === 36 &&
+    /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/gi.test(
+      s
+    )
+  )
 }
 
 export function genID() {
@@ -259,9 +266,9 @@ export function setupInjectedStyle(
   el.textContent = style
 
   attrs &&
-  Object.entries(attrs).forEach(([k, v]) => {
-    el.setAttribute(k, v)
-  })
+    Object.entries(attrs).forEach(([k, v]) => {
+      el.setAttribute(k, v)
+    })
 
   document.head.append(el)
 
@@ -337,22 +344,22 @@ export function setupInjectedUI(
 
     // update attributes
     attrs &&
-    Object.entries(attrs).forEach(([k, v]) => {
-      el.setAttribute(k, v)
-    })
+      Object.entries(attrs).forEach(([k, v]) => {
+        el.setAttribute(k, v)
+      })
 
     let positionDirty = el.dataset.dx != null
     ui.style &&
-    Object.entries(ui.style).forEach(([k, v]) => {
-      if (
-        positionDirty &&
-        ['left', 'top', 'bottom', 'right', 'width', 'height'].includes(k)
-      ) {
-        return
-      }
-
-      el.style[k] = v
-    })
+      Object.entries(ui.style).forEach(([k, v]) => {
+        if (
+          positionDirty &&
+          ['left', 'top', 'bottom', 'right', 'width', 'height'].includes(k)
+        ) {
+          return
+        }
+
+        el.style[k] = v
+      })
     return
   }
 
@@ -372,14 +379,14 @@ export function setupInjectedUI(
   content.innerHTML = ui.template
 
   attrs &&
-  Object.entries(attrs).forEach(([k, v]) => {
-    el.setAttribute(k, v)
-  })
+    Object.entries(attrs).forEach(([k, v]) => {
+      el.setAttribute(k, v)
+    })
 
   ui.style &&
-  Object.entries(ui.style).forEach(([k, v]) => {
-    el.style[k] = v
-  })
+    Object.entries(ui.style).forEach(([k, v]) => {
+      el.style[k] = v
+    })
 
   let teardownUI: () => void
   let disposeFloat: () => void
@@ -392,11 +399,11 @@ export function setupInjectedUI(
     el.classList.add('lsp-ui-float-container', 'visible')
     disposeFloat =
       (pl._setupResizableContainer(el, key),
-        pl._setupDraggableContainer(el, {
-          key,
-          close: () => teardownUI(),
-          title: attrs?.title,
-        }))
+      pl._setupDraggableContainer(el, {
+        key,
+        close: () => teardownUI(),
+        title: attrs?.title,
+      }))
   }
 
   if (!!slot && ui.reset) {
@@ -424,7 +431,7 @@ export function setupInjectedUI(
     'keydown',
     'change',
     'input',
-    'contextmenu'
+    'contextmenu',
   ].forEach((type) => {
     el.addEventListener(
       type,
@@ -435,7 +442,8 @@ export function setupInjectedUI(
 
         const { preventDefault } = trigger.dataset
         const msgType = trigger.dataset[`on${ucFirst(type)}`]
-        if (msgType) pl.caller?.callUserModel(msgType, transformableEvent(trigger, e))
+        if (msgType)
+          pl.caller?.callUserModel(msgType, transformableEvent(trigger, e))
         if (preventDefault?.toLowerCase() === 'true') e.preventDefault()
       },
       false
@@ -455,12 +463,12 @@ export function setupInjectedUI(
   return teardownUI
 }
 
-export function cleanInjectedUI(
-  id: string
-) {
+export function cleanInjectedUI(id: string) {
   if (!injectedUIEffects.has(id)) return
   const clean = injectedUIEffects.get(id)
-  try { clean() } catch (e) {
+  try {
+    clean()
+  } catch (e) {
     console.warn('[CLEAN Injected UI] ', id, e)
   }
 }

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

@@ -76,7 +76,8 @@ export class LSPluginExperiments {
 
     return host.logseq.api.exper_register_extensions_enhancer(
       this.ctx.baseInfo.id,
-      type, enhancer
+      type,
+      enhancer
     )
   }
 

+ 34 - 36
libs/src/modules/LSPlugin.Request.ts

@@ -31,23 +31,19 @@ export class LSPluginRequestTask<R = any> {
     private _requestId: RequestTaskID,
     private _requestOptions: Partial<IRequestOptions> = {}
   ) {
-
     this._promise = new Promise<any>((resolve, reject) => {
       if (!this._requestId) {
         return reject(null)
       }
 
       // task result listener
-      this._client.once(
-        genTaskCallbackType(this._requestId),
-        (e) => {
-          if (e && e instanceof Error) {
-            reject(e)
-          } else {
-            resolve(e)
-          }
+      this._client.once(genTaskCallbackType(this._requestId), (e) => {
+        if (e && e instanceof Error) {
+          reject(e)
+        } else {
+          resolve(e)
         }
-      )
+      })
     })
 
     const { success, fail, final } = this._requestOptions
@@ -65,15 +61,9 @@ export class LSPluginRequestTask<R = any> {
   }
 
   abort() {
-    if (
-      !this._requestOptions.abortable ||
-      this._aborted
-    ) return
-
-    this._client.ctx._execCallableAPI(
-      'http_request_abort',
-      this._requestId
-    )
+    if (!this._requestOptions.abortable || this._aborted) return
+
+    this._client.ctx._execCallableAPI('http_request_abort', this._requestId)
 
     this._aborted = true
   }
@@ -99,15 +89,12 @@ export class LSPluginRequest extends EventEmitter {
     super()
 
     // request callback listener
-    this.ctx.caller.on(
-      CLIENT_MSG_CALLBACK,
-      (e: any) => {
-        const reqId = e?.requestId
-        if (!reqId) return
+    this.ctx.caller.on(CLIENT_MSG_CALLBACK, (e: any) => {
+      const reqId = e?.requestId
+      if (!reqId) return
 
-        this.emit(genTaskCallbackType(reqId), e?.payload)
-      }
-    )
+      this.emit(genTaskCallbackType(reqId), e?.payload)
+    })
   }
 
   static createRequestTask(
@@ -115,21 +102,32 @@ export class LSPluginRequest extends EventEmitter {
     requestID: RequestTaskID,
     requestOptions: Partial<IRequestOptions>
   ) {
-    return new LSPluginRequestTask(
-      client, requestID, requestOptions
-    )
+    return new LSPluginRequestTask(client, requestID, requestOptions)
   }
 
-  async _request<R extends {},
-    T extends WithOptional<IRequestOptions<R>, keyof Omit<IRequestOptions, 'url'>>>(options: T):
-    Promise<T extends Pick<IRequestOptions, 'abortable'> ? LSPluginRequestTask<R> : R> {
+  async _request<
+    R extends {},
+    T extends WithOptional<
+      IRequestOptions<R>,
+      keyof Omit<IRequestOptions, 'url'>
+    >
+  >(
+    options: T
+  ): Promise<
+    T extends Pick<IRequestOptions, 'abortable'> ? LSPluginRequestTask<R> : R
+  > {
     const pid = this.ctx.baseInfo.id
     const { success, fail, final, ...requestOptions } = options
-    const reqID = this.ctx.Experiments.invokeExperMethod('request', pid, requestOptions)
+    const reqID = this.ctx.Experiments.invokeExperMethod(
+      'request',
+      pid,
+      requestOptions
+    )
 
     const task = LSPluginRequest.createRequestTask(
       this.ctx.Request,
-      reqID, options
+      reqID,
+      options
     )
 
     if (!requestOptions.abortable) {
@@ -142,4 +140,4 @@ export class LSPluginRequest extends EventEmitter {
   get ctx(): LSPluginUser {
     return this._ctx
   }
-}
+}

+ 48 - 51
libs/src/modules/LSPlugin.Search.ts

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

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

@@ -73,7 +73,7 @@ class LSPluginFileStorage implements IAsyncStorage {
   allKeys(): Promise<Array<string>> {
     return this.ctx.caller.callAsync(`api:call`, {
       method: 'list-plugin-storage-files',
-      args: [this.ctxId, this.opts?.assets]
+      args: [this.ctxId, this.opts?.assets],
     })
   }
 

+ 20 - 3
libs/src/postmate/index.ts

@@ -81,7 +81,9 @@ export const sanitize = (message, allowedOrigin) => {
  */
 export const resolveValue = (model, property, args) => {
   const unwrappedContext =
-    typeof model[property] === 'function' ? model[property].apply(null, args) : model[property]
+    typeof model[property] === 'function'
+      ? model[property].apply(null, args)
+      : model[property]
   return Promise.resolve(unwrappedContext)
 }
 
@@ -135,13 +137,17 @@ export class ParentAPI {
   }
 
   get(property, ...args) {
-    return new Promise((resolve) => {
+    return new Promise((resolve, reject) => {
       // Extract data from response and kill listeners
       const uid = generateNewMessageId()
       const transact = (e) => {
         if (e.data.uid === uid && e.data.postmate === 'reply') {
           this.parent.removeEventListener('message', transact, false)
-          resolve(e.data.value)
+          if (e.data.error) {
+            reject(e.data.error)
+          } else {
+            resolve(e.data.value)
+          }
         }
       }
 
@@ -243,6 +249,17 @@ export class ChildAPI {
           },
           e.origin
         )
+      }).catch((error) => {
+        ;(e.source as WindowProxy).postMessage(
+          {
+            property,
+            postmate: 'reply',
+            type: messageType,
+            uid,
+            error,
+          },
+          e.origin
+        )
       })
     })
   }

+ 5 - 0
libs/yarn.lock

@@ -1483,6 +1483,11 @@ [email protected], debug@^4.1.0, debug@^4.1.1:
   dependencies:
     ms "2.1.2"
 
+deepmerge@^4.3.1:
+  version "4.3.1"
+  resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a"
+  integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
+
 [email protected]:
   version "2.3.8"
   resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.3.8.tgz#224fe9ae57d7ebd9a1ae1ac18c1c1ca3f532226f"

+ 2 - 2
package.json

@@ -91,11 +91,11 @@
         "@capawesome/capacitor-background-task": "^2.0.0",
         "@emoji-mart/data": "^1.1.2",
         "@emoji-mart/react": "^1.1.1",
-        "@excalidraw/excalidraw": "0.12.0",
+        "@excalidraw/excalidraw": "0.15.3",
         "@highlightjs/cdn-assets": "10.4.1",
         "@hugotomazi/capacitor-navigation-bar": "^2.0.0",
         "@isomorphic-git/lightning-fs": "^4.6.0",
-        "@logseq/capacitor-file-sync": "0.0.32",
+        "@logseq/capacitor-file-sync": "0.0.35",
         "@logseq/diff-merge": "0.2.2",
         "@logseq/react-tweet-embed": "1.3.1-1",
         "@radix-ui/colors": "^0.1.8",

+ 1 - 1
resources/forge.config.js

@@ -4,7 +4,7 @@ module.exports = {
   packagerConfig: {
     name: 'Logseq',
     icon: './icons/logseq_big_sur.icns',
-    buildVersion: 67,
+    buildVersion: 70,
     protocols: [
       {
         "protocol": "logseq",

File diff suppressed because it is too large
+ 0 - 0
resources/js/lsplugin.core.js


+ 2 - 2
resources/package.json

@@ -1,7 +1,7 @@
 {
   "name": "Logseq",
   "productName": "Logseq",
-  "version": "0.9.14",
+  "version": "0.9.17",
   "main": "electron.js",
   "author": "Logseq",
   "license": "AGPL-3.0",
@@ -30,7 +30,7 @@
     "fs-extra": "9.1.0",
     "node-fetch": "2.6.7",
     "open": "7.3.1",
-    "semver": "7.3.5",
+    "semver": "7.5.2",
     "update-electron-app": "2.0.1",
     "extract-zip": "2.0.1",
     "diff-match-patch": "1.0.5",

+ 1 - 1
scripts/get-pkg-version.js

@@ -21,7 +21,7 @@ if (match) {
 if (process.argv[2] === 'nightly' || process.argv[2] === '') {
   const today = new Date()
   console.log(
-    ver + '-nightly.' + today.toISOString().split('T')[0].replaceAll('-', '')
+    ver + '-alpha+nightly.' + today.toISOString().split('T')[0].replaceAll('-', '')
   )
 } else {
   console.log(ver)

+ 5 - 5
scripts/src/logseq/tasks/lang.clj

@@ -18,7 +18,7 @@
        (into {})))
 
 (defn list-langs
-  "List translated langagues with their number of translations"
+  "List translated languages with their number of translations"
   []
   (let [dicts (get-dicts)
         en-count (count (dicts :en))
@@ -172,12 +172,12 @@
             :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 
+   :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 :right-side-bar/history-global}
    :tr #{:help/awesome-logseq}
-   })
+   :id #{:host :port :on-boarding/section-app :right-side-bar/history-global}})
 
 (defn- validate-languages-dont-have-duplicates
   "Looks up duplicates for all languages"

+ 9 - 2
src/electron/electron/core.cljs

@@ -184,11 +184,18 @@
                                                   ;; Avoid conflict with `Control+N` shortcut to move down in the text editor on Windows/Linux
                                                   "Shift+CommandOrControl+N")}
                                   (if mac?
-                                    {:role "close"}
+                                    ;; Disable Command+W shortcut
+                                    {:role "close"
+                                     :accelerator false}
                                     {:role "quit"})]}
                        {:role "editMenu"}
                        {:role "viewMenu"}
-                       {:role "windowMenu"})
+                       {:role "windowMenu"
+                        :submenu (when-not mac? [{:role "minimize"}
+                                                 {:role "zoom"}
+                                                 ;; Disable Control+W shortcut
+                                                 {:role "close"
+                                                  :accelerator false}])})
         ;; Windows has no about role
         template (conj template
                        (if mac?

+ 1 - 2
src/main/frontend/common.css

@@ -111,7 +111,7 @@ html[data-theme='dark'] {
   --color-level-6: #3a7e8e;
 }
 
-/* You should always use .light-theme for light mode, the .white-theme is just for backword compatibility.
+/* You should always use .light-theme for light mode, the .white-theme is just for backward compatibility.
 
 See: https://github.com/logseq/logseq/pull/4652. */
 .white-theme,
@@ -795,7 +795,6 @@ mark {
   letter-spacing: 0;
   background-color: var(--ls-page-inline-code-bg-color, #eee);
   color: var(--ls-page-inline-code-color);
-  word-spacing: -0.15em;
   text-rendering: optimizeSpeed;
 }
 

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

@@ -967,7 +967,7 @@
        (if (and (not paragraph?)
                 (mldoc/block-with-title? (ffirst ast)))
          (markup-elements-cp (assoc config :block/format format) ast)
-         (inline-text format macro-content)))
+         (inline-text config format macro-content)))
      [:span.warning {:title (str "Unsupported macro name: " name)}
       (macro->text name arguments)])])
 

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

@@ -1,7 +1,7 @@
 (ns frontend.components.command-palette
   (:require [frontend.handler.command-palette :as cp]
             [frontend.modules.shortcut.core :as shortcut]
-            [frontend.modules.shortcut.data-helper :as shortcut-helper]
+            [frontend.modules.shortcut.utils :as shortcut-utils]
             [frontend.context.i18n :refer [t]]
             [frontend.search :as search]
             [frontend.ui :as ui]
@@ -11,7 +11,7 @@
 
 (defn translate [t {:keys [id desc]}]
   (when id
-    (let [desc-i18n (t (shortcut-helper/decorate-namespace id))]
+    (let [desc-i18n (t (shortcut-utils/decorate-namespace id))]
       (if (string/starts-with? desc-i18n "{Missing key")
         desc
         desc-i18n))))

+ 1 - 18
src/main/frontend/components/container.css

@@ -682,14 +682,6 @@ html[data-theme='dark'] {
       }
     }
 
-    &:not(:hover) {
-      ::-webkit-scrollbar-thumb,
-      ::-webkit-scrollbar,
-      ::-webkit-scrollbar-thumb:active {
-        background-color: transparent;
-      }
-    }
-
     .initial {
       flex: 1;
     }
@@ -698,22 +690,13 @@ html[data-theme='dark'] {
       @apply h-full;
 
       .button {
-        @apply hidden p-0 ml-2 flex items-center;
+        @apply p-0 ml-2 flex items-center;
 
         &:focus {
           @apply flex;
         }
       }
     }
-
-    .is-mobile &,
-    &:hover {
-      .item-actions {
-        .button {
-          @apply flex;
-        }
-      }
-    }
   }
 }
 

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

@@ -59,9 +59,9 @@
     (ui/button (t :file-rn/confirm-proceed) ;; the button is for triple-lowbar only
                :class "text-md p-2 mr-1"
                :on-click #(do (reset! *target-format :triple-lowbar)
-                            (reset! *dir-format (state/get-filename-format repo)) ;; assure it's uptodate
-                            (write-filename-format! repo :triple-lowbar)
-                            (reset! *solid-format :triple-lowbar)))]
+                              (reset! *dir-format (state/get-filename-format repo)) ;; assure it's uptodate
+                              (write-filename-format! repo :triple-lowbar)
+                              (reset! *solid-format :triple-lowbar)))]
    [:p (t :file-rn/instruct-3)]])
 
 (rum/defc filename-format-select
@@ -171,7 +171,7 @@
                  [:tr {:key (:block/name page)}
                   [:td [:div [:p "📄 " old-title]]
                    (case status
-                     :breaking ;; if properety title override the title, it't not breaking change
+                     :breaking ;; if property title override the title, it't not breaking change
                      [:div [:p "🟡 " (t :file-rn/suggest-rename) rename-but]
                       [:p (t :file-rn/otherwise-breaking) " \"" changed-title \"]]
                      :unreachable

+ 2 - 0
src/main/frontend/components/plugins.cljs

@@ -433,6 +433,8 @@
                           (assoc opts :test (util/trim-safe (util/evalue %))))
           :value       (:test opts)}]
         [:datalist#proxy-test-url-datalist
+         [:option "https://api.logseq.com/logseq/version"]
+         [:option "https://logseq-connectivity-testing-prod.s3.us-east-1.amazonaws.com/logseq-connectivity-testing"]
          [:option "https://www.google.com"]
          [:option "https://s3.amazonaws.com"]
          [:option "https://clients3.google.com/generate_204"]]]

+ 2 - 2
src/main/frontend/components/plugins.css

@@ -609,7 +609,7 @@
 
     .cp__settings-inner {
       aside {
-        @apply max-h-[70vh] overflow-auto mb-[-17px] p-3;
+        @apply max-h-[70vh] overflow-auto p-3;
 
         ul {
           @apply list-none p-0 m-0;
@@ -991,7 +991,7 @@ html[data-theme='dark'] {
 .ui__modal[label=plugins-dashboard] {
   .panel-content {
     overflow-y: auto;
-    max-height: calc(100vh - 100px);
+    max-height: calc(100vh - 50px);
   }
 }
 

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

@@ -29,7 +29,7 @@
   [val {:keys [key type title default description inputAs]} update-setting!]
 
   [:div.desc-item.as-input
-   {:data-key key}
+   {:data-key key :key key}
    [:h2 [:code key] (ui/icon "caret-right") [:strong title]]
 
    [:label.form-control

+ 3 - 2
src/main/frontend/components/query_table.cljs

@@ -139,7 +139,7 @@
 (defn- build-column-value
   "Builds a column's tuple value for a query table given a row, column and
   options"
-  [row column {:keys [page? ->elem map-inline config comma-separated-property?]}]
+  [row column {:keys [page? ->elem map-inline comma-separated-property?]}]
   (case column
     :page
     [:string (if page?
@@ -150,13 +150,14 @@
 
     :block       ; block title
     (let [content (:block/content row)
+          uuid (:block/uuid row)
           {:block/keys [title]} (block/parse-title-and-body
                                  (:block/uuid row)
                                  (:block/format row)
                                  (:block/pre-block? row)
                                  content)]
       (if (seq title)
-        [:element (->elem :div (map-inline config title))]
+        [:element (->elem :div (map-inline {:block/uuid uuid} title))]
         [:string content]))
 
     :created-at

+ 12 - 2
src/main/frontend/components/reference.cljs

@@ -183,16 +183,25 @@
             (concat children (rest blocks))
             (conj result fb))))))))
 
+(rum/defc sub-page-properties-changed < rum/static
+  [page-name v filters-atom]
+  (rum/use-effect!
+    (fn []
+      (reset! filters-atom
+              (page-handler/get-filters (util/page-name-sanity-lc page-name))))
+    [page-name v filters-atom])
+  [:<>])
+
 (rum/defcs references* < rum/reactive db-mixins/query
   (rum/local nil ::ref-pages)
   {:init (fn [state]
            (let [page-name (first (:rum/args state))
-                 filters (when page-name
-                           (atom (page-handler/get-filters (util/page-name-sanity-lc page-name))))]
+                 filters (when page-name (atom nil))]
              (assoc state ::filters filters)))}
   [state page-name]
   (when page-name
     (let [page-name (util/page-name-sanity-lc page-name)
+          page-props-v (state/sub-page-properties-changed page-name)
           *ref-pages (::ref-pages state)
           repo (state/get-current-repo)
           filters-atom (get state ::filters)
@@ -236,6 +245,7 @@
       (reset! *ref-pages ref-pages)
       (when (or (seq filter-state) (> filter-n 0))
         [:div.references.page-linked.flex-1.flex-row
+         (sub-page-properties-changed page-name page-props-v filters-atom)
          [:div.content.pt-6
           (references-cp page-name filters filters-atom filter-state total filter-n filtered-ref-blocks' *ref-pages)]]))))
 

+ 32 - 15
src/main/frontend/components/settings.cljs

@@ -23,6 +23,7 @@
             [frontend.mobile.util :as mobile-util]
             [frontend.modules.instrumentation.core :as instrument]
             [frontend.modules.shortcut.data-helper :as shortcut-helper]
+            [frontend.components.shortcut2 :as shortcut2]
             [frontend.spec.storage :as storage-spec]
             [frontend.state :as state]
             [frontend.storage :as storage]
@@ -463,15 +464,6 @@
             (let [value (not enable-all-pages-public?)]
               (config-handler/set-config! :publishing/all-pages-public? value)))))
 
-(rum/defc keyboard-shortcuts-row [t]
-  (row-with-button-action
-    {:left-label   (t :settings-page/customize-shortcuts)
-     :button-label (t :settings-page/shortcut-settings)
-     :on-click      (fn []
-                      (state/close-settings!)
-                      (route-handler/redirect! {:to :shortcut-setting}))
-     :-for         "customize_shortcuts"}))
-
 (defn zotero-settings-row []
   [:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start
    [:label.block.text-sm.font-medium.leading-5.opacity-70
@@ -576,7 +568,7 @@
                  "direct" "Direct"
                  (and protocol host port (str protocol "://" host ":" port)))]
               (ui/icon "edit")]
-             :small? true
+             :class "text-sm p-1"
              :on-click #(state/set-sub-modal!
                          (fn [_] (plugins/user-proxy-settings-panel agent-opts))
                          {:id :https-proxy-panel :center? true})))
@@ -637,8 +629,7 @@
      (when (config/global-config-enabled?) (edit-global-config-edn))
      (when current-repo (edit-config-edn))
      (when current-repo (edit-custom-css))
-     (when current-repo (edit-export-css))
-     (keyboard-shortcuts-row t)]))
+     (when current-repo (edit-export-css))]))
 
 (rum/defcs settings-editor < rum/reactive
   [_state current-repo]
@@ -1028,27 +1019,49 @@
 
 (def DEFAULT-ACTIVE-TAB-STATE (if config/ENABLE-SETTINGS-ACCOUNT-TAB [:account :account] [:general :general]))
 
+(rum/defc settings-effect
+  < rum/static
+  [active]
+
+  (rum/use-effect!
+    (fn []
+      (let [active (and (sequential? active) (name (first active)))
+            ^js ds (.-dataset js/document.body)]
+        (if active
+          (set! (.-settingsTab ds) active)
+          (js-delete ds "settingsTab"))
+        #(js-delete ds "settingsTab")))
+    [active])
+
+  [:<>])
+
 (rum/defcs settings
   < (rum/local DEFAULT-ACTIVE-TAB-STATE ::active)
     {:will-mount
      (fn [state]
        (state/load-app-user-cfgs)
        state)
+     :did-mount
+     (fn [state]
+       (let [active-tab (first (:rum/args state))
+             *active (::active state)]
+         (when (keyword? active-tab)
+           (reset! *active [active-tab nil])))
+       state)
      :will-unmount
      (fn [state]
        (state/close-settings!)
        state)}
     rum/reactive
-  [state]
+  [state _active-tab]
   (let [current-repo (state/sub :git/current-repo)
         _installed-plugins (state/sub :plugin/installed-plugins)
         plugins-of-settings (and config/lsp-enabled? (seq (plugin-handler/get-enabled-plugins-if-setting-schema)))
         *active (::active state)]
 
     [:div#settings.cp__settings-main
-
+     (settings-effect @*active)
      [:div.cp__settings-inner
-
       [:aside.md:w-64 {:style {:min-width "10rem"}}
        [:header.cp__settings-header
         (ui/icon "settings")
@@ -1059,6 +1072,7 @@
                 [: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")]
+               [:keymap "keymap" (t :settings-page/tab-keymap) (ui/icon "keyboard")]
 
                (when (util/electron?)
                  [:version-control "git" (t :settings-page/tab-version-control) (ui/icon "history")])
@@ -1104,6 +1118,9 @@
          :editor
          (settings-editor current-repo)
 
+         :keymap
+         (shortcut2/shortcut-keymap-x)
+
          :version-control
          (settings-git)
 

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

@@ -1,23 +1,35 @@
 .cp__settings {
-  &-main {
-    aside {
+  &-inner {
+    @apply flex flex-col md:flex-row;
+
+    > aside {
       @apply bg-gray-400/5 p-4;
+
+      > ul > li {
+        > a {
+          @apply mb-2;
+
+          > strong {
+            font-size: 14px;
+            font-weight: normal;
+            padding-left: 5px;
+            opacity: .9;
+          }
+        }
+
+        &.active {
+          background-color: var(--ls-quaternary-background-color);
+        }
+      }
     }
 
-    article {
+    > 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 {
+    > aside > .cp__settings-header,
+    > article > .cp__settings-header {
       @apply h-10 py-2 flex flex-row items-center justify-start gap-2;
     }
 
@@ -41,13 +53,13 @@
       @apply text-xl lowercase;
     }
 
-    h1.cp__settings-modal-title:first-letter, 
+    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; 
+      @apply p-0 m-0 mt-4;
     }
 
     .settings-menu-item {
@@ -56,46 +68,10 @@
     }
 
     .settings-menu-link {
-      @apply px-2 py-1.5 select-none; 
+      @apply px-2 py-1.5 select-none;
       color: var(--ls-primary-text-color);
     }
-  }
-
-  &-inner {
-    @apply flex flex-col md:flex-row;
-
-    > aside {
-
-      ul {
 
-        > li {
-
-          > a {
-
-            > i {
-              overflow: hidden;
-              opacity: .9;
-            }
-
-            > strong {
-              font-size: 14px;
-              font-weight: normal;
-              padding-left: 5px;
-              margin-top: 2px;
-              opacity: .9;
-            }
-          }
-
-          &.active {
-            background-color: var(--ls-quaternary-background-color);
-
-            i {
-              opacity: 1;
-            }
-          }
-        }
-      }
-    }
 
     &.no-aside {
       > article {
@@ -392,7 +368,7 @@
         z-index: 1;
         width: 100px;
         max-height: 180px;
-        border:1px solid var(--ls-border-color);
+        border: 1px solid var(--ls-border-color);
         border-radius: 4px;
         overflow: auto;
         overflow: overlay;
@@ -465,3 +441,15 @@ svg.git {
 svg.cmd {
   margin-left: -1px;
 }
+
+body[data-settings-tab=keymap] {
+  .cp__settings-inner {
+    > article {
+      @apply md:w-[70vw] xl:max-w-[850px] p-0;
+
+      > header {
+        @apply p-4 pb-2 h-auto;
+      }
+    }
+  }
+}

+ 4 - 15
src/main/frontend/components/shortcut.cljs

@@ -3,6 +3,7 @@
             [frontend.context.i18n :refer [t]]
             [frontend.modules.shortcut.core :as shortcut]
             [frontend.modules.shortcut.data-helper :as dh]
+            [frontend.modules.shortcut.utils :as shortcut-utils]
             [frontend.state :as state]
             [frontend.ui :as ui]
             [frontend.extensions.latex :as latex]
@@ -104,7 +105,7 @@
                                [:code.text-xs (namespace k)]
                                [:small.pl-1 (:desc cmd)]]
 
-                              (not plugin?) (-> k (dh/decorate-namespace) (t))
+                              (not plugin?) (-> k (shortcut-utils/decorate-namespace) (t))
                               :else (str k))]
                   [:tr {:key (str k)}
                    [:td.text-left.flex.items-center label]
@@ -204,23 +205,11 @@
    (shortcut-table :shortcut.category/block-selection true)
    (shortcut-table :shortcut.category/formatting true)
    (shortcut-table :shortcut.category/toggle true)
-   (when (state/enable-whiteboards?) (shortcut-table :shortcut.category/whiteboard 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}}]

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

@@ -25,4 +25,169 @@
       }
     }
   }
+}
+
+.cp__shortcut-page-x {
+  @apply relative;
+
+  &-pane-controls {
+    @apply flex space-x-3 absolute top-[-4px] right-4 items-center;
+
+    .search-input-wrap {
+      @apply pr-1 relative;
+
+      a.x {
+        @apply flex items-center absolute right-1 top-0 py-[7px] px-1 opacity-60
+        hover:opacity-90;
+      }
+    }
+
+    input.form-input {
+      @apply py-1;
+    }
+
+    a.icon-link {
+      @apply opacity-80 hover:opacity-100 active:opacity-40 select-none;
+
+      color: var(--ls-secondary-text-color);
+    }
+
+    .keyboard-filter {
+      .dropdown-wrapper {
+        @apply shadow-lg w-[18rem];
+      }
+
+      .keyboard-filter-record {
+        > h2 {
+          @apply flex items-center justify-between px-1.5 py-1;
+
+          background-color: var(--ls-secondary-background-color);
+
+          > strong {
+            @apply text-[12px] opacity-80;
+
+            font-weight: 400;
+          }
+        }
+      }
+    }
+  }
+
+  > header {
+    @apply px-4 pb-4 pt-2;
+
+    > h2 {
+      @apply relative top-[-6px];
+    }
+  }
+
+  > article {
+    @apply relative pb-4 overflow-y-auto;
+
+    max-height: calc(70vh - 100px);
+    overflow-y: overlay;
+
+    > ul {
+      @apply px-4 m-0 py-0;
+
+      li {
+        @apply text-[15px] px-1;
+
+        &.th {
+          @apply rounded mb-2 sticky top-0 cursor-pointer
+          select-none active:opacity-80 px-2 py-1 z-[1];
+
+          background-color: var(--ls-tertiary-background-color);
+        }
+
+        .label-wrap {
+          @apply flex flex-1;
+        }
+
+        .action-wrap {
+          @apply flex space-x-2 items-center flex-nowrap
+          select-none active:opacity-70;
+
+          &.disabled {
+            @apply opacity-60 cursor-default;
+          }
+        }
+      }
+    }
+  }
+
+  &-record-dialog-inner {
+    @apply py-[28px] m-[-30px] px-[20px];
+
+    h1 {
+      @apply relative top-[-8px];
+    }
+
+    &:active, &:focus, &:focus-within {
+      outline: burlywood hidden medium;
+    }
+
+    .shortcuts-keys-wrap {
+      @apply flex items-center my-4 flex-wrap;
+
+      .shortcut-record-control {
+        @apply flex space-x-1 items-center select-none
+        rounded border-[2px] py-[2px] px-[2px];
+      }
+
+      .keyboard-shortcut {
+        > code {
+          @apply relative select-none tracking-wider;
+
+          a.x {
+            @apply hidden absolute right-[-8px] top-[-6px] h-[16px] w-[16px]
+            rounded-full bg-red-500 text-white leading-none items-center
+            justify-center cursor-pointer opacity-80 hover:opacity-100 active:opacity-50;
+          }
+
+          &:hover a.x {
+            @apply flex;
+          }
+        }
+      }
+    }
+
+    &.keypressed {
+      .shortcut-record-control {
+        @apply pt-0
+      }
+    }
+
+    .action-btns {
+      .keyboard-shortcut code {
+        @apply rounded-[3px];
+      }
+    }
+
+    .reset-btn {
+      @apply ml-4 opacity-50 cursor-default;
+    }
+
+    &.dirty {
+      .reset-btn {
+        @apply opacity-100 cursor-pointer;
+      }
+    }
+  }
+}
+
+.cp__shortcut-conflicts-list {
+  &-wrap {
+    > section {
+      @apply bg-gray-3 border-[2px] mb-3 dark:bg-transparent;
+
+      > ul {
+        @apply px-2 pb-2 m-0 list-none;
+      }
+
+      > h2 {
+        @apply flex items-center p-2 text-red-9 text-sm space-x-1 font-extrabold;
+      }
+    }
+  }
 }

+ 476 - 0
src/main/frontend/components/shortcut2.cljs

@@ -0,0 +1,476 @@
+(ns frontend.components.shortcut2
+  (:require [clojure.string :as string]
+            [rum.core :as rum]
+            [frontend.context.i18n :refer [t]]
+            [cljs-bean.core :as bean]
+            [frontend.state :as state]
+            [frontend.search :as search]
+            [frontend.ui :as ui]
+            [frontend.rum :as r]
+            [goog.events :as events]
+            [promesa.core :as p]
+            [frontend.handler.notification :as notification]
+            [frontend.modules.shortcut.core :as shortcut]
+            [frontend.modules.shortcut.data-helper :as dh]
+            [frontend.util :as util]
+            [frontend.modules.shortcut.utils :as shortcut-utils]
+            [frontend.modules.shortcut.config :as shortcut-config])
+  (:import [goog.events KeyHandler]))
+
+(defonce categories
+         (vector :shortcut.category/basics
+                 :shortcut.category/navigating
+                 :shortcut.category/block-editing
+                 :shortcut.category/block-command-editing
+                 :shortcut.category/block-selection
+                 :shortcut.category/formatting
+                 :shortcut.category/toggle
+                 :shortcut.category/whiteboard
+                 :shortcut.category/plugins
+                 :shortcut.category/others))
+
+(defonce *refresh-sentry (atom 0))
+(defn refresh-shortcuts-list! [] (reset! *refresh-sentry (inc @*refresh-sentry)))
+(defonce *global-listener-setup? (atom false))
+(defonce *customize-modal-life-sentry (atom 0))
+
+(defn- to-vector [v]
+  (when-not (nil? v)
+    (if (sequential? v) (vec v) [v])))
+
+(declare customize-shortcut-dialog-inner)
+
+(rum/defc keyboard-filter-record-inner
+  [keystroke set-keystroke! close-fn]
+
+  (let [keypressed? (not= "" keystroke)]
+
+    (rum/use-effect!
+      (fn []
+        (let [key-handler (KeyHandler. js/document)]
+          ;; setup
+          (util/profile
+            "[shortcuts] unlisten*"
+            (shortcut/unlisten-all! true))
+          (events/listen key-handler "key"
+                         (fn [^js e]
+                           (.preventDefault e)
+                           (set-keystroke! #(util/trim-safe (str % (shortcut/keyname e))))))
+
+          ;; teardown
+          #(do
+             (util/profile
+               "[shortcuts] listen*"
+               (shortcut/listen-all!))
+             (.dispose key-handler))))
+      [])
+
+    [:div.keyboard-filter-record
+     [:h2
+      [:strong (t :keymap/keystroke-filter)]
+      [:span.flex.space-x-2
+       (when keypressed?
+         [:a.flex.items-center
+          {:on-click #(set-keystroke! "")} (ui/icon "zoom-reset" {:size 12})])
+       [:a.flex.items-center
+        {:on-click #(do (close-fn) (set-keystroke! ""))} (ui/icon "x" {:size 12})]]]
+     [:div.wrap.p-2
+      (if-not keypressed?
+        [:small (t :keymap/keystroke-record-desc)]
+        (when-not (string/blank? keystroke)
+          (ui/render-keyboard-shortcut [keystroke])))]]))
+
+(rum/defc pane-controls
+  [q set-q! filters set-filters! keystroke set-keystroke! toggle-categories-fn]
+  (let [*search-ref (rum/use-ref nil)]
+    [:div.cp__shortcut-page-x-pane-controls
+     [:a.flex.items-center.icon-link
+      {:on-click toggle-categories-fn
+       :title "Toggle categories pane"}
+      (ui/icon "fold")]
+
+     [:a.flex.items-center.icon-link
+      {:on-click refresh-shortcuts-list!
+       :title "Refresh all"}
+      (ui/icon "refresh")]
+
+     [:span.search-input-wrap
+      [:input.form-input.is-small
+       {:placeholder (t :keymap/search)
+        :ref         *search-ref
+        :value       (or q "")
+        :auto-focus  true
+        :on-key-down #(when (= 27 (.-keyCode %))
+                        (util/stop %)
+                        (if (string/blank? q)
+                          (some-> (rum/deref *search-ref) (.blur))
+                          (set-q! "")))
+        :on-change   #(let [v (util/evalue %)]
+                        (set-q! v))}]
+
+      (when-not (string/blank? q)
+        [:a.x
+         {:on-click (fn []
+                      (set-q! "")
+                      (js/setTimeout #(some-> (rum/deref *search-ref) (.focus)) 50))}
+         (ui/icon "x" {:size 14})])]
+
+     ;; keyboard filter
+     (ui/dropdown
+       (fn [{:keys [toggle-fn]}]
+         [:a.flex.items-center.icon-link
+          {:on-click toggle-fn} (ui/icon "keyboard")
+
+          (when-not (string/blank? keystroke)
+            (ui/point "bg-red-600.absolute" 4 {:style {:right -2 :top -2}}))])
+       (fn [{:keys [close-fn]}]
+         (keyboard-filter-record-inner keystroke set-keystroke! close-fn))
+       {:outside?      true
+        :trigger-class "keyboard-filter"})
+
+     ;; other filter
+     (ui/dropdown-with-links
+       (fn [{:keys [toggle-fn]}]
+         [:a.flex.items-center.icon-link.relative
+          {:on-click toggle-fn}
+          (ui/icon "filter")
+
+          (when (seq filters)
+            (ui/point "bg-red-600.absolute" 4 {:style {:right -2 :top -2}}))])
+
+       (for [k [:All :Disabled :Unset :Custom]
+             :let [all? (= k :All)
+                   checked? (or (contains? filters k) (and all? (nil? (seq filters))))]]
+
+         {:title   (if all? (t :keymap/all) (t (keyword :keymap (string/lower-case (name k)))))
+          :icon    (ui/icon (if checked? "checkbox" "square"))
+          :options {:on-click #(set-filters! (if all? #{} (let [f (if checked? disj conj)] (f filters k))))}})
+
+       nil)]))
+
+(rum/defc shortcut-desc-label
+  [id binding-map]
+  (when-let [id' (and id binding-map (some-> (str id) (string/replace "plugin." "")))]
+    [:span {:title (str id' "#" (some-> (:handler-id binding-map) (name)))}
+     [:span.pl-1 (dh/get-shortcut-desc (assoc binding-map :id id))]
+     [:small.pl-1 [:code.text-xs (str id')]]]))
+
+(defn- open-customize-shortcut-dialog!
+  [id]
+  (when-let [{:keys [binding user-binding] :as m} (dh/shortcut-item id)]
+    (let [binding (to-vector binding)
+          user-binding (and user-binding (to-vector user-binding))
+          modal-id (str :customize-shortcut id)
+          label (shortcut-desc-label id m)
+          args [id label binding user-binding
+                {:saved-cb (fn [] (-> (p/delay 500) (p/then refresh-shortcuts-list!)))
+                 :modal-id modal-id}]]
+      (state/set-sub-modal!
+        (fn [] (apply customize-shortcut-dialog-inner args))
+        {:center? true
+         :id      modal-id
+         :payload args}))))
+
+(rum/defc shortcut-conflicts-display
+  [_k conflicts-map]
+
+  [:div.cp__shortcut-conflicts-list-wrap
+   (for [[g ks] conflicts-map]
+     [:section.relative
+      [:h2 (ui/icon "alert-triangle" {:size 15})
+       [:span (t :keymap/conflicts-for-label)]
+       [:code (shortcut-utils/decorate-binding g)]]
+      [:ul
+       (for [v (vals ks)
+             :let [k (first v)
+                   vs (second v)]]
+         (for [[id' handler-id] vs
+               :let [m (dh/shortcut-item id')]
+               :when (not (nil? m))]
+           [:li
+            {:key (str id')}
+            [:a.select-none.hover:underline
+             {:on-click #(open-customize-shortcut-dialog! id')
+              :title (str handler-id)}
+             [:code.inline-block.mr-1.text-xs
+              (shortcut-utils/decorate-binding k)]
+             [:span
+              (dh/get-shortcut-desc m)
+              (ui/icon "external-link" {:size 18})]
+             [:code [:small (str id')]]]]))]])])
+
+(rum/defc ^:large-vars/cleanup-todo customize-shortcut-dialog-inner
+  [k action-name binding user-binding {:keys [saved-cb modal-id]}]
+  (let [*ref-el (rum/use-ref nil)
+        [modal-life _] (r/use-atom *customize-modal-life-sentry)
+        [keystroke set-keystroke!] (rum/use-state "")
+        [current-binding set-current-binding!] (rum/use-state (or user-binding binding))
+        [key-conflicts set-key-conflicts!] (rum/use-state nil)
+
+        handler-id (rum/use-memo #(dh/get-group k))
+        dirty? (not= (or user-binding binding) current-binding)
+        keypressed? (not= "" keystroke)
+
+        save-keystroke-fn!
+        (fn []
+          ;; parse current binding conflicts
+          (if-let [current-conflicts (seq (dh/parse-conflicts-from-binding current-binding keystroke))]
+            (notification/show!
+              (str "Shortcut conflicts from existing binding: "
+                   (pr-str (some->> current-conflicts (map #(shortcut-utils/decorate-binding %)))))
+              :error true :shortcut-conflicts/warning 5000)
+
+            ;; get conflicts from the existed bindings map
+            (let [conflicts-map (dh/get-conflicts-by-keys keystroke handler-id)]
+              (if-not (seq conflicts-map)
+                (do (set-current-binding! (conj current-binding keystroke))
+                    (set-keystroke! "")
+                    (set-key-conflicts! nil))
+
+                ;; show conflicts
+                (set-key-conflicts! conflicts-map)))))]
+
+    (rum/use-effect!
+      (fn []
+        (let [mid (state/sub :modal/id)
+              mid' (some-> (state/sub :modal/subsets) (last) (:modal/id))
+              el (rum/deref *ref-el)]
+          (when (or (and (not mid') (= mid modal-id))
+                    (= mid' modal-id))
+            (some-> el (.focus))
+            (js/setTimeout
+              #(some-> (.querySelector el ".shortcut-record-control a.submit")
+                       (.click)) 200))))
+      [modal-life])
+
+    (rum/use-effect!
+      (fn []
+        (let [^js el (rum/deref *ref-el)
+              key-handler (KeyHandler. el)
+
+              teardown-global!
+              (when-not @*global-listener-setup?
+                (shortcut/unlisten-all! true)
+                (reset! *global-listener-setup? true)
+                (fn []
+                  (shortcut/listen-all!)
+                  (reset! *global-listener-setup? false)))]
+
+          ;; setup
+          (events/listen key-handler "key"
+                         (fn [^js e]
+                           (.preventDefault e)
+                           (set-key-conflicts! nil)
+                           (set-keystroke! #(util/trim-safe (str % (shortcut/keyname e))))))
+
+          ;; active
+          (.focus el)
+
+          ;; teardown
+          #(do (some-> teardown-global! (apply nil))
+               (.dispose key-handler)
+               (swap! *customize-modal-life-sentry inc))))
+      [])
+
+    [:div.cp__shortcut-page-x-record-dialog-inner
+     {:class     (util/classnames [{:keypressed keypressed? :dirty dirty?}])
+      :tab-index -1
+      :ref       *ref-el}
+     [:div.sm:w-lsm
+      [:h1.text-2xl.pb-2
+       (t :keymap/customize-for-label)]
+
+      [:p.mb-4.text-md [:b action-name]]
+
+      [:div.shortcuts-keys-wrap
+       [:span.keyboard-shortcut.flex.flex-wrap.mr-2.space-x-2
+        (for [x current-binding]
+          [:code.tracking-wider
+           (-> x (string/trim) (string/lower-case) (shortcut-utils/decorate-binding))
+           [:a.x {:on-click (fn [] (set-current-binding!
+                                     (->> current-binding (remove #(= x %)) (into []))))}
+            (ui/icon "x" {:size 12})]])]
+
+       ;; add shortcut
+       [:div.shortcut-record-control
+        ;; keypressed state
+        (if keypressed?
+          [:<>
+           (when-not (string/blank? keystroke)
+             (ui/render-keyboard-shortcut [keystroke]))
+
+           [:a.flex.items-center.active:opacity-90.submit
+            {:on-click save-keystroke-fn!}
+            (ui/icon "check" {:size 14})]
+           [:a.flex.items-center.text-red-600.hover:text-red-700.active:opacity-90.cancel
+            {:on-click (fn []
+                         (set-keystroke! "")
+                         (set-key-conflicts! nil))}
+            (ui/icon "x" {:size 14})]]
+
+          [:code.flex.items-center
+           [:small.pr-1 (t :keymap/keystroke-record-setup-label)] (ui/icon "keyboard" {:size 14})])]]]
+
+     ;; conflicts results
+     (when (seq key-conflicts)
+       (shortcut-conflicts-display k key-conflicts))
+
+     [:div.action-btns.text-right.mt-6.flex.justify-between.items-center
+      ;; restore default
+      (when (sequential? binding)
+        [:a.flex.items-center.space-x-1.text-sm.opacity-70.hover:opacity-100
+         {:on-click #(set-current-binding! binding)}
+         (t :keymap/restore-to-default)
+         (for [it (some->> binding (map #(some->> % (dh/mod-key) (shortcut-utils/decorate-binding))))]
+           [:span.keyboard-shortcut.ml-1 [:code it]])])
+
+      [:span
+       (ui/button
+         (t :save)
+         :disabled (not dirty?)
+         :on-click (fn []
+                     ;; TODO: check conflicts for the single same leader key
+                     (let [binding' (if (nil? current-binding) [] current-binding)
+                           conflicts (dh/get-conflicts-by-keys binding' handler-id {:exclude-ids #{k}})]
+                       (if (seq conflicts)
+                         (set-key-conflicts! conflicts)
+                         (let [binding' (if (= binding binding') nil binding')]
+                           (shortcut/persist-user-shortcut! k binding')
+                           ;(notification/show! "Saved!" :success)
+                           (state/close-modal!)
+                           (saved-cb))))))
+
+       [:a.reset-btn
+        {:on-click (fn [] (set-current-binding! (or user-binding binding)))}
+        (t :reset)]]]]))
+
+(defn build-categories-map
+  []
+  (->> categories
+       (map #(vector % (into (sorted-map) (dh/binding-by-category %))))))
+
+(rum/defc ^:large-vars/cleanup-todo shortcut-keymap-x
+  []
+  (let [_ (r/use-atom shortcut-config/*category)
+        _ (r/use-atom *refresh-sentry)
+        [ready?, set-ready!] (rum/use-state false)
+        [filters, set-filters!] (rum/use-state #{})
+        [keystroke, set-keystroke!] (rum/use-state "")
+        [q set-q!] (rum/use-state nil)
+
+        categories-list-map (build-categories-map)
+        all-categories (into #{} (map first categories-list-map))
+        in-filters? (boolean (seq filters))
+        in-query? (not (string/blank? (util/trim-safe q)))
+        in-keystroke? (not (string/blank? keystroke))
+
+        [folded-categories set-folded-categories!] (rum/use-state #{})
+
+        matched-list-map
+        (when (and in-query? (not in-keystroke?))
+          (->> categories-list-map
+               (map (fn [[c binding-map]]
+                      [c (search/fuzzy-search
+                           binding-map q
+                           :extract-fn
+                           #(let [[id m] %]
+                              (str (name id) " " (dh/get-shortcut-desc (assoc m :id id)))))]))))
+
+        result-list-map (or matched-list-map categories-list-map)
+        toggle-categories! #(if (= folded-categories all-categories)
+                              (set-folded-categories! #{})
+                              (set-folded-categories! all-categories))]
+
+    (rum/use-effect!
+      (fn []
+        (js/setTimeout #(set-ready! true) 100))
+      [])
+
+    [:div.cp__shortcut-page-x
+     [:header.relative
+      [:h2.text-xs.opacity-70
+       (str (t :keymap/total)
+            " "
+            (if ready?
+              (apply + (map #(count (second %)) result-list-map))
+              " ..."))]
+
+      (pane-controls q set-q! filters set-filters! keystroke set-keystroke! toggle-categories!)]
+
+     [:article
+      (when-not ready?
+        [:p.py-8.flex.justify-center (ui/loading "")])
+
+      (when ready?
+        [:ul.list-none.m-0.py-3
+         (for [[c binding-map] result-list-map
+               :let [folded? (contains? folded-categories c)]]
+           [:<>
+            ;; category row
+            (when (and (not in-query?)
+                       (not in-filters?)
+                       (not in-keystroke?))
+              [:li.flex.justify-between.th
+               {:key      (str c)
+                :on-click #(let [f (if folded? disj conj)]
+                             (set-folded-categories! (f folded-categories c)))}
+               [:strong.font-semibold (t c)]
+               [:i.flex.items-center
+                (ui/icon (if folded? "chevron-left" "chevron-down"))]])
+
+            ;; binding row
+            (when (or in-query? in-filters? (not folded?))
+              (for [[id {:keys [binding user-binding] :as m}] binding-map
+                    :let [binding (to-vector binding)
+                          user-binding (and user-binding (to-vector user-binding))
+                          label (shortcut-desc-label id m)
+                          custom? (not (nil? user-binding))
+                          disabled? (or (false? user-binding)
+                                        (false? (first binding)))
+                          unset? (and (not disabled?)
+                                      (= user-binding []))]]
+
+                (when (or (nil? (seq filters))
+                          (when (contains? filters :Custom) custom?)
+                          (when (contains? filters :Disabled) disabled?)
+                          (when (contains? filters :Unset) unset?))
+
+                  ;; keystrokes filter
+                  (when (or (not in-keystroke?)
+                            (and (not disabled?)
+                                 (not unset?)
+                                 (let [binding' (or user-binding binding)
+                                       keystroke' (some-> (shortcut-utils/safe-parse-string-binding keystroke) (bean/->clj))]
+                                   (when (sequential? binding')
+                                     (some #(when-let [s (some-> % (dh/mod-key) (shortcut-utils/safe-parse-string-binding) (bean/->clj))]
+                                              (or (= s keystroke')
+                                                  (and (sequential? s) (sequential? keystroke')
+                                                       (apply = (map first [s keystroke']))))) binding')))))
+
+                    [:li.flex.items-center.justify-between.text-sm
+                     {:key (str id)}
+                     [:span.label-wrap label]
+
+                     [:a.action-wrap
+                      {:class    (util/classnames [{:disabled disabled?}])
+                       :on-click (when-not disabled?
+                                   #(open-customize-shortcut-dialog! id))}
+
+                      (cond
+                        (or user-binding (false? user-binding))
+                        [:code.dark:bg-green-800.bg-green-300
+                         (if unset?
+                           (t :keymap/unset)
+                           (str (t :keymap/custom) ": "
+                                (if disabled?
+                                  (t :keymap/disabled)
+                                  (bean/->js
+                                    (map #(if (false? %)
+                                            (t :keymap/disabled)
+                                            (shortcut-utils/decorate-binding %)) user-binding)))))]
+
+                        (not unset?)
+                        (for [x binding]
+                          [:code.tracking-wide
+                           {:key (str x)}
+                           (dh/binding-for-display id x)]))]]))))])])]]))

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

@@ -92,7 +92,7 @@
     (rum/use-effect!
      #(state/set-modal!
        (when settings-open?
-         (fn [] [:div.settings-modal (settings/settings)])))
+         (fn [] [:div.settings-modal (settings/settings settings-open?)])))
      [settings-open?])
 
     (rum/use-effect!

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

@@ -291,7 +291,7 @@
      (tldraw-app page-name block-id)]))
 
 (rum/defc whiteboard-route <
-(shortcut/mixin :shortcut.handler/whiteboard)
+(shortcut/mixin :shortcut.handler/whiteboard false)
   [route-match]
   (let [name (get-in route-match [:parameters :path :name])
         {:keys [block-id]} (get-in route-match [:parameters :query])]

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

@@ -44,7 +44,9 @@
       (def REGION "us-east-1")
       (def USER-POOL-ID "us-east-1_dtagLnju8")
       (def IDENTITY-POOL-ID "us-east-1:d6d3b034-1631-402b-b838-b44513e93ee0")
-      (def OAUTH-DOMAIN "logseq-prod.auth.us-east-1.amazoncognito.com"))
+      (def OAUTH-DOMAIN "logseq-prod.auth.us-east-1.amazoncognito.com")
+      (def CONNECTIVITY-TESTING-S3-URL "https://logseq-connectivity-testing-prod.s3.us-east-1.amazonaws.com/logseq-connectivity-testing")
+      )
 
   (do (def FILE-SYNC-PROD? false)
       (def LOGIN-URL
@@ -56,7 +58,8 @@
       (def REGION "us-east-2")
       (def USER-POOL-ID "us-east-2_kAqZcxIeM")
       (def IDENTITY-POOL-ID "us-east-2:cc7d2ad3-84d0-4faf-98fe-628f6b52c0a5")
-      (def OAUTH-DOMAIN "logseq-test2.auth.us-east-2.amazoncognito.com")))
+      (def OAUTH-DOMAIN "logseq-test2.auth.us-east-2.amazoncognito.com")
+      (def CONNECTIVITY-TESTING-S3-URL "https://logseq-connectivity-testing-prod.s3.us-east-1.amazonaws.com/logseq-connectivity-testing")))
 
 
 (goog-define ENABLE-RTC-SYNC-PRODUCTION false)

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

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

+ 11 - 0
src/main/frontend/db/persist.cljs

@@ -47,3 +47,14 @@
     (if (util/electron?)
       (ipc/ipc "deleteGraph" graph key db-based?)
      (idb/remove-item! key))))
+
+(defn rename-graph!
+  [old-repo new-repo]
+  (let [old-key (db-conn/datascript-db old-repo)
+        new-key (db-conn/datascript-db new-repo)]
+    (if (util/electron?)
+      (do
+        (js/console.error "rename-graph! is not supported in electron")
+        (idb/rename-item! old-key new-key))
+      (idb/rename-item! old-key new-key))))
+

+ 4 - 2
src/main/frontend/dicts.cljc

@@ -58,7 +58,8 @@
    :ko      (edn-resource "dicts/ko.edn")
    :pl      (edn-resource "dicts/pl.edn")
    :sk      (edn-resource "dicts/sk.edn")
-   :uk      (edn-resource "dicts/uk.edn")})
+   :uk      (edn-resource "dicts/uk.edn")
+   :id      (edn-resource "dicts/id.edn")})
 
 (def languages
   "List of languages presented to user"
@@ -80,7 +81,8 @@
    {:label "Türkçe" :value :tr}
    {:label "Українська" :value :uk}
    {:label "한국어" :value :ko}
-   {:label "Slovenčina" :value :sk}])
+   {:label "Slovenčina" :value :sk}
+   {:label "Bahasa Indonesia" :value :id}])
 
 (assert (= (set (keys dicts)) (set (map :value languages)))
         "List of user-facing languages must match list of dictionaries")

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

@@ -74,7 +74,8 @@
    :did-update update-draw-content-width
    :will-unmount (fn [state] (.disconnect @(::resize-observer state)))}
   [state data option]
-  (let [*draw-width (get state ::draw-width)
+  (let [ref (rum/create-ref)
+        *draw-width (get state ::draw-width)
         *zen-mode? (get state ::zen-mode?)
         *view-mode? (get state ::view-mode?)
         *grid-mode? (get state ::grid-mode?)
@@ -96,7 +97,8 @@
                                (editor-handler/edit-block! block :max block-uuid))}
          "Edit Block"]]
        [:div.draw-wrap
-        {:on-mouse-down (fn [e]
+        {:ref ref
+         :on-mouse-down (fn [e]
                           (util/stop e)
                           (state/set-block-component-editing-mode! true))
          :on-blur #(state/set-block-component-editing-mode! false)
@@ -121,6 +123,7 @@
            :zen-mode-enabled @*zen-mode?
            :view-mode-enabled @*view-mode?
            :grid-mode-enabled @*grid-mode?
+           :on-pointer-down #(.. (rum/deref ref) -firstChild focus)
            :initial-data data
            :theme (excalidraw-theme (state/sub :ui/theme))}))]])))
 

+ 0 - 801
src/main/frontend/extensions/pdf/_viewer.css

@@ -1,801 +0,0 @@
-/* Copyright 2014 Mozilla Foundation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-.textLayer {
-  position: absolute;
-  text-align: initial;
-  left: 0;
-  top: 0;
-  right: 0;
-  bottom: 0;
-  overflow: hidden;
-  opacity: 0.2;
-  line-height: 1;
-  -webkit-text-size-adjust: none;
-     -moz-text-size-adjust: none;
-          text-size-adjust: none;
-  forced-color-adjust: none;
-}
-
-.textLayer span,
-.textLayer br {
-  color: transparent;
-  position: absolute;
-  white-space: pre;
-  cursor: text;
-  transform-origin: 0% 0%;
-}
-
-/* Only necessary in Google Chrome, see issue 14205, and most unfortunately
- * the problem doesn't show up in "text" reference tests. */
-.textLayer span.markedContent {
-  top: 0;
-  height: 0;
-}
-
-.textLayer .highlight {
-  margin: -1px;
-  padding: 1px;
-  background-color: rgba(180, 0, 170, 1);
-  border-radius: 4px;
-}
-
-.textLayer .highlight.appended {
-  position: initial;
-}
-
-.textLayer .highlight.begin {
-  border-radius: 4px 0 0 4px;
-}
-
-.textLayer .highlight.end {
-  border-radius: 0 4px 4px 0;
-}
-
-.textLayer .highlight.middle {
-  border-radius: 0;
-}
-
-.textLayer .highlight.selected {
-  background-color: rgba(0, 100, 0, 1);
-}
-
-.textLayer ::-moz-selection {
-  background: rgba(0, 0, 255, 1);
-}
-
-.textLayer ::selection {
-  background: rgba(0, 0, 255, 1);
-}
-
-/* Avoids https://github.com/mozilla/pdf.js/issues/13840 in Chrome */
-.textLayer br::-moz-selection {
-  background: transparent;
-}
-.textLayer br::selection {
-  background: transparent;
-}
-
-.textLayer .endOfContent {
-  display: block;
-  position: absolute;
-  left: 0;
-  top: 100%;
-  right: 0;
-  bottom: 0;
-  z-index: -1;
-  cursor: default;
-  -webkit-user-select: none;
-     -moz-user-select: none;
-          user-select: none;
-}
-
-.textLayer .endOfContent.active {
-  top: 0;
-}
-
-
-:root {
-  --annotation-unfocused-field-background: url("data:image/svg+xml;charset=UTF-8,<svg width='1px' height='1px' xmlns='http://www.w3.org/2000/svg'><rect width='100%' height='100%' style='fill:rgba(0, 54, 255, 0.13);'/></svg>");
-}
-
-.annotationLayer section {
-  position: absolute;
-  text-align: initial;
-}
-
-.annotationLayer .linkAnnotation > a,
-.annotationLayer .buttonWidgetAnnotation.pushButton > a {
-  position: absolute;
-  font-size: 1em;
-  top: 0;
-  left: 0;
-  width: 100%;
-  height: 100%;
-}
-
-.annotationLayer .buttonWidgetAnnotation.pushButton > canvas {
-  position: relative;
-  top: 0;
-  left: 0;
-  z-index: -1;
-}
-
-.annotationLayer .linkAnnotation > a:hover,
-.annotationLayer .buttonWidgetAnnotation.pushButton > a:hover {
-  opacity: 0.2;
-  background: rgba(255, 255, 0, 1);
-  box-shadow: 0 2px 10px rgba(255, 255, 0, 1);
-}
-
-.annotationLayer .textAnnotation img {
-  position: absolute;
-  cursor: pointer;
-}
-
-.annotationLayer .textWidgetAnnotation input,
-.annotationLayer .textWidgetAnnotation textarea,
-.annotationLayer .choiceWidgetAnnotation select,
-.annotationLayer .buttonWidgetAnnotation.checkBox input,
-.annotationLayer .buttonWidgetAnnotation.radioButton input {
-  background-image: var(--annotation-unfocused-field-background);
-  border: 1px solid transparent;
-  box-sizing: border-box;
-  font-size: 9px;
-  height: 100%;
-  margin: 0;
-  padding: 0 3px;
-  vertical-align: top;
-  width: 100%;
-}
-
-.annotationLayer .choiceWidgetAnnotation select option {
-  padding: 0;
-}
-
-.annotationLayer .buttonWidgetAnnotation.radioButton input {
-  border-radius: 50%;
-}
-
-.annotationLayer .textWidgetAnnotation textarea {
-  font: message-box;
-  font-size: 9px;
-  resize: none;
-}
-
-.annotationLayer .textWidgetAnnotation input[disabled],
-.annotationLayer .textWidgetAnnotation textarea[disabled],
-.annotationLayer .choiceWidgetAnnotation select[disabled],
-.annotationLayer .buttonWidgetAnnotation.checkBox input[disabled],
-.annotationLayer .buttonWidgetAnnotation.radioButton input[disabled] {
-  background: none;
-  border: 1px solid transparent;
-  cursor: not-allowed;
-}
-
-.annotationLayer .textWidgetAnnotation input:hover,
-.annotationLayer .textWidgetAnnotation textarea:hover,
-.annotationLayer .choiceWidgetAnnotation select:hover,
-.annotationLayer .buttonWidgetAnnotation.checkBox input:hover,
-.annotationLayer .buttonWidgetAnnotation.radioButton input:hover {
-  border: 1px solid rgba(0, 0, 0, 1);
-}
-
-.annotationLayer .textWidgetAnnotation input:focus,
-.annotationLayer .textWidgetAnnotation textarea:focus,
-.annotationLayer .choiceWidgetAnnotation select:focus {
-  background: none;
-  border: 1px solid transparent;
-}
-
-.annotationLayer .textWidgetAnnotation input :focus,
-.annotationLayer .textWidgetAnnotation textarea :focus,
-.annotationLayer .choiceWidgetAnnotation select :focus,
-.annotationLayer .buttonWidgetAnnotation.checkBox :focus,
-.annotationLayer .buttonWidgetAnnotation.radioButton :focus {
-  background-image: none;
-  background-color: transparent;
-  outline: auto;
-}
-
-.annotationLayer .buttonWidgetAnnotation.checkBox input:checked:before,
-.annotationLayer .buttonWidgetAnnotation.checkBox input:checked:after,
-.annotationLayer .buttonWidgetAnnotation.radioButton input:checked:before {
-  background-color: rgba(0, 0, 0, 1);
-  content: "";
-  display: block;
-  position: absolute;
-}
-
-.annotationLayer .buttonWidgetAnnotation.checkBox input:checked:before,
-.annotationLayer .buttonWidgetAnnotation.checkBox input:checked:after {
-  height: 80%;
-  left: 45%;
-  width: 1px;
-}
-
-.annotationLayer .buttonWidgetAnnotation.checkBox input:checked:before {
-  transform: rotate(45deg);
-}
-
-.annotationLayer .buttonWidgetAnnotation.checkBox input:checked:after {
-  transform: rotate(-45deg);
-}
-
-.annotationLayer .buttonWidgetAnnotation.radioButton input:checked:before {
-  border-radius: 50%;
-  height: 50%;
-  left: 30%;
-  top: 20%;
-  width: 50%;
-}
-
-.annotationLayer .textWidgetAnnotation input.comb {
-  font-family: monospace;
-  padding-left: 2px;
-  padding-right: 0;
-}
-
-.annotationLayer .textWidgetAnnotation input.comb:focus {
-  /*
-   * Letter spacing is placed on the right side of each character. Hence, the
-   * letter spacing of the last character may be placed outside the visible
-   * area, causing horizontal scrolling. We avoid this by extending the width
-   * when the element has focus and revert this when it loses focus.
-   */
-  width: 103%;
-}
-
-.annotationLayer .buttonWidgetAnnotation.checkBox input,
-.annotationLayer .buttonWidgetAnnotation.radioButton input {
-  -webkit-appearance: none;
-     -moz-appearance: none;
-          appearance: none;
-  padding: 0;
-}
-
-.annotationLayer .popupWrapper {
-  position: absolute;
-  width: 20em;
-}
-
-.annotationLayer .popup {
-  position: absolute;
-  z-index: 200;
-  max-width: 20em;
-  background-color: rgba(255, 255, 153, 1);
-  box-shadow: 0 2px 5px rgba(136, 136, 136, 1);
-  border-radius: 2px;
-  padding: 6px;
-  margin-left: 5px;
-  cursor: pointer;
-  font: message-box;
-  font-size: 9px;
-  white-space: normal;
-  word-wrap: break-word;
-}
-
-.annotationLayer .popup > * {
-  font-size: 9px;
-}
-
-.annotationLayer .popup h1 {
-  display: inline-block;
-}
-
-.annotationLayer .popupDate {
-  display: inline-block;
-  margin-left: 5px;
-}
-
-.annotationLayer .popupContent {
-  border-top: 1px solid rgba(51, 51, 51, 1);
-  margin-top: 2px;
-  padding-top: 2px;
-}
-
-.annotationLayer .richText > * {
-  white-space: pre-wrap;
-}
-
-.annotationLayer .highlightAnnotation,
-.annotationLayer .underlineAnnotation,
-.annotationLayer .squigglyAnnotation,
-.annotationLayer .strikeoutAnnotation,
-.annotationLayer .freeTextAnnotation,
-.annotationLayer .lineAnnotation svg line,
-.annotationLayer .squareAnnotation svg rect,
-.annotationLayer .circleAnnotation svg ellipse,
-.annotationLayer .polylineAnnotation svg polyline,
-.annotationLayer .polygonAnnotation svg polygon,
-.annotationLayer .caretAnnotation,
-.annotationLayer .inkAnnotation svg polyline,
-.annotationLayer .stampAnnotation,
-.annotationLayer .fileAttachmentAnnotation {
-  cursor: pointer;
-}
-
-
-:root {
-  --xfa-unfocused-field-background: url("data:image/svg+xml;charset=UTF-8,<svg width='1px' height='1px' xmlns='http://www.w3.org/2000/svg'><rect width='100%' height='100%' style='fill:rgba(0, 54, 255, 0.13);'/></svg>");
-}
-
-.xfaLayer .highlight {
-  margin: -1px;
-  padding: 1px;
-  background-color: rgba(239, 203, 237, 1);
-  border-radius: 4px;
-}
-
-.xfaLayer .highlight.appended {
-  position: initial;
-}
-
-.xfaLayer .highlight.begin {
-  border-radius: 4px 0 0 4px;
-}
-
-.xfaLayer .highlight.end {
-  border-radius: 0 4px 4px 0;
-}
-
-.xfaLayer .highlight.middle {
-  border-radius: 0;
-}
-
-.xfaLayer .highlight.selected {
-  background-color: rgba(203, 223, 203, 1);
-}
-
-.xfaLayer ::-moz-selection {
-  background: rgba(0, 0, 255, 1);
-}
-
-.xfaLayer ::selection {
-  background: rgba(0, 0, 255, 1);
-}
-
-.xfaPage {
-  overflow: hidden;
-  position: relative;
-}
-
-.xfaContentarea {
-  position: absolute;
-}
-
-.xfaPrintOnly {
-  display: none;
-}
-
-.xfaLayer {
-  position: absolute;
-  text-align: initial;
-  top: 0;
-  left: 0;
-  transform-origin: 0 0;
-  line-height: 1.2;
-}
-
-.xfaLayer * {
-  color: inherit;
-  font: inherit;
-  font-style: inherit;
-  font-weight: inherit;
-  font-kerning: inherit;
-  letter-spacing: -0.01px;
-  text-align: inherit;
-  text-decoration: inherit;
-  box-sizing: border-box;
-  background-color: transparent;
-  padding: 0;
-  margin: 0;
-  pointer-events: auto;
-  line-height: inherit;
-}
-
-.xfaLayer div {
-  pointer-events: none;
-}
-
-.xfaLayer svg {
-  pointer-events: none;
-}
-
-.xfaLayer svg * {
-  pointer-events: none;
-}
-
-.xfaLayer a {
-  color: blue;
-}
-
-.xfaRich li {
-  margin-left: 3em;
-}
-
-.xfaFont {
-  color: black;
-  font-weight: normal;
-  font-kerning: none;
-  font-size: 10px;
-  font-style: normal;
-  letter-spacing: 0;
-  text-decoration: none;
-  vertical-align: 0;
-}
-
-.xfaCaption {
-  overflow: hidden;
-  flex: 0 0 auto;
-}
-
-.xfaCaptionForCheckButton {
-  overflow: hidden;
-  flex: 1 1 auto;
-}
-
-.xfaLabel {
-  height: 100%;
-  width: 100%;
-}
-
-.xfaLeft {
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-}
-
-.xfaRight {
-  display: flex;
-  flex-direction: row-reverse;
-  align-items: center;
-}
-
-.xfaLeft > .xfaCaption,
-.xfaLeft > .xfaCaptionForCheckButton,
-.xfaRight > .xfaCaption,
-.xfaRight > .xfaCaptionForCheckButton {
-  max-height: 100%;
-}
-
-.xfaTop {
-  display: flex;
-  flex-direction: column;
-  align-items: flex-start;
-}
-
-.xfaBottom {
-  display: flex;
-  flex-direction: column-reverse;
-  align-items: flex-start;
-}
-
-.xfaTop > .xfaCaption,
-.xfaTop > .xfaCaptionForCheckButton,
-.xfaBottom > .xfaCaption,
-.xfaBottom > .xfaCaptionForCheckButton {
-  width: 100%;
-}
-
-.xfaBorder {
-  background-color: transparent;
-  position: absolute;
-  pointer-events: none;
-}
-
-.xfaWrapped {
-  width: 100%;
-  height: 100%;
-}
-
-.xfaTextfield:focus,
-.xfaSelect:focus {
-  background-image: none;
-  background-color: transparent;
-  outline: auto;
-  outline-offset: -1px;
-}
-
-.xfaCheckbox:focus,
-.xfaRadio:focus {
-  outline: auto;
-}
-
-.xfaTextfield,
-.xfaSelect {
-  height: 100%;
-  width: 100%;
-  flex: 1 1 auto;
-  border: none;
-  resize: none;
-  background-image: var(--xfa-unfocused-field-background);
-}
-
-.xfaTop > .xfaTextfield,
-.xfaTop > .xfaSelect,
-.xfaBottom > .xfaTextfield,
-.xfaBottom > .xfaSelect {
-  flex: 0 1 auto;
-}
-
-.xfaButton {
-  cursor: pointer;
-  width: 100%;
-  height: 100%;
-  border: none;
-  text-align: center;
-}
-
-.xfaLink {
-  width: 100%;
-  height: 100%;
-  position: absolute;
-  top: 0;
-  left: 0;
-}
-
-.xfaCheckbox,
-.xfaRadio {
-  width: 100%;
-  height: 100%;
-  flex: 0 0 auto;
-  border: none;
-}
-
-.xfaRich {
-  white-space: pre-wrap;
-  width: 100%;
-  height: 100%;
-}
-
-.xfaImage {
-  -o-object-position: left top;
-     object-position: left top;
-  -o-object-fit: contain;
-     object-fit: contain;
-  width: 100%;
-  height: 100%;
-}
-
-.xfaLrTb,
-.xfaRlTb,
-.xfaTb {
-  display: flex;
-  flex-direction: column;
-  align-items: stretch;
-}
-
-.xfaLr {
-  display: flex;
-  flex-direction: row;
-  align-items: stretch;
-}
-
-.xfaRl {
-  display: flex;
-  flex-direction: row-reverse;
-  align-items: stretch;
-}
-
-.xfaTb > div {
-  justify-content: left;
-}
-
-.xfaPosition {
-  position: relative;
-}
-
-.xfaArea {
-  position: relative;
-}
-
-.xfaValignMiddle {
-  display: flex;
-  align-items: center;
-}
-
-.xfaTable {
-  display: flex;
-  flex-direction: column;
-  align-items: stretch;
-}
-
-.xfaTable .xfaRow {
-  display: flex;
-  flex-direction: row;
-  align-items: stretch;
-}
-
-.xfaTable .xfaRlRow {
-  display: flex;
-  flex-direction: row-reverse;
-  align-items: stretch;
-  flex: 1;
-}
-
-.xfaTable .xfaRlRow > div {
-  flex: 1;
-}
-
-.xfaNonInteractive input,
-.xfaNonInteractive textarea,
-.xfaDisabled input,
-.xfaDisabled textarea,
-.xfaReadOnly input,
-.xfaReadOnly textarea {
-  background: initial;
-}
-
-@media print {
-  .xfaTextfield,
-  .xfaSelect {
-    background: transparent;
-  }
-
-  .xfaSelect {
-    -webkit-appearance: none;
-       -moz-appearance: none;
-            appearance: none;
-    text-indent: 1px;
-    text-overflow: "";
-  }
-}
-
-:root {
-  --viewer-container-height: 0;
-  --pdfViewer-padding-bottom: 0;
-  --page-margin: 1px auto -8px;
-  --page-border: 9px solid transparent;
-  --spreadHorizontalWrapped-margin-LR: -3.5px;
-  --zoom-factor: 1;
-}
-
-@media screen and (forced-colors: active) {
-  :root {
-    --pdfViewer-padding-bottom: 9px;
-    --page-margin: 9px auto 0;
-    --page-border: none;
-    --spreadHorizontalWrapped-margin-LR: 4.5px;
-  }
-}
-
-.pdfViewer {
-  padding-bottom: var(--pdfViewer-padding-bottom);
-}
-
-.pdfViewer .canvasWrapper {
-  overflow: hidden;
-}
-
-.pdfViewer .page {
-  direction: ltr;
-  width: 816px;
-  height: 1056px;
-  margin: var(--page-margin);
-  position: relative;
-  overflow: visible;
-  border: var(--page-border);
-  background-clip: content-box;
-  -o-border-image: url(images/shadow.png) 9 9 repeat;
-     border-image: url(images/shadow.png) 9 9 repeat;
-  background-color: rgba(255, 255, 255, 1);
-}
-
-.pdfViewer .dummyPage {
-  position: relative;
-  width: 0;
-  height: var(--viewer-container-height);
-}
-
-.pdfViewer.removePageBorders .page {
-  margin: 0 auto 10px;
-  border: none;
-}
-
-.pdfViewer.singlePageView {
-  display: inline-block;
-}
-
-.pdfViewer.singlePageView .page {
-  margin: 0;
-  border: none;
-}
-
-.pdfViewer.scrollHorizontal,
-.pdfViewer.scrollWrapped,
-.spread {
-  margin-left: 3.5px;
-  margin-right: 3.5px;
-  text-align: center;
-}
-
-.pdfViewer.scrollHorizontal,
-.spread {
-  white-space: nowrap;
-}
-
-.pdfViewer.removePageBorders,
-.pdfViewer.scrollHorizontal .spread,
-.pdfViewer.scrollWrapped .spread {
-  margin-left: 0;
-  margin-right: 0;
-}
-
-.spread .page,
-.spread .dummyPage,
-.pdfViewer.scrollHorizontal .page,
-.pdfViewer.scrollWrapped .page,
-.pdfViewer.scrollHorizontal .spread,
-.pdfViewer.scrollWrapped .spread {
-  display: inline-block;
-  vertical-align: middle;
-}
-
-.spread .page,
-.pdfViewer.scrollHorizontal .page,
-.pdfViewer.scrollWrapped .page {
-  margin-left: var(--spreadHorizontalWrapped-margin-LR);
-  margin-right: var(--spreadHorizontalWrapped-margin-LR);
-}
-
-.pdfViewer.removePageBorders .spread .page,
-.pdfViewer.removePageBorders.scrollHorizontal .page,
-.pdfViewer.removePageBorders.scrollWrapped .page {
-  margin-left: 5px;
-  margin-right: 5px;
-}
-
-.pdfViewer .page canvas {
-  margin: 0;
-  display: block;
-}
-
-.pdfViewer .page canvas[hidden] {
-  display: none;
-}
-
-.pdfViewer .page .loadingIcon {
-  position: absolute;
-  display: block;
-  left: 0;
-  top: 0;
-  right: 0;
-  bottom: 0;
-}
-
-.pdfViewer .page .loadingIcon.notVisible {
-  background: none;
-}
-
-.pdfViewer.enablePermissions .textLayer span {
-  -webkit-user-select: none !important;
-     -moz-user-select: none !important;
-          user-select: none !important;
-  cursor: not-allowed;
-}
-
-.pdfPresentationMode .pdfViewer {
-  padding-bottom: 0;
-}
-
-.pdfPresentationMode .spread {
-  margin: 0;
-}
-
-.pdfPresentationMode .pdfViewer .page {
-  margin: 0 auto;
-  border: 2px solid transparent;
-}

+ 11 - 6
src/main/frontend/extensions/pdf/core.cljs

@@ -910,8 +910,8 @@
              opts          {:url           url
                             :password      (or doc-password "")
                             :ownerDocument (.-ownerDocument loader-el)
-                            :cMapUrl       "./cmaps/"
-                            ;;:cMapUrl       "https://cdn.jsdelivr.net/npm/pdfjs-dist@2.8.335/cmaps/"
+                            :cMapUrl       "./js/pdfjs/cmaps/"
+                            ;:cMapUrl       "https://cdn.jsdelivr.net/npm/pdfjs-dist@3.9.179/cmaps/"
                             :cMapPacked    true}]
 
          (set-loader-state! {:status :loading})
@@ -988,6 +988,11 @@
                             {:set-dirty-hls! set-dirty-hls!
                              :set-hls-extra! set-hls-extra!}) "pdf-viewer")])))])))
 
+(rum/defc pdf-container-outer
+  < (shortcut/mixin :shortcut.handler/pdf false)
+  [child]
+  [:<> child])
+
 (rum/defc pdf-container
   [{:keys [identity] :as pdf-current}]
   (let [[prepared set-prepared!] (rum/use-state false)
@@ -1029,8 +1034,7 @@
 
 (rum/defcs default-embed-playground
   < rum/static rum/reactive
-    (shortcut/mixin :shortcut.handler/pdf)
-  []
+  [state]
   (let [pdf-current (state/sub :pdf/current)
         system-win? (state/sub :pdf/system-win?)]
     [:div.extensions__pdf-playground
@@ -1040,8 +1044,9 @@
 
      (when (and (not system-win?) pdf-current)
        (js/ReactDOM.createPortal
-        (pdf-container pdf-current)
-        (js/document.querySelector "#app-single-container")))]))
+         (pdf-container-outer
+           (pdf-container pdf-current))
+         (js/document.querySelector "#app-single-container")))]))
 
 (rum/defcs system-embed-playground
   < rum/reactive

+ 10 - 5
src/main/frontend/extensions/pdf/pdf.css

@@ -1,5 +1,3 @@
-@import "_viewer.css";
-
 :root {
   --ph-highlight-color-blue: var(--color-blue-300);
   --ph-highlight-color-green: var(--color-green-300);
@@ -736,9 +734,12 @@ input::-webkit-inner-spin-button {
     background-color: #042f3c;
 
     .pdfViewer {
-      -webkit-filter: invert(100%);
-      filter: invert(100%);
       background: transparent;
+
+      .page {
+        -webkit-filter: invert(100%);
+        filter: invert(100%);
+      }
     }
 
     .textLayer {
@@ -1039,4 +1040,8 @@ html.is-system-window {
   to {
     opacity: 1;
   }
-}
+}
+
+.pdfViewer .page.loadingIcon::after {
+  background: none;
+}

+ 1 - 1
src/main/frontend/extensions/srs.cljs

@@ -513,7 +513,7 @@
            [:div.my-3 (ui/button "Review cards" :small? true)])]))))
 
 (rum/defc view-modal <
-  (shortcut/mixin :shortcut.handler/cards)
+  (shortcut/mixin :shortcut.handler/cards false)
   [blocks option card-index]
   [:div#cards-modal
    (if (seq blocks)

+ 17 - 16
src/main/frontend/fs/capacitor_fs.cljs

@@ -66,8 +66,8 @@
 
 (defn- <readdir [path]
   (-> (p/chain (.readdir Filesystem (clj->js {:path path}))
-              #(js->clj % :keywordize-keys true)
-              :files)
+               #(js->clj % :keywordize-keys true)
+               :files)
       (p/catch (fn [error]
                  (js/console.error "readdir Error: " path ": " error)
                  nil))))
@@ -98,7 +98,7 @@
     result))
 
 (defn- get-files
-  "get all files recursively"
+  "get all files and contents recursively"
   [path]
   (p/let [result (p/loop [result []
                           dirs [path]]
@@ -150,17 +150,18 @@
                                           ""))]
     (path/path-join repo-dir bak-dir relative-path)))
 
-(defn- truncate-old-versioned-files!
+(defn- <truncate-old-versioned-files!
   "reserve the latest 6 version files"
   [dir]
-  (->
-   (p/let [files (get-files dir)
-           files (js->clj files :keywordize-keys true)
-           old-versioned-files (drop 6 (reverse (sort-by :mtime files)))]
-     (mapv (fn [file]
-             (.deleteFile Filesystem (clj->js {:path (:path file)})))
-           old-versioned-files))
-   (p/catch (fn [_]))))
+  (-> (p/let [files (.readdir Filesystem (clj->js {:path dir}))
+
+              files (:files (js->clj files :keywordize-keys true))]
+        (drop 6 (reverse (sort-by :mtime files))))
+      (p/then (fn [old-version-files]
+                (p/all (mapv (fn [file]
+                               (.deleteFile Filesystem (clj->js {:path (:uri file)})))
+                             old-version-files))))
+      (p/catch (fn [_]))))
 
 ;; TODO: move this to FS protocol
 (defn backup-file
@@ -177,7 +178,7 @@
         new-path (path/path-join dir (str (string/replace (.toISOString (js/Date.)) ":" "_") "." (mobile-util/platform) "." ext))]
 
     (<write-file-with-utf8 new-path content)
-    (truncate-old-versioned-files! dir)))
+    (<truncate-old-versioned-files! dir)))
 
 (defn backup-file-handle-changed!
   [repo-dir file-path content]
@@ -197,7 +198,7 @@
         file-path (path/path-join file-root
                                   (str (string/replace (.toISOString (js/Date.)) ":" "_") "." (mobile-util/platform) file-extname))]
     (<write-file-with-utf8 file-path content)
-    (truncate-old-versioned-files! file-root)))
+    (<truncate-old-versioned-files! file-root)))
 
 (defn- write-file-impl!
   [repo dir rpath content {:keys [ok-handler error-handler old-content skip-compare?]} stat]
@@ -253,7 +254,7 @@
   (if (mobile-util/native-ios?)
     (cond
       (or (string/includes? path "///private/")
-          ;; virtual matchine
+          ;; virtual machine
           (string/starts-with? path "file:///Users/"))
       path
 
@@ -303,7 +304,7 @@
               (state/pub-event! [:modal/show-instruction]))
           exists? (<dir-exists? path)
           _ (when-not exists?
-             (p/rejected (str "Cannot access selected directory: " path)))
+              (p/rejected (str "Cannot access selected directory: " path)))
           _ (when (mobile-util/is-iCloud-container-path? path)
               (p/rejected (str "Please avoid accessing the top-level iCloud container path: " path)))
           path (if (mobile-util/native-ios?)

+ 308 - 238
src/main/frontend/fs/sync.cljs

@@ -193,6 +193,11 @@
 
 ;;; ### configs ends
 
+(defn- guard-ex
+  [x]
+  (when (instance? ExceptionInfo x) x))
+
+
 (def ws-addr config/WS-URL)
 
 ;; Warning: make sure to `persist-var/-load` graphs-txid before using it.
@@ -232,17 +237,25 @@
     (when-let [ws (:ws @*ws)]
       (.close ws))))
 
+(def *ws-connect-retries (atom 0))
+(declare <sync-stop)
 (defn- ws-listen!*
   [graph-uuid *ws remote-changes-chan]
   (reset! *ws {:ws (js/WebSocket. (util/format ws-addr graph-uuid)) :stop false})
   (ws-ping-loop (:ws @*ws))
-  ;; (set! (.-onopen (:ws @*ws)) #(println (util/format "ws opened: graph '%s'" graph-uuid %)))
+  (set! (.-onopen (:ws @*ws)) #(reset! *ws-connect-retries 0))
   (set! (.-onclose (:ws @*ws)) (fn [_e]
-                                 (when-not (true? (:stop @*ws))
-                                   (go
-                                     (timeout 1000)
-                                     (println "re-connecting graph" graph-uuid)
-                                     (ws-listen!* graph-uuid *ws remote-changes-chan)))))
+                                 (if (> @*ws-connect-retries 3)
+                                   (do (println "ws-listen! retry count > 3, stop retry")
+                                       (reset! *ws-connect-retries 0)
+                                       (swap! *ws (fn [o] (assoc o :stop true)))
+                                       (<sync-stop))
+                                   (when-not (true? (:stop @*ws))
+                                     (go
+                                       (timeout 1000)
+                                       (println "re-connecting graph" graph-uuid)
+                                       (swap! *ws-connect-retries inc)
+                                       (ws-listen!* graph-uuid *ws remote-changes-chan))))))
   (set! (.-onmessage (:ws @*ws)) (fn [e]
                                    (let [data (js->clj (js/JSON.parse (.-data e)) :keywordize-keys true)]
                                      (when (some? (:txid data))
@@ -264,7 +277,10 @@
 (defn- get-json-body [body]
   (or (and (not (string? body)) body)
       (or (string/blank? body) nil)
-      (js->clj (js/JSON.parse body) :keywordize-keys true)))
+      (try (js->clj (js/JSON.parse body) :keywordize-keys true)
+           (catch :default e
+             (prn :invalid-json body)
+             e))))
 
 (defn- get-resp-json-body [resp]
   (-> resp (:body) (get-json-body)))
@@ -841,9 +857,8 @@
   (<get-local-all-files-meta [this graph-uuid base-path]
     (go
       (let [r (<! (<retry-rsapi #(p->c (ipc/ipc "get-local-all-files-meta" graph-uuid base-path))))]
-        (if (instance? ExceptionInfo r)
-          r
-          (<! (<build-local-file-metadatas this graph-uuid r))))))
+        (or (guard-ex r)
+            (<! (<build-local-file-metadatas this graph-uuid r))))))
   (<get-local-files-meta [this graph-uuid base-path filepaths]
     (go
       (let [r (<! (<retry-rsapi #(p->c (ipc/ipc "get-local-files-meta" graph-uuid base-path filepaths))))]
@@ -857,20 +872,21 @@
     (println "update-local-files" graph-uuid base-path filepaths)
     (go
       (<! (<rsapi-cancel-all-requests))
-      (let [token (<! (<get-token this))]
-        (<! (p->c (ipc/ipc "update-local-files" graph-uuid base-path filepaths token))))))
+      (let [token-or-exp (<! (<get-token this))]
+        (or (guard-ex token-or-exp)
+            (<! (p->c (ipc/ipc "update-local-files" graph-uuid base-path filepaths token-or-exp)))))))
   (<fetch-remote-files [this graph-uuid base-path filepaths]
     (go
       (<! (<rsapi-cancel-all-requests))
-      (let [token (<! (<get-token this))]
-        (<! (p->c (ipc/ipc "fetch-remote-files" graph-uuid base-path filepaths token))))))
+      (let [token-or-exp (<! (<get-token this))]
+        (or (guard-ex token-or-exp)
+            (<! (p->c (ipc/ipc "fetch-remote-files" graph-uuid base-path filepaths token-or-exp)))))))
 
   (<download-version-files [this graph-uuid base-path filepaths]
     (go
-      (let [token (<! (<get-token this))
-            r (<! (<retry-rsapi
-                   #(p->c (ipc/ipc "download-version-files" graph-uuid base-path filepaths token))))]
-        r)))
+      (let [token-or-exp (<! (<get-token this))]
+        (or (guard-ex token-or-exp)
+            (<! (<retry-rsapi #(p->c (ipc/ipc "download-version-files" graph-uuid base-path filepaths token-or-exp))))))))
 
   (<delete-local-files [_ graph-uuid base-path filepaths]
     (let [normalized-filepaths (mapv path-normalize filepaths)]
@@ -883,17 +899,21 @@
     (let [normalized-filepaths (mapv path-normalize filepaths)]
       (go
         (<! (<rsapi-cancel-all-requests))
-        (let [token (<! (<get-token this))]
-          (<! (<retry-rsapi
-               #(p->c (ipc/ipc "update-remote-files" graph-uuid base-path normalized-filepaths local-txid token))))))))
+        (let [token-or-exp (<! (<get-token this))]
+          (or (guard-ex token-or-exp)
+              (<! (<retry-rsapi
+                   #(p->c (ipc/ipc "update-remote-files" graph-uuid base-path normalized-filepaths local-txid token-or-exp)))))))))
 
   (<delete-remote-files [this graph-uuid base-path filepaths local-txid]
     (let [normalized-filepaths (mapv path-normalize filepaths)]
       (go
-        (let [token (<! (<get-token this))]
-          (<!
-           (<retry-rsapi
-            #(p->c (ipc/ipc "delete-remote-files" graph-uuid base-path normalized-filepaths local-txid token))))))))
+        (let [token-or-exp (<! (<get-token this))]
+          (or (guard-ex token-or-exp)
+              (<!
+               (<retry-rsapi
+                #(p->c
+                  (ipc/ipc "delete-remote-files" graph-uuid base-path normalized-filepaths local-txid token-or-exp)))))))))
+
   (<encrypt-fnames [_ graph-uuid fnames] (go (js->clj (<! (p->c (ipc/ipc "encrypt-fnames" graph-uuid fnames))))))
   (<decrypt-fnames [_ graph-uuid fnames] (go
                                            (let [r (<! (p->c (ipc/ipc "decrypt-fnames" graph-uuid fnames)))]
@@ -931,9 +951,8 @@
     (go
       (let [r (<! (p->c (.getLocalAllFilesMeta mobile-util/file-sync (clj->js {:graphUUID graph-uuid
                                                                                :basePath base-path}))))]
-        (if (instance? ExceptionInfo r)
-          r
-          (<! (<build-local-file-metadatas this graph-uuid (.-result r)))))))
+        (or (guard-ex r)
+            (<! (<build-local-file-metadatas this graph-uuid (.-result r)))))))
 
   (<get-local-files-meta [this graph-uuid base-path filepaths]
     (go
@@ -953,32 +972,35 @@
 
   (<update-local-files [this graph-uuid base-path filepaths]
     (go
-      (let [token (<! (<get-token this))
+      (let [token-or-exp (<! (<get-token this))
             filepaths' (map path-normalize filepaths)]
-        (<! (p->c (.updateLocalFiles mobile-util/file-sync (clj->js {:graphUUID graph-uuid
-                                                                     :basePath base-path
-                                                                     :filePaths filepaths'
-                                                                     :token token})))))))
+        (or (guard-ex token-or-exp)
+            (<! (p->c (.updateLocalFiles mobile-util/file-sync (clj->js {:graphUUID graph-uuid
+                                                                         :basePath base-path
+                                                                         :filePaths filepaths'
+                                                                         :token token-or-exp}))))))))
   (<fetch-remote-files [this graph-uuid base-path filepaths]
     (go
-      (let [token (<! (<get-token this))
-            r (<! (<retry-rsapi
+      (let [token-or-exp (<! (<get-token this))]
+        (or (guard-ex token-or-exp)
+            (js->clj
+             (.-value
+              (<! (<retry-rsapi
                    #(p->c (.fetchRemoteFiles mobile-util/file-sync
                                              (clj->js {:graphUUID graph-uuid
                                                        :basePath base-path
                                                        :filePaths filepaths
-                                                       :token token})))))]
-        (js->clj (.-value r)))))
+                                                       :token token-or-exp})))))))))))
   (<download-version-files [this graph-uuid base-path filepaths]
     (go
-      (let [token (<! (<get-token this))
-            r (<! (<retry-rsapi
-                   #(p->c (.updateLocalVersionFiles mobile-util/file-sync
-                                                    (clj->js {:graphUUID graph-uuid
-                                                              :basePath base-path
-                                                              :filePaths filepaths
-                                                              :token token})))))]
-        r)))
+      (let [token-or-exp (<! (<get-token this))]
+        (or (guard-ex token-or-exp)
+            (<! (<retry-rsapi
+                 #(p->c (.updateLocalVersionFiles mobile-util/file-sync
+                                                  (clj->js {:graphUUID graph-uuid
+                                                            :basePath base-path
+                                                            :filePaths filepaths
+                                                            :token token-or-exp})))))))))
 
   (<delete-local-files [_ graph-uuid base-path filepaths]
     (let [normalized-filepaths (mapv path-normalize filepaths)]
@@ -992,40 +1014,39 @@
   (<update-remote-files [this graph-uuid base-path filepaths local-txid]
     (let [normalized-filepaths (mapv path-normalize filepaths)]
       (go
-        (let [token (<! (<get-token this))
-              r (<! (p->c (.updateRemoteFiles mobile-util/file-sync
-                                              (clj->js {:graphUUID graph-uuid
-                                                        :basePath base-path
-                                                        :filePaths normalized-filepaths
-                                                        :txid local-txid
-                                                        :token token
-                                                        :fnameEncryption true}))))]
-          (if (instance? ExceptionInfo r)
-            r
-            (get (js->clj r) "txid"))))))
+        (let [token-or-exp (<! (<get-token this))
+              r (or (guard-ex token-or-exp)
+                    (<! (p->c (.updateRemoteFiles mobile-util/file-sync
+                                                  (clj->js {:graphUUID graph-uuid
+                                                            :basePath base-path
+                                                            :filePaths normalized-filepaths
+                                                            :txid local-txid
+                                                            :token token-or-exp
+                                                            :fnameEncryption true})))))]
+          (or (guard-ex r)
+              (get (js->clj r) "txid"))))))
 
   (<delete-remote-files [this graph-uuid base-path filepaths local-txid]
     (let [normalized-filepaths (mapv path-normalize filepaths)]
       (go
-        (let [token (<! (<get-token this))
-              r (<! (p->c (.deleteRemoteFiles mobile-util/file-sync
-                                              (clj->js {:graphUUID graph-uuid
-                                                        :basePath base-path
-                                                        :filePaths normalized-filepaths
-                                                        :txid local-txid
-                                                        :token token}))))]
-          (if (instance? ExceptionInfo r)
-            r
-            (get (js->clj r) "txid"))))))
+        (let [token-or-exp (<! (<get-token this))
+              r (or (guard-ex token-or-exp)
+                    (<! (p->c (.deleteRemoteFiles mobile-util/file-sync
+                                                  (clj->js {:graphUUID graph-uuid
+                                                            :basePath base-path
+                                                            :filePaths normalized-filepaths
+                                                            :txid local-txid
+                                                            :token token-or-exp})))))]
+          (or (guard-ex r)
+              (get (js->clj r) "txid"))))))
 
   (<encrypt-fnames [_ graph-uuid fnames]
     (go
       (let [r (<! (p->c (.encryptFnames mobile-util/file-sync
                                         (clj->js {:graphUUID graph-uuid
                                                   :filePaths fnames}))))]
-        (if (instance? ExceptionInfo r)
-          (.-cause r)
-          (get (js->clj r) "value")))))
+        (or (guard-ex r)
+            (get (js->clj r) "value")))))
   (<decrypt-fnames [_ graph-uuid fnames]
     (go (let [r (<! (p->c (.decryptFnames mobile-util/file-sync
                                           (clj->js {:graphUUID graph-uuid
@@ -1147,19 +1168,21 @@
 
   (<request [this api-name body]
     (go
-      (let [resp (<! (<request api-name body (<! (<get-token this)) *stopped?))]
-        (if (http/unexceptional-status? (:status resp))
-          (get-resp-json-body resp)
-          (let [exp (ex-info "request failed"
-                             {:err          resp
-                              :body         (:body resp)
-                              :api-name     api-name
-                              :request-body body})]
-            (fire-file-sync-storage-exceed-limit-event! exp)
-            (fire-file-sync-graph-count-exceed-limit-event! exp)
-            exp)))))
-
-  ;; for test
+      (let [token-or-exp (<! (<get-token this))]
+        (or (guard-ex token-or-exp)
+            (let [resp (<! (<request api-name body token-or-exp *stopped?))]
+              (if (http/unexceptional-status? (:status resp))
+                (get-resp-json-body resp)
+                (let [exp (ex-info "request failed"
+                                   {:err          resp
+                                    :body         (:body resp)
+                                    :api-name     api-name
+                                    :request-body body})]
+                  (fire-file-sync-storage-exceed-limit-event! exp)
+                  (fire-file-sync-graph-count-exceed-limit-event! exp)
+                  exp)))))))
+
+;; for test
   (update-files [this graph-uuid txid files]
     {:pre [(map? files)
            (number? txid)]}
@@ -1232,68 +1255,63 @@
                                       {}
                                       (remove (comp nil? second)
                                               {:GraphUUID graph-uuid :ContinuationToken continuation-token}))))]
-                (if (instance? ExceptionInfo r)
-                  r
-                  (let [next-continuation-token (:NextContinuationToken r)
-                        objs                    (:Objects r)]
-                    (apply conj! encrypted-path-list (map (comp remove-user-graph-uuid-prefix :Key) objs))
-                    (apply conj! file-meta-list
-                           (map
-                            #(hash-map :checksum (:checksum %)
-                                       :encrypted-path (remove-user-graph-uuid-prefix (:Key %))
-                                       :size (:Size %)
-                                       :last-modified (:LastModified %)
-                                       :txid (:Txid %))
-                            objs))
-                    (when-not (empty? next-continuation-token)
-                      (recur next-continuation-token)))))))]
-       (if (instance? ExceptionInfo exp-r)
-         exp-r
-         (let [file-meta-list*      (persistent! file-meta-list)
-               encrypted-path-list* (persistent! encrypted-path-list)
-               path-list-or-exp     (<! (<decrypt-fnames rsapi graph-uuid encrypted-path-list*))]
-           (if (instance? ExceptionInfo path-list-or-exp)
-             path-list-or-exp
-             (let [encrypted-path->path-map (zipmap encrypted-path-list* path-list-or-exp)]
-               (set
-                (mapv
-                 #(->FileMetadata (:size %)
-                                  (:checksum %)
-                                  (get encrypted-path->path-map (:encrypted-path %))
-                                  (:encrypted-path %)
-                                  (:last-modified %)
-                                  true
-                                  (:txid %)
-                                  nil)
-                 (-> file-meta-list*
-                     (filter-files-with-unnormalized-path encrypted-path->path-map)
-                     (filter-case-different-same-files encrypted-path->path-map)))))))))))
+                (or (guard-ex r)
+                    (let [next-continuation-token (:NextContinuationToken r)
+                          objs                    (:Objects r)]
+                      (apply conj! encrypted-path-list (map (comp remove-user-graph-uuid-prefix :Key) objs))
+                      (apply conj! file-meta-list
+                             (map
+                              #(hash-map :checksum (:checksum %)
+                                         :encrypted-path (remove-user-graph-uuid-prefix (:Key %))
+                                         :size (:Size %)
+                                         :last-modified (:LastModified %)
+                                         :txid (:Txid %))
+                              objs))
+                      (when-not (empty? next-continuation-token)
+                        (recur next-continuation-token)))))))]
+       (or (guard-ex exp-r)
+           (let [file-meta-list*      (persistent! file-meta-list)
+                 encrypted-path-list* (persistent! encrypted-path-list)
+                 path-list-or-exp     (<! (<decrypt-fnames rsapi graph-uuid encrypted-path-list*))]
+             (or (guard-ex path-list-or-exp)
+                 (let [encrypted-path->path-map (zipmap encrypted-path-list* path-list-or-exp)]
+                   (set
+                    (mapv
+                     #(->FileMetadata (:size %)
+                                      (:checksum %)
+                                      (get encrypted-path->path-map (:encrypted-path %))
+                                      (:encrypted-path %)
+                                      (:last-modified %)
+                                      true
+                                      (:txid %)
+                                      nil)
+                     (-> file-meta-list*
+                         (filter-files-with-unnormalized-path encrypted-path->path-map)
+                         (filter-case-different-same-files encrypted-path->path-map)))))))))))
 
   (<get-remote-files-meta [this graph-uuid filepaths]
     {:pre [(coll? filepaths)]}
     (user/<wrap-ensure-id&access-token
      (let [encrypted-paths* (<! (<encrypt-fnames rsapi graph-uuid filepaths))
            r                (<! (.<request this "get_files_meta" {:GraphUUID graph-uuid :Files encrypted-paths*}))]
-       (if (instance? ExceptionInfo r)
-         r
-         (let [encrypted-paths (mapv :FilePath r)
-               paths-or-exp    (<! (<decrypt-fnames rsapi graph-uuid encrypted-paths))]
-           (if (instance? ExceptionInfo paths-or-exp)
-             paths-or-exp
-             (let [encrypted-path->path-map (zipmap encrypted-paths paths-or-exp)]
-               (into #{}
-                     (comp
-                      (filter #(not= "filepath too long" (:Error %)))
-                      (map #(->FileMetadata (:Size %)
-                                            (:Checksum %)
-                                            (some->> (get encrypted-path->path-map (:FilePath %))
-                                                     path-normalize)
-                                            (:FilePath %)
-                                            (:LastModified %)
-                                            true
-                                            (:Txid %)
-                                            nil)))
-                     r))))))))
+       (or (guard-ex r)
+           (let [encrypted-paths (mapv :FilePath r)
+                 paths-or-exp    (<! (<decrypt-fnames rsapi graph-uuid encrypted-paths))]
+             (or (guard-ex paths-or-exp)
+                 (let [encrypted-path->path-map (zipmap encrypted-paths paths-or-exp)]
+                   (into #{}
+                         (comp
+                          (filter #(not= "filepath too long" (:Error %)))
+                          (map #(->FileMetadata (:Size %)
+                                                (:Checksum %)
+                                                (some->> (get encrypted-path->path-map (:FilePath %))
+                                                         path-normalize)
+                                                (:FilePath %)
+                                                (:LastModified %)
+                                                true
+                                                (:Txid %)
+                                                nil)))
+                         r))))))))
 
   (<get-remote-graph [this graph-name-opt graph-uuid-opt]
     {:pre [(or graph-name-opt graph-uuid-opt)]}
@@ -1320,23 +1338,22 @@
   (<get-deletion-logs [this graph-uuid from-txid]
     (user/<wrap-ensure-id&access-token
      (let [r (<! (.<request this "get_deletion_log_v20221212" {:GraphUUID graph-uuid :FromTXId from-txid}))]
-       (if (instance? ExceptionInfo r)
-         r
-         (let [txns-with-encrypted-paths (mapv (fn [txn]
-                                                 (assoc txn :paths
-                                                        (mapv remove-user-graph-uuid-prefix (:paths txn))))
-                                               (:Transactions r))
-               encrypted-paths           (mapcat :paths txns-with-encrypted-paths)
-               encrypted-path->path-map
-               (zipmap
-                encrypted-paths
-                (<! (<decrypt-fnames rsapi graph-uuid encrypted-paths)))
-               txns
-               (mapv
-                (fn [txn]
-                  (assoc txn :paths (mapv #(get encrypted-path->path-map %) (:paths txn))))
-                txns-with-encrypted-paths)]
-           txns)))))
+       (or (guard-ex r)
+           (let [txns-with-encrypted-paths (mapv (fn [txn]
+                                                   (assoc txn :paths
+                                                          (mapv remove-user-graph-uuid-prefix (:paths txn))))
+                                                 (:Transactions r))
+                 encrypted-paths           (mapcat :paths txns-with-encrypted-paths)
+                 encrypted-path->path-map
+                 (zipmap
+                  encrypted-paths
+                  (<! (<decrypt-fnames rsapi graph-uuid encrypted-paths)))
+                 txns
+                 (mapv
+                  (fn [txn]
+                    (assoc txn :paths (mapv #(get encrypted-path->path-map %) (:paths txn))))
+                  txns-with-encrypted-paths)]
+             txns)))))
 
   (<get-diff [this graph-uuid from-txid]
     ;; TODO: path in transactions should be relative path(now s3 key, which includes graph-uuid and user-uuid)
@@ -1418,9 +1435,11 @@
      (let [partitioned-files (partition-all 20 (<! (<encrypt-fnames rsapi graph-uuid filepaths)))]
        (loop [[files & others] partitioned-files]
          (when files
-           (let [current-txid (:TXId (<! (<get-remote-txid this graph-uuid)))]
-             (<! (.<request this "delete_files" {:GraphUUID graph-uuid :TXId current-txid :Files files}))
-             (recur others))))))))
+           (let [r (<! (<get-remote-txid this graph-uuid))]
+             (or (guard-ex r)
+                 (let [current-txid (:TXId r)]
+                   (<! (.<request this "delete_files" {:GraphUUID graph-uuid :TXId current-txid :Files files}))
+                   (recur others))))))))))
 
 (comment
   (declare remoteapi)
@@ -1445,8 +1464,9 @@
       (if (< now expired-at)
         r
         (let [r (<! (<get-graph-salt remoteapi graph-uuid))]
-          (swap! *get-graph-salt-memoize-cache conj [graph-uuid r])
-          r)))))
+          (or (guard-ex r)
+              (do (swap! *get-graph-salt-memoize-cache conj [graph-uuid r])
+                  r)))))))
 
 (def ^:private *get-graph-encrypt-keys-memoize-cache (atom {}))
 (defn update-graph-encrypt-keys-cache [graph-uuid v]
@@ -1512,9 +1532,11 @@
 (defn- assert-local-txid<=remote-txid
   []
   (when-let [local-txid (last @graphs-txid)]
-    (go (let [remote-txid (:TXId (<! (<get-remote-txid remoteapi (second @graphs-txid))))]
-          (assert (<= local-txid remote-txid)
-                  [@graphs-txid local-txid remote-txid])))))
+    (go (let [r (<! (<get-remote-txid remoteapi (second @graphs-txid)))]
+          (when-not (guard-ex r)
+            (let [remote-txid (:TXId r)]
+              (assert (<= local-txid remote-txid)
+                      [@graphs-txid local-txid remote-txid])))))))
 
 (defn- get-local-files-checksum
   [graph-uuid base-path relative-paths]
@@ -2034,17 +2056,17 @@
   "- persist encrypted pwd at local-storage"
   [pwd graph-uuid]
   (go
-    (let [[value expired-at gone?]
-          ((juxt :value :expired-at #(-> % ex-data :err :status (= 410)))
-           (<! (<get-graph-salt-memoize remoteapi graph-uuid)))
-          [salt-value _expired-at]
-          (if gone?
-            (let [r (<! (<create-graph-salt remoteapi graph-uuid))]
-              (update-graph-salt-cache graph-uuid r)
-              ((juxt :value :expired-at) r))
-            [value expired-at])
-          encrypted-pwd (<! (<encrypt-content pwd salt-value))]
-      (persist-pwd! encrypted-pwd graph-uuid))))
+    (let [[value _expired-at gone?] ((juxt :value :expired-at #(-> % ex-data :err :status (= 410)))
+                                     (<! (<get-graph-salt-memoize remoteapi graph-uuid)))]
+      (if gone?
+        (let [r (<! (<create-graph-salt remoteapi graph-uuid))]
+          (or (guard-ex r)
+              (do (update-graph-salt-cache graph-uuid r)
+                  (let [[salt-value _expired-at] ((juxt :value :expired-at) r)
+                        encrypted-pwd (<! (<encrypt-content pwd salt-value))]
+                    (persist-pwd! encrypted-pwd graph-uuid)))))
+        (let [encrypted-pwd (<! (<encrypt-content pwd value))]
+          (persist-pwd! encrypted-pwd graph-uuid))))))
 
 (defn restore-pwd!
   "restore pwd from persisted encrypted-pwd, update `pwd-map`"
@@ -2386,8 +2408,8 @@
   if local-txid != remote-txid, return {:need-sync-remote true}"))
 
 (defrecord ^:large-vars/cleanup-todo
-  Remote->LocalSyncer [user-uuid graph-uuid base-path repo *txid *txid-for-get-deletion-log *sync-state remoteapi
-                       ^:mutable local->remote-syncer *stopped *paused]
+ Remote->LocalSyncer [user-uuid graph-uuid base-path repo *txid *txid-for-get-deletion-log *sync-state remoteapi
+                      ^:mutable local->remote-syncer *stopped *paused]
   Object
   (set-local->remote-syncer! [_ s] (set! local->remote-syncer s))
   (sync-files-remote->local!
@@ -2426,34 +2448,33 @@
     (go
       (let [r
             (let [diff-r (<! (<get-diff remoteapi graph-uuid @*txid))]
-              (if (instance? ExceptionInfo diff-r)
-                diff-r
-                (let [[diff-txns latest-txid min-txid] diff-r]
-                  (if (> (dec min-txid) @*txid) ;; min-txid-1 > @*txid, need to remote->local-full-sync
-                    (do (println "min-txid" min-txid "request-txid" @*txid)
-                        {:need-remote->local-full-sync true})
-
-                    (when (pos-int? latest-txid)
-                      (let [filtered-diff-txns (-> (transduce (diffs->filetxns) conj '() (reverse diff-txns))
-                                                   filter-download-files-with-reserved-chars)
-                            partitioned-filetxns (transduce (partition-filetxns download-batch-size)
-                                                            (completing (fn [r i] (conj r (reverse i)))) ;reverse
-                                                            '()
-                                                            filtered-diff-txns)]
-                        (put-sync-event! {:event :start
-                                          :data  {:type       :remote->local
-                                                  :graph-uuid graph-uuid
-                                                  :full-sync? false
-                                                  :epoch      (tc/to-epoch (t/now))}})
-                        (if (empty? (flatten partitioned-filetxns))
-                          (do
-                            (swap! *sync-state #(sync-state-reset-full-remote->local-files % []))
-                            (<! (<update-graphs-txid! latest-txid graph-uuid user-uuid repo))
-                            (reset! *txid latest-txid)
-                            {:succ true})
-                          (<! (apply-filetxns-partitions
-                               *sync-state user-uuid graph-uuid base-path
-                               partitioned-filetxns repo *txid *stopped *paused false)))))))))]
+              (or (guard-ex diff-r)
+                  (let [[diff-txns latest-txid min-txid] diff-r]
+                    (if (> (dec min-txid) @*txid) ;; min-txid-1 > @*txid, need to remote->local-full-sync
+                      (do (println "min-txid" min-txid "request-txid" @*txid)
+                          {:need-remote->local-full-sync true})
+
+                      (when (pos-int? latest-txid)
+                        (let [filtered-diff-txns (-> (transduce (diffs->filetxns) conj '() (reverse diff-txns))
+                                                     filter-download-files-with-reserved-chars)
+                              partitioned-filetxns (transduce (partition-filetxns download-batch-size)
+                                                              (completing (fn [r i] (conj r (reverse i)))) ;reverse
+                                                              '()
+                                                              filtered-diff-txns)]
+                          (put-sync-event! {:event :start
+                                            :data  {:type       :remote->local
+                                                    :graph-uuid graph-uuid
+                                                    :full-sync? false
+                                                    :epoch      (tc/to-epoch (t/now))}})
+                          (if (empty? (flatten partitioned-filetxns))
+                            (do
+                              (swap! *sync-state #(sync-state-reset-full-remote->local-files % []))
+                              (<! (<update-graphs-txid! latest-txid graph-uuid user-uuid repo))
+                              (reset! *txid latest-txid)
+                              {:succ true})
+                            (<! (apply-filetxns-partitions
+                                 *sync-state user-uuid graph-uuid base-path
+                                 partitioned-filetxns repo *txid *stopped *paused false)))))))))]
         (cond
           (instance? ExceptionInfo r)       {:unknown r}
           @*stopped                         {:stop true}
@@ -2468,7 +2489,8 @@
             remote-all-files-meta-or-exp (<! remote-all-files-meta-c)]
         (if (or (storage-exceed-limit? remote-all-files-meta-or-exp)
                 (sync-stop-when-api-flying? remote-all-files-meta-or-exp)
-                (decrypt-exp? remote-all-files-meta-or-exp))
+                (decrypt-exp? remote-all-files-meta-or-exp)
+                (instance? ExceptionInfo remote-all-files-meta-or-exp))
           (do (put-sync-event! {:event :exception-decrypt-failed
                                 :data  {:graph-uuid graph-uuid
                                         :exp        remote-all-files-meta-or-exp
@@ -2478,11 +2500,11 @@
                 local-all-files-meta    (<! local-all-files-meta-c)
                 {diff-remote-files :result elapsed-time :time}
                 (util/with-time (diff-file-metadata-sets remote-all-files-meta local-all-files-meta))
-                 _ (println ::diff-file-metadata-sets-elapsed-time elapsed-time "ms")
+                _ (println ::diff-file-metadata-sets-elapsed-time elapsed-time "ms")
                 recent-10-days-range    ((juxt #(tc/to-long (t/minus % (t/days 10))) #(tc/to-long %)) (t/today))
                 sorted-diff-remote-files
-                                        (sort-by
-                                         (sort-file-metadata-fn :recent-days-range recent-10-days-range) > diff-remote-files)
+                (sort-by
+                 (sort-file-metadata-fn :recent-days-range recent-10-days-range) > diff-remote-files)
                 remote-txid-or-ex       (<! (<get-remote-txid remoteapi graph-uuid))
                 latest-txid             (:TXId remote-txid-or-ex)]
             (if (or (instance? ExceptionInfo remote-txid-or-ex) (nil? latest-txid))
@@ -2769,7 +2791,8 @@
         (cond
           (or (storage-exceed-limit? remote-all-files-meta-or-exp)
               (sync-stop-when-api-flying? remote-all-files-meta-or-exp)
-              (decrypt-exp? remote-all-files-meta-or-exp))
+              (decrypt-exp? remote-all-files-meta-or-exp)
+              (instance? ExceptionInfo remote-all-files-meta-or-exp))
           (do (put-sync-event! {:event :get-remote-all-files-failed
                                 :data  {:graph-uuid graph-uuid
                                         :exp        remote-all-files-meta-or-exp
@@ -2931,7 +2954,8 @@
           remote->local
           (let [txid
                 (if (true? remote->local)
-                  {:txid (:TXId (<! (<get-remote-txid remoteapi graph-uuid)))}
+                  (let [r (<! (<get-remote-txid remoteapi graph-uuid))]
+                    (when-not (guard-ex r) {:txid (:TXId r)}))
                   remote->local)]
             (when (some? txid)
               (>! ops-chan {:remote->local txid}))
@@ -3257,8 +3281,6 @@
     (reset! current-sm-graph-uuid graph-uuid)
     (sync-manager user-uuid graph-uuid base-path repo txid *sync-state)))
 
-;; Avoid sync reentrancy
-(defonce *sync-entered? (atom false))
 
 (defn <sync-stop []
   (go
@@ -3269,8 +3291,6 @@
 
       (<! (-stop! sm))
 
-      (reset! *sync-entered? false)
-
       (println "[SyncManager]" "stopped"))
 
     (reset! current-sm-graph-uuid nil)))
@@ -3332,30 +3352,72 @@
   (when-let [graph-uuid (second @graphs-txid)]
     (get-pwd graph-uuid)))
 
+
+(defn- <connectivity-testing
+  []
+  (go
+    (let [api-url (str "https://" config/API-DOMAIN "/logseq/version")
+          r1 (http/get api-url {:with-credentials? false})
+          r2 (http/get config/CONNECTIVITY-TESTING-S3-URL {:with-credentials? false})
+          r1* (<! r1)
+          r2* (<! r2)
+          ok? (and (= 200 (:status r1*))
+                   (= 200 (:status r2*))
+                   (= "OK" (:body r2*)))]
+      (if ok?
+        (notification/clear! :sync-connection-failed)
+        (notification/show! [:div
+                             (t :file-sync/connectivity-testing-failed)
+                             [:a {:href api-url} api-url]
+                             " and "
+                             [:a
+                              {:href config/CONNECTIVITY-TESTING-S3-URL}
+                              config/CONNECTIVITY-TESTING-S3-URL]]
+                            :warning
+                            false
+                            :sync-connection-failed))
+      ok?)))
+
 (declare network-online-cursor)
 
+(def ^:private *sync-starting
+  "Avoid running multiple sync instances simultaneously."
+  (atom false))
+
+(defn- <should-start-sync?
+  []
+  (go
+    (and (state/enable-sync?)
+         @network-online-cursor     ;; is online
+         (user/has-refresh-token?)  ;; has refresh token, should bring up sync
+         (or (= ::stop (:state (state/get-file-sync-state))) ;; state=stopped
+             (nil? (state/get-file-sync-state)))  ;; the whole sync state not inited yet, happens when app starts without network
+         (<! (p->c (persist-var/-load graphs-txid))))))  ;; not a sync graph))
+
 (defn <sync-start
   []
-  (when-not (false? (state/enable-sync?))
-    (go
-      (when (false? @*sync-entered?)
-        (reset! *sync-entered? true)
-        (let [*sync-state                 (atom (sync-state))
-              current-user-uuid           (<! (user/<user-uuid))
+  (go
+    (when-not @*sync-starting
+      (reset! *sync-starting true)
+      (if-not (and (<! (<should-start-sync?))
+                   (<! (<connectivity-testing)))
+        (reset! *sync-starting false)
+        (try
+          (let [*sync-state                 (atom (sync-state))
+                current-user-uuid           (<! (user/<user-uuid))
               ;; put @graph-uuid & get-current-repo together,
               ;; prevent to get older repo dir and current graph-uuid.
-              _                           (<! (p->c (persist-var/-load graphs-txid)))
-              [user-uuid graph-uuid txid] @graphs-txid
-              txid                        (or txid 0)
-              repo                        (state/get-current-repo)]
-          (when-not (instance? ExceptionInfo current-user-uuid)
-            (when (and repo
-                       @network-online-cursor
-                       user-uuid graph-uuid txid
-                       (graph-sync-off? graph-uuid)
-                       (user/logged-in?)
-                       (not (config/demo-graph? repo)))
-              (try
+                _                           (<! (p->c (persist-var/-load graphs-txid)))
+                [user-uuid graph-uuid txid] @graphs-txid
+                txid                        (or txid 0)
+                repo                        (state/get-current-repo)]
+            (when-not (instance? ExceptionInfo current-user-uuid)
+              (when (and repo
+                         @network-online-cursor
+                         user-uuid graph-uuid txid
+                         (graph-sync-off? graph-uuid)
+                         (user/logged-in?)
+                         (not (config/demo-graph? repo)))
                 (when-let [sm (sync-manager-singleton current-user-uuid graph-uuid
                                                       (config/get-repo-dir repo) repo
                                                       txid *sync-state)]
@@ -3366,7 +3428,7 @@
                         (state/set-file-sync-state graph-uuid @*sync-state)
                         (state/set-file-sync-manager graph-uuid sm)
 
-                        ;; update global state when *sync-state changes
+                      ;; update global state when *sync-state changes
                         (add-watch *sync-state ::update-global-state
                                    (fn [_ _ _ n]
                                      (state/set-file-sync-state graph-uuid n)))
@@ -3376,11 +3438,12 @@
                         (.start sm)
 
                         (offer! remote->local-full-sync-chan true)
-                        (offer! full-sync-chan true)))))
-                (catch :default e
-                  (prn "Sync start error: ")
-                  (log/error :exception e)))))
-          (reset! *sync-entered? false))))))
+                        (offer! full-sync-chan true))))))))
+          (catch :default e
+            (prn "Sync start error: ")
+            (log/error :exception e))
+          (finally
+            (reset! *sync-starting false)))))))
 
 (defn- restart-if-stopped!
   [is-active?]
@@ -3450,6 +3513,13 @@
              (when (nil? n)
                (<sync-stop))))
 
+;; try to re-start sync when state=stopped every 1min
+(go-loop []
+  (<! (timeout 60000))
+  (when (<! (<should-start-sync?))
+    (println "trying to restart sync..." (tc/to-string (t/now)))
+    (<sync-start))
+  (recur))
 
 
 ;;; ### some sync events handler

+ 106 - 24
src/main/frontend/fs/watcher_handler.cljs

@@ -3,12 +3,16 @@
   (:require [clojure.set :as set]
             [clojure.string :as string]
             [frontend.config :as config]
+            [frontend.date :as date]
             [frontend.db :as db]
             [frontend.db.model :as model]
             [frontend.fs :as fs]
             [logseq.common.path :as path]
             [frontend.handler.property :as property-handler]
+            [frontend.handler.editor :as editor-handler]
             [frontend.handler.file :as file-handler]
+            [frontend.handler.global-config :as global-config-handler]
+            [frontend.handler.notification :as notification]
             [frontend.handler.page :as page-handler]
             [frontend.handler.ui :as ui-handler]
             [frontend.state :as state]
@@ -17,8 +21,7 @@
             [lambdaisland.glogi :as log]
             [logseq.graph-parser.config :as gp-config]
             [logseq.graph-parser.util.block-ref :as block-ref]
-            [promesa.core :as p]
-            [frontend.handler.global-config :as global-config-handler]))
+            [promesa.core :as p]))
 
 ;; all IPC paths must be normalized! (via gp-util/path-normalize)
 
@@ -129,22 +132,83 @@
       ;; return nil, otherwise the entire db will be transferred by ipc
       nil)))
 
+(defn preload-graph-homepage-files!
+  "Preload the homepage file for the current graph. Return loaded file paths.
+
+   Prerequisites:
+   - current graph is set
+   - config is loaded"
+  []
+  (when-let [repo (state/get-current-repo)]
+    (when (and (not (state/loading-files? repo))
+               (config/local-file-based-graph? repo))
+      (let [repo-dir (config/get-repo-dir repo)
+            page-name (if (state/enable-journals? repo)
+                        (date/today)
+                        (or (:page (state/get-default-home)) "Contents"))
+            page-name (util/page-name-sanity-lc page-name)
+            file-rpath (or (:file/path (db/get-page-file page-name))
+                           (let [format (state/get-preferred-format repo)
+                                 ext (config/get-file-extension format)
+                                 file-name (if (state/enable-journals? repo)
+                                             (date/journal-title->default (date/today))
+                                             (or (:page (state/get-default-home)) "contents"))
+                                 parent-dir (if (state/enable-journals? repo)
+                                              (config/get-journals-directory)
+                                              (config/get-pages-directory))]
+                             (str parent-dir "/" file-name "." ext)))]
+        (prn ::preload-homepage file-rpath)
+        (p/let [file-exists? (fs/file-exists? repo-dir file-rpath)
+                _ (when file-exists?
+                    ;; BUG: avoid active-editing block content overwrites incoming fs changes
+                    (editor-handler/escape-editing false))
+                file-content (when file-exists?
+                               (fs/read-file repo-dir file-rpath))
+                file-mtime (when file-exists?
+                             (:mtime (fs/stat repo-dir file-rpath)))
+                db-empty? (db/page-empty? repo page-name)
+                db-content (if-not db-empty?
+                             (db/get-file repo file-rpath)
+                             "")]
+          (p/do!
+           (cond
+             (and file-exists?
+                  db-empty?)
+             (handle-add-and-change! repo file-rpath file-content db-content file-mtime false)
+
+             (and file-exists?
+                  (not db-empty?)
+                  (not= file-content db-content))
+             (handle-add-and-change! repo file-rpath file-content db-content file-mtime true))
+
+           (ui-handler/re-render-root!)
+
+           [file-rpath]))))))
+
 (defn load-graph-files!
-  [graph]
+  "This fn replaces the former initial fs watcher"
+  [graph exclude-files]
   (when graph
     (let [repo-dir (config/get-repo-dir graph)
           db-files (->> (db/get-files graph)
-                        (map first))]
+                        (map first))
+          exclude-files (set (or exclude-files []))]
       ;; read all files in the repo dir, notify if readdir error
       (p/let [[files deleted-files]
               (-> (fs/readdir repo-dir :path-only? true)
                   (p/chain (fn [files]
                              (->> files
                                   (map #(path/relative-path repo-dir %))
-                                  (remove #(fs-util/ignored-path? repo-dir %))))
+                                  (remove #(fs-util/ignored-path? repo-dir %))
+                                  (sort-by (fn [f] [(not (string/starts-with? f "logseq/"))
+                                                    (not (string/starts-with? f "journals/"))
+                                                    (not (string/starts-with? f "pages/"))
+                                                    (string/lower-case f)]))))
                            (fn [files]
                              (let [deleted-files (set/difference (set db-files) (set files))]
-                               [files deleted-files])))
+                               [(->> files
+                                     (remove #(contains? exclude-files %)))
+                                deleted-files])))
                   (p/catch (fn [error]
                              (when-not (config/demo-graph? graph)
                                (js/console.error "reading" graph)
@@ -152,26 +216,44 @@
                                                   {:content (str "The graph " graph " can not be read:" error)
                                                    :status :error
                                                    :clear? false}]))
-                             [nil nil])))]
-        (prn ::init-watcher repo-dir {:deleted (count deleted-files)
-                                      :total (count files)})
+                             [nil nil])))
+              ;; notifies user when large initial change set is detected
+              ;; NOTE: this is an estimation, not accurate
+              notification-uid (when (or (> (abs (- (count db-files) (count files)))
+                                            100)
+                                         (> (count deleted-files)
+                                            100))
+                                 (prn ::init-watcher-large-change-set)
+                                 (notification/show! "Loading changes from disk..."
+                                                     :info
+                                                     false))]
+        (prn ::initial-watcher repo-dir {:deleted (count deleted-files)
+                                         :total (count files)})
         (when (seq deleted-files)
           (let [delete-tx-data (->> (db/delete-files deleted-files)
                                     (concat (db/delete-blocks graph deleted-files nil))
                                     (remove nil?))]
             (db/transact! graph delete-tx-data {:delete-files? true})))
-        (doseq [file-rpath files]
-          (when-let [_ext (util/get-file-ext file-rpath)]
-            (->
-             (p/let [content (fs/read-file repo-dir file-rpath)
-                     stat (fs/stat repo-dir file-rpath)
-                     type (if (db/file-exists? graph file-rpath)
-                            "change"
-                            "add")]
-               (handle-changed! type
-                                {:dir repo-dir
-                                 :path file-rpath
-                                 :content content
-                                 :stat stat}))
-             (p/catch (fn [error]
-                        (js/console.dir error))))))))))
+        (-> (p/delay 500) ;; workaround for notification ui not showing
+            (p/then #(p/all (map (fn [file-rpath]
+                                   (p/let [stat (fs/stat repo-dir file-rpath)
+                                           content (fs/read-file repo-dir file-rpath)
+                                           type (if (db/file-exists? graph file-rpath)
+                                                  "change"
+                                                  "add")]
+                                     (handle-changed! type
+                                                      {:dir repo-dir
+                                                       :path file-rpath
+                                                       :content content
+                                                       :stat stat})))
+                                 files)))
+            (p/then (fn []
+                      (when notification-uid
+                        (prn ::init-notify)
+                        (notification/clear! notification-uid)
+                        (state/pub-event! [:notification/show {:content (str "The graph " graph " is loaded.")
+                                                               :status :success
+                                                               :clear? true}]))))
+            (p/catch (fn [error]
+                       (js/console.dir error))))))))
+

+ 3 - 2
src/main/frontend/handler/command_palette.cljs

@@ -2,6 +2,7 @@
   "System-component-like ns for command palette's functionality"
   (:require [cljs.spec.alpha :as s]
             [frontend.modules.shortcut.data-helper :as shortcut-helper]
+            [frontend.handler.plugin :as plugin-handler]
             [frontend.spec :as spec]
             [frontend.state :as state]
             [lambdaisland.glogi :as log]
@@ -50,10 +51,10 @@
 (defn add-history [{:keys [id]}]
   (storage/set "commands-history" (conj (history) {:id id :timestamp (.getTime (js/Date.))})))
 
-(defn invoke-command [{:keys [action] :as cmd}]
+(defn invoke-command [{:keys [id action] :as cmd}]
   (add-history cmd)
   (state/close-modal!)
-  (action))
+  (plugin-handler/hook-lifecycle-fn! id action))
 
 (defn top-commands [limit]
   (->> (get-commands)

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

@@ -18,6 +18,9 @@
       (repo-config-handler/read-repo-config content)
       (let [result (parse-repo-config content)
             ks (if (vector? k) k [k])
+            v (cond->> v
+                       (map? v)
+                       (reduce-kv (fn [a k v] (rewrite/assoc a k v)) (rewrite/parse-string "{}")))
             new-result (rewrite/assoc-in result ks v)
             new-content (str new-result)]
         (file-handler/set-file-content! repo path new-content) nil))))

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

@@ -16,6 +16,7 @@
             [frontend.fs :as fs]
             [frontend.fs.nfs :as nfs]
             [logseq.common.path :as path]
+            [frontend.extensions.pdf.utils :as pdf-utils]
             [frontend.handler.assets :as assets-handler]
             [frontend.handler.block :as block-handler]
             [frontend.handler.common :as common-handler]
@@ -203,7 +204,7 @@
 (defn open-block-in-sidebar!
   [block-id]
   (when block-id
-    (when-let [block (db/entity [:block/uuid block-id])]
+    (when-let [block (db/entity (if (number? block-id) block-id [:block/uuid block-id]))]
       (let [page? (nil? (:block/page block))]
         (state/sidebar-add-block!
          (state/get-current-repo)
@@ -252,7 +253,9 @@
                :block/content value}]
     (profile
      "Save block: "
-     (let [original-uuid (:block/uuid (db/entity (:db/id block)))
+     (let [original-block (db/entity (:db/id block))
+           original-uuid (:block/uuid original-block)
+           original-props (:block/properties original-block)
            uuid-changed? (not= (:block/uuid block) original-uuid)
            block' (-> (wrap-parse-block block)
                       ;; :block/uuid might be changed when backspace/delete
@@ -265,7 +268,12 @@
 
        (outliner-tx/transact!
         opts'
-        (outliner-core/save-block! block'))
+        (outliner-core/save-block! block')
+        ;; page properties changed
+        (when-let [page-name (and (:block/pre-block? block')
+                                  (not= original-props (:block/properties block'))
+                                  (some-> (:block/page block') :db/id (db-utils/pull) :block/name))]
+          (state/set-page-properties-changed! page-name)))
 
        ;; sanitized page name changed
        (when-let [title (get-in block' [:block/properties :title])]
@@ -3122,7 +3130,8 @@
   "shortcut copy action:
   * when in selection mode, copy selected blocks
   * when in edit mode but no text selected, copy current block ref
-  * when in edit mode with text selected, copy selected text as normal"
+  * when in edit mode with text selected, copy selected text as normal
+  * when text is selected on a PDF, copy the highlighted text"
   [e]
   (when-not (auto-complete?)
     (cond
@@ -3135,7 +3144,13 @@
             selected-end (util/get-selection-end input)]
         (save-current-block!)
         (when (= selected-start selected-end)
-          (copy-current-block-ref "ref"))))))
+          (copy-current-block-ref "ref")))
+
+      (and (state/get-current-pdf)
+           (.closest (.. js/window getSelection -baseNode -parentElement)  ".pdfViewer"))
+      (util/copy-to-clipboard!
+       (pdf-utils/fix-selection-text-breakline (.. js/window getSelection toString))
+       nil))))
 
 (defn shortcut-copy-text
   "shortcut copy action:

+ 32 - 36
src/main/frontend/handler/events.cljs

@@ -10,7 +10,6 @@
             [clojure.core.async.interop :refer [p->c]]
             [clojure.set :as set]
             [clojure.string :as string]
-            [datascript.core :as d]
             [frontend.commands :as commands]
             [frontend.components.command-palette :as command-palette]
             [frontend.components.conversion :as conversion-component]
@@ -23,7 +22,6 @@
             [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]]
@@ -151,7 +149,7 @@
   (when (= (:url repo) current-repo)
     (file-sync-restart!)))
 
-;; FIXME: awful multi-arty function.
+;; FIXME(andelf): awful multi-arty function.
 ;; Should use a `-impl` function instead of the awful `skip-ios-check?` param with nested callback.
 (defn- graph-switch
   ([graph]
@@ -403,11 +401,15 @@
       (when (and (not dir-exists?)
                  (not util/nfs?))
         (state/pub-event! [:graph/dir-gone dir]))))
-  ;; FIXME: an ugly implementation for redirecting to page on new window is restored
-  (repo-handler/graph-ready! repo)
-  ;; Replace initial fs watcher
-  (when-not (config/db-based-graph? repo)
-    (fs-watcher/load-graph-files! repo))
+  (p/let [loaded-homepage-files (when-not (config/db-based-graph? repo)
+                                  (fs-watcher/preload-graph-homepage-files!))
+          ;; re-render-root is async and delegated to rum, so we need to wait for main ui to refresh
+          _ (js/setTimeout #(mobile/mobile-postinit) 1000)
+          ;; FIXME: an ugly implementation for redirecting to page on new window is restored
+          _ (repo-handler/graph-ready! repo)
+          _ (when-not (config/db-based-graph? repo)
+              (fs-watcher/load-graph-files! repo loaded-homepage-files))]
+
   ;; TODO(junyi): Notify user to update filename format when the UX is smooth enough
   ;; (when-not config/test?
   ;;   (js/setTimeout
@@ -419,7 +421,7 @@
   ;;                   (not= filename-format :triple-lowbar))
   ;;          (state/pub-event! [:ui/notify-outdated-filename-format []]))))
   ;;    3000))
-  )
+    ))
 
 (defmethod handle :notification/show [[_ {:keys [content status clear?]}]]
   (notification/show! content status clear?))
@@ -478,8 +480,8 @@
   (commands/exec-plugin-simple-command! pid cmd action))
 
 (defmethod handle :shortcut-handler-refreshed [[_]]
-  (when-not @st/*inited?
-    (reset! st/*inited? true)
+  (when-not @st/*pending-inited?
+    (reset! st/*pending-inited? true)
     (st/consume-pending-shortcuts!)))
 
 (defmethod handle :mobile/keyboard-will-show [[_ keyboard-height]]
@@ -530,22 +532,7 @@
       (when-let [toolbar (.querySelector main-node "#mobile-editor-toolbar")]
         (set! (.. toolbar -style -bottom) 0)))))
 
-(defn update-file-path [deprecated-repo current-repo deprecated-app-id current-app-id]
-  (let [files (db-model/get-files-entity deprecated-repo)
-        conn (conn/get-db deprecated-repo false)
-        tx (mapv (fn [[id path]]
-                   (let [new-path (string/replace path deprecated-app-id current-app-id)]
-                     {:db/id id
-                      :file/path new-path}))
-                 files)]
-    (d/transact! conn tx)
-    (reset! conn/conns
-            (update-keys @conn/conns
-                         (fn [key] (if (string/includes? key deprecated-repo)
-                                     (string/replace key deprecated-repo current-repo)
-                                     key))))))
-
-(defn get-ios-app-id
+(defn- get-ios-app-id
   [repo-url]
   (when repo-url
     (let [app-id (-> (first (string/split repo-url "/Documents"))
@@ -555,11 +542,12 @@
 
 (defmethod handle :validate-appId [[_ graph-switch-f graph]]
   (when-let [deprecated-repo (or graph (state/get-current-repo))]
-    ;; Installation is not changed for iCloud
     (if (mobile-util/in-iCloud-container-path? deprecated-repo)
+      ;; Installation is not changed for iCloud
       (when graph-switch-f
         (graph-switch-f graph true)
         (state/pub-event! [:graph/ready (state/get-current-repo)]))
+      ;; Installation is changed for App Documents directory
       (p/let [deprecated-app-id (get-ios-app-id deprecated-repo)
               current-document-url (.getUri Filesystem #js {:path ""
                                                             :directory (.-Documents Directory)})
@@ -568,24 +556,34 @@
         (if (= deprecated-app-id current-app-id)
           (when graph-switch-f (graph-switch-f graph true))
           (do
+            (notification/show! [:div "Migrating from previous App installation..."]
+                                :warning
+                                true)
+            (prn ::migrate-app-id :from deprecated-app-id :to current-app-id)
             (file-sync-stop!)
             (.unwatch mobile-util/fs-watcher)
             (let [current-repo (string/replace deprecated-repo deprecated-app-id current-app-id)
                   current-repo-dir (config/get-repo-dir current-repo)]
               (try
-                (update-file-path deprecated-repo current-repo deprecated-app-id current-app-id)
-                (db-persist/delete-graph! deprecated-repo)
+                ;; replace app-id part of repo url
+                (reset! conn/conns
+                        (update-keys @conn/conns
+                                     (fn [key]
+                                       (if (string/includes? key deprecated-app-id)
+                                         (string/replace key deprecated-app-id current-app-id)
+                                         key))))
+                (db-persist/rename-graph! deprecated-repo current-repo)
                 (search/remove-db! deprecated-repo)
-                (state/delete-repo! {:url deprecated-repo})
                 (state/add-repo! {:url current-repo :nfs? true})
+                (state/delete-repo! {:url deprecated-repo})
                 (catch :default e
                   (js/console.error e)))
               (state/set-current-repo! current-repo)
               (db-listener/listen-and-persist! current-repo)
               (db-listener/persist-if-idle! current-repo)
               (repo-config-handler/restore-repo-config! current-repo)
-              (.watch mobile-util/fs-watcher #js {:path current-repo-dir})
               (when graph-switch-f (graph-switch-f current-repo true))
+              (.watch mobile-util/fs-watcher #js {:path current-repo-dir})
               (file-sync-restart!))))
         (state/pub-event! [:graph/ready (state/get-current-repo)])))))
 
@@ -970,10 +968,8 @@
 (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 :modal/keymap [[_]]
+  (state/open-settings! :keymap))
 
 (defmethod handle :editor/toggle-own-number-list [[_ blocks]]
   (let [batch? (sequential? blocks)

+ 8 - 3
src/main/frontend/handler/file_sync.cljs

@@ -91,14 +91,19 @@
 
 (defn <list-graphs
   []
-  (go (:Graphs (<! (sync/<list-remote-graphs sync/remoteapi)))))
+  (go
+    (let [r (<! (sync/<list-remote-graphs sync/remoteapi))]
+      (if (instance? ExceptionInfo r)
+        r
+        (:Graphs r)))))
 
 (defn load-session-graphs
   []
   (when-not (state/sub [:file-sync/remote-graphs :loading])
     (go (state/set-state! [:file-sync/remote-graphs :loading] true)
-        (let [graphs (<! (<list-graphs))]
-          (state/set-state! :file-sync/remote-graphs {:loading false :graphs graphs})))))
+        (let [graphs-or-exp (<! (<list-graphs))]
+          (when-not (instance? ExceptionInfo graphs-or-exp)
+            (state/set-state! :file-sync/remote-graphs {:loading false :graphs graphs-or-exp}))))))
 
 (defn reset-session-graphs
   []

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

@@ -8,6 +8,7 @@
             [shadow.resource :as rc]
             [clojure.edn :as edn]
             [electron.ipc :as ipc]
+            [borkdude.rewrite-edn :as rewrite]
             [logseq.common.path :as path]))
 
 ;; Use defonce to avoid broken state on dev reload
@@ -38,7 +39,7 @@
 (defn set-global-config-state!
   [content]
   (let [config (edn/read-string content)]
-    (state/set-global-config! config)
+    (state/set-global-config! config content)
     config))
 
 (def default-content (rc/inline "templates/global-config.edn"))
@@ -59,6 +60,22 @@
     (p/let [config-content (fs/read-file nil config-path)]
            (set-global-config-state! config-content))))
 
+(defn set-global-config-kv!
+  [k v]
+  (let [result (rewrite/parse-string
+                 (or (state/get-global-config-str-content) "{}"))
+        ks (if (sequential? k) k [k])
+        v (cond->> v
+                   (map? v)
+                   (reduce-kv (fn [a k v] (rewrite/assoc a k v)) (rewrite/parse-string "{}")))
+        new-result (if (and (= 1 (count ks))
+                            (nil? v))
+                     (rewrite/dissoc result (first ks))
+                     (rewrite/assoc-in result ks v))
+        new-str-content (str new-result)]
+    (fs/write-file! nil nil (global-config-path) new-str-content {:skip-compare? true})
+    (state/set-global-config! (rewrite/sexpr new-result) new-str-content)))
+
 (defn start
   "This component has four responsibilities on start:
 - Fetch root-dir for later use with config paths

+ 5 - 3
src/main/frontend/handler/notification.cljs

@@ -18,11 +18,13 @@
   ([content]
    (show! content :info true nil 2000 nil))
   ([content status]
-   (show! content status true nil 1500 nil))
+   (show! content status (not= status :error) nil 1500 nil))
   ([content status clear?]
    (show! content status clear? nil 1500 nil))
   ([content status clear? uid]
    (show! content status clear? uid 1500 nil))
+  ([content status clear? uid timeout]
+   (show! content status clear? uid timeout nil))
   ([content status clear? uid timeout close-cb]
    (let [contents (state/get-notification-contents)
          uid (or uid (keyword (util/unique-id)))]
@@ -31,7 +33,7 @@
                                                           :status status
                                                           :close-cb close-cb}))
 
-     (when (and clear? (not= status :error))
-       (js/setTimeout #(clear! uid) (or timeout 1500)))
+     (when (and clear? (or timeout (not= status :error)))
+       (js/setTimeout #(clear! uid) (or timeout 2000)))
 
      uid)))

+ 24 - 24
src/main/frontend/handler/page.cljs

@@ -67,7 +67,7 @@
     (gp-util/safe-subs s 0 200)))
 
 (defn- build-title [page]
-  ;; Don't wrap `\"` anymore, as tiitle property is not effected by `,` now
+  ;; Don't wrap `\"` anymore, as title property is not effected by `,` now
   ;; The previous extract behavior isn't unwrapping the `'"` either. So no need
   ;; to maintain the compatibility.
   (:block/original-name page))
@@ -383,12 +383,12 @@
 (defn toggle-favorite! []
   ;; NOTE: in journals or settings, current-page is nil
   (when-let [page-name (state/get-current-page)]
-   (let [favorites  (:favorites (state/sub-config))
-         favorited? (contains? (set (map string/lower-case favorites))
-                               (string/lower-case page-name))]
-    (if favorited?
-      (unfavorite-page! page-name)
-      (favorite-page! page-name)))))
+    (let [favorites  (:favorites (state/sub-config))
+          favorited? (contains? (set (map string/lower-case favorites))
+                                (string/lower-case page-name))]
+      (if favorited?
+        (unfavorite-page! page-name)
+        (favorite-page! page-name)))))
 
 (defn db-refs->page
   "Replace [[page name]] with page name"
@@ -835,30 +835,30 @@
   "Accepts unsanitized page names"
   ([old-name new-name] (file-based-rename! old-name new-name true))
   ([old-name new-name redirect?]
-    (let [repo          (state/get-current-repo)
-          old-name      (string/trim old-name)
-          new-name      (string/trim new-name)
-          old-page-name (util/page-name-sanity-lc old-name)
-          new-page-name (util/page-name-sanity-lc new-name)
-          name-changed? (not= old-name new-name)]
-      (if (and old-name
+   (let [repo          (state/get-current-repo)
+         old-name      (string/trim old-name)
+         new-name      (string/trim new-name)
+         old-page-name (util/page-name-sanity-lc old-name)
+         new-page-name (util/page-name-sanity-lc new-name)
+         name-changed? (not= old-name new-name)]
+     (if (and old-name
               new-name
               (not (string/blank? new-name))
               name-changed?)
-        (do
-          (cond
-            (= old-page-name new-page-name)
-            (rename-page-aux old-name new-name redirect?)
+       (do
+         (cond
+           (= old-page-name new-page-name)
+           (rename-page-aux old-name new-name redirect?)
 
             (db/pull [:block/name new-page-name])
             (file-based-merge-pages! old-page-name new-page-name)
 
-            :else
-            (rename-namespace-pages! repo old-name new-name))
-          (rename-nested-pages old-name new-name))
-        (when (string/blank? new-name)
-          (notification/show! "Please use a valid name, empty name is not allowed!" :error)))
-      (ui-handler/re-render-root!))))
+           :else
+           (rename-namespace-pages! repo old-name new-name))
+         (rename-nested-pages old-name new-name))
+       (when (string/blank? new-name)
+         (notification/show! "Please use a valid name, empty name is not allowed!" :error)))
+     (ui-handler/re-render-root!))))
 
 (defn rename!
   ([old-name new-name] (rename! old-name new-name true))

+ 12 - 3
src/main/frontend/handler/plugin.cljs

@@ -7,6 +7,7 @@
             [logseq.graph-parser.mldoc :as gp-mldoc]
             [frontend.handler.notification :as notification]
             [frontend.handler.common.plugin :as plugin-common-handler]
+            [frontend.modules.shortcut.utils :as shortcut-utils]
             [frontend.storage :as storage]
             [camel-snake-kebab.core :as csk]
             [frontend.state :as state]
@@ -175,7 +176,7 @@
 
 (defn has-setting-schema?
   [id]
-  (when-let [pl (and id (get-plugin-inst (name id)))]
+  (when-let [^js pl (and id (get-plugin-inst (name id)))]
     (boolean (.-settingsSchema pl))))
 
 (defn get-enabled-plugins-if-setting-schema
@@ -297,7 +298,7 @@
   (let [id      (keyword (str "plugin." pid "/" key))
         binding (:binding keybinding)
         binding (some->> (if (string? binding) [binding] (seq binding))
-                         (map util/normalize-user-keyname))
+                         (map shortcut-utils/undecorate-binding))
         binding (if util/mac?
                   (or (:mac keybinding) binding) binding)
         mode    (or (:mode keybinding) :global)
@@ -658,6 +659,15 @@
                        :remove disj)]
       (save-plugin-preferences! {:pinnedToolbarItems (op-fn pinned (name key))}))))
 
+(defn hook-lifecycle-fn!
+  [type f & args]
+  (when (and type (fn? f))
+    (when config/lsp-enabled?
+      (hook-plugin-app (str :before-command-invoked type) nil))
+    (apply f args)
+    (when config/lsp-enabled?
+      (hook-plugin-app (str :after-command-invoked type) nil))))
+
 ;; components
 (rum/defc lsp-indicator < rum/reactive
   []
@@ -788,7 +798,6 @@
     (callback)
     (init-plugins! callback)))
 
-
 (comment
   {:pending        (count (:plugin/updates-pending @state/state))
    :auto-checking? (boolean (:plugin/updates-auto-checking? @state/state))

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

@@ -538,7 +538,8 @@
 (defn graph-ready!
   ;; FIXME: Call electron that the graph is loaded, an ugly implementation for redirect to page when graph is restored
   [graph]
-  (ipc/ipc "graphReady" graph))
+  (when (util/electron?)
+    (ipc/ipc "graphReady" graph)))
 
 (defn- create-db [full-graph-name]
   (p/let [_ (persist-db/<new full-graph-name)

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

@@ -79,11 +79,13 @@
      (recent-handler/add-page-to-recent! (state/get-current-repo) page-name
                                          click-from-recent?)
      (let [m (cond->
-              (default-page-route page-name)
+               (default-page-route page-name)
+
                anchor
                (assoc :query-params {:anchor anchor})
-               push
-               (assoc :push push))]
+
+              (boolean? push)
+              (assoc :push push))]
        (redirect! m)))))
 
 (defn redirect-to-whiteboard!

+ 11 - 5
src/main/frontend/handler/user.cljs

@@ -12,7 +12,8 @@
             [cljs.core.async :as async :refer [go <!]]
             [goog.crypt.Sha256]
             [goog.crypt.Hmac]
-            [goog.crypt :as crypt]))
+            [goog.crypt :as crypt]
+            [frontend.handler.notification :as notification]))
 
 (defn set-preferred-format!
   [format]
@@ -143,7 +144,7 @@
           nil                           ; do nothing
 
           (not (http/unexceptional-status? (:status resp)))
-          (clear-tokens true)
+          (notification/show! "exceptional status when refresh-token" :warning true)
 
           :else                         ; ok
           (when (and (:id_token (:body resp)) (:access_token (:body resp)))
@@ -160,6 +161,11 @@
         ;; refresh remote graph list by pub login event
         (when (user-uuid) (state/pub-event! [:user/fetch-info-and-graphs]))))))
 
+(defn has-refresh-token?
+  "Has refresh-token"
+  []
+  (boolean (js/localStorage.getItem "refresh-token")))
+
 (defn login-callback
   [session]
   (set-tokens!
@@ -199,14 +205,14 @@
   (state/clear-user-info!)
   (state/pub-event! [:user/logout]))
 
-(defn upgrade [] 
+(defn upgrade []
   (let [base-upgrade-url "https://logseqdemo.lemonsqueezy.com/checkout/buy/13e194b5-c927-41a8-af58-ed1a36d6000d"
         user-uuid (user-uuid)
         url (cond-> base-upgrade-url
               user-uuid (str "?checkout[custom][user_uuid]=" (name user-uuid)))]
     (println " ~~~ LEMON: " url " ~~~ ")
     (js/window.open url)))
-  ; (js/window.open 
+  ; (js/window.open
   ;   "https://logseqdemo.lemonsqueezy.com/checkout/buy/13e194b5-c927-41a8-af58-ed1a36d6000d"))
 
 (defn <ensure-id&access-token
@@ -218,7 +224,7 @@
       (<! (<refresh-id-token&access-token))
       (when (or (nil? (state/get-auth-id-token))
                 (-> (state/get-auth-id-token) parse-jwt expired?))
-        (ex-info "empty or expired token and refresh failed" {})))))
+        (ex-info "empty or expired token and refresh failed" {:anom :expired-token})))))
 
 (defn <user-uuid
   []

+ 28 - 0
src/main/frontend/handler/web/nfs.cljs

@@ -21,6 +21,7 @@
             [lambdaisland.glogi :as log]
             [logseq.graph-parser.util :as gp-util]
             [promesa.core :as p]
+            [logseq.common.path :as path]
             [frontend.db.listener :as db-listener]))
 
 (defn remove-ignore-files
@@ -87,6 +88,32 @@
                        (keyword (util/get-file-ext (:file/path file)))))
           files))
 
+(defn- precheck-graph-dir
+  "Check graph dir, notify user if:
+
+   - Grame dir name is `logseq`, the same as app, which might cause confusion
+   - Graph dir contains a nested graph, which should be avoided
+   - Over 10000 files found in graph dir, which might cause performance issues"
+  [dir files]
+  (when (= (string/lower-case (path/basename dir))
+           "logseq")
+    (state/pub-event!
+     [:notification/show {:content [:div "The folder name "
+                                    [:code "logseq"]
+                                    " is not suitable for a graph name. Please unlink this graph and choose a different name."]
+                          :status :warning
+                          :clear?  false}]))
+  (when (some #(string/ends-with? (:path %) "/logseq/config.edn") files)
+    (state/pub-event!
+     [:notification/show {:content "It seems that you are trying to open a Logseq graph folder with nested graph. Please unlink this graph and choose a correct folder."
+                          :status :warning
+                          :clear? false}]))
+  (when (>= (count files) 10000)
+    (state/pub-event!
+     [:notification/show {:content "It seems that you are trying to open a Logseq graph folder that contains an excessive number of files, This might lead to performance issues."
+                          :status :warning
+                          :clear? true}])))
+
 ;; TODO: extract code for `ls-dir-files` and `reload-dir!`
 (defn ls-dir-files-with-handler!
   "Read files from directory and setup repo (for the first time setup a repo)"
@@ -114,6 +141,7 @@
         (reset! *repo repo)
         (when-not (string/blank? root-dir)
           (p/let [files (:files result)
+                  _ (precheck-graph-dir root-dir (:files result))
                   files (-> (->db-files files nfs?)
                             ;; filter again, in case fs backend does not handle this
                             (remove-ignore-files root-dir nfs?))

+ 8 - 0
src/main/frontend/idb.cljs

@@ -40,6 +40,14 @@
   (when (and key @store)
     (idb-keyval/set key value @store)))
 
+(defn rename-item!
+  [old-key new-key]
+  (when (and old-key new-key @store)
+    (p/let [value (idb-keyval/get old-key @store)]
+      (when value
+        (idb-keyval/set new-key value @store)
+        (idb-keyval/del old-key @store)))))
+
 (defn set-batch!
   [items]
   (when (and (seq items) @store)

+ 26 - 24
src/main/frontend/mixins.cljs

@@ -29,31 +29,33 @@
 
 (defn hide-when-esc-or-outside
   [state & {:keys [on-hide node visibilitychange? outside?]}]
-  (try
-    (let [dom-node (rum/dom-node state)]
-      (when-let [dom-node (or node dom-node)]
-        (let [click-fn (fn [e]
-                         (let [target (.. e -target)]
-                           ;; If the click target is outside of current node
-                           (when (and
-                                  (not (dom/contains dom-node target))
-                                  (not (.contains (.-classList target) "ignore-outside-event")))
-                             (on-hide state e :click))))]
-          (when-not (false? outside?)
-            (listen state js/window "mousedown" click-fn)))
-        (listen state js/window "keydown"
-                (fn [e]
-                  (case (.-keyCode e)
-                    ;; Esc
-                    27 (on-hide state e :esc)
-                    nil)))
-        (when visibilitychange?
-          (listen state js/window "visibilitychange"
+  (let [opts (last (:rum/args state))
+        outside? (cond-> opts (nil? outside?) (:outside?))]
+    (try
+      (let [dom-node (rum/dom-node state)]
+        (when-let [dom-node (or node dom-node)]
+          (let [click-fn (fn [e]
+                           (let [target (.. e -target)]
+                             ;; If the click target is outside of current node
+                             (when (and
+                                     (not (dom/contains dom-node target))
+                                     (not (.contains (.-classList target) "ignore-outside-event")))
+                               (on-hide state e :click))))]
+            (when-not (false? outside?)
+              (listen state js/window "mousedown" click-fn)))
+          (listen state js/window "keydown"
                   (fn [e]
-                    (on-hide state e :visibilitychange))))))
-    (catch :default _e
-      ;; TODO: Unable to find node on an unmounted component.
-      nil)))
+                    (case (.-keyCode e)
+                      ;; Esc
+                      27 (on-hide state e :esc)
+                      nil)))
+          (when visibilitychange?
+            (listen state js/window "visibilitychange"
+                    (fn [e]
+                      (on-hide state e :visibilitychange))))))
+      (catch :default _e
+        ;; TODO: Unable to find node on an unmounted component.
+        nil))))
 
 (defn on-enter
   [state & {:keys [on-enter node]}]

+ 11 - 10
src/main/frontend/mobile/core.cljs

@@ -15,7 +15,7 @@
             [frontend.config :as config]
             [frontend.handler.repo :as repo-handler]))
 
-(def *url (atom nil))
+(def *init-url (atom nil))
 ;; FIXME: `appUrlOpen` are fired twice when receiving a same intent.
 ;; The following two variable atoms are used to compare whether
 ;; they are from the same intent share.
@@ -29,6 +29,15 @@
     ;; Caution: This must be called before any file accessing
     (capacitor-fs/ios-ensure-documents!)))
 
+
+(defn mobile-postinit
+  "postinit logic of mobile platforms: handle deeplink and intent"
+  []
+  (when (mobile-util/native-ios?)
+    (when @*init-url
+      (deeplink/deeplink @*init-url)
+      (reset! *init-url nil))))
+
 (defn- ios-init
   "Initialize iOS-specified event listeners"
   []
@@ -44,19 +53,11 @@
   (when (not (config/demo-graph?))
     (state/pub-event! [:validate-appId]))
 
-  (.addEventListener js/window
-                     "load"
-                     (fn [_event]
-                       (when @*url
-                         (js/setTimeout #(deeplink/deeplink @*url)
-                                        1000))))
-
   (mobile-util/check-ios-zoomed-display)
 
   ;; keep this the same logic as src/main/electron/listener.cljs
   (.addListener mobile-util/file-sync "debug"
                 (fn [event]
-                  (js/console.log "🔄" event)
                   (let [event (js->clj event :keywordize-keys true)
                         payload (:data event)]
                     (when (or (= (:event event) "download:progress")
@@ -115,7 +116,7 @@
                 (fn [^js data]
                   (when-let [url (.-url data)]
                     (if-not (= (.-readyState js/document) "complete")
-                      (reset! *url url)
+                      (reset! *init-url url)
                       (when-not (and (= @*last-shared-url url)
                                      (<= (- (.getSeconds (js/Date.)) @*last-shared-seconds) 1))
                         (reset! *last-shared-url url)

+ 672 - 695
src/main/frontend/modules/shortcut/config.cljs

@@ -1,5 +1,6 @@
 (ns frontend.modules.shortcut.config
-  (:require [frontend.components.commit :as commit]
+  (:require [clojure.string :as str]
+            [frontend.components.commit :as commit]
             [frontend.extensions.srs.handler :as srs]
             [frontend.extensions.pdf.utils :as pdf-utils]
             [frontend.handler.config :as config-handler]
@@ -40,207 +41,207 @@
 ;;  * :fn - Fn or a qualified keyword that represents a fn
 ;;  * :inactive - Optional boolean to disable a shortcut for certain conditions
 ;;    e.g. a given platform or feature condition
-(def ^:large-vars/data-var all-default-keyboard-shortcuts
+(def ^:large-vars/data-var all-built-in-keyboard-shortcuts
   ;; BUG: Actually, "enter" is registered by mixin behind a "when inputing" guard
   ;; So this setting item does not cover all cases.
   ;; See-also: frontend.components.datetime/time-repeater
-  {:date-picker/complete         {:binding "enter"
-                                  :fn      ui-handler/shortcut-complete}
+  {:date-picker/complete                    {:binding "enter"
+                                             :fn      ui-handler/shortcut-complete}
 
-   :date-picker/prev-day         {:binding "left"
-                                  :fn      ui-handler/shortcut-prev-day}
+   :date-picker/prev-day                    {:binding "left"
+                                             :fn      ui-handler/shortcut-prev-day}
 
-   :date-picker/next-day         {:binding "right"
-                                  :fn      ui-handler/shortcut-next-day}
+   :date-picker/next-day                    {:binding "right"
+                                             :fn      ui-handler/shortcut-next-day}
 
-   :date-picker/prev-week        {:binding ["up" "ctrl+p"]
-                                  :fn      ui-handler/shortcut-prev-week}
+   :date-picker/prev-week                   {:binding ["up" "ctrl+p"]
+                                             :fn      ui-handler/shortcut-prev-week}
 
-   :date-picker/next-week        {:binding ["down" "ctrl+n"]
-                                  :fn      ui-handler/shortcut-next-week}
+   :date-picker/next-week                   {:binding ["down" "ctrl+n"]
+                                             :fn      ui-handler/shortcut-next-week}
 
-   :pdf/previous-page            {:binding "alt+p"
-                                  :fn      pdf-utils/prev-page}
+   :pdf/previous-page                       {:binding "alt+p"
+                                             :fn      pdf-utils/prev-page}
 
-   :pdf/next-page                {:binding "alt+n"
-                                  :fn      pdf-utils/next-page}
+   :pdf/next-page                           {:binding "alt+n"
+                                             :fn      pdf-utils/next-page}
 
-   :pdf/close                    {:binding "alt+x"
-                                  :fn      #(state/set-state! :pdf/current nil)}
+   :pdf/close                               {:binding "alt+x"
+                                             :fn      #(state/set-state! :pdf/current nil)}
 
-   :pdf/find                     {:binding "alt+f"
-                                  :fn      pdf-utils/open-finder}
+   :pdf/find                                {:binding "alt+f"
+                                             :fn      pdf-utils/open-finder}
 
-   :whiteboard/select            {:binding ["1" "w s"]
-                                  :fn      #(.selectTool ^js (state/active-tldraw-app) "select")}
+   :whiteboard/select                       {:binding ["1" "w s"]
+                                             :fn      #(.selectTool ^js (state/active-tldraw-app) "select")}
 
-   :whiteboard/pan               {:binding ["2" "w p"]
-                                  :fn      #(.selectTool ^js (state/active-tldraw-app) "move")}
+   :whiteboard/pan                          {:binding ["2" "w p"]
+                                             :fn      #(.selectTool ^js (state/active-tldraw-app) "move")}
 
-   :whiteboard/portal            {:binding ["3" "w b"]
-                                  :fn      #(.selectTool ^js (state/active-tldraw-app) "logseq-portal")}
+   :whiteboard/portal                       {:binding ["3" "w b"]
+                                             :fn      #(.selectTool ^js (state/active-tldraw-app) "logseq-portal")}
 
-   :whiteboard/pencil            {:binding ["4" "w d"]
-                                  :fn      #(.selectTool ^js (state/active-tldraw-app) "pencil")}
+   :whiteboard/pencil                       {:binding ["4" "w d"]
+                                             :fn      #(.selectTool ^js (state/active-tldraw-app) "pencil")}
 
-   :whiteboard/highlighter       {:binding ["5" "w h"]
-                                  :fn      #(.selectTool ^js (state/active-tldraw-app) "highlighter")}
+   :whiteboard/highlighter                  {:binding ["5" "w h"]
+                                             :fn      #(.selectTool ^js (state/active-tldraw-app) "highlighter")}
 
-   :whiteboard/eraser            {:binding ["6" "w e"]
-                                  :fn      #(.selectTool ^js (state/active-tldraw-app) "erase")}
+   :whiteboard/eraser                       {:binding ["6" "w e"]
+                                             :fn      #(.selectTool ^js (state/active-tldraw-app) "erase")}
 
-   :whiteboard/connector         {:binding ["7" "w c"]
-                                  :fn      #(.selectTool ^js (state/active-tldraw-app) "line")}
+   :whiteboard/connector                    {:binding ["7" "w c"]
+                                             :fn      #(.selectTool ^js (state/active-tldraw-app) "line")}
 
-   :whiteboard/text              {:binding ["8" "w t"]
-                                  :fn      #(.selectTool ^js (state/active-tldraw-app) "text")}
+   :whiteboard/text                         {:binding ["8" "w t"]
+                                             :fn      #(.selectTool ^js (state/active-tldraw-app) "text")}
 
-   :whiteboard/rectangle         {:binding ["9" "w r"]
-                                  :fn      #(.selectTool ^js (state/active-tldraw-app) "box")}
+   :whiteboard/rectangle                    {:binding ["9" "w r"]
+                                             :fn      #(.selectTool ^js (state/active-tldraw-app) "box")}
 
-   :whiteboard/ellipse           {:binding ["o" "w o"]
-                                  :fn      #(.selectTool ^js (state/active-tldraw-app) "ellipse")}
+   :whiteboard/ellipse                      {:binding ["o" "w o"]
+                                             :fn      #(.selectTool ^js (state/active-tldraw-app) "ellipse")}
 
-   :whiteboard/reset-zoom        {:binding "shift+0"
-                                  :fn      #(.resetZoom (.-api ^js (state/active-tldraw-app)))}
+   :whiteboard/reset-zoom                   {:binding "shift+0"
+                                             :fn      #(.resetZoom (.-api ^js (state/active-tldraw-app)))}
 
-   :whiteboard/zoom-to-fit       {:binding "shift+1"
-                                  :fn      #(.zoomToFit (.-api ^js (state/active-tldraw-app)))}
+   :whiteboard/zoom-to-fit                  {:binding "shift+1"
+                                             :fn      #(.zoomToFit (.-api ^js (state/active-tldraw-app)))}
 
-   :whiteboard/zoom-to-selection {:binding "shift+2"
-                                  :fn      #(.zoomToSelection (.-api ^js (state/active-tldraw-app)))}
+   :whiteboard/zoom-to-selection            {:binding "shift+2"
+                                             :fn      #(.zoomToSelection (.-api ^js (state/active-tldraw-app)))}
 
-   :whiteboard/zoom-out          {:binding "shift+dash"
-                                  :fn      #(.zoomOut (.-api ^js (state/active-tldraw-app)) false)}
+   :whiteboard/zoom-out                     {:binding "shift+dash"
+                                             :fn      #(.zoomOut (.-api ^js (state/active-tldraw-app)) false)}
 
-   :whiteboard/zoom-in           {:binding "shift+equals"
-                                  :fn      #(.zoomIn (.-api ^js (state/active-tldraw-app)) false)}
+   :whiteboard/zoom-in                      {:binding "shift+equals"
+                                             :fn      #(.zoomIn (.-api ^js (state/active-tldraw-app)) false)}
 
-   :whiteboard/send-backward     {:binding "open-square-bracket"
-                                  :fn      #(.sendBackward ^js (state/active-tldraw-app))}
+   :whiteboard/send-backward                {:binding "open-square-bracket"
+                                             :fn      #(.sendBackward ^js (state/active-tldraw-app))}
 
-   :whiteboard/send-to-back      {:binding "shift+open-square-bracket"
-                                  :fn      #(.sendToBack ^js (state/active-tldraw-app))}
+   :whiteboard/send-to-back                 {:binding "shift+open-square-bracket"
+                                             :fn      #(.sendToBack ^js (state/active-tldraw-app))}
 
-   :whiteboard/bring-forward     {:binding "close-square-bracket"
-                                  :fn      #(.bringForward ^js (state/active-tldraw-app))}
+   :whiteboard/bring-forward                {:binding "close-square-bracket"
+                                             :fn      #(.bringForward ^js (state/active-tldraw-app))}
 
-   :whiteboard/bring-to-front    {:binding "shift+close-square-bracket"
-                                  :fn      #(.bringToFront ^js (state/active-tldraw-app))}
+   :whiteboard/bring-to-front               {:binding "shift+close-square-bracket"
+                                             :fn      #(.bringToFront ^js (state/active-tldraw-app))}
 
-   :whiteboard/lock              {:binding "mod+l"
-                                  :fn      #(.setLocked ^js (state/active-tldraw-app) true)}
+   :whiteboard/lock                         {:binding "mod+l"
+                                             :fn      #(.setLocked ^js (state/active-tldraw-app) true)}
 
-   :whiteboard/unlock            {:binding "mod+shift+l"
-                                  :fn      #(.setLocked ^js (state/active-tldraw-app) false)}
+   :whiteboard/unlock                       {:binding "mod+shift+l"
+                                             :fn      #(.setLocked ^js (state/active-tldraw-app) false)}
 
-   :whiteboard/group             {:binding "mod+g"
-                                  :fn      #(.doGroup (.-api ^js (state/active-tldraw-app)))}
+   :whiteboard/group                        {:binding "mod+g"
+                                             :fn      #(.doGroup (.-api ^js (state/active-tldraw-app)))}
 
-   :whiteboard/ungroup           {:binding "mod+shift+g"
-                                  :fn      #(.unGroup (.-api ^js (state/active-tldraw-app)))}
+   :whiteboard/ungroup                      {:binding "mod+shift+g"
+                                             :fn      #(.unGroup (.-api ^js (state/active-tldraw-app)))}
 
-   :whiteboard/toggle-grid       {:binding "t g"
-                                  :fn      #(.toggleGrid (.-api ^js (state/active-tldraw-app)))}
+   :whiteboard/toggle-grid                  {:binding "t g"
+                                             :fn      #(.toggleGrid (.-api ^js (state/active-tldraw-app)))}
 
-   :auto-complete/complete       {:binding "enter"
-                                  :fn      ui-handler/auto-complete-complete}
+   :auto-complete/complete                  {:binding "enter"
+                                             :fn      ui-handler/auto-complete-complete}
 
-   :auto-complete/prev           {:binding ["up" "ctrl+p"]
-                                  :fn      ui-handler/auto-complete-prev}
+   :auto-complete/prev                      {:binding ["up" "ctrl+p"]
+                                             :fn      ui-handler/auto-complete-prev}
 
-   :auto-complete/next           {:binding ["down" "ctrl+n"]
-                                  :fn      ui-handler/auto-complete-next}
+   :auto-complete/next                      {:binding ["down" "ctrl+n"]
+                                             :fn      ui-handler/auto-complete-next}
 
-   :auto-complete/shift-complete {:binding "shift+enter"
-                                  :fn      ui-handler/auto-complete-shift-complete}
+   :auto-complete/shift-complete            {:binding "shift+enter"
+                                             :fn      ui-handler/auto-complete-shift-complete}
 
-   :auto-complete/open-link      {:binding "mod+o"
-                                  :fn      ui-handler/auto-complete-open-link}
+   :auto-complete/open-link                 {:binding "mod+o"
+                                             :fn      ui-handler/auto-complete-open-link}
 
-   :cards/toggle-answers         {:binding "s"
-                                  :fn      srs/toggle-answers}
+   :cards/toggle-answers                    {:binding "s"
+                                             :fn      srs/toggle-answers}
 
-   :cards/next-card              {:binding "n"
-                                  :fn      srs/next-card}
+   :cards/next-card                         {:binding "n"
+                                             :fn      srs/next-card}
 
-   :cards/forgotten              {:binding "f"
-                                  :fn      srs/forgotten}
+   :cards/forgotten                         {:binding "f"
+                                             :fn      srs/forgotten}
 
-   :cards/remembered             {:binding "r"
-                                  :fn      srs/remembered}
+   :cards/remembered                        {:binding "r"
+                                             :fn      srs/remembered}
 
-   :cards/recall                 {:binding "t"
-                                  :fn      srs/recall}
+   :cards/recall                            {:binding "t"
+                                             :fn      srs/recall}
 
-   :editor/escape-editing        {:binding false
-                                  :fn      (fn [_ _]
-                                             (editor-handler/escape-editing))}
+   :editor/escape-editing                   {:binding false
+                                             :fn      (fn [_ _]
+                                                        (editor-handler/escape-editing))}
 
-   :editor/backspace             {:binding "backspace"
-                                  :fn      editor-handler/editor-backspace}
+   :editor/backspace                        {:binding "backspace"
+                                             :fn      editor-handler/editor-backspace}
 
-   :editor/delete                {:binding "delete"
-                                  :fn      editor-handler/editor-delete}
+   :editor/delete                           {:binding "delete"
+                                             :fn      editor-handler/editor-delete}
 
-   :editor/new-block             {:binding "enter"
-                                  :fn      editor-handler/keydown-new-block-handler}
+   :editor/new-block                        {:binding "enter"
+                                             :fn      editor-handler/keydown-new-block-handler}
 
-   :editor/new-line              {:binding "shift+enter"
-                                  :fn      editor-handler/keydown-new-line-handler}
+   :editor/new-line                         {:binding "shift+enter"
+                                             :fn      editor-handler/keydown-new-line-handler}
 
-   :editor/new-whiteboard        {:binding "n w"
-                                  :fn      #(whiteboard-handler/create-new-whiteboard-and-redirect!)}
+   :editor/new-whiteboard                   {:binding "n w"
+                                             :fn      #(whiteboard-handler/create-new-whiteboard-and-redirect!)}
 
-   :editor/follow-link           {:binding "mod+o"
-                                  :fn      editor-handler/follow-link-under-cursor!}
+   :editor/follow-link                      {:binding "mod+o"
+                                             :fn      editor-handler/follow-link-under-cursor!}
 
-   :editor/open-link-in-sidebar  {:binding "mod+shift+o"
-                                  :fn      editor-handler/open-link-in-sidebar!}
+   :editor/open-link-in-sidebar             {:binding "mod+shift+o"
+                                             :fn      editor-handler/open-link-in-sidebar!}
 
-   :editor/bold                  {:binding "mod+b"
-                                  :fn      editor-handler/bold-format!}
+   :editor/bold                             {:binding "mod+b"
+                                             :fn      editor-handler/bold-format!}
 
-   :editor/italics               {:binding "mod+i"
-                                  :fn      editor-handler/italics-format!}
+   :editor/italics                          {:binding "mod+i"
+                                             :fn      editor-handler/italics-format!}
 
-   :editor/highlight             {:binding "mod+shift+h"
-                                  :fn      editor-handler/highlight-format!}
+   :editor/highlight                        {:binding "mod+shift+h"
+                                             :fn      editor-handler/highlight-format!}
 
-   :editor/strike-through        {:binding "mod+shift+s"
-                                  :fn      editor-handler/strike-through-format!}
+   :editor/strike-through                   {:binding "mod+shift+s"
+                                             :fn      editor-handler/strike-through-format!}
 
-   :editor/clear-block           {:binding (if mac? "ctrl+l" "alt+l")
-                                  :fn      editor-handler/clear-block-content!}
+   :editor/clear-block                      {:binding (if mac? "ctrl+l" "alt+l")
+                                             :fn      editor-handler/clear-block-content!}
 
-   :editor/kill-line-before      {:binding (if mac? "ctrl+u" "alt+u")
-                                  :fn      editor-handler/kill-line-before!}
+   :editor/kill-line-before                 {:binding (if mac? "ctrl+u" "alt+u")
+                                             :fn      editor-handler/kill-line-before!}
 
-   :editor/kill-line-after       {:binding (if mac? false "alt+k")
-                                  :fn      editor-handler/kill-line-after!}
+   :editor/kill-line-after                  {:binding (if mac? false "alt+k")
+                                             :fn      editor-handler/kill-line-after!}
 
-   :editor/beginning-of-block    {:binding (if mac? false "alt+a")
-                                  :fn      editor-handler/beginning-of-block}
+   :editor/beginning-of-block               {:binding (if mac? false "alt+a")
+                                             :fn      editor-handler/beginning-of-block}
 
-   :editor/end-of-block          {:binding (if mac? false "alt+e")
-                                  :fn      editor-handler/end-of-block}
+   :editor/end-of-block                     {:binding (if mac? false "alt+e")
+                                             :fn      editor-handler/end-of-block}
 
-   :editor/forward-word          {:binding (if mac? "ctrl+shift+f" "alt+f")
-                                  :fn      editor-handler/cursor-forward-word}
+   :editor/forward-word                     {:binding (if mac? "ctrl+shift+f" "alt+f")
+                                             :fn      editor-handler/cursor-forward-word}
 
-   :editor/backward-word         {:binding (if mac? "ctrl+shift+b" "alt+b")
-                                  :fn      editor-handler/cursor-backward-word}
+   :editor/backward-word                    {:binding (if mac? "ctrl+shift+b" "alt+b")
+                                             :fn      editor-handler/cursor-backward-word}
 
-   :editor/forward-kill-word     {:binding (if mac? "ctrl+w" "alt+d")
-                                  :fn      editor-handler/forward-kill-word}
+   :editor/forward-kill-word                {:binding (if mac? "ctrl+w" "alt+d")
+                                             :fn      editor-handler/forward-kill-word}
 
-   :editor/backward-kill-word    {:binding (if mac? false "alt+w")
-                                  :fn      editor-handler/backward-kill-word}
+   :editor/backward-kill-word               {:binding (if mac? false "alt+w")
+                                             :fn      editor-handler/backward-kill-word}
 
    :editor/replace-block-reference-at-point {:binding "mod+shift+r"
                                              :fn      editor-handler/replace-block-reference-with-content-at-point}
-   :editor/copy-embed {:binding "mod+e"
-                       :fn      editor-handler/copy-current-block-embed}
+   :editor/copy-embed                       {:binding "mod+e"
+                                             :fn      editor-handler/copy-current-block-embed}
 
    :editor/paste-text-in-one-block-at-point {:binding "mod+shift+v"
                                              :fn      paste-handler/editor-on-paste-raw!}
@@ -248,304 +249,304 @@
    :editor/insert-youtube-timestamp         {:binding "mod+shift+y"
                                              :fn      commands/insert-youtube-timestamp}
 
-   :editor/cycle-todo              {:binding "mod+enter"
-                                    :fn      editor-handler/cycle-todo!}
+   :editor/cycle-todo                       {:binding "mod+enter"
+                                             :fn      editor-handler/cycle-todo!}
 
-   :editor/up                      {:binding ["up" "ctrl+p"]
-                                    :fn      (editor-handler/shortcut-up-down :up)}
+   :editor/up                               {:binding ["up" "ctrl+p"]
+                                             :fn      (editor-handler/shortcut-up-down :up)}
 
-   :editor/down                    {:binding ["down" "ctrl+n"]
-                                    :fn      (editor-handler/shortcut-up-down :down)}
+   :editor/down                             {:binding ["down" "ctrl+n"]
+                                             :fn      (editor-handler/shortcut-up-down :down)}
 
-   :editor/left                    {:binding "left"
-                                    :fn      (editor-handler/shortcut-left-right :left)}
+   :editor/left                             {:binding "left"
+                                             :fn      (editor-handler/shortcut-left-right :left)}
 
-   :editor/right                   {:binding "right"
-                                    :fn      (editor-handler/shortcut-left-right :right)}
+   :editor/right                            {:binding "right"
+                                             :fn      (editor-handler/shortcut-left-right :right)}
 
-   :editor/move-block-up           {:binding (if mac? "mod+shift+up" "alt+shift+up")
-                                    :fn      (editor-handler/move-up-down true)}
+   :editor/move-block-up                    {:binding (if mac? "mod+shift+up" "alt+shift+up")
+                                             :fn      (editor-handler/move-up-down true)}
 
-   :editor/move-block-down         {:binding (if mac? "mod+shift+down" "alt+shift+down")
-                                    :fn      (editor-handler/move-up-down false)}
+   :editor/move-block-down                  {:binding (if mac? "mod+shift+down" "alt+shift+down")
+                                             :fn      (editor-handler/move-up-down false)}
 
    ;; FIXME: add open edit in non-selection mode
-   :editor/open-edit               {:binding "enter"
-                                    :fn      (partial editor-handler/open-selected-block! :right)}
+   :editor/open-edit                        {:binding "enter"
+                                             :fn      (partial editor-handler/open-selected-block! :right)}
 
-   :editor/select-block-up         {:binding "alt+up"
-                                    :fn      (editor-handler/on-select-block :up)}
+   :editor/select-block-up                  {:binding "alt+up"
+                                             :fn      (editor-handler/on-select-block :up)}
 
-   :editor/select-block-down       {:binding "alt+down"
-                                    :fn      (editor-handler/on-select-block :down)}
+   :editor/select-block-down                {:binding "alt+down"
+                                             :fn      (editor-handler/on-select-block :down)}
 
-   :editor/select-up               {:binding "shift+up"
-                                    :fn      (editor-handler/shortcut-select-up-down :up)}
+   :editor/select-up                        {:binding "shift+up"
+                                             :fn      (editor-handler/shortcut-select-up-down :up)}
 
-   :editor/select-down             {:binding "shift+down"
-                                    :fn      (editor-handler/shortcut-select-up-down :down)}
+   :editor/select-down                      {:binding "shift+down"
+                                             :fn      (editor-handler/shortcut-select-up-down :down)}
 
-   :editor/delete-selection        {:binding ["backspace" "delete"]
-                                    :fn      editor-handler/delete-selection}
+   :editor/delete-selection                 {:binding ["backspace" "delete"]
+                                             :fn      editor-handler/delete-selection}
 
-   :editor/expand-block-children   {:binding "mod+down"
-                                    :fn      editor-handler/expand!}
+   :editor/expand-block-children            {:binding "mod+down"
+                                             :fn      editor-handler/expand!}
 
-   :editor/collapse-block-children {:binding "mod+up"
-                                    :fn      editor-handler/collapse!}
+   :editor/collapse-block-children          {:binding "mod+up"
+                                             :fn      editor-handler/collapse!}
 
-   :editor/indent                  {:binding "tab"
-                                    :fn      (editor-handler/keydown-tab-handler :right)}
+   :editor/indent                           {:binding "tab"
+                                             :fn      (editor-handler/keydown-tab-handler :right)}
 
-   :editor/outdent                 {:binding "shift+tab"
-                                    :fn      (editor-handler/keydown-tab-handler :left)}
+   :editor/outdent                          {:binding "shift+tab"
+                                             :fn      (editor-handler/keydown-tab-handler :left)}
 
-   :editor/copy                    {:binding "mod+c"
-                                    :fn      editor-handler/shortcut-copy}
+   :editor/copy                             {:binding "mod+c"
+                                             :fn      editor-handler/shortcut-copy}
 
-   :editor/copy-text               {:binding "mod+shift+c"
-                                    :fn      editor-handler/shortcut-copy-text}
+   :editor/copy-text                        {:binding "mod+shift+c"
+                                             :fn      editor-handler/shortcut-copy-text}
 
-   :editor/cut                     {:binding "mod+x"
-                                    :fn      editor-handler/shortcut-cut}
+   :editor/cut                              {:binding "mod+x"
+                                             :fn      editor-handler/shortcut-cut}
 
-   :editor/undo                    {:binding "mod+z"
-                                    :fn      history/undo!}
+   :editor/undo                             {:binding "mod+z"
+                                             :fn      history/undo!}
 
-   :editor/redo                    {:binding ["mod+shift+z" "mod+y"]
-                                    :fn      history/redo!}
+   :editor/redo                             {:binding ["mod+shift+z" "mod+y"]
+                                             :fn      history/redo!}
 
-   :editor/insert-link             {:binding "mod+l"
-                                    :fn      #(editor-handler/html-link-format!)}
+   :editor/insert-link                      {:binding "mod+l"
+                                             :fn      #(editor-handler/html-link-format!)}
 
-   :editor/select-all-blocks       {:binding "mod+shift+a"
-                                    :fn      editor-handler/select-all-blocks!}
+   :editor/select-all-blocks                {:binding "mod+shift+a"
+                                             :fn      editor-handler/select-all-blocks!}
 
-   :editor/select-parent           {:binding "mod+a"
-                                    :fn      editor-handler/select-parent}
+   :editor/select-parent                    {:binding "mod+a"
+                                             :fn      editor-handler/select-parent}
 
-   :editor/zoom-in                 {:binding (if mac? "mod+." "alt+right")
-                                    :fn      editor-handler/zoom-in!}
+   :editor/zoom-in                          {:binding (if mac? "mod+." "alt+right")
+                                             :fn      editor-handler/zoom-in!}
 
-   :editor/zoom-out                {:binding (if mac? "mod+," "alt+left")
-                                    :fn      editor-handler/zoom-out!}
+   :editor/zoom-out                         {:binding (if mac? "mod+," "alt+left")
+                                             :fn      editor-handler/zoom-out!}
 
-   :editor/toggle-undo-redo-mode   {:binding false
-                                    :fn      undo-redo/toggle-undo-redo-mode!}
+   :editor/toggle-undo-redo-mode            {:binding false
+                                             :fn      undo-redo/toggle-undo-redo-mode!}
 
-   :editor/toggle-number-list      {:binding "t n"
-                                    :fn #(state/pub-event! [:editor/toggle-own-number-list (state/get-selection-block-ids)])}
+   :editor/toggle-number-list               {:binding "t n"
+                                             :fn      #(state/pub-event! [:editor/toggle-own-number-list (state/get-selection-block-ids)])}
 
-   :ui/toggle-brackets             {:binding "mod+c mod+b"
-                                    :fn      config-handler/toggle-ui-show-brackets!}
+   :ui/toggle-brackets                      {:binding "mod+c mod+b"
+                                             :fn      config-handler/toggle-ui-show-brackets!}
 
-   :go/search-in-page              {:binding "mod+shift+k"
-                                    :fn      #(do
-                                                (editor-handler/escape-editing)
-                                                (route-handler/go-to-search! :page))}
+   :go/search-in-page                       {:binding "mod+shift+k"
+                                             :fn      #(do
+                                                         (editor-handler/escape-editing)
+                                                         (route-handler/go-to-search! :page))}
 
-   :go/search                      {:binding "mod+k"
-                                    :fn      #(do
-                                                (editor-handler/escape-editing false)
-                                                (route-handler/go-to-search! :global))}
+   :go/search                               {:binding "mod+k"
+                                             :fn      #(do
+                                                         (editor-handler/escape-editing false)
+                                                         (route-handler/go-to-search! :global))}
 
-   :go/electron-find-in-page       {:binding "mod+f"
-                                    :inactive (not (util/electron?))
-                                    :fn      #(search-handler/open-find-in-page!)}
+   :go/electron-find-in-page                {:binding  "mod+f"
+                                             :inactive (not (util/electron?))
+                                             :fn       #(search-handler/open-find-in-page!)}
 
-   :go/electron-jump-to-the-next {:binding ["enter" "mod+g"]
-                                  :inactive (not (util/electron?))
-                                  :fn      #(search-handler/loop-find-in-page! false)}
+   :go/electron-jump-to-the-next            {:binding  ["enter" "mod+g"]
+                                             :inactive (not (util/electron?))
+                                             :fn       #(search-handler/loop-find-in-page! false)}
 
-   :go/electron-jump-to-the-previous {:binding ["shift+enter" "mod+shift+g"]
-                                      :inactive (not (util/electron?))
-                                      :fn      #(search-handler/loop-find-in-page! true)}
+   :go/electron-jump-to-the-previous        {:binding  ["shift+enter" "mod+shift+g"]
+                                             :inactive (not (util/electron?))
+                                             :fn       #(search-handler/loop-find-in-page! true)}
 
-   :go/journals                    {:binding "g j"
-                                    :fn      route-handler/go-to-journals!}
+   :go/journals                             {:binding "g j"
+                                             :fn      route-handler/go-to-journals!}
 
-   :go/backward                    {:binding "mod+open-square-bracket"
-                                    :fn      (fn [_] (js/window.history.back))}
+   :go/backward                             {:binding "mod+open-square-bracket"
+                                             :fn      (fn [_] (js/window.history.back))}
 
-   :go/forward                     {:binding "mod+close-square-bracket"
-                                    :fn      (fn [_] (js/window.history.forward))}
+   :go/forward                              {:binding "mod+close-square-bracket"
+                                             :fn      (fn [_] (js/window.history.forward))}
 
-   :search/re-index                {:binding "mod+c mod+s"
-                                    :fn      (fn [_] (search-handler/rebuild-indices! true))}
+   :search/re-index                         {:binding "mod+c mod+s"
+                                             :fn      (fn [_] (search-handler/rebuild-indices! true))}
 
-   :sidebar/open-today-page        {:binding (if mac? "mod+shift+j" "alt+shift+j")
-                                    :fn      page-handler/open-today-in-sidebar}
+   :sidebar/open-today-page                 {:binding (if mac? "mod+shift+j" "alt+shift+j")
+                                             :fn      page-handler/open-today-in-sidebar}
 
-   :sidebar/close-top              {:binding "c t"
-                                    :fn      #(state/sidebar-remove-block! 0)}
+   :sidebar/close-top                       {:binding "c t"
+                                             :fn      #(state/sidebar-remove-block! 0)}
 
-   :sidebar/clear                  {:binding "mod+c mod+c"
-                                    :fn      #(do
-                                                (state/clear-sidebar-blocks!)
-                                                (state/hide-right-sidebar!))}
+   :sidebar/clear                           {:binding "mod+c mod+c"
+                                             :fn      #(do
+                                                         (state/clear-sidebar-blocks!)
+                                                         (state/hide-right-sidebar!))}
 
-   :misc/copy                      {:binding "mod+c"
-                                    :fn      (fn [] (js/document.execCommand "copy"))}
+   :misc/copy                               {:binding "mod+c"
+                                             :fn      (fn [] (js/document.execCommand "copy"))}
 
-   :command-palette/toggle         {:binding "mod+shift+p"
-                                    :fn      #(do
-                                                (editor-handler/escape-editing)
-                                                (state/pub-event! [:modal/command-palette]))}
+   :command-palette/toggle                  {:binding "mod+shift+p"
+                                             :fn      #(do
+                                                         (editor-handler/escape-editing)
+                                                         (state/pub-event! [:modal/command-palette]))}
 
-   :graph/export-as-html           {:fn #(export-handler/download-repo-as-html!
-                                          (state/get-current-repo))
-                                    :binding false}
+   :graph/export-as-html                    {:fn      #(export-handler/download-repo-as-html!
+                                                        (state/get-current-repo))
+                                             :binding false}
 
-   :graph/open                     {:fn      #(do
-                                                (editor-handler/escape-editing)
-                                                (state/set-state! :ui/open-select :graph-open))
-                                    :binding "alt+shift+g"}
+   :graph/open                              {:fn      #(do
+                                                         (editor-handler/escape-editing)
+                                                         (state/set-state! :ui/open-select :graph-open))
+                                             :binding "alt+shift+g"}
 
-   :graph/remove                   {:fn      #(do
-                                                (editor-handler/escape-editing)
-                                                (state/set-state! :ui/open-select :graph-remove))
-                                    :binding false}
+   :graph/remove                            {:fn      #(do
+                                                         (editor-handler/escape-editing)
+                                                         (state/set-state! :ui/open-select :graph-remove))
+                                             :binding false}
 
-   :graph/add                      {:fn (fn [] (route-handler/redirect! {:to :repo-add}))
-                                    :binding false}
+   :graph/add                               {:fn      (fn [] (route-handler/redirect! {:to :repo-add}))
+                                             :binding false}
 
-   :graph/db-add                   {:fn #(state/pub-event! [:graph/new-db-graph])
-                                    ;; TODO: Remove this once feature is released
-                                    :inactive (not config/db-graph-enabled?)
-                                    :binding false}
+   :graph/db-add                            {:fn #(state/pub-event! [:graph/new-db-graph])
+                                             ;; TODO: Remove this once feature is released
+                                             :inactive (not config/db-graph-enabled?)
+                                             :binding false}
 
-   :graph/save                     {:fn #(state/pub-event! [:graph/save])
-                                    :binding false}
+   :graph/save                              {:fn      #(state/pub-event! [:graph/save])
+                                             :binding false}
 
-   :graph/re-index                 {:fn (fn []
-                                          (p/let [multiple-windows? (ipc/ipc "graphHasMultipleWindows" (state/get-current-repo))]
-                                            (state/pub-event! [:graph/ask-for-re-index (atom multiple-windows?) nil])))
-                                    :binding false}
+   :graph/re-index                          {:fn      (fn []
+                                                        (p/let [multiple-windows? (ipc/ipc "graphHasMultipleWindows" (state/get-current-repo))]
+                                                          (state/pub-event! [:graph/ask-for-re-index (atom multiple-windows?) nil])))
+                                             :binding false}
 
-   :command/run                    {:binding "mod+shift+1"
-                                    :inactive (not (util/electron?))
-                                    :fn      #(do
-                                                (editor-handler/escape-editing)
-                                                (state/pub-event! [:command/run]))}
+   :command/run                             {:binding  "mod+shift+1"
+                                             :inactive (not (util/electron?))
+                                             :fn       #(do
+                                                          (editor-handler/escape-editing)
+                                                          (state/pub-event! [:command/run]))}
 
-   :go/home                        {:binding "g h"
-                                    :fn      #(route-handler/redirect-to-home!)}
+   :go/home                                 {:binding "g h"
+                                             :fn      #(route-handler/redirect-to-home!)}
 
-   :go/all-pages                   {:binding "g a"
-                                    :fn      route-handler/redirect-to-all-pages!}
+   :go/all-pages                            {:binding "g a"
+                                             :fn      route-handler/redirect-to-all-pages!}
 
-   :go/graph-view                  {:binding "g g"
-                                    :fn      route-handler/redirect-to-graph-view!}
+   :go/graph-view                           {:binding "g g"
+                                             :fn      route-handler/redirect-to-graph-view!}
 
-   :go/all-graphs                  {:binding "g shift+g"
-                                    :fn      route-handler/redirect-to-all-graphs}
+   :go/all-graphs                           {:binding "g shift+g"
+                                             :fn      route-handler/redirect-to-all-graphs}
 
-   :go/whiteboards                  {:binding "g w"
-                                     :fn      route-handler/redirect-to-whiteboard-dashboard!}
+   :go/whiteboards                          {:binding "g w"
+                                             :fn      route-handler/redirect-to-whiteboard-dashboard!}
 
-   :go/keyboard-shortcuts          {:binding "g s"
-                                    :fn      #(state/pub-event! [:modal/keymap-manager])}
+   :go/keyboard-shortcuts                   {:binding "g s"
+                                             :fn      #(state/pub-event! [:modal/keymap])}
 
-   :go/tomorrow                    {:binding "g t"
-                                    :fn      journal-handler/go-to-tomorrow!}
+   :go/tomorrow                             {:binding "g t"
+                                             :fn      journal-handler/go-to-tomorrow!}
 
-   :go/next-journal                {:binding "g n"
-                                    :fn      journal-handler/go-to-next-journal!}
+   :go/next-journal                         {:binding "g n"
+                                             :fn      journal-handler/go-to-next-journal!}
 
-   :go/prev-journal                {:binding "g p"
-                                    :fn      journal-handler/go-to-prev-journal!}
+   :go/prev-journal                         {:binding "g p"
+                                             :fn      journal-handler/go-to-prev-journal!}
 
-   :go/flashcards                  {:binding "g f"
-                                    :fn      (fn []
-                                               (if (state/modal-opened?)
-                                                 (state/close-modal!)
-                                                 (state/pub-event! [:modal/show-cards])))}
+   :go/flashcards                           {:binding "g f"
+                                             :fn      (fn []
+                                                        (if (state/modal-opened?)
+                                                          (state/close-modal!)
+                                                          (state/pub-event! [:modal/show-cards])))}
 
-   :ui/toggle-document-mode        {:binding "t d"
-                                    :fn      state/toggle-document-mode!}
+   :ui/toggle-document-mode                 {:binding "t d"
+                                             :fn      state/toggle-document-mode!}
 
-   :ui/toggle-settings              {:binding (if mac? "t s" ["t s" "mod+,"])
-                                     :fn      ui-handler/toggle-settings-modal!}
+   :ui/toggle-settings                      {:binding (if mac? ["t s" "mod+,"] "t s")
+                                             :fn      ui-handler/toggle-settings-modal!}
 
-   :ui/toggle-right-sidebar         {:binding "t r"
-                                     :fn      ui-handler/toggle-right-sidebar!}
+   :ui/toggle-right-sidebar                 {:binding "t r"
+                                             :fn      ui-handler/toggle-right-sidebar!}
 
-   :ui/toggle-left-sidebar          {:binding "t l"
-                                     :fn      state/toggle-left-sidebar!}
+   :ui/toggle-left-sidebar                  {:binding "t l"
+                                             :fn      state/toggle-left-sidebar!}
 
-   :ui/toggle-help                  {:binding "shift+/"
-                                     :fn      ui-handler/toggle-help!}
+   :ui/toggle-help                          {:binding "shift+/"
+                                             :fn      ui-handler/toggle-help!}
 
-   :ui/toggle-theme                 {:binding "t t"
-                                     :fn      state/toggle-theme!}
+   :ui/toggle-theme                         {:binding "t t"
+                                             :fn      state/toggle-theme!}
 
-   :ui/toggle-contents              {:binding "alt+shift+c"
-                                     :fn      ui-handler/toggle-contents!}
+   :ui/toggle-contents                      {:binding "alt+shift+c"
+                                             :fn      ui-handler/toggle-contents!}
 
-   :command/toggle-favorite         {:binding "mod+shift+f"
-                                     :fn      page-handler/toggle-favorite!}
+   :command/toggle-favorite                 {:binding "mod+shift+f"
+                                             :fn      page-handler/toggle-favorite!}
 
-   :editor/open-file-in-default-app {:binding "mod+d mod+a"
-                                     :inactive (not (util/electron?))
-                                     :fn      page-handler/open-file-in-default-app}
+   :editor/open-file-in-default-app         {:binding  "mod+d mod+a"
+                                             :inactive (not (util/electron?))
+                                             :fn       page-handler/open-file-in-default-app}
 
-   :editor/open-file-in-directory   {:binding "mod+d mod+i"
-                                     :inactive (not (util/electron?))
-                                     :fn      page-handler/open-file-in-directory}
+   :editor/open-file-in-directory           {:binding  "mod+d mod+i"
+                                             :inactive (not (util/electron?))
+                                             :fn       page-handler/open-file-in-directory}
 
-   :editor/copy-current-file        {:binding false
-                                     :inactive (not (util/electron?))
-                                     :fn      page-handler/copy-current-file}
+   :editor/copy-current-file                {:binding  false
+                                             :inactive (not (util/electron?))
+                                             :fn       page-handler/copy-current-file}
 
-   :editor/copy-page-url            {:binding false
-                                     :inactive (not (util/electron?))
-                                     :fn      #(page-handler/copy-page-url)}
+   :editor/copy-page-url                    {:binding  false
+                                             :inactive (not (util/electron?))
+                                             :fn       #(page-handler/copy-page-url)}
 
-   :ui/toggle-wide-mode             {:binding "t w"
-                                     :fn      ui-handler/toggle-wide-mode!}
+   :ui/toggle-wide-mode                     {:binding "t w"
+                                             :fn      ui-handler/toggle-wide-mode!}
 
-   :ui/select-theme-color           {:binding "t i"
-                                     :fn      plugin-handler/show-themes-modal!}
+   :ui/select-theme-color                   {:binding "t i"
+                                             :fn      plugin-handler/show-themes-modal!}
 
-   :ui/goto-plugins                 {:binding "t p"
-                                     :inactive (not config/lsp-enabled?)
-                                     :fn      plugin-handler/goto-plugins-dashboard!}
+   :ui/goto-plugins                         {:binding  "t p"
+                                             :inactive (not config/lsp-enabled?)
+                                             :fn       plugin-handler/goto-plugins-dashboard!}
 
-   :ui/install-plugins-from-file    {:binding false
-                                     :inactive (not (config/plugin-config-enabled?))
-                                     :fn       plugin-config-handler/open-replace-plugins-modal}
+   :ui/install-plugins-from-file            {:binding  false
+                                             :inactive (not (config/plugin-config-enabled?))
+                                             :fn       plugin-config-handler/open-replace-plugins-modal}
 
-   :ui/clear-all-notifications      {:binding false
-                                     :fn      :frontend.handler.notification/clear-all!}
+   :ui/clear-all-notifications              {:binding false
+                                             :fn      :frontend.handler.notification/clear-all!}
 
-   :editor/toggle-open-blocks       {:binding "t o"
-                                     :fn      editor-handler/toggle-open!}
+   :editor/toggle-open-blocks               {:binding "t o"
+                                             :fn      editor-handler/toggle-open!}
 
-   :ui/toggle-cards                 {:binding "t c"
-                                     :fn      ui-handler/toggle-cards!}
+   :ui/toggle-cards                         {:binding "t c"
+                                             :fn      ui-handler/toggle-cards!}
 
-   :git/commit                      {:binding "mod+g c"
-                                     :inactive (not (util/electron?))
-                                     :fn      commit/show-commit-modal!}
+   :git/commit                              {:binding  "mod+g c"
+                                             :inactive (not (util/electron?))
+                                             :fn       commit/show-commit-modal!}
 
-   :dev/show-block-data            {:binding false
-                                    :inactive (not (state/developer-mode?))
-                                    :fn :frontend.handler.common.developer/show-block-data}
+   :dev/show-block-data                     {:binding  false
+                                             :inactive (not (state/developer-mode?))
+                                             :fn       :frontend.handler.common.developer/show-block-data}
 
-   :dev/show-block-ast             {:binding false
-                                    :inactive (not (state/developer-mode?))
-                                    :fn :frontend.handler.common.developer/show-block-ast}
+   :dev/show-block-ast                      {:binding  false
+                                             :inactive (not (state/developer-mode?))
+                                             :fn       :frontend.handler.common.developer/show-block-ast}
 
-   :dev/show-page-data             {:binding false
-                                    :inactive (not (state/developer-mode?))
-                                    :fn :frontend.handler.common.developer/show-page-data}
+   :dev/show-page-data                      {:binding  false
+                                             :inactive (not (state/developer-mode?))
+                                             :fn       :frontend.handler.common.developer/show-page-data}
 
-   :dev/show-page-ast              {:binding false
-                                    :inactive (not (state/developer-mode?))
-                                    :fn :frontend.handler.common.developer/show-page-ast}})
+   :dev/show-page-ast                       {:binding  false
+                                             :inactive (not (state/developer-mode?))
+                                             :fn       :frontend.handler.common.developer/show-page-ast}})
 
 (let [keyboard-commands
-      {::commands (set (keys all-default-keyboard-shortcuts))
+      {::commands       (set (keys all-built-in-keyboard-shortcuts))
        ::dicts/commands dicts/abbreviated-commands}]
   (assert (= (::commands keyboard-commands) (::dicts/commands keyboard-commands))
           (str "Keyboard commands must have an english label"
@@ -562,7 +563,16 @@
       (throw (ex-info (str "Unable to resolve " keyword-fn " to a fn") {})))))
 
 (defn build-category-map [ks]
-  (->> (select-keys all-default-keyboard-shortcuts ks)
+  (->> (if (sequential? ks)
+         ks (let [{:keys [ns includes excludes]} ks]
+              (->> (keys all-built-in-keyboard-shortcuts)
+                   (filter (fn [k]
+                             (and (or (and ns (keyword? k)
+                                           (contains? (->> (if (seqable? ns) (seq ns) [ns]) (map #(name %)) (set))
+                                                      (namespace k)))
+                                      (and includes (contains? (set includes) k)))
+                                  (if (not (seq excludes)) true (not (contains? (set excludes) k)))))))))
+       (select-keys all-built-in-keyboard-shortcuts)
        (remove (comp :inactive val))
        ;; Convert keyword fns to real fns
        (map (fn [[k v]]
@@ -572,393 +582,360 @@
        (into {})))
 
 ;; This is the only var that should be publicly expose :fn functionality
-(defonce ^:large-vars/data-var config
+(defonce ^:large-vars/data-var *config
   (atom
    {:shortcut.handler/date-picker
-    (build-category-map [:date-picker/complete
-                         :date-picker/prev-day
-                         :date-picker/next-day
-                         :date-picker/prev-week
-                         :date-picker/next-week])
+    (build-category-map {:ns :date-picker})
 
     :shortcut.handler/pdf
-    (-> (build-category-map [:pdf/previous-page
-                             :pdf/next-page
-                             :pdf/close
-                             :pdf/find])
+    (-> (build-category-map {:ns :pdf})
         (with-meta {:before m/enable-when-not-editing-mode!}))
 
     :shortcut.handler/whiteboard
-    (-> (build-category-map [:whiteboard/select
-                             :whiteboard/pan
-                             :whiteboard/portal
-                             :whiteboard/pencil
-                             :whiteboard/highlighter
-                             :whiteboard/eraser
-                             :whiteboard/connector
-                             :whiteboard/text
-                             :whiteboard/rectangle
-                             :whiteboard/ellipse
-                             :whiteboard/reset-zoom
-                             :whiteboard/zoom-to-fit
-                             :whiteboard/zoom-to-selection
-                             :whiteboard/zoom-out
-                             :whiteboard/zoom-in
-                             :whiteboard/send-backward
-                             :whiteboard/send-to-back
-                             :whiteboard/bring-forward
-                             :whiteboard/bring-to-front
-                             :whiteboard/lock
-                             :whiteboard/unlock
-                             :whiteboard/group
-                             :whiteboard/ungroup
-                             :whiteboard/toggle-grid])
+    (-> (build-category-map {:ns :whiteboard})
         (with-meta {:before m/enable-when-not-editing-mode!}))
 
     :shortcut.handler/auto-complete
-    (build-category-map [:auto-complete/complete
-                         :auto-complete/prev
-                         :auto-complete/next
-                         :auto-complete/shift-complete
-                         :auto-complete/open-link])
+    (build-category-map {:ns :auto-complete})
 
     :shortcut.handler/cards
-    (-> (build-category-map [:cards/toggle-answers
-                             :cards/next-card
-                             :cards/forgotten
-                             :cards/remembered
-                             :cards/recall])
+    (-> (build-category-map {:ns :cards})
         (with-meta {:before m/enable-when-not-editing-mode!}))
 
     :shortcut.handler/block-editing-only
-    (->
-     (build-category-map [:editor/escape-editing
-                          :editor/backspace
-                          :editor/delete
-                          :editor/new-block
-                          :editor/new-line
-                          :editor/follow-link
-                          :editor/open-link-in-sidebar
-                          :editor/bold
-                          :editor/italics
-                          :editor/highlight
-                          :editor/strike-through
-                          :editor/clear-block
-                          :editor/kill-line-before
-                          :editor/kill-line-after
-                          :editor/beginning-of-block
-                          :editor/end-of-block
-                          :editor/forward-word
-                          :editor/backward-word
-                          :editor/forward-kill-word
-                          :editor/backward-kill-word
-                          :editor/replace-block-reference-at-point
-                          :editor/copy-embed
-                          :editor/paste-text-in-one-block-at-point
-                          :editor/insert-youtube-timestamp])
-     (with-meta {:before m/enable-when-editing-mode!}))
+    (-> (build-category-map
+          [:editor/escape-editing
+           :editor/backspace
+           :editor/delete
+           :editor/zoom-in
+           :editor/zoom-out
+           :editor/new-block
+           :editor/new-line
+           :editor/follow-link
+           :editor/open-link-in-sidebar
+           :editor/bold
+           :editor/italics
+           :editor/highlight
+           :editor/strike-through
+           :editor/clear-block
+           :editor/kill-line-before
+           :editor/kill-line-after
+           :editor/beginning-of-block
+           :editor/end-of-block
+           :editor/forward-word
+           :editor/backward-word
+           :editor/forward-kill-word
+           :editor/backward-kill-word
+           :editor/replace-block-reference-at-point
+           :editor/copy-embed
+           :editor/paste-text-in-one-block-at-point
+           :editor/insert-youtube-timestamp])
+        (with-meta {:before m/enable-when-editing-mode!}))
 
     :shortcut.handler/editor-global
-    (->
-     (build-category-map [:graph/export-as-html
-                          :graph/open
-                          :graph/remove
-                          :graph/add
-                          :graph/db-add
-                          :graph/save
-                          :graph/re-index
-                          :editor/cycle-todo
-                          :editor/up
-                          :editor/down
-                          :editor/left
-                          :editor/right
-                          :editor/select-up
-                          :editor/select-down
-                          :editor/move-block-up
-                          :editor/move-block-down
-                          :editor/open-edit
-                          :editor/select-block-up
-                          :editor/select-block-down
-                          :editor/select-parent
-                          :editor/delete-selection
-                          :editor/expand-block-children
-                          :editor/collapse-block-children
-                          :editor/indent
-                          :editor/outdent
-                          :editor/copy
-                          :editor/copy-text
-                          :editor/cut
-                          :command/toggle-favorite])
-     (with-meta {:before m/enable-when-not-component-editing!}))
+    (-> (build-category-map
+          [:graph/export-as-html
+           :graph/open
+           :graph/remove
+           :graph/add
+           :graph/db-add
+           :graph/save
+           :graph/re-index
+           :editor/cycle-todo
+           :editor/up
+           :editor/down
+           :editor/left
+           :editor/right
+           :editor/select-up
+           :editor/select-down
+           :editor/move-block-up
+           :editor/move-block-down
+           :editor/open-edit
+           :editor/select-block-up
+           :editor/select-block-down
+           :editor/select-parent
+           :editor/delete-selection
+           :editor/expand-block-children
+           :editor/collapse-block-children
+           :editor/indent
+           :editor/outdent
+           :editor/copy
+           :editor/copy-text
+           :editor/cut
+           :command/toggle-favorite])
+        (with-meta {:before m/enable-when-not-component-editing!}))
 
     :shortcut.handler/global-prevent-default
-    (->
-     (build-category-map [:editor/insert-link
-                          :editor/select-all-blocks
-                          :editor/zoom-in
-                          :editor/zoom-out
-                          :editor/toggle-undo-redo-mode
-                          :editor/toggle-number-list
-                          :editor/undo
-                          :editor/redo
-                          :ui/toggle-brackets
-                          :go/search-in-page
-                          :go/search
-                          :go/electron-find-in-page
-                          :go/electron-jump-to-the-next
-                          :go/electron-jump-to-the-previous
-                          :go/backward
-                          :go/forward
-                          :search/re-index
-                          :sidebar/open-today-page
-                          :sidebar/clear
-                          :command/run
-                          :command-palette/toggle])
-     (with-meta {:before m/prevent-default-behavior}))
+    (-> (build-category-map
+          [:editor/insert-link
+           :editor/select-all-blocks
+           :editor/toggle-undo-redo-mode
+           :editor/toggle-number-list
+           :editor/undo
+           :editor/redo
+           :ui/toggle-brackets
+           :go/search-in-page
+           :go/search
+           :go/electron-find-in-page
+           :go/electron-jump-to-the-next
+           :go/electron-jump-to-the-previous
+           :go/backward
+           :go/forward
+           :search/re-index
+           :sidebar/open-today-page
+           :sidebar/clear
+           :command/run
+           :command-palette/toggle])
+        (with-meta {:before m/prevent-default-behavior}))
+
+    :shortcut.handler/global-non-editing-only
+    (-> (build-category-map
+          [:go/home
+           :go/journals
+           :go/all-pages
+           :go/flashcards
+           :go/graph-view
+           :go/all-graphs
+           :go/whiteboards
+           :go/keyboard-shortcuts
+           :go/tomorrow
+           :go/next-journal
+           :go/prev-journal
+           :ui/toggle-document-mode
+           :ui/toggle-settings
+           :ui/toggle-right-sidebar
+           :ui/toggle-left-sidebar
+           :ui/toggle-help
+           :ui/toggle-theme
+           :ui/toggle-contents
+           :editor/open-file-in-default-app
+           :editor/open-file-in-directory
+           :editor/copy-current-file
+           :editor/copy-page-url
+           :editor/new-whiteboard
+           :ui/toggle-wide-mode
+           :ui/select-theme-color
+           :ui/goto-plugins
+           :ui/install-plugins-from-file
+           :editor/toggle-open-blocks
+           :ui/toggle-cards
+           :ui/clear-all-notifications
+           :git/commit
+           :sidebar/close-top
+           :dev/show-block-data
+           :dev/show-block-ast
+           :dev/show-page-data
+           :dev/show-page-ast])
+        (with-meta {:before m/enable-when-not-editing-mode!}))
 
     :shortcut.handler/misc
     ;; always overrides the copy due to "mod+c mod+s"
-    {:misc/copy              (:misc/copy              all-default-keyboard-shortcuts)}
-
-    :shortcut.handler/global-non-editing-only
-    (->
-     (build-category-map [:go/home
-                          :go/journals
-                          :go/all-pages
-                          :go/flashcards
-                          :go/graph-view
-                          :go/all-graphs
-                          :go/whiteboards
-                          :go/keyboard-shortcuts
-                          :go/tomorrow
-                          :go/next-journal
-                          :go/prev-journal
-                          :ui/toggle-document-mode
-                          :ui/toggle-settings
-                          :ui/toggle-right-sidebar
-                          :ui/toggle-left-sidebar
-                          :ui/toggle-help
-                          :ui/toggle-theme
-                          :ui/toggle-contents
-                          :editor/open-file-in-default-app
-                          :editor/open-file-in-directory
-                          :editor/copy-current-file
-                          :editor/copy-page-url
-                          :editor/new-whiteboard
-                          :ui/toggle-wide-mode
-                          :ui/select-theme-color
-                          :ui/goto-plugins
-                          :ui/install-plugins-from-file
-                          :editor/toggle-open-blocks
-                          :ui/toggle-cards
-                          :ui/clear-all-notifications
-                          :git/commit
-                          :sidebar/close-top
-                          :dev/show-block-data
-                          :dev/show-block-ast
-                          :dev/show-page-data
-                          :dev/show-page-ast])
-     (with-meta {:before m/enable-when-not-editing-mode!}))}))
+    {:misc/copy (:misc/copy all-built-in-keyboard-shortcuts)}}))
 
 ;; To add a new entry to this map, first add it here and then
 ;; a description for it in frontend.dicts.en/dicts
-(def ^:large-vars/data-var category*
-  "Full list of categories for docs purpose"
-  {:shortcut.category/basics
-   [:editor/new-block
-    :editor/new-line
-    :editor/indent
-    :editor/outdent
-    :editor/select-all-blocks
-    :editor/select-parent
-    :go/search
-    :go/search-in-page
-    :go/electron-find-in-page
-    :go/electron-jump-to-the-next
-    :go/electron-jump-to-the-previous
-    :editor/undo
-    :editor/redo
-    :editor/copy
-    :editor/copy-text
-    :editor/cut]
-
-   :shortcut.category/formatting
-   [:editor/bold
-    :editor/insert-link
-    :editor/italics
-    :editor/strike-through
-    :editor/highlight]
-
-   :shortcut.category/navigating
-   [:editor/up
-    :editor/down
-    :editor/left
-    :editor/right
-    :editor/zoom-in
-    :editor/zoom-out
-    :editor/collapse-block-children
-    :editor/expand-block-children
-    :editor/toggle-open-blocks
-    :go/backward
-    :go/forward
-    :go/home
-    :go/journals
-    :go/all-pages
-    :go/graph-view
-    :go/all-graphs
-    :go/whiteboards
-    :go/flashcards
-    :go/tomorrow
-    :go/next-journal
-    :go/prev-journal
-    :go/keyboard-shortcuts]
-
-   :shortcut.category/block-editing
-   [:editor/backspace
-    :editor/delete
-    :editor/indent
-    :editor/outdent
-    :editor/new-block
-    :editor/new-line
-    :editor/zoom-in
-    :editor/zoom-out
-    :editor/cycle-todo
-    :editor/follow-link
-    :editor/open-link-in-sidebar
-    :editor/move-block-up
-    :editor/move-block-down
-    :editor/escape-editing]
-
-   :shortcut.category/block-command-editing
-   [:editor/backspace
-    :editor/clear-block
-    :editor/kill-line-before
-    :editor/kill-line-after
-    :editor/beginning-of-block
-    :editor/end-of-block
-    :editor/forward-word
-    :editor/backward-word
-    :editor/forward-kill-word
-    :editor/backward-kill-word
-    :editor/replace-block-reference-at-point
-    :editor/copy-embed
-    :editor/paste-text-in-one-block-at-point
-    :editor/select-up
-    :editor/select-down]
-
-   :shortcut.category/block-selection
-   [:editor/open-edit
-    :editor/select-all-blocks
-    :editor/select-parent
-    :editor/select-block-up
-    :editor/select-block-down
-    :editor/delete-selection]
-
-   :shortcut.category/toggle
-   [:ui/toggle-help
-    :editor/toggle-open-blocks
-    :editor/toggle-undo-redo-mode
-    :editor/toggle-number-list
-    :ui/toggle-wide-mode
-    :ui/toggle-cards
-    :ui/toggle-document-mode
-    :ui/toggle-brackets
-    :ui/toggle-theme
-    :ui/toggle-left-sidebar
-    :ui/toggle-right-sidebar
-    :ui/toggle-settings
-    :ui/toggle-contents]
-
-   :shortcut.category/whiteboard
-   [:editor/new-whiteboard
-    :whiteboard/select
-    :whiteboard/pan
-    :whiteboard/portal
-    :whiteboard/pencil
-    :whiteboard/highlighter
-    :whiteboard/eraser
-    :whiteboard/connector
-    :whiteboard/text
-    :whiteboard/rectangle
-    :whiteboard/ellipse
-    :whiteboard/reset-zoom
-    :whiteboard/zoom-to-fit
-    :whiteboard/zoom-to-selection
-    :whiteboard/zoom-out
-    :whiteboard/zoom-in
-    :whiteboard/send-backward
-    :whiteboard/send-to-back
-    :whiteboard/bring-forward
-    :whiteboard/bring-to-front
-    :whiteboard/lock
-    :whiteboard/unlock
-    :whiteboard/group
-    :whiteboard/ungroup
-    :whiteboard/toggle-grid]
-
-   :shortcut.category/others
-   [:pdf/previous-page
-    :pdf/next-page
-    :pdf/close
-    :pdf/find
-    :command/toggle-favorite
-    :command/run
-    :command-palette/toggle
-    :graph/export-as-html
-    :graph/open
-    :graph/remove
-    :graph/add
-    :graph/save
-    :graph/re-index
-    :sidebar/close-top
-    :sidebar/clear
-    :sidebar/open-today-page
-    :search/re-index
-    :editor/insert-youtube-timestamp
-    :editor/open-file-in-default-app
-    :editor/open-file-in-directory
-    :editor/copy-page-url
-    :auto-complete/prev
-    :auto-complete/next
-    :auto-complete/complete
-    :auto-complete/shift-complete
-    :auto-complete/open-link
-    :date-picker/prev-day
-    :date-picker/next-day
-    :date-picker/prev-week
-    :date-picker/next-week
-    :date-picker/complete
-    :git/commit
-    :dev/show-block-data
-    :dev/show-block-ast
-    :dev/show-page-data
-    :dev/show-page-ast
-    :ui/clear-all-notifications]
-
-   :shortcut.category/plugins
-   []})
-
-(let [category-maps {::category (set (keys category*))
+;; Full list of categories for docs purpose
+(defonce ^:large-vars/data-var *category
+  (atom
+   {:shortcut.category/basics
+    [:editor/new-block
+     :editor/new-line
+     :editor/indent
+     :editor/outdent
+     :editor/select-all-blocks
+     :editor/select-parent
+     :go/search
+     :go/search-in-page
+     :go/electron-find-in-page
+     :go/electron-jump-to-the-next
+     :go/electron-jump-to-the-previous
+     :editor/undo
+     :editor/redo
+     :editor/copy
+     :editor/copy-text
+     :editor/cut]
+
+    :shortcut.category/formatting
+    [:editor/bold
+     :editor/insert-link
+     :editor/italics
+     :editor/strike-through
+     :editor/highlight]
+
+    :shortcut.category/navigating
+    [:editor/up
+     :editor/down
+     :editor/left
+     :editor/right
+     :editor/collapse-block-children
+     :editor/expand-block-children
+     :editor/toggle-open-blocks
+     :go/backward
+     :go/forward
+     :go/home
+     :go/journals
+     :go/all-pages
+     :go/graph-view
+     :go/all-graphs
+     :go/whiteboards
+     :go/flashcards
+     :go/tomorrow
+     :go/next-journal
+     :go/prev-journal
+     :go/keyboard-shortcuts]
+
+    :shortcut.category/block-editing
+    [:editor/backspace
+     :editor/delete
+     :editor/indent
+     :editor/outdent
+     :editor/new-block
+     :editor/new-line
+     :editor/zoom-in
+     :editor/zoom-out
+     :editor/cycle-todo
+     :editor/follow-link
+     :editor/open-link-in-sidebar
+     :editor/move-block-up
+     :editor/move-block-down
+     :editor/escape-editing]
+
+    :shortcut.category/block-command-editing
+    [:editor/backspace
+     :editor/clear-block
+     :editor/kill-line-before
+     :editor/kill-line-after
+     :editor/beginning-of-block
+     :editor/end-of-block
+     :editor/forward-word
+     :editor/backward-word
+     :editor/forward-kill-word
+     :editor/backward-kill-word
+     :editor/replace-block-reference-at-point
+     :editor/copy-embed
+     :editor/paste-text-in-one-block-at-point
+     :editor/select-up
+     :editor/select-down]
+
+    :shortcut.category/block-selection
+    [:editor/open-edit
+     :editor/select-all-blocks
+     :editor/select-parent
+     :editor/select-block-up
+     :editor/select-block-down
+     :editor/delete-selection]
+
+    :shortcut.category/toggle
+    [:ui/toggle-help
+     :editor/toggle-open-blocks
+     :editor/toggle-undo-redo-mode
+     :editor/toggle-number-list
+     :ui/toggle-wide-mode
+     :ui/toggle-cards
+     :ui/toggle-document-mode
+     :ui/toggle-brackets
+     :ui/toggle-theme
+     :ui/toggle-left-sidebar
+     :ui/toggle-right-sidebar
+     :ui/toggle-settings
+     :ui/toggle-contents]
+
+    :shortcut.category/whiteboard
+    [:editor/new-whiteboard
+     :whiteboard/select
+     :whiteboard/pan
+     :whiteboard/portal
+     :whiteboard/pencil
+     :whiteboard/highlighter
+     :whiteboard/eraser
+     :whiteboard/connector
+     :whiteboard/text
+     :whiteboard/rectangle
+     :whiteboard/ellipse
+     :whiteboard/reset-zoom
+     :whiteboard/zoom-to-fit
+     :whiteboard/zoom-to-selection
+     :whiteboard/zoom-out
+     :whiteboard/zoom-in
+     :whiteboard/send-backward
+     :whiteboard/send-to-back
+     :whiteboard/bring-forward
+     :whiteboard/bring-to-front
+     :whiteboard/lock
+     :whiteboard/unlock
+     :whiteboard/group
+     :whiteboard/ungroup
+     :whiteboard/toggle-grid]
+
+    :shortcut.category/others
+    [:pdf/previous-page
+     :pdf/next-page
+     :pdf/close
+     :pdf/find
+     :command/toggle-favorite
+     :command/run
+     :command-palette/toggle
+     :graph/export-as-html
+     :graph/open
+     :graph/remove
+     :graph/add
+     :graph/save
+     :graph/re-index
+     :sidebar/close-top
+     :sidebar/clear
+     :sidebar/open-today-page
+     :search/re-index
+     :editor/insert-youtube-timestamp
+     :editor/open-file-in-default-app
+     :editor/open-file-in-directory
+     :editor/copy-page-url
+     :auto-complete/prev
+     :auto-complete/next
+     :auto-complete/complete
+     :auto-complete/shift-complete
+     :auto-complete/open-link
+     :date-picker/prev-day
+     :date-picker/next-day
+     :date-picker/prev-week
+     :date-picker/next-week
+     :date-picker/complete
+     :git/commit
+     :dev/show-block-data
+     :dev/show-block-ast
+     :dev/show-page-data
+     :dev/show-page-ast
+     :ui/clear-all-notifications]
+
+    :shortcut.category/plugins
+    []}))
+
+(let [category-maps {::category       (set (keys @*category))
                      ::dicts/category dicts/categories}]
   (assert (= (::category category-maps) (::dicts/category category-maps))
           (str "Keys for category maps must have an english label "
                (data/diff (::category category-maps) (::dicts/category category-maps)))))
 
-(def category
+(defn get-category-shortcuts
   "Active list of categories for docs purpose"
-  (update-vals
-   category*
-   (fn [v]
-     (vec (remove #(:inactive (get all-default-keyboard-shortcuts %)) v)))))
+  [name]
+  (get @*category name))
 
 (def *shortcut-cmds (atom {}))
 
 (defn add-shortcut!
   [handler-id id shortcut-map]
-  (swap! config assoc-in [handler-id id] shortcut-map)
-  (swap! *shortcut-cmds assoc id (:cmd shortcut-map)))
+  (swap! *config assoc-in [handler-id id] shortcut-map)
+  (swap! *shortcut-cmds assoc id (:cmd shortcut-map))
+  (let [plugin? (str/starts-with? (str id) ":plugin.")
+        category (or (:category shortcut-map)
+                     (if plugin?
+                       :shortcut.category/plugins
+                       :shortcut.category/others))]
+    (swap! *category update category #(conj % id))))
 
 (defn remove-shortcut!
   [handler-id id]
-  (swap! config medley/dissoc-in [handler-id id])
-  (swap! *shortcut-cmds dissoc id))
+  (swap! *config medley/dissoc-in [handler-id id])
+  (swap! *shortcut-cmds dissoc id)
+  (doseq [category (keys @*category)]
+    (swap! *category update category (fn [ids] (remove #(= % id) ids)))))

+ 158 - 74
src/main/frontend/modules/shortcut/core.cljs

@@ -1,9 +1,12 @@
 (ns frontend.modules.shortcut.core
   (:require [clojure.string :as str]
             [frontend.handler.config :as config-handler]
+            [frontend.handler.global-config :as global-config-handler]
+            [frontend.handler.plugin :as plugin-handler]
             [frontend.handler.notification :as notification]
             [frontend.modules.shortcut.data-helper :as dh]
             [frontend.modules.shortcut.config :as shortcut-config]
+            [frontend.modules.shortcut.utils :as shortcut-utils]
             [frontend.state :as state]
             [frontend.util :as util]
             [goog.events :as events]
@@ -13,15 +16,15 @@
   (:import [goog.events KeyCodes KeyHandler KeyNames]
            [goog.ui KeyboardShortcutHandler]))
 
-(def *installed (atom {}))
-(def *inited? (atom false))
-(def *pending (atom []))
+(defonce *installed-handlers (atom {}))
+(defonce *pending-inited? (atom false))
+(defonce *pending-shortcuts (atom []))
 
 (def global-keys #js
-                  [KeyCodes/TAB
-                   KeyCodes/ENTER
-                   KeyCodes/BACKSPACE KeyCodes/DELETE
-                   KeyCodes/UP KeyCodes/LEFT KeyCodes/DOWN KeyCodes/RIGHT])
+        [KeyCodes/TAB
+         KeyCodes/ENTER
+         KeyCodes/BACKSPACE KeyCodes/DELETE
+         KeyCodes/UP KeyCodes/LEFT KeyCodes/DOWN KeyCodes/RIGHT])
 
 (def key-names (js->clj KeyNames))
 
@@ -29,16 +32,25 @@
 
 (defn consume-pending-shortcuts!
   []
-  (when (and @*inited? (seq @*pending))
-    (doseq [[handler-id id shortcut] @*pending]
+  (when (and @*pending-inited? (seq @*pending-shortcuts))
+    (doseq [[handler-id id shortcut] @*pending-shortcuts]
       (register-shortcut! handler-id id shortcut))
-    (reset! *pending [])))
+    (reset! *pending-shortcuts [])))
 
 (defn- get-handler-by-id
   [handler-id]
-  (-> (filter #(= (:group %) handler-id) (vals @*installed))
-      first
-      :handler))
+  (->> (vals @*installed-handlers)
+       (filter #(= (:group %) handler-id))
+       first
+       :handler))
+
+(defn- get-installed-ids-by-handler-id
+  [handler-id]
+  (some->> @*installed-handlers
+           (filter #(= (:group (second %)) handler-id))
+           (map first)
+           (remove nil?)
+           (vec)))
 
 (defn register-shortcut!
   "Register a shortcut, notice the id need to be a namespaced keyword to avoid
@@ -50,14 +62,14 @@
   ([handler-id id]
    (register-shortcut! handler-id id nil))
   ([handler-id id shortcut-map]
-   (if (and (keyword? handler-id) (not @*inited?))
-     (swap! *pending conj [handler-id id shortcut-map])
-     (when-let [handler (if (or (string? handler-id) (keyword? handler-id))
-                          (let [handler-id (keyword handler-id)]
-                            (get-handler-by-id handler-id))
+   (if (and (keyword? handler-id) (not @*pending-inited?))
+     (swap! *pending-shortcuts conj [handler-id id shortcut-map])
+     (when-let [^js handler (if (or (string? handler-id) (keyword? handler-id))
+                              (let [handler-id (keyword handler-id)]
+                                (get-handler-by-id handler-id))
 
-                          ;; handler
-                          handler-id)]
+                              ;; as Handler instance
+                              handler-id)]
 
        (when shortcut-map
          (shortcut-config/add-shortcut! handler-id id shortcut-map))
@@ -66,7 +78,7 @@
          (doseq [k (dh/shortcut-binding id)]
            (try
              (log/debug :shortcut/register-shortcut {:id id :binding k})
-             (.registerShortcut handler (util/keyname id) (util/normalize-user-keyname k))
+             (.registerShortcut handler (util/keyname id) (shortcut-utils/undecorate-binding k))
              (catch :default e
                (log/error :shortcut/register-shortcut {:id      id
                                                        :binding k
@@ -81,15 +93,17 @@
   (when-let [handler (get-handler-by-id handler-id)]
     (when-let [ks (dh/shortcut-binding shortcut-id)]
       (doseq [k ks]
-        (.unregisterShortcut ^js handler (util/normalize-user-keyname k))))
+        (.unregisterShortcut ^js handler (shortcut-utils/undecorate-binding k))))
     (shortcut-config/remove-shortcut! handler-id shortcut-id)))
 
 (defn uninstall-shortcut-handler!
-  [install-id]
-  (when-let [handler (-> (get @*installed install-id)
-                         :handler)]
-    (.dispose ^js handler)
-    (swap! *installed dissoc install-id)))
+  ([install-id] (uninstall-shortcut-handler! install-id false))
+  ([install-id refresh?]
+   (when-let [handler (-> (get @*installed-handlers install-id)
+                          :handler)]
+     (.dispose ^js handler)
+     (log/debug :shortcuts/uninstall-handler (-> @*installed-handlers (get install-id) :group (str (if refresh? "*" ""))))
+     (swap! *installed-handlers dissoc install-id))))
 
 (defn install-shortcut-handler!
   [handler-id {:keys [set-global-keys?
@@ -97,11 +111,15 @@
                       state]
                :or   {set-global-keys? true
                       prevent-default? false}}]
-  (when-let [install-id (get-handler-by-id handler-id)]
-    (uninstall-shortcut-handler! install-id))
+
+  ;; force uninstall existed handler
+  (some->>
+    (get-installed-ids-by-handler-id handler-id)
+    (map #(uninstall-shortcut-handler! % true))
+    (doall))
 
   (let [shortcut-map (dh/shortcut-map handler-id state)
-        handler      (new KeyboardShortcutHandler js/window)]
+        handler (new KeyboardShortcutHandler js/window)]
     ;; set arrows enter, tab to global
     (when set-global-keys?
       (.setGlobalKeys handler global-keys))
@@ -114,66 +132,109 @@
       (register-shortcut! handler id))
 
     (let [f (fn [e]
-              (let [shortcut-map (dh/shortcut-map handler-id state)
-                    dispatch-fn (get shortcut-map (keyword (.-identifier e)))]
+              (let [id (keyword (.-identifier e))
+                    shortcut-map (dh/shortcut-map handler-id state) ;; required to get shortcut map dynamically
+                    dispatch-fn (get shortcut-map id)]
                 ;; trigger fn
-                (when dispatch-fn (dispatch-fn e))))
+                (when dispatch-fn
+                  (plugin-handler/hook-lifecycle-fn! id dispatch-fn e))))
           install-id (random-uuid)
-          data       {install-id
-                      {:group      handler-id
-                       :dispatch-fn f
-                       :handler    handler}}]
+          data {install-id
+                {:group       handler-id
+                 :dispatch-fn f
+                 :handler     handler}}]
 
       (.listen handler EventType/SHORTCUT_TRIGGERED f)
 
-      (swap! *installed merge data)
+      (log/debug :shortcuts/install-handler (str handler-id))
+      (swap! *installed-handlers merge data)
 
       install-id)))
 
 (defn- install-shortcuts!
-  []
-  (->> [:shortcut.handler/misc
-        :shortcut.handler/editor-global
-        :shortcut.handler/global-non-editing-only
-        :shortcut.handler/global-prevent-default]
+  [handler-ids]
+  (->> (or (seq handler-ids)
+           [:shortcut.handler/misc
+            :shortcut.handler/editor-global
+            :shortcut.handler/global-non-editing-only
+            :shortcut.handler/global-prevent-default])
        (map #(install-shortcut-handler! % {}))
        doall))
 
-(defn mixin [handler-id]
+(defn mixin
+  ([handler-id] (mixin handler-id true))
+  ([handler-id remount-reinstall?]
+   (cond->
+     {:did-mount
+      (fn [state]
+        (let [install-id (install-shortcut-handler! handler-id {:state state})]
+          (assoc state ::install-id install-id)))
+
+      :will-unmount
+      (fn [state]
+        (when-let [install-id (::install-id state)]
+          (uninstall-shortcut-handler! install-id))
+        state)}
+
+     remount-reinstall?
+     (assoc
+       :will-remount
+       (fn [old-state new-state]
+         (util/profile "[shortcuts] reinstalled:"
+                       (uninstall-shortcut-handler! (::install-id old-state))
+                       (when-let [install-id (install-shortcut-handler! handler-id {:state new-state})]
+                         (assoc new-state ::install-id install-id))))))))
+
+(defn mixin*
+  "This is an optimized version compared to (mixin).
+   And the shortcuts will not be frequently loaded and unloaded.
+   As well as ensuring unnecessary updates of components."
+  [handler-id]
   {:did-mount
    (fn [state]
-     (let [install-id (install-shortcut-handler! handler-id {:state state})]
-       (assoc state ::install-id install-id)))
+     (let [*state (volatile! state)
+           install-id (install-shortcut-handler! handler-id {:state *state})]
+       (assoc state ::install-id install-id
+                    ::*state *state)))
+
+   :will-remount
+   (fn [old-state new-state]
+     (when-let [*state (::*state old-state)]
+       (vreset! *state new-state))
+     new-state)
 
-   :will-remount (fn [old-state new-state]
-                  (uninstall-shortcut-handler! (::install-id old-state))
-                  (when-let [install-id (install-shortcut-handler! handler-id {:state new-state})]
-                    (assoc new-state ::install-id install-id)))
    :will-unmount
    (fn [state]
      (when-let [install-id (::install-id state)]
-       (uninstall-shortcut-handler! install-id))
+       (uninstall-shortcut-handler! install-id)
+       (some-> (::*state state) (vreset! nil)))
      state)})
 
-(defn unlisten-all []
-  (doseq [{:keys [handler group]} (vals @*installed)
-          :when (not= group :shortcut.handler/misc)]
-    (.removeAllListeners handler)))
-
-(defn listen-all []
-  (doseq [{:keys [handler group dispatch-fn]} (vals @*installed)
+(defn unlisten-all!
+  ([] (unlisten-all! false))
+  ([dispose?]
+   (doseq [{:keys [handler group dispatch-fn]} (vals @*installed-handlers)
+           :when (not= group :shortcut.handler/misc)]
+     (if dispose?
+       (.dispose handler)
+       (events/unlisten handler EventType/SHORTCUT_TRIGGERED dispatch-fn)))))
+
+(defn listen-all! []
+  (doseq [{:keys [handler group dispatch-fn]} (vals @*installed-handlers)
           :when (not= group :shortcut.handler/misc)]
-    (events/listen handler EventType/SHORTCUT_TRIGGERED dispatch-fn)))
+    (if (.isDisposed handler)
+      (install-shortcut-handler! group {})
+      (events/listen handler EventType/SHORTCUT_TRIGGERED dispatch-fn))))
 
 (def disable-all-shortcuts
   {:will-mount
    (fn [state]
-     (unlisten-all)
+     (unlisten-all!)
      state)
 
    :will-unmount
    (fn [state]
-     (listen-all)
+     (listen-all!)
      state)})
 
 (defn refresh-internal!
@@ -182,27 +243,29 @@
   (when-not (:ui/shortcut-handler-refreshing? @state/state)
     (state/set-state! :ui/shortcut-handler-refreshing? true)
 
-    (doseq [id (keys @*installed)]
-      (uninstall-shortcut-handler! id))
-    (install-shortcuts!)
+    (let [ids (keys @*installed-handlers)
+          _handler-ids (set (map :group (vals @*installed-handlers)))]
+      (doseq [id ids] (uninstall-shortcut-handler! id))
+      ;; TODO: should re-install existed handlers
+      (install-shortcuts! nil))
     (state/pub-event! [:shortcut-handler-refreshed])
     (state/set-state! :ui/shortcut-handler-refreshing? false)))
 
 (def refresh! (debounce refresh-internal! 1000))
 
 (defn- name-with-meta [e]
-  (let [ctrl    (.-ctrlKey e)
-        alt     (.-altKey e)
-        meta    (.-metaKey e)
-        shift   (.-shiftKey e)
+  (let [ctrl (.-ctrlKey e)
+        alt (.-altKey e)
+        meta (.-metaKey e)
+        shift (.-shiftKey e)
         keyname (get key-names (str (.-keyCode e)))]
     (cond->> keyname
-      ctrl  (str "ctrl+")
-      alt   (str "alt+")
-      meta  (str "meta+")
-      shift (str "shift+"))))
+             ctrl (str "ctrl+")
+             alt (str "alt+")
+             meta (str "meta+")
+             shift (str "shift+"))))
 
-(defn- keyname [e]
+(defn keyname [e]
   (let [name (get key-names (str (.-keyCode e)))]
     (case name
       nil nil
@@ -215,7 +278,7 @@
      (let [handler (KeyHandler. js/document)
            keystroke (:rum/local state)]
 
-       (doseq [id (keys @*installed)]
+       (doseq [id (keys @*installed-handlers)]
          (uninstall-shortcut-handler! id))
 
        (events/listen handler "key"
@@ -240,6 +303,27 @@
      (when-let [^js handler (::key-record-handler state)]
        (.dispose handler))
 
+     ;; force re-install shortcut handlers
      (js/setTimeout #(refresh!) 500)
 
      (dissoc state ::key-record-handler))})
+
+(defn persist-user-shortcut!
+  [id binding]
+  (let [graph-shortcuts (or (:shortcuts (state/get-graph-config)) {})
+        global-shortcuts (or (:shortcuts (state/get-global-config)) {})
+        global? true]
+    (letfn [(into-shortcuts [shortcuts]
+              (cond-> shortcuts
+                      (nil? binding)
+                      (dissoc id)
+
+                      (and global?
+                           (or (string? binding)
+                               (vector? binding)
+                               (boolean? binding)))
+                      (assoc id binding)))]
+      ;; TODO: exclude current graph config shortcuts
+      (when (nil? binding)
+        (config-handler/set-config! :shortcuts (into-shortcuts graph-shortcuts)))
+      (global-config-handler/set-global-config-kv! :shortcuts (into-shortcuts global-shortcuts)))))

+ 173 - 73
src/main/frontend/modules/shortcut/data_helper.cljs

@@ -1,11 +1,14 @@
 (ns frontend.modules.shortcut.data-helper
   (:require [borkdude.rewrite-edn :as rewrite]
+            [clojure.set :refer [rename-keys] :as set]
             [clojure.string :as str]
-            [clojure.set :refer [rename-keys]]
+            [cljs-bean.core :as bean]
+            [frontend.context.i18n :refer [t]]
             [frontend.config :as config]
             [frontend.db :as db]
             [frontend.handler.file :as file]
             [frontend.modules.shortcut.config :as shortcut-config]
+            [frontend.modules.shortcut.utils :as shortcut-utils]
             [frontend.state :as state]
             [frontend.util :as util]
             [lambdaisland.glogi :as log]
@@ -13,29 +16,74 @@
             [frontend.handler.config :as config-handler])
   (:import [goog.ui KeyboardShortcutHandler]))
 
+(declare get-group)
+
 ;; function vals->bindings is too time-consuming. Here we cache the results.
-(defn- flatten-key-bindings
-  [config]
-  (->> config
-       (into {})
-       (map (fn [[k {:keys [binding]}]]
-              {k binding}))
+(defn- flatten-bindings-by-id
+  [config user-shortcuts binding-only?]
+  (->> (vals config)
+       (apply merge)
+       (map (fn [[id {:keys [binding] :as opts}]]
+              {id (if binding-only?
+                    (get user-shortcuts id binding)
+                    (assoc opts :user-binding (get user-shortcuts id)
+                                :handler-id (get-group id)
+                                :id id))}))
        (into {})))
 
-(def m-flatten-key-bindings (util/memoize-last flatten-key-bindings))
+(defn- flatten-bindings-by-key
+  [config user-shortcuts]
+  (reduce-kv
+    (fn [r handler-id vs]
+      (reduce-kv
+        (fn [r id {:keys [binding]}]
+          (if-let [ks (get user-shortcuts id binding)]
+            (let [ks (if (sequential? ks) ks [ks])]
+              (reduce (fn [a k]
+                        (let [k (shortcut-utils/undecorate-binding k)
+                              k' (shortcut-utils/safe-parse-string-binding k)
+                              k' (bean/->clj k')]
+                          (-> a
+                              (assoc-in [k' :key] k)
+                              (assoc-in [k' :refs id] handler-id)))) r ks))
+            r)) r vs))
+    {} config))
+
+(def m-flatten-bindings-by-id
+  (util/memoize-last flatten-bindings-by-id))
+
+(def m-flatten-bindings-by-key
+  (util/memoize-last flatten-bindings-by-key))
 
 (defn get-bindings
   []
-  (m-flatten-key-bindings (vals @shortcut-config/config)))
+  (m-flatten-bindings-by-id @shortcut-config/*config (state/shortcuts) true))
 
-(defn- mod-key [shortcut]
-  (str/replace shortcut #"(?i)mod"
-               (if util/mac? "meta" "ctrl")))
+(defn get-bindings-keys-map
+  []
+  (m-flatten-bindings-by-key @shortcut-config/*config (state/shortcuts)))
+
+(defn get-bindings-ids-map
+  []
+  (m-flatten-bindings-by-id @shortcut-config/*config (state/shortcuts) false))
+
+(defn get-shortcut-desc
+  [binding-map]
+  (let [{:keys [id desc cmd]} binding-map
+        desc (or desc (:desc cmd) (some-> id (shortcut-utils/decorate-namespace) (t)))]
+    (if (or (nil? desc)
+            (and (string? desc) (str/starts-with? desc "{Missing")))
+      (str id) desc)))
+
+(defn mod-key [shortcut]
+  (when (string? shortcut)
+    (str/replace shortcut #"(?i)mod"
+                 (if util/mac? "meta" "ctrl"))))
 
 (defn shortcut-binding
+  "override by user custom binding"
   [id]
-  (let [shortcut (get (state/shortcuts) id
-                      (get (get-bindings) id))]
+  (let [shortcut (get (get-bindings) id)]
     (cond
       (nil? shortcut)
       (log/warn :shortcut/binding-not-found {:id id})
@@ -47,62 +95,48 @@
 
       :else
       (->>
-       (if (string? shortcut)
-         [shortcut]
-         shortcut)
-       (mapv mod-key)))))
+        (if (string? shortcut)
+          [shortcut]
+          shortcut)
+        (mapv mod-key)))))
 
 (defn shortcut-cmd
   [id]
   (get @shortcut-config/*shortcut-cmds id))
 
+(defn shortcut-item
+  [id]
+  (get (get-bindings-ids-map) id))
+
 ;; returns a vector to preserve order
 (defn binding-by-category [name]
-  (let [dict    (->> (vals @shortcut-config/config)
-                     (apply merge)
-                     (map (fn [[k _]]
-                            {k {:binding (shortcut-binding k)}}))
-                     (into {}))
+  (let [dict (get-bindings-ids-map)
         plugin? (= name :shortcut.category/plugins)]
     (->> (if plugin?
            (->> (keys dict) (filter #(str/starts-with? (str %) ":plugin.")))
-           (shortcut-config/category name))
-         (mapv (fn [k] [k (k dict)])))))
+           (shortcut-config/get-category-shortcuts name))
+         (mapv (fn [k] [k (assoc (get dict k) :category name)])))))
 
 (defn shortcut-map
   ([handler-id]
    (shortcut-map handler-id nil))
   ([handler-id state]
-   (let [raw       (get @shortcut-config/config handler-id)
+   (let [raw (get @shortcut-config/*config handler-id)
          handler-m (->> raw
                         (map (fn [[k {:keys [fn]}]]
                                {k fn}))
                         (into {}))
-         before    (-> raw meta :before)]
+         before (-> raw meta :before)]
      (cond->> handler-m
-       state  (reduce-kv (fn [r k handle-fn]
-                           (assoc r k (partial handle-fn state)))
-                         {})
-       before (reduce-kv (fn [r k v]
-                           (assoc r k (before v)))
-                         {})))))
-
-(defn decorate-namespace [k]
-  (let [n (name k)
-        ns (namespace k)]
-    (keyword (str "command." ns) n)))
-
-(defn decorate-binding [binding]
-  (-> (if (string? binding) binding (str/join "+"  binding))
-      (str/replace "mod" (if util/mac? "⌘" "ctrl"))
-      (str/replace "alt" (if util/mac? "opt" "alt"))
-      (str/replace "shift+/" "?")
-      (str/replace "left" "←")
-      (str/replace "right" "→")
-      (str/replace "shift" "⇧")
-      (str/replace "open-square-bracket" "[")
-      (str/replace "close-square-bracket" "]")
-      (str/lower-case)))
+              state (reduce-kv (fn [r k handle-fn]
+                                 (let [handle-fn' (if (volatile? state)
+                                                    (fn [*state & args] (apply handle-fn (cons @*state args)))
+                                                    handle-fn)]
+                                   (assoc r k (partial handle-fn' state))))
+                               {})
+              before (reduce-kv (fn [r k v]
+                                  (assoc r k (before v)))
+                                {})))))
 
 ;; if multiple bindings, gen seq for first binding only for now
 (defn gen-shortcut-seq [id]
@@ -111,24 +145,24 @@
       []
       (-> bindings
           first
-          (str/split  #" |\+")))))
+          (str/split #" |\+")))))
 
 (defn binding-for-display [k binding]
   (let [tmp (cond
               (false? binding)
               (cond
-                (and util/mac? (= k :editor/kill-line-after))    "system default: ctrl+k"
+                (and util/mac? (= k :editor/kill-line-after)) "system default: ctrl+k"
                 (and util/mac? (= k :editor/beginning-of-block)) "system default: ctrl+a"
-                (and util/mac? (= k :editor/end-of-block))       "system default: ctrl+e"
+                (and util/mac? (= k :editor/end-of-block)) "system default: ctrl+e"
                 (and util/mac? (= k :editor/backward-kill-word)) "system default: opt+delete"
-                :else "disabled")
+                :else (t :keymap/disabled))
 
               (string? binding)
-              (decorate-binding binding)
+              (shortcut-utils/decorate-binding binding)
 
               :else
               (->> binding
-                   (map decorate-binding)
+                   (map shortcut-utils/decorate-binding)
                    (str/join " | ")))]
 
     ;; Display "cmd" rather than "meta" to the user to describe the Mac
@@ -157,26 +191,92 @@
   "Given shortcut key, return handler group
   eg: :editor/new-line -> :shortcut.handler/block-editing-only"
   [k]
-  (->> @shortcut-config/config
+  (->> @shortcut-config/*config
        (filter (fn [[_ v]] (contains? v k)))
        (map key)
        (first)))
 
-(defn potential-conflict? [k]
-  (if-not (shortcut-binding k)
+(defn should-be-included-to-global-handler
+  [from-handler-id]
+  (if (contains? #{:shortcut.handler/pdf} from-handler-id)
+    #{from-handler-id :shortcut.handler/global-prevent-default}
+    #{from-handler-id}))
+
+(defn get-conflicts-by-keys
+  ([ks] (get-conflicts-by-keys ks :shortcut.handler/global-prevent-default {:group-global? true}))
+  ([ks handler-id] (get-conflicts-by-keys ks handler-id {:group-global? true}))
+  ([ks handler-id {:keys [exclude-ids group-global?]}]
+   (let [global-handlers #{:shortcut.handler/editor-global
+                           :shortcut.handler/global-non-editing-only
+                           :shortcut.handler/global-prevent-default
+                           :shortcut.handler/misc}
+         ks-bindings (get-bindings-keys-map)
+         handler-ids (should-be-included-to-global-handler handler-id)
+         global? (when group-global? (seq (set/intersection global-handlers handler-ids)))]
+     (->> (if (string? ks) [ks] ks)
+          (map (fn [k]
+                 (when-let [k' (shortcut-utils/undecorate-binding k)]
+                   (let [k (shortcut-utils/safe-parse-string-binding k')
+                         k (bean/->clj k)
+
+                         same-leading-key?
+                         (fn [[k' _]]
+                           (when (sequential? k)
+                             (or (= k k')
+                                 (and (> (count k') (count k))
+                                      (= (first k) (first k'))))))
+
+                         into-conflict-refs
+                         (fn [[k o]]
+                           (when-let [{:keys [key refs]} o]
+                             [k [key (reduce-kv (fn [r id handler-id']
+                                                  (if (and
+                                                        (not (contains? exclude-ids id))
+                                                        (or (= handler-ids #{handler-id'})
+                                                            (and (set? handler-ids) (contains? handler-ids handler-id'))
+                                                            (and global? (contains? global-handlers handler-id'))))
+                                                    (assoc r id handler-id')
+                                                    r)
+                                                  ) {} refs)]]))]
+
+                     [k' (->> ks-bindings
+                              (filterv same-leading-key?)
+                              (mapv into-conflict-refs)
+                              (remove #(empty? (second (second %1))))
+                              (into {}))]
+                     ))))
+          (remove #(empty? (vals (second %1))))
+          (into {})))))
+
+(defn parse-conflicts-from-binding
+  [from-binding target]
+  (when-let [from-binding (and (string? target)
+                               (sequential? from-binding)
+                               (seq from-binding))]
+    (when-let [target (some-> target (mod-key) (shortcut-utils/safe-parse-string-binding) (bean/->clj))]
+      (->> from-binding
+           (filterv
+             #(when-let [from (some-> % (mod-key) (shortcut-utils/safe-parse-string-binding) (bean/->clj))]
+                (or (= from target)
+                    (and (or (= (count from) 1)
+                             (= (count target) 1))
+                         (= (first target) (first from))))))))))
+
+(defn potential-conflict? [shortcut-id]
+  (if-not (shortcut-binding shortcut-id)
     false
-    (let [handler-id    (get-group k)
-          shortcut-m    (shortcut-map handler-id)
+    (let [handler-id (get-group shortcut-id)
+          shortcut-m (shortcut-map handler-id)
           parse-shortcut #(try
-                           (KeyboardShortcutHandler/parseStringShortcut %)
-                           (catch :default e
-                             (js/console.error "[shortcut/parse-error]" (str % " - " (.-message e)))))
-          bindings      (->> (shortcut-binding k)
-                             (map mod-key)
-                             (map parse-shortcut)
-                             (map js->clj))
+                            (KeyboardShortcutHandler/parseStringShortcut %)
+                            (catch :default e
+                              (js/console.error "[shortcut/parse-error]" (str % " - " (.-message e)))))
+          bindings (->> (shortcut-binding shortcut-id)
+                        (map mod-key)
+                        (map parse-shortcut)
+                        (map js->clj))
           rest-bindings (->> (map key shortcut-m)
-                             (remove #{k})
+                             (remove #{shortcut-id})
                              (map shortcut-binding)
                              (filter vector?)
                              (mapcat identity)
@@ -188,16 +288,16 @@
 
 (defn shortcut-data-by-id [id]
   (let [binding (shortcut-binding id)
-        data    (->> (vals @shortcut-config/config)
-                     (into  {})
-                     id)]
+        data (->> (vals @shortcut-config/*config)
+                  (into {})
+                  id)]
     (assoc
       data
       :binding
       (binding-for-display id binding))))
 
 (defn shortcuts->commands [handler-id]
-  (let [m (get @shortcut-config/config handler-id)]
+  (let [m (get @shortcut-config/*config handler-id)]
     (->> m
          (map (fn [[id _]] (-> (shortcut-data-by-id id)
                                (assoc :id id :handler-id handler-id)

+ 58 - 0
src/main/frontend/modules/shortcut/utils.cljs

@@ -0,0 +1,58 @@
+(ns frontend.modules.shortcut.utils
+  (:require [clojure.string :as str]
+            [frontend.util :as util])
+  (:import [goog.ui KeyboardShortcutHandler]))
+
+(defn safe-parse-string-binding
+  [binding]
+  (try
+    (KeyboardShortcutHandler/parseStringShortcut binding)
+    (catch js/Error e
+      (js/console.warn "[shortcuts] parse key error: " e) binding)))
+
+(defn mod-key [binding]
+  (str/replace binding #"(?i)mod"
+               (if util/mac? "meta" "ctrl")))
+
+(defn undecorate-binding
+  [binding]
+  (when (string? binding)
+    (let [keynames {";" "semicolon"
+                    "=" "equals"
+                    "-" "dash"
+                    "[" "open-square-bracket"
+                    "]" "close-square-bracket"
+                    "'" "single-quote"
+                    "(" "shift+9"
+                    ")" "shift+0"
+                    "~" "shift+`"
+                    "⇧" "shift"
+                    "←" "left"
+                    "→" "right"}]
+      (-> binding
+          (str/replace #"[;=-\[\]'\(\)\~\→\←\⇧]" #(get keynames %))
+          (str/replace #"\s+" " ")
+          (mod-key)
+          (str/lower-case)))))
+
+(defn decorate-namespace [k]
+  (let [n (name k)
+        ns (namespace k)]
+    (keyword (str "command." ns) n)))
+
+(defn decorate-binding [binding]
+  (when (or (string? binding)
+            (sequential? binding))
+    (-> (if (string? binding) binding (str/join "+" binding))
+        (str/replace "mod" (if util/mac? "⌘" "ctrl"))
+        (str/replace "meta" (if util/mac? "⌘" "⊞ win"))
+        (str/replace "alt" (if util/mac? "opt" "alt"))
+        (str/replace "shift+/" "?")
+        (str/replace "left" "←")
+        (str/replace "right" "→")
+        (str/replace "shift" "⇧")
+        (str/replace "open-square-bracket" "[")
+        (str/replace "close-square-bracket" "]")
+        (str/replace "equals" "=")
+        (str/replace "semicolon" ";")
+        (str/lower-case))))

+ 1 - 6
src/main/frontend/routes.cljs

@@ -9,8 +9,7 @@
             [frontend.components.repo :as repo]
             [frontend.components.search :as search]
             [frontend.components.settings :as settings]
-            [frontend.components.shortcut :as shortcut]
-            [frontend.components.whiteboard :as whiteboard]
+            [frontend.components.whiteboard :as whiteboard] 
             [frontend.extensions.zotero :as zotero]
             [frontend.components.bug-report :as bug-report]
             [frontend.components.user.login :as login]))
@@ -69,10 +68,6 @@
     {:name :settings
      :view settings/settings}]
 
-   ["/settings/shortcut"
-    {:name :shortcut-setting
-     :view shortcut/shortcut-page}]
-
    ["/settings/zotero"
     {:name :zotero-setting
      :view zotero/settings}]

+ 0 - 2
src/main/frontend/search/agency.cljs

@@ -46,12 +46,10 @@
       (protocol/rebuild-blocks-indice! e1)))
 
   (transact-blocks! [_this data]
-    (println "D:Search > Transact blocks!:" repo)
     (doseq [e (get-flatten-registered-engines repo)]
       (protocol/transact-blocks! e data)))
 
   (transact-pages! [_this data]
-    (println "D:Search > Transact pages!:" repo)
     (doseq [e (get-flatten-registered-engines repo)]
       (protocol/transact-pages! e data)))
 

+ 41 - 12
src/main/frontend/state.cljs

@@ -63,6 +63,7 @@
       :modal/label                           ""
       :modal/show?                           false
       :modal/panel-content                   nil
+      :modal/payload                         nil
       :modal/fullscreen?                     false
       :modal/close-btn?                      nil
       :modal/close-backdrop?                 true
@@ -141,6 +142,7 @@
 
       :editor/code-block-context             {}
 
+      :db/properties-changed-pages           {}
       :db/last-transact-time                 (atom {})
       ;; whether database is persisted
       :db/persisted?                         {}
@@ -350,6 +352,18 @@
              (merge current new)
              new)))))
 
+(defn get-global-config
+  []
+  (get-in @state [:config ::global-config]))
+
+(defn get-global-config-str-content
+  []
+  (get-in @state [:config ::global-config-str-content]))
+
+(defn get-graph-config
+  ([] (get-graph-config (get-current-repo)))
+  ([repo-url] (get-in @state [:config repo-url])))
+
 (defn get-config
   "User config for the given repo or current repo if none given. All config fetching
 should be done through this fn in order to get global config and config defaults"
@@ -358,8 +372,8 @@ should be done through this fn in order to get global config and config defaults
   ([repo-url]
    (merge-configs
     default-config
-    (get-in @state [:config ::global-config])
-    (get-in @state [:config repo-url]))))
+    (get-global-config)
+    (get-graph-config repo-url))))
 
 (defonce publishing? (atom nil))
 
@@ -1399,7 +1413,7 @@ Similar to re-frame subscriptions"
   ([panel-content]
    (set-sub-modal! panel-content
                    {:close-btn? true}))
-  ([panel-content {:keys [id label close-btn? close-backdrop? show? center?] :as opts}]
+  ([panel-content {:keys [id label payload close-btn? close-backdrop? show? center?] :as opts}]
    (if (not (modal-opened?))
      (set-modal! panel-content opts)
      (let [modals (:modal/subsets @state)
@@ -1409,6 +1423,7 @@ Similar to re-frame subscriptions"
                    #(not (nil? %1))
                    {:modal/id            id
                     :modal/label         (or label (if center? "ls-modal-align-center" ""))
+                    :modal/payload       payload
                     :modal/show?         (if (boolean? show?) show? true)
                     :modal/panel-content panel-content
                     :modal/close-btn?    close-btn?
@@ -1438,7 +1453,7 @@ Similar to re-frame subscriptions"
    (set-modal! modal-panel-content
                {:fullscreen? false
                 :close-btn?  true}))
-  ([modal-panel-content {:keys [id label fullscreen? close-btn? close-backdrop? center?]}]
+  ([modal-panel-content {:keys [id label payload fullscreen? close-btn? close-backdrop? center?]}]
    (let [opened? (modal-opened?)]
      (when opened?
        (close-modal!))
@@ -1453,6 +1468,7 @@ Similar to re-frame subscriptions"
               :modal/label (or label (if center? "ls-modal-align-center" ""))
               :modal/show? (boolean modal-panel-content)
               :modal/panel-content modal-panel-content
+              :modal/payload payload
               :modal/fullscreen? fullscreen?
               :modal/close-btn? close-btn?
               :modal/close-backdrop? (if (boolean? close-backdrop?) close-backdrop? true))))
@@ -1466,6 +1482,7 @@ Similar to re-frame subscriptions"
       (swap! state assoc
              :modal/id nil
              :modal/label ""
+             :modal/payload nil
              :modal/show? false
              :modal/fullscreen? false
              :modal/panel-content nil
@@ -1531,9 +1548,11 @@ Similar to re-frame subscriptions"
   (when value (set-state! [:config repo-url] value)))
 
 (defn set-global-config!
-  [value]
+  [value str-content]
   ;; Placed under :config so cursors can work seamlessly
-  (when value (set-config! ::global-config value)))
+  (when value
+    (set-config! ::global-config value)
+    (set-config! ::global-config-str-content str-content)))
 
 (defn get-wide-mode?
   []
@@ -1553,13 +1572,13 @@ Similar to re-frame subscriptions"
 
 (defn get-plugins-commands-with-type
   [type]
-  (filterv #(= (keyword (first %)) (keyword type))
-           (apply concat (vals (:plugin/simple-commands @state)))))
+  (->> (apply concat (vals (:plugin/simple-commands @state)))
+       (filterv #(= (keyword (first %)) (keyword type)))))
 
 (defn get-plugins-ui-items-with-type
   [type]
-  (filterv #(= (keyword (first %)) (keyword type))
-           (apply concat (vals (:plugin/installed-ui-items @state)))))
+  (->> (apply concat (vals (:plugin/installed-ui-items @state)))
+       (filterv #(= (keyword (first %)) (keyword type)))))
 
 (defn get-plugin-resources-with-type
   [pid type]
@@ -1792,8 +1811,8 @@ Similar to re-frame subscriptions"
   (set-state! :ui/settings-open? false))
 
 (defn open-settings!
-  []
-  (set-state! :ui/settings-open? true))
+  ([] (open-settings! true))
+  ([active-tab] (set-state! :ui/settings-open? active-tab)))
 
 ;; TODO: Move those to the uni `state`
 
@@ -2235,3 +2254,13 @@ Similar to re-frame subscriptions"
 (defn next-blocks-container-id
   []
   (swap! (:ui/blocks-container-id @state) inc))
+
+(defn set-page-properties-changed!
+  [page-name]
+  (when-not (string/blank? page-name)
+    (update-state! [:db/properties-changed-pages page-name] #(inc %))))
+
+(defn sub-page-properties-changed
+  [page-name]
+  (when-not (string/blank? page-name)
+    (sub [:db/properties-changed-pages page-name])))

+ 8 - 7
src/main/frontend/ui.cljs

@@ -26,7 +26,7 @@
             [frontend.mobile.util :as mobile-util]
             [frontend.modules.shortcut.config :as shortcut-config]
             [frontend.modules.shortcut.core :as shortcut]
-            [frontend.modules.shortcut.data-helper :as shortcut-helper]
+            [frontend.modules.shortcut.utils :as shortcut-utils]
             [frontend.rum :as r]
             [frontend.state :as state]
             [frontend.storage :as storage]
@@ -96,7 +96,7 @@
                 (let [^js el (rum/dom-node state)]
                   ;; Passing aria-label as a prop to TextareaAutosize removes the dash
                   (.setAttribute el "aria-label" "editing block")
-                  (. el addEventListener "mouseup"
+                  (. el addEventListener "select"
                      #(let [start (util/get-selection-start el)
                             end (util/get-selection-end el)]
                         (when (and start end)
@@ -188,7 +188,7 @@
                    sequence)]
     [:span.keyboard-shortcut
      (map-indexed (fn [i key]
-                    (let [key' (shortcut-helper/decorate-binding (str key))]
+                    (let [key' (shortcut-utils/decorate-binding (str key))]
                       [:code {:key i}
                       ;; Display "cmd" rather than "meta" to the user to describe the Mac
                       ;; mod key, because that's what the Mac keyboards actually say.
@@ -521,7 +521,7 @@
 
 (rum/defcs auto-complete <
   (rum/local 0 ::current-idx)
-  (shortcut/mixin :shortcut.handler/auto-complete)
+  (shortcut/mixin* :shortcut.handler/auto-complete)
   [state
    matched
    {:keys [on-chosen
@@ -581,9 +581,10 @@
        :aria-hidden "true"}]]]))
 
 (defn keyboard-shortcut-from-config [shortcut-name]
-  (let [default-binding (:binding (get shortcut-config/all-default-keyboard-shortcuts shortcut-name))
-        custom-binding  (when (state/shortcuts) (get (state/shortcuts) shortcut-name))]
-    (or custom-binding default-binding)))
+  (let [built-in-binding (:binding (get shortcut-config/all-built-in-keyboard-shortcuts shortcut-name))
+        custom-binding  (when (state/shortcuts) (get (state/shortcuts) shortcut-name))
+        binding         (or custom-binding built-in-binding)]
+    (shortcut-utils/decorate-binding binding)))
 
 (rum/defc modal-overlay
   [state close-fn close-backdrop?]

+ 1 - 1
src/main/frontend/ui/date_picker.cljs

@@ -172,7 +172,7 @@
   {:init (fn [state]
            (reset! *internal-model (first (:rum/args state)))
            state)}
-  (shortcut/mixin :shortcut.handler/date-picker)
+  (shortcut/mixin :shortcut.handler/date-picker false)
   [_model {:keys [on-change disabled? start-of-week class style attr]
            :or   {start-of-week (state/get-start-of-week)} ;; Default to Sunday
            :as   args}]

+ 0 - 13
src/main/frontend/util.cljc

@@ -73,19 +73,6 @@
   [parts]
   (string/join "/" parts))
 
-(defn normalize-user-keyname
-  [k]
-  (let [keynames {";" "semicolon"
-                  "=" "equals"
-                  "-" "dash"
-                  "[" "open-square-bracket"
-                  "]" "close-square-bracket"
-                  "'" "single-quote"}]
-    (some-> (str k)
-            (string/lower-case)
-            (string/replace #"[;=-\[\]']" (fn [s]
-                                            (get keynames s))))))
-
 #?(:cljs
    (defn safe-re-find
      {:malli/schema [:=> [:cat :any :string] [:or :nil :string [:vector [:maybe :string]]]]}

+ 1 - 1
src/main/frontend/util/keycode.cljs

@@ -1,7 +1,7 @@
 (ns frontend.util.keycode
   "Provides names for common keycodes")
 
-;; code / keycode should all be deprecated for non funcional keys
+;; code / keycode should all be deprecated for non functional keys
 ;; (def left-square-bracket 219) ;; deprecated
 ;; (def left-paren 57) ;; deprecated
 (def enter 13)

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

@@ -1,3 +1,3 @@
 (ns ^:no-doc frontend.version)
 
-(defonce version "0.9.14")
+(defonce version "0.9.17")

+ 24 - 14
src/main/logseq/api.cljs

@@ -14,6 +14,7 @@
             [frontend.config :as config]
             [frontend.handler.config :as config-handler]
             [frontend.handler.recent :as recent-handler]
+            [frontend.handler.route :as route-handler]
             [frontend.db :as db]
             [frontend.db.model :as db-model]
             [frontend.db.query-dsl :as query-dsl]
@@ -366,9 +367,9 @@
                                (if palette?
                                  (palette-handler/invoke-command palette-cmd)
                                  (action')))
-                [handler-id id shortcut-map] (update shortcut-args 2 assoc :fn dispatch-cmd :cmd palette-cmd)]
-            (println :shortcut/register-shortcut [handler-id id shortcut-map])
-            (st/register-shortcut! handler-id id shortcut-map)))))))
+                [mode-id id shortcut-map] (update shortcut-args 2 merge cmd {:fn dispatch-cmd :cmd palette-cmd})]
+            (println :shortcut/register-shortcut [mode-id id shortcut-map])
+            (st/register-shortcut! mode-id id shortcut-map)))))))
 
 (defn ^:export unregister_plugin_simple_command
   [pid]
@@ -421,7 +422,7 @@
                            (util/safe-lower-case)
                            (keyword)))]
       (when-let [action (get-in (palette-handler/get-commands-unique) [id :action])]
-        (apply action args)))))
+        (apply plugin-handler/hook-lifecycle-fn! id action args)))))
 
 ;; flag - boolean | 'toggle'
 (def ^:export set_left_sidebar_visible
@@ -448,17 +449,23 @@
 
 (def ^:export push_state
   (fn [^js k ^js params ^js query]
-    (rfe/push-state
-      (keyword k)
-      (bean/->clj params)
-      (bean/->clj query))))
+    (let [k (keyword k)
+          page? (= k :page)
+          params (bean/->clj params)
+          query (bean/->clj query)]
+      (if-let [page-name (and page? (:name params))]
+        (route-handler/redirect-to-page! page-name {:anchor (:anchor query) :push true})
+        (rfe/push-state k params query)))))
 
 (def ^:export replace_state
   (fn [^js k ^js params ^js query]
-    (rfe/replace-state
-      (keyword k)
-      (bean/->clj params)
-      (bean/->clj query))))
+    (let [k (keyword k)
+          page? (= k :page)
+          params (bean/->clj params)
+          query (bean/->clj query)]
+      (if-let [page-name (and page? (:name params))]
+        (route-handler/redirect-to-page! page-name {:anchor (:anchor query) :push false})
+        (rfe/replace-state k params query)))))
 
 (defn ^:export get_external_plugin
   [pid]
@@ -562,8 +569,11 @@
   page-handler/rename!)
 
 (defn ^:export open_in_right_sidebar
-  [block-uuid]
-  (editor-handler/open-block-in-sidebar! (sdk-utils/uuid-or-throw-error block-uuid)))
+  [block-id-or-uuid]
+  (editor-handler/open-block-in-sidebar!
+    (if (number? block-id-or-uuid)
+      block-id-or-uuid
+      (sdk-utils/uuid-or-throw-error block-id-or-uuid))))
 
 (defn ^:export new_block_uuid []
   (str (db/new-block-id)))

+ 1 - 1
src/main/logseq/api/block.cljs

@@ -12,7 +12,7 @@
   [id-or-uuid ^js opts]
   (when-let [block (if (number? id-or-uuid)
                      (db-utils/pull id-or-uuid)
-                     (db-model/query-block-by-uuid (sdk-utils/uuid-or-throw-error id-or-uuid)))]
+                     (and id-or-uuid (db-model/query-block-by-uuid (sdk-utils/uuid-or-throw-error id-or-uuid))))]
     (when-not (contains? block :block/name)
       (when-let [uuid (:block/uuid block)]
         (let [{:keys [includeChildren]} (bean/->clj opts)

+ 0 - 2
src/resources/dicts/de.edn

@@ -576,7 +576,6 @@
  :settings-page/custom-date-format-warning "Neuindizierung erforderlich! Vorhandene Journal-Verweise würden nicht mehr funktionieren!"
  :settings-page/custom-global-configuration "Benutzerdefinierte globale Konfiguration"
  :settings-page/custom-theme "Individuelles Theme"
- :settings-page/customize-shortcuts "Tastaturbefehle"
  :settings-page/developer-mode "Entwicklermodus"
  :settings-page/developer-mode-desc "Der Entwicklermodus hilft Mitwirkenden und Entwicklern von Erweiterungen, ihre Integration mit Logseq effizienter zu testen."
  :settings-page/disable-sentry "Nutzungs- und Diagnostik-Daten an Logseq senden"
@@ -604,7 +603,6 @@
  :settings-page/preferred-outdenting "Logische Ausrückung"
  :settings-page/preferred-pasting-file "Einfügen der Datei bevorzugen"
  :settings-page/preferred-workflow "Bevorzugter Workflow"
- :settings-page/shortcut-settings "Verknüpfungen anpassen"
  :settings-page/show-brackets "Klammern anzeigen"
  :settings-page/show-full-blocks "Alle Zeilen einer Blockreferenz anzeigen"
  :settings-page/spell-checker "Rechtschreibprüfung"

+ 18 - 3
src/resources/dicts/en.edn

@@ -310,8 +310,6 @@
  :settings-page/enable-tooltip "Tooltips"
  :settings-page/enable-journals "Journals"
  :settings-page/enable-all-pages-public "All pages public when publishing"
- :settings-page/customize-shortcuts "Keyboard shortcuts"
- :settings-page/shortcut-settings "Customize shortcuts"
  :settings-page/home-default-page "Set the default home page"
  :settings-page/clear-cache "Clear cache"
  :settings-page/clear "Clear"
@@ -321,6 +319,7 @@
  :settings-page/current-version "Current version"
  :settings-page/tab-general "General"
  :settings-page/tab-editor "Editor"
+ :settings-page/tab-keymap "Keymap"
  :settings-page/tab-version-control "Version control"
  :settings-page/tab-account "Account"
  :settings-page/tab-advanced "Advanced"
@@ -360,6 +359,7 @@
  :close "Close"
  :delete "Delete"
  :save "Save"
+ :reset "Reset"
  :type "Type"
  :host "Host"
  :port "Port"
@@ -627,6 +627,7 @@
  :file-sync/other-user-graph "Current local graph is bound to other user's remote graph. So can't start syncing."
  :file-sync/graph-deleted "The current remote graph has been deleted"
  :file-sync/rsapi-cannot-upload-err "Unable to start synchronization, please check if the local time is correct."
+ :file-sync/connectivity-testing-failed "Network connection testing failed. Please check your network settings. Test URLs: "
 
  :notification/clear-all "Clear all"
 
@@ -637,9 +638,23 @@
  :shortcut.category/block-command-editing "Block command editing"
  :shortcut.category/block-selection "Block selection (press Esc to quit selection)"
  :shortcut.category/toggle "Toggle"
- :shortcut.category/whiteboard "Whiteboard"
  :shortcut.category/others "Others"
  :shortcut.category/plugins "Plugins"
+ :shortcut.category/whiteboard "Whiteboard"
+
+ :keymap/all "All"
+ :keymap/disabled "Disabled"
+ :keymap/unset "Unset"
+ :keymap/custom "Custom"
+ :keymap/search "Search"
+ :keymap/total "Total shortcuts"
+ :keymap/keystroke-filter "Keystroke filter"
+ :keymap/keystroke-record-desc "Press any sequence of keys to filter shortcuts"
+ :keymap/keystroke-record-setup-label "Press any sequence of keys to set a shortcut"
+ :keymap/restore-to-default "Restore to system default"
+ :keymap/customize-for-label "Customize shortcuts"
+ :keymap/conflicts-for-label "Keymap conflicts for"
+
  :window/minimize "Minimize"
  :window/maximize "Maximize"
  :window/restore "Restore"

+ 0 - 2
src/resources/dicts/es.edn

@@ -609,7 +609,6 @@
  :settings-page/custom-date-format-notification     "Debes reindexar tu grafo para que este cambio tome efecto"
  :settings-page/custom-date-format-warning          "¡Se requiere reindexar! ¡Las referencias existentes del diario podrían estar rotas!"
  :settings-page/custom-global-configuration         "Configuración global personalizada"
- :settings-page/customize-shortcuts                 "Atajos de teclado"
  :settings-page/custom-theme                        "Tema personalizado"
  :settings-page/developer-mode-desc                 "El modo desarrollador permite a los colaboradores y desarrolladores de extensiones probar sus integraciones con Logseq más eficientemente."
  :settings-page/developer-mode                      "Modo desarrollador"
@@ -650,7 +649,6 @@
  :settings-page/preferred-pasting-file              "Preferir pegar archivo"
  :settings-page/preferred-workflow                  "Flujo de trabajo preferido"
  :settings-page/revision                            "Revisión: "
- :settings-page/shortcut-settings                   "Personalizar atajos"
  :settings-page/show-brackets                       "Mostrar corchetes"
  :settings-page/show-full-blocks                    "Mostrar todas las líneas de una referencia a bloque"
  :settings-page/spell-checker                       "Corrector ortográfico"

+ 0 - 2
src/resources/dicts/fr.edn

@@ -259,7 +259,6 @@
     :settings-page/custom-date-format "Format de date préféré"
     :settings-page/custom-global-configuration "Configuration globale personnalisée"
     :settings-page/custom-theme "Thème personnalisé"
-    :settings-page/customize-shortcuts "Raccourcis clavier"
     :settings-page/disable-sentry "Envoyer des données d'utilisation et de diagnostique à Logseq"
     :settings-page/edit-custom-css "Modifier custom.css"
     :settings-page/edit-export-css "Modifier export.css"
@@ -281,7 +280,6 @@
     :settings-page/network-proxy "Proxy réseau"
     :settings-page/plugin-system "Extensions"
     :settings-page/preferred-outdenting "Mise en retrait logique"
-    :settings-page/shortcut-settings "Personnaliser les raccourcis"
     :settings-page/show-brackets "Montrer les parenthèses, crochets et accolades"
     :settings-page/spell-checker "Vérification orthographique"
     :settings-page/sync "Synchronisation"

+ 821 - 0
src/resources/dicts/id.edn

@@ -0,0 +1,821 @@
+{:accessibility/skip-to-main-content "Lompat ke konten utama"
+ :tutorial/text #resource "tutorials/tutorial-id.md"
+ :tutorial/dummy-notes #resource "tutorials/dummy-notes-id.md"
+ :on-boarding/demo-graph "Ini adalah grafik demo, perubahan tidak akan disimpan sampai Anda membuka folder lokal."
+ :on-boarding/add-graph "Tambahkan grafik"
+ :on-boarding/open-local-dir "Buka direktori lokal"
+ :on-boarding/new-graph-desc-1 "Logseq mendukung mode Markdown dan Org. Anda dapat membuka direktori yang sudah ada atau membuat direktori baru pada perangkat Anda, direktori juga dikenal sebagai folder. Data Anda hanya akan disimpan pada perangkat ini."
+ :on-boarding/new-graph-desc-2 "Setelah Anda membuka direktori Anda, ini akan membuat tiga folder dalam direktori tersebut:"
+ :on-boarding/new-graph-desc-3 "/journals - menyimpan halaman jurnal Anda"
+ :on-boarding/new-graph-desc-4 "/pages - menyimpan halaman lainnya"
+ :on-boarding/new-graph-desc-5 "/logseq - menyimpan konfigurasi, custom.css, dan beberapa metadata."
+ :on-boarding/welcome-whiteboard-modal-title "Kanvas baru untuk pikiran Anda."
+ :on-boarding/welcome-whiteboard-modal-description "Papan tulis adalah alat yang hebat untuk curah pendapat dan pengorganisasian. Sekarang Anda dapat menempatkan pemikiran Anda dari basis pengetahuan atau pemikiran baru di samping satu sama lain pada kanvas spasial untuk menghubungkan, mengasosiasikan, dan memahami dengan cara yang baru."
+ :on-boarding/welcome-whiteboard-modal-skip "Lewati"
+ :on-boarding/welcome-whiteboard-modal-start "Mulai menulis di papan tulis"
+ :on-boarding/tour-whiteboard-home "{1} Rumah untuk papan tulis Anda"
+ :on-boarding/tour-whiteboard-home-description "Papan tulis memiliki bagiannya sendiri di aplikasi di mana Anda dapat melihatnya sekilas, membuat yang baru, atau menghapusnya dengan mudah."
+ :on-boarding/tour-whiteboard-new "{1} Buat papan tulis baru"
+ :on-boarding/tour-whiteboard-new-description "Ada beberapa cara untuk membuat papan tulis baru. Salah satunya selalu ada di sini, di dasbor."
+ :on-boarding/tour-whiteboard-btn-next "Berikutnya"
+ :on-boarding/tour-whiteboard-btn-back "Kembali"
+ :on-boarding/tour-whiteboard-btn-finish "Selesai"
+ :on-boarding/quick-tour-btn-next "Berikutnya"
+ :on-boarding/quick-tour-btn-back "Kembali"
+ :on-boarding/quick-tour-btn-finish "Selesai"
+ :on-boarding/quick-tour-btn-skip "Lewati Tur Singkat"
+ :on-boarding/quick-tour-steps "LANGKAH "
+ :on-boarding/quick-tour-help-title "❓ Bantuan"
+ :on-boarding/quick-tour-help-desc "Anda bisa klik di sini untuk bantuan dan informasi lain tentang Logseq."
+ :on-boarding/quick-tour-journal-page-title "📆 Halaman Jurnal Harian"
+ :on-boarding/quick-tour-journal-page-desc-1 "Ini adalah halaman jurnal harian hari ini. Di sini Anda dapat menuangkan pemikiran, pembelajaran, dan ide Anda. Jangan khawatir tentang pengorganisasian. Tulis saja dan"
+ :on-boarding/quick-tour-journal-page-desc-2 "[[tautan]]"
+ :on-boarding/quick-tour-journal-page-desc-3 "pemikiran Anda."
+ :on-boarding/quick-tour-left-sidebar-title "👀 Bilah Sisi Kiri"
+ :on-boarding/quick-tour-left-sidebar-desc "Buka bilah sisi kiri untuk menjelajahi item menu penting di Logseq."
+ :on-boarding/quick-tour-favorites-title "⭐️ Favorit"
+ :on-boarding/quick-tour-favorites-desc-1 "Sematkan halaman favorit Anda melalui menu `... `di halaman mana pun."
+ :on-boarding/quick-tour-favorites-desc-2 "Kami juga telah menambahkan beberapa halaman template di sini untuk membantu Anda memulai. Anda dapat menghapusnya setelah Anda mulai menulis catatan Anda sendiri."
+ :on-boarding/command-palette-quick-tour "Tur singkat untuk orientasi"
+ :on-boarding/importing-main-title "Mengimpor catatan yang ada"
+ :on-boarding/importing-main-desc "Anda juga dapat melakukan ini nanti di aplikasi."
+ :on-boarding/importing-title "Apakah Anda sudah memiliki catatan yang ingin Anda impor?"
+ :on-boarding/importing-desc "Jika ia dalam format JSON, EDN atau Markdown, Logseq dapat bekerja dengannya."
+ :on-boarding/importing-roam-desc "Mengimpor Ekspor JSON dari grafik Roam Anda"
+ :on-boarding/importing-lsq-desc "Mengimpor EDN atau Ekspor JSON dari grafik Logseq Anda"
+ :on-boarding/importing-opml-desc " Mengimpor berkas OPML"
+ :on-boarding/main-title (fn [] ["Selamat datang di " [:strong "Logseq!"]])
+ :on-boarding/main-desc "Pertama, Anda harus memilih folder di mana Logseq akan menyimpan pemikiran, ide, catatan Anda."
+ :on-boarding/section-btn-title "Pilih folder"
+ :on-boarding/section-btn-desc "Buka direktori yang ada atau Buat baru"
+ :on-boarding/section-title "Bagaimana Logseq menghemat pekerjaan Anda"
+ :on-boarding/section-desc "Di dalam direktori yang Anda pilih, Logseq akan membuat 4 folder."
+ :on-boarding/section-tip-1 "Setiap halaman adalah berkas yang disimpan hanya pada {1} Anda."
+ :on-boarding/section-tip-2 "Anda dapat memilih untuk menyinkronkannya nanti."
+ :on-boarding/section-assets "Grafik & Dokumen"
+ :on-boarding/section-computer "komputer"
+ :on-boarding/section-journals "Catatan harian"
+ :on-boarding/section-pages "HALAMAN"
+ :on-boarding/section-phone "telepon"
+ :on-boarding/section-app "APP Internal"
+ :on-boarding/section-config "Berkas Konfigurasi"
+ :query/config-property-settings "Pengaturan properti untuk kueri ini:"
+ :bug-report/main-title "Laporan bug"
+ :bug-report/clipboard-inspector-title "Pemeriksa data papan klip"
+ :bug-report/main-desc "Dapatkah Anda membantu kami dengan mengirimkan laporan bug? Kami akan menyelesaikannya sesegera mungkin."
+ :bug-report/section-clipboard-title "Apakah bug yang Anda temui terkait dengan fitur-fitur ini?"
+ :bug-report/section-clipboard-desc "Anda dapat menggunakan alat bantu praktis ini untuk memberikan informasi tambahan kepada kami."
+ :bug-report/section-clipboard-btn-title "Pembantu papan klip"
+ :bug-report/section-clipboard-btn-desc "Memeriksa dan mengumpulkan data papan klip"
+ :bug-report/section-issues-title "Atau..."
+ :bug-report/section-issues-desc "Jika tidak ada alat bantu yang tersedia bagi Anda untuk mengumpulkan informasi tambahan, silakan laporkan bug secara langsung."
+ :bug-report/section-issues-btn-title "Mengirimkan laporan bug"
+ :bug-report/section-issues-btn-desc "Bantu Jadikan Logseq Lebih Baik!"
+ :bug-report/inspector-page-desc-1 "Tekan Ctrl+V / ⌘+V untuk memeriksa data papan klip Anda"
+ :bug-report/inspector-page-desc-2 "atau klik di sini untuk menempelkan jika Anda menggunakan versi seluler"
+ :bug-report/inspector-page-placeholder "Tekan lama di sini untuk menempelkan jika Anda menggunakan ponsel"
+ :bug-report/inspector-page-tip "Ada yang salah? Tidak masalah, klik untuk kembali ke langkah sebelumnya."
+ :bug-report/inspector-page-btn-back "Kembali"
+ :bug-report/inspector-page-btn-copy "Salin hasilnya"
+ :bug-report/inspector-page-copy-notif "Disalin ke papan klip!"
+ :bug-report/inspector-page-btn-create-issue "Buat masalah"
+ :bug-report/inspector-page-desc-clipboard "Berikut ini adalah data yang dibaca dari clipboard."
+ :bug-report/inspector-page-desc-copy "Jika ini boleh dibagikan, klik tombol salin."
+ :bug-report/inspector-page-desc-create-issue "Sekarang Anda dapat melaporkan hasil yang ditempelkan ke clipboard Anda. Silakan tempelkan hasilnya di bagian 'Konteks Tambahan' dan sebutkan dari mana Anda menyalin konten asli. Terima kasih!"
+ :help/title-usage "Penggunaan"
+ :help/title-community "Komunitas"
+ :help/title-development "Pengembangan"
+ :help/title-about "Tentang"
+ :help/title-terms "Ketentuan"
+ :help/start "Memulai"
+ :help/about "Tentang Logseq"
+ :help/roadmap "Peta jalan"
+ :help/bug "Laporan bug"
+ :help/feature "Permintaan fitur"
+ :help/changelog "Catatan perubahan"
+ :help/blog "Blog Logseq"
+ :help/docs "Dokumentasi"
+ :help/privacy "Kebijakan privasi"
+ :help/terms "Ketentuan"
+ :help/forum-community "Komunitas forum"
+ :help/awesome-logseq "Logseq yang mengagumkan"
+ :help/shortcuts "Pintasan keyboard"
+ :help/shortcuts-triggers "Pemicu"
+ :help/shortcut "Pintasan"
+ :help/slash-autocomplete "Pelengkapan otomatis garis miring"
+ :help/block-content-autocomplete "Pelengkapan otomatis konten blok"
+ :help/reference-autocomplete "Pelengkapan otomatis referensi halaman"
+ :help/block-reference "Blok referensi"
+ :help/open-link-in-sidebar "Buka tautan di bilah sisi"
+ :more "Lebih lanjut"
+ :search/result-for "Hasil pencarian untuk "
+ :search/items "item"
+ :search/page-names "Nama halaman pencarian"
+ :search/recent "Pencarian terbaru:"
+ :search/blocks-in-page "Blok pencarian dalam halaman:"
+ :search/command-palette-tip-1 "Tip:"
+ :search/command-palette-tip-2 "Untuk membuka palet perintah"
+ :search/cache-outdated "Cache sudah kedaluwarsa. Silakan klik tombol 'Indeks ulang' di menu tarik-turun grafik."
+ :search-item/whiteboard "Papan tulis"
+ :search-item/page "Halaman"
+ :search-item/file "Berkas"
+ :search-item/block "Blok"
+ :search-item/no-result "Tidak ada hasil yang cocok"
+ :help/context-menu "Blokir menu konteks"
+ :help/markdown-syntax "Sintaks penurunan harga"
+ :help/org-mode-syntax "Sintaks mode org"
+ :bold "Cetak tebal"
+ :italics "Cetak miring"
+ :highlight "Sorot"
+ :strikethrough "Dicoret"
+ :code "Kode"
+ :untitled "Tanpa judul"
+ :right-side-bar/help "Bantuan"
+ :right-side-bar/switch-theme "Mode tema"
+ :right-side-bar/contents "Konten"
+ :right-side-bar/page-graph "Grafik halaman"
+ :right-side-bar/history "(Dev) Urungkan/Ubah riwayat"
+ :right-side-bar/history-undos "Urungkan"
+ :right-side-bar/history-redos "Pengulangan"
+ :right-side-bar/history-global "global"
+ :right-side-bar/history-pageonly "halaman saja"
+ :right-side-bar/block-ref "Referensi blok"
+ :right-side-bar/graph-view "Tampilan grafik"
+ :right-side-bar/all-pages "Semua halaman"
+ :right-side-bar/whiteboards "Papan tulis"
+ :right-side-bar/flashcards "Kartu flash"
+ :right-side-bar/new-page "Halaman baru"
+ :right-side-bar/show-journals "Tampilkan Jurnal"
+ :right-side-bar/separator "Pengatur ukuran bilah sisi kanan"
+ :right-side-bar/toggle-right-sidebar "Beralih ke bilah sisi kanan"
+ :right-side-bar/pane-close "Tutup"
+ :right-side-bar/pane-close-others "Tutup yang lain"
+ :right-side-bar/pane-close-all "Tutup semua"
+ :right-side-bar/pane-collapse "Menciutkan"
+ :right-side-bar/pane-collapse-others "Menciutkan yang lain"
+ :right-side-bar/pane-collapse-all "Tutup semua"
+ :right-side-bar/pane-expand "Memperluas"
+ :right-side-bar/pane-expand-all "Perluas semua"
+ :right-side-bar/pane-open-as-page "Buka sebagai halaman"
+ :right-side-bar/pane-more "Lebih banyak"
+ :left-side-bar/switch "Beralih ke:"
+ :left-side-bar/journals "Jurnal"
+ :left-side-bar/create "Membuat"
+ :left-side-bar/new-page "Halaman baru"
+ :left-side-bar/new-whiteboard "Papan tulis baru"
+ :left-side-bar/nav-favorites "Favorit"
+ :left-side-bar/nav-recent-pages "Halaman terbaru"
+ :page/something-went-wrong "Ada masalah"
+ :page/logseq-is-having-a-problem "Logseq mengalami masalah. Untuk mencoba mengembalikannya ke keadaan yang berfungsi, silakan coba langkah-langkah aman berikut ini secara berurutan:"
+ :page/step "Langkah {1}"
+ :page/try "Coba"
+ :page/slide-view "Tampilkan sebagai slide"
+ :page/slide-view-tip-go-fullscreen (fn [] [[:span.opacity-70 "Tip: tekan "] [:code "f"] [:span.opacity-70 " untuk masuk ke mode layar penuh"]])
+ :page/delete-confirmation "Apakah Anda yakin ingin menghapus halaman ini beserta berkasnya?"
+ :page/open-in-finder "Buka dalam direktori"
+ :page/open-with-default-app "Buka dengan aplikasi default"
+ :page/make-public "Buat publik untuk dipublikasikan"
+ :page/version-history "Lihat riwayat halaman"
+ :page/open-backup-directory "Buka direktori cadangan halaman"
+ :page/make-private "Buat pribadi"
+ :page/delete "Hapus halaman"
+ :page/add-to-favorites "Tambahkan ke Favorit"
+ :page/unfavorite "Hapus dari Favorit"
+ :page/show-journals "Tampilkan jurnal"
+ :page/show-whiteboards "Tampilkan papan tulis"
+ :block/name "Nama Halaman"
+ :page/earlier "Lebih awal"
+ :page/copy-page-url "Salin URL halaman"
+ :page/illegal-page-name "Nama halaman tidak sah!"
+ :page/page-already-exists "Halaman “{1}” sudah ada!"
+ :page/whiteboard-to-journal-error "Halaman papan tulis tidak dapat diubah namanya menjadi judul jurnal!"
+ :file/name "Nama berkas"
+ :file/last-modified-at "Terakhir diubah pada"
+ :file/no-data "Tidak ada data"
+ :file/format-not-supported "Format .{1} tidak didukung."
+ :file/validate-existing-file-error "Halaman sudah ada dengan berkas lain: {1}, berkas saat ini: {2}. Harap pertahankan hanya satu dari mereka dan re-indeks grafik Anda."
+ :file-rn/re-index "Re-indeks sangat dianjurkan setelah berkas diubah nama dan pada perangkat lain setelah sinkronisasi."
+ :file-rn/need-action "Tindakan penggantian nama berkas disarankan agar sesuai dengan format baru. Re-indeks diperlukan pada semua perangkat saat berkas yang diubah nama disinkronkan."
+ :file-rn/or-select-actions " atau ubah nama berkas secara individual di bawah, lalu "
+ :file-rn/or-select-actions-2 ". Tindakan ini tidak tersedia setelah Anda menutup panel ini."
+ :file-rn/legend "🟢 Tindakan penggantian nama opsional; 🟡 Tindakan penggantian nama diperlukan untuk menghindari perubahan judul; 🔴 Perubahan besar."
+ :file-rn/close-panel "Tutup Panel"
+ :file-rn/all-action "Terapkan Semua Tindakan! ({1})"
+ :file-rn/select-format "(Opsi Mode Pengembang, Berbahaya!) Pilih format nama berkas"
+ :file-rn/rename "ubah nama berkas \"{1}\" menjadi \"{2}\""
+ :file-rn/apply-rename "Terapkan operasi penggantian nama berkas"
+ :file-rn/suggest-rename "Tindakan diperlukan: "
+ :file-rn/otherwise-breaking "Atau judul akan menjadi"
+ :file-rn/no-action "Bagus! Tidak diperlukan tindakan lebih lanjut."
+ :file-rn/confirm-proceed "Perbarui format!"
+ :file-rn/select-confirm-proceed "Pengembang: tulis format"
+ :file-rn/unreachable-title "Peringatan! Nama halaman akan menjadi {1} dalam format nama berkas saat ini, kecuali properti title:: diatur secara manual"
+ :file-rn/optional-rename "Saran: "
+ :file-rn/format-deprecated "Anda saat ini menggunakan format yang sudah ketinggalan zaman. Diperbarui ke format terbaru sangat disarankan. Harap cadangkan data Anda dan tutup aplikasi Logseq di perangkat lain sebelum operasi ini."
+ :file-rn/filename-desc-1 "Pengaturan ini mengkonfigurasi bagaimana sebuah halaman disimpan ke dalam berkas Logseq menyimpan halaman ke dalam berkas dengan nama yang sama."
+ :file-rn/filename-desc-2 "Beberapa karakter seperti \"/\" atau \"?\" tidak valid untuk nama berkas"
+ :file-rn/filename-desc-3 "Logseq mengganti karakter yang tidak valid dengan ekivalen URL mereka untuk membuatnya valid (misalnya, \"?\" menjadi \"%3F\")."
+ :file-rn/filename-desc-4 "Pemisah ruang nama \"/\" juga diganti dengan \"___\" (tiga garis bawah) untuk pertimbangan estetika."
+ :file-rn/instruct-1 "Ini adalah proses 2 langkah untuk memperbarui format nama berkas:"
+ :file-rn/instruct-2 "1. Klik "
+ :file-rn/instruct-3 "2. Ikuti instruksi di bawah ini untuk mengubah nama berkas ke format baru:"
+ :page/created-at "Dibuat pada"
+ :page/updated-at "Diperbarui pada"
+ :page/backlinks "Tautan balik"
+ :linked-references/filter-search "Cari di halaman terhubung"
+ :editor/block-search "Cari blok"
+ :text/image "Gambar"
+ :asset/show-in-folder "Tampilkan gambar di folder"
+ :asset/open-in-browser "Buka gambar di peramban"
+ :asset/delete "Hapus gambar"
+ :asset/copy "Salin gambar"
+ :asset/maximize "Perbesar gambar"
+ :asset/confirm-delete "Apakah Anda yakin ingin menghapus {1} ini?"
+ :asset/physical-delete "Hapus juga berkasnya (perhatikan bahwa ini tidak dapat dikembalikan)"
+ :color/gray "Abu-abu"
+ :color/red "Merah"
+ :color/yellow "Kuning"
+ :color/green "Hijau"
+ :color/blue "Biru"
+ :color/purple "Ungu"
+ :color/pink "Merah muda"
+ :editor/copy "Salin"
+ :editor/cut "Potong"
+ :editor/expand-block-children "Perluas semua"
+ :editor/collapse-block-children "Tutup semua"
+ :editor/delete-selection "Hapus blok terpilih"
+ :editor/cycle-todo "Putar status TODO item saat ini"
+ :dev/show-page-data "(Dev) Tampilkan data halaman"
+ :dev/show-block-data "(Dev) Tampilkan data blok"
+ :dev/show-block-ast "(Dev) Tampilkan AST blok"
+ :dev/show-page-ast "(Dev) Tampilkan AST halaman"
+ :content/copy-export-as "Salin / Ekspor sebagai.."
+ :content/copy-block-url "Salin URL blok"
+ :content/copy-block-ref "Salin referensi blok"
+ :content/copy-block-emebed "Salin penanaman blok"
+ :content/copy-ref "Salin referensi ini"
+ :content/delete-ref "Hapus referensi ini"
+ :content/replace-with-text "Ganti dengan teks"
+ :content/replace-with-embed "Ganti dengan penanaman"
+ :content/open-in-sidebar "Buka di sidebar"
+ :content/click-to-edit "Klik untuk menyunting"
+ :context-menu/make-a-flashcard "Buat Kartu Belajar"
+ :context-menu/toggle-number-list "Alihkan daftar nomor"
+ :context-menu/preview-flashcard "Pratinjau Kartu Belajar"
+ :context-menu/make-a-template "Buat Templat"
+ :context-menu/input-template-name "Apa nama templatnya?"
+ :context-menu/template-include-parent-block "Termasuk blok induk dalam templat?"
+ :context-menu/template-exists-warning "Templat sudah ada!"
+ :settings-page/git-tip "Jika Anda telah mengaktifkan Sinkronisasi Logseq, Anda dapat melihat riwayat oenyuntingan halaman secara langsung. Bagian ini hanya untuk yang berpengalaman dalam teknologi."
+ :settings-page/git-desc-1 "Untuk melihat riwayat penyuntingan halaman, klik tiga titik horizontal di sudut kanan atas dan pilih \"Lihat riwayat halaman\"."
+ :settings-page/git-desc-2 "Untuk pengguna profesional, Logseq juga mendukung penggunaan "
+ :settings-page/git-desc-3 " untuk kontrol versi. Gunakan Git dengan risiko Anda sendiri karena masalah umum Git tidak didukung oleh tim Logseq."
+ :settings-page/git-switcher-label "Aktifkan komit otomatis Git"
+ :settings-page/git-commit-delay "Detik komit otomatis Git"
+ :settings-page/git-confirm "Anda perlu me-restart aplikasi setelah memperbarui pengaturan Git."
+ :settings-page/edit-config-edn "Sunting config.edn"
+ :settings-page/edit-global-config-edn "Sunting global config.edn"
+ :settings-page/edit-custom-css "Sunting custom.css"
+ :settings-page/edit-export-css "Sunting export.css"
+ :settings-page/edit-setting "Sunting"
+ :settings-page/custom-configuration "Konfigurasi kustom"
+ :settings-page/custom-global-configuration "Konfigurasi global kustom"
+ :settings-page/theme-light "terang"
+ :settings-page/theme-dark "gelap"
+ :settings-page/theme-system "sistem"
+ :settings-page/custom-theme "Tema kustom"
+ :settings-page/export-theme "Ekspor tema"
+ :settings-page/show-brackets "Tampilkan tanda kurung"
+ :settings-page/spell-checker "Pemeriksa ejaan"
+ :settings-page/auto-updater "Pembaruan otomatis"
+ :settings-page/disable-sentry "Kirim data penggunaan dan diagnosa ke Logseq"
+ :settings-page/disable-sentry-desc "Logseq tidak akan pernah mengumpulkan database grafik lokal Anda atau menjual data Anda."
+ :settings-page/preferred-outdenting "Pengaturan penyingkiran logis"
+ :settings-page/preferred-outdenting-tip "Sisi kiri menunjukkan penyingkiran dengan pengaturan default, dan sisi kanan menunjukkan penyingkiran logis yang diaktifkan "
+ :settings-page/preferred-outdenting-tip-more "→ Pelajari lebih lanjut"
+ :settings-page/show-full-blocks "Tampilkan semua baris referensi blok"
+ :settings-page/auto-expand-block-refs "Perluas referensi blok secara otomatis saat zoom-in"
+ :settings-page/auto-expand-block-refs-tip "Opsi ini mengontrol apakah referensi blok akan diperluas secara otomatis saat zoom-in."
+ :settings-page/custom-date-format "Format tanggal yang diinginkan"
+ :settings-page/custom-date-format-warning "Diperlukan re-indeks! Referensi jurnal yang ada akan rusak!"
+ :settings-page/custom-date-format-notification "Anda harus me-re-indeks grafik Anda agar perubahan ini berlaku"
+ :settings-page/preferred-pasting-file-hint "Ketika diaktifkan, menempelkan gambar dari internet akan mengunduh dan menyisipkan gambar. Ketika dinonaktifkan, akan menempelkan tautan ke gambar."
+ :settings-page/preferred-file-format "Format berkas yang diinginkan"
+ :settings-page/preferred-workflow "Alur kerja yang diinginkan"
+ :settings-page/preferred-pasting-file "Lebih suka menempelkan berkas"
+ :settings-page/enable-shortcut-tooltip "Aktifkan tooltip pintasan"
+ :settings-page/enable-timetracking "Pelacakan waktu"
+ :settings-page/enable-tooltip "Tooltip"
+ :settings-page/enable-journals "Jurnal"
+ :settings-page/enable-all-pages-public "Semua halaman menjadi publik saat dipublikasikan"
+ :settings-page/home-default-page "Atur halaman beranda default"
+ :settings-page/enable-block-time "Waktu blok"
+ :settings-page/clear-cache "Hapus cache"
+ :settings-page/clear "Hapus"
+ :settings-page/clear-cache-warning "Menghapus cache akan menghapus grafik yang terbuka. Anda akan kehilangan perubahan yang belum disimpan."
+ :settings-page/developer-mode "Mode pengembang"
+ :settings-page/developer-mode-desc "Mode pengembang membantu kontributor dan pengembang ekstensi menguji integrasi mereka dengan Logseq dengan lebih efisien."
+ :settings-page/current-version "Versi saat ini"
+ :settings-page/tab-general "Umum"
+ :settings-page/tab-editor "Penyunting"
+ :settings-page/tab-keymap "Pemetaan tombol"
+ :settings-page/tab-version-control "Kontrol versi"
+ :settings-page/tab-account "Akun"
+ :settings-page/tab-advanced "Lanjutan"
+ :settings-page/tab-assets "Aset"
+ :settings-page/tab-features "Fitur"
+ :settings-page/plugin-system "Pengaya"
+ :settings-page/enable-flashcards "Kartu Belajar"
+ :settings-page/network-proxy "Proxy jaringan"
+ :settings-page/filename-format "Format nama berkas"
+ :settings-page/alpha-features "Fitur Alpha"
+ :settings-page/beta-features "Fitur Beta"
+ :settings-page/login-prompt "Untuk mengakses fitur-fitur baru sebelum orang lain, Anda harus menjadi Sponsor atau Pendukung Logseq di Open Collective dan oleh karena itu harus masuk terlebih dahulu."
+ :settings-page/sync "Sinkronisasi"
+ :settings-page/sync-desc-1 "Klik"
+ :settings-page/sync-desc-2 "di sini"
+ :settings-page/sync-desc-3 "untuk petunjuk tentang cara menyiapkan dan menggunakan Sinkronisasi."
+ :settings-page/sync-diff-merge "Aktifkan penggabungan cerdas saat sinkronisasi"
+ :settings-page/sync-diff-merge-desc "Gabungkan pembaruan lokal dengan berkas remote secara otomatis saat terjadi konflik, daripada menimpa berkas remote."
+ :settings-page/sync-diff-merge-warn "Kemampuan penggabungan cerdas hanya diaktifkan pada perangkat setelah sinkronisasi pertama yang berhasil dengan server remote pada grafik dalam versi Logseq yang baru. Aktifkan ini di semua perangkat untuk mencapai pengalaman terbaik."
+ :settings-page/enable-whiteboards "Papan tulis"
+ :settings-page/native-titlebar "Bilah judul asli"
+ :settings-page/native-titlebar-desc "Aktifkan bilah judul jendela asli di Windows dan Linux."
+ :settings-page/check-for-updates "Periksa pembaruan"
+ :settings-page/checking "Memeriksa ..."
+ :settings-page/revision "Revisi: "
+ :settings-page/changelog "Apa yang baru?"
+ :settings-page/app-updated "Aplikasi Anda sudah terbaru 🎉"
+ :settings-page/update-available "Ditemukan rilis baru "
+ :settings-page/update-error-1 "⚠️ Ups, Ada Sesuatu yang Salah!"
+ :settings-page/update-error-2 " Silakan cek "
+ :settings-permission/start-granting "Berikan"
+
+ :yes "Ya"
+
+ :submit "Kirim"
+ :cancel "Batal"
+ :close "Tutup"
+ :delete "Hapus"
+ :save "Simpan"
+ :reset "Atur ulang"
+ :type "Jenis"
+ :host "Host"
+ :port "Port"
+ :re-index "Re-indeks"
+ :re-index-detail "Membangun ulang grafik"
+ :re-index-multiple-windows-warning "Anda perlu menutup jendela lain sebelum me-re-indeks grafik ini."
+ :re-index-discard-unsaved-changes-warning "Re-indeks akan membuang grafik saat ini, dan kemudian memproses semua berkas lagi sesuai dengan yang saat ini tersimpan di disk. Anda akan kehilangan perubahan yang belum disimpan dan ini mungkin memakan waktu. Lanjutkan?"
+ :open-new-window "Jendela baru"
+ :sync-from-local-files "Segarkan"
+ :sync-from-local-files-detail "Impor perubahan dari berkas lokal"
+ :sync-from-local-changes-detected "Segarkan mendeteksi dan memproses berkas yang diubah di disk Anda yang telah berbeda dari konten halaman Logseq saat ini. Lanjutkan?"
+ 
+ :search/publishing "Cari"
+ :search "Cari atau buat halaman"
+ :whiteboard/link-whiteboard-or-block "Tautkan papan tulis/halaman/blok"
+ :whiteboard/align-left "Rata kiri"
+ :whiteboard/align-center-horizontally "Rata tengah secara horizontal"
+ :whiteboard/align-right "Rata kanan"
+ :whiteboard/distribute-horizontally "Sebarkan secara horizontal"
+ :whiteboard/align-top "Rata atas"
+ :whiteboard/align-center-vertically "Rata tengah secara vertikal"
+ :whiteboard/align-bottom "Rata bawah"
+ :whiteboard/distribute-vertically "Sebarkan secara vertikal"
+ :whiteboard/pack-into-rectangle "Paket ke dalam persegi panjang"
+ :whiteboard/zoom-to-fit "Perbesar untuk cocokkan"
+ :whiteboard/ungroup "Pecah kelompok"
+ :whiteboard/group "Kelompok"
+ :whiteboard/cut "Potong"
+ :whiteboard/copy "Salin"
+ :whiteboard/paste "Tempel"
+ :whiteboard/paste-as-link "Tempel sebagai tautan"
+ :whiteboard/export "Ekspor"
+ :whiteboard/select-all "Pilih semua"
+ :whiteboard/deselect-all "Batal pilih semua"
+ :whiteboard/lock "Kunci"
+ :whiteboard/unlock "Buka kunci"
+ :whiteboard/delete "Hapus"
+ :whiteboard/flip-horizontally "Putar secara horizontal"
+ :whiteboard/flip-vertically "Putar secara vertikal"
+ :whiteboard/move-to-front "Pindah ke depan"
+ :whiteboard/move-to-back "Pindah ke belakang"
+ :whiteboard/dev-print-shape-props "(Dev) Cetak properti bentuk"
+ :whiteboard/auto-resize "Perbesar otomatis"
+ :whiteboard/expand "Perluas"
+ :whiteboard/collapse "Ciutkan"
+ :whiteboard/website-url "URL situs web"
+ :whiteboard/reload "Muat ulang"
+ :whiteboard/open-website-url "Buka URL situs web"
+ :whiteboard/youtube-url "URL YouTube"
+ :whiteboard/open-youtube-url "Buka URL YouTube"
+ :whiteboard/twitter-url "URL Twitter"
+ :whiteboard/open-twitter-url "Buka URL Twitter"
+ :whiteboard/fill "Isi"
+ :whiteboard/stroke-type "Jenis garis"
+ :whiteboard/arrow-head "Kepala panah"
+ :whiteboard/bold "Tebal"
+ :whiteboard/italic "Miring"
+ :whiteboard/undo "Batal"
+ :whiteboard/redo "Ulang"
+ :whiteboard/zoom-in "Perbesar"
+ :whiteboard/zoom-out "Perkecil"
+ :whiteboard/select "Pilih"
+ :whiteboard/pan "Geser"
+ :whiteboard/add-block-or-page "Tambahkan blok atau halaman"
+ :whiteboard/draw "Gambar"
+ :whiteboard/highlight "Sorot"
+ :whiteboard/eraser "Penghapus"
+ :whiteboard/connector "Penyambung"
+ :whiteboard/text "Teks"
+ :whiteboard/color "Warna"
+ :whiteboard/select-custom-color "Pilih warna kustom"
+ :whiteboard/opacity "Keburaman"
+ :whiteboard/extra-small "Sangat Kecil"
+ :whiteboard/small "Kecil"
+ :whiteboard/medium "Sedang"
+ :whiteboard/large "Besar"
+ :whiteboard/extra-large "Sangat Besar"
+ :whiteboard/huge "Besar Sekali"
+ :whiteboard/scale-level "Tingkat Skala"
+ :whiteboard/rectangle "Persegi Panjang"
+ :whiteboard/circle "Lingkaran"
+ :whiteboard/triangle "Segitiga"
+ :whiteboard/shape "Bentuk"
+ :whiteboard/open-page "Buka halaman"
+ :whiteboard/open-page-in-sidebar "Buka halaman di sidebar"
+ :whiteboard/remove-link "Hapus tautan"
+ :whiteboard/link "Tautan"
+ :whiteboard/references "Referensi"
+ :whiteboard/link-to-any-page-or-block "Tautkan ke halaman atau blok apa pun"
+ :whiteboard/start-typing-to-search "Mulai mengetik untuk mencari..."
+ :whiteboard/new-block-no-colon "Blok baru"
+ :whiteboard/new-block "Blok baru:"
+ :whiteboard/new-page "Halaman baru:"
+ :whiteboard/new-whiteboard "Papan tulis baru"
+ :whiteboard/search-only-blocks "Cari hanya blok"
+ :whiteboard/search-only-pages "Cari hanya halaman"
+ :whiteboard/cache-outdated "Cache sudah ketinggalan zaman. Klik tombol 'Re-indeks' dalam menu tarik-turun grafik."
+ :whiteboard/shape-quick-links "Tautan Cepat Bentuk"
+ :whiteboard/edit-pdf "Sunting PDF"
+ :whiteboard/dashboard-card-new-whiteboard "Papan tulis baru"
+ :whiteboard/dashboard-card-created "Dibuat "
+ :whiteboard/dashboard-card-edited "Disunting "
+ :whiteboard/toggle-grid "Alihkan grid"
+ :whiteboard/snap-to-grid "Tetapkan ke grid"
+ :flashcards/modal-welcome-title "Saatnya membuat kartu!"
+ :flashcards/modal-welcome-desc-1 "Anda dapat menambahkan \"#card\" ke blok apa pun untuk mengubahnya menjadi kartu atau memicu \"/cloze\" untuk menambahkan beberapa cloze."
+ :flashcards/modal-welcome-desc-2 "Anda dapat "
+ :flashcards/modal-welcome-desc-3 "klik tautan ini"
+ :flashcards/modal-welcome-desc-4 " untuk memeriksa dokumentasinya."
+ :flashcards/modal-btn-show-answers "Tampilkan jawaban"
+ :flashcards/modal-btn-hide-answers "Sembunyikan jawaban"
+ :flashcards/modal-btn-show-clozes "Tampilkan cloze"
+ :flashcards/modal-btn-next-card "Berikutnya"
+ :flashcards/modal-btn-reset "Atur ulang"
+ :flashcards/modal-btn-reset-tip "Atur ulang kartu ini sehingga Anda dapat memeriksanya segera."
+ :flashcards/modal-btn-forgotten "Lupa"
+ :flashcards/modal-btn-remembered "Ingat"
+ :flashcards/modal-btn-recall "Memerlukan waktu untuk diingat"
+ :flashcards/modal-finished "Selamat, Anda telah meninjau semua kartu untuk pertanyaan ini, sampai jumpa berikutnya! 💯"
+ :flashcards/modal-select-all "Semua"
+ :flashcards/modal-select-switch "Beralih ke"
+ :flashcards/modal-current-total "Saat ini/Total"
+ :flashcards/modal-overdue-total "Ketinggalan/Total"
+ :flashcards/modal-toggle-preview-mode "Alihkan mode pratinjau"
+ :flashcards/modal-toggle-random-mode "Alihkan mode acak"
+
+ :page-search "Cari di halaman ini"
+ :graph-search "Cari grafik"
+ :home "Beranda"
+ :new-page "Halaman baru:"
+ :whiteboard "Papan tulis"
+ :whiteboards "Papan tulis"
+ :new-whiteboard "Papan tulis baru:"
+ :new-graph "Tambahkan grafik baru"
+ :graph "Grafik"
+ :graph/persist "Logseq sedang menyinkronkan status internal, harap tunggu beberapa detik."
+ :graph/persist-error "Sinkronisasi status internal gagal."
+ :graph/save "Menyimpan..."
+ :graph/save-success "Tersimpan dengan sukses"
+ :graph/save-error "Gagal menyimpan"
+ :graph/all-graphs "Semua grafik"
+ :graph/local-graphs "Grafik lokal:"
+ :graph/remote-graphs "Grafik jarak jauh:"
+ :export "Ekspor"
+ :export-graph "Ekspor grafik"
+ :export-page "Ekspor halaman"
+ :export-markdown "Ekspor sebagai Markdown standar (tanpa properti blok)"
+ :export-opml "Ekspor sebagai OPML"
+ :export-public-pages "Ekspor halaman publik"
+ :export-json "Ekspor sebagai JSON"
+ :export-roam-json "Ekspor sebagai JSON Roam"
+ :export-edn "Ekspor sebagai EDN"
+ :export-transparent-background "Latar belakang transparan"
+ :export-copy-to-clipboard "Salin ke clipboard"
+ :export-copied-to-clipboard "Disalin ke clipboard!"
+ :export-save-to-file "Simpan ke berkas"
+ :all-graphs "Semua grafik"
+ :all-pages "Semua halaman"
+ :all-whiteboards "Semua papan tulis"
+ :all-files "Semua berkas"
+ :remove-orphaned-pages "Hapus halaman yang tidak terhubung?"
+ :all-journals "Semua jurnal"
+ :settings "Pengaturan"
+ :settings-of-plugins "Pengaya"
+ :plugins "Pengaya"
+ :themes "Tema"
+ :relaunch-confirm-to-work "Harus mengulang aplikasi untuk membuatnya berfungsi. Apakah Anda ingin me-restart sekarang?"
+ :import "Impor"
+ :importing "Mengimpor"
+ :join-community "Bergabung dengan komunitas"
+ :discourse-title "Forum kami!"
+ :help-shortcut-title "Klik untuk memeriksa pintasan dan tips lainnya"
+ :loading "Memuat..."
+ :parsing-files "Menganalisis berkas"
+ :loading-files "Memuat berkas"
+ :login "Masuk"
+ :logout "Keluar"
+ :logout-user "Keluar ({1})"
+ :download "Unduh"
+ :language "Bahasa"
+ :remove-background "Hapus latar belakang"
+ :remove-heading "Hapus judul"
+ :heading "Judul {1}"
+ :auto-heading "Judul otomatis"
+ :open-a-directory "Buka direktori lokal"
+ :toggle-theme "Alihkan tema"
+
+ :help/shortcut-page-title "Pintasan Papan Ketik"
+
+ :plugin/installed "Terpasang"
+ :plugin/installed-plugin "Plugin Terpasang: {1}"
+ :plugin/not-installed "Belum Terpasang"
+ :plugin/installing "Sedang Menginstal"
+ :plugin/install "Instal"
+ :plugin/reload "Muat Ulang"
+ :plugin/update "Perbarui"
+ :plugin/update-plugin "Perbarui Plugin: {1} - {2}"
+ :plugin/check-update "Periksa Pembaruan"
+ :plugin/check-all-updates "Periksa Semua Pembaruan"
+ :plugin/found-updates "Pembaruan Baru"
+ :plugin/found-n-updates "Ditemukan {1} Pembaruan"
+ :plugin/update-all-selected "Perbarui Semua yang Dipilih"
+ :plugin/all-updated "Semua Telah Diperbarui!"
+ :plugin/updates-downloading "Mengunduh Pembaruan"
+ :plugin/refresh-lists "Segarkan Daftar"
+ :plugin/enabled "Aktif"
+ :plugin/disabled "Nonaktif"
+ :plugin/update-available "Pembaruan Tersedia"
+ :plugin/updating "Sedang Memperbarui"
+ :plugin/uninstall "Uninstal"
+ :plugin/marketplace "Pasar"
+ :plugin/downloads "Unduhan"
+ :plugin/stars "Bintang"
+ :plugin/title "Judul ({1})"
+ :plugin/all "Semua"
+ :plugin/unpacked "Diekstrak"
+ :plugin/delete-alert "Apakah Anda yakin ingin menginstal plugin ini [{1}]?"
+ :plugin/open-settings "Buka pengaturan"
+ :plugin/open-package "Buka paket"
+ :plugin/load-unpacked "Muatkan plugin yang diekstrak"
+ :plugin/restart "Restart Aplikasi"
+ :plugin/unpacked-tips "Pilih direktori plugin"
+ :plugin/contribute "✨ Tulis dan kirimkan plugin baru"
+ :plugin/up-to-date "Ini terbaru {1}"
+ :plugin/custom-js-alert "Ditemukan berkas custom.js, izinkan eksekusi? (Jika Anda tidak memahami kontennya, disarankan untuk tidak mengizinkan eksekusi, yang memiliki risiko keamanan tertentu.)"
+ :plugin/security-warning "Plugin dapat mengakses grafik dan berkas lokal Anda, mengeluarkan permintaan jaringan.
+       Mereka juga dapat menyebabkan kerusakan atau kehilangan data. Kami sedang mengerjakan aturan akses yang tepat untuk grafik Anda.
+       Sementara itu, pastikan Anda memiliki cadangan rutin dari grafik Anda dan hanya menginstal plugin ketika Anda dapat membaca dan
+       mengerti kode sumbernya."
+ :plugin/search-plugin "Cari plugin"
+ :plugin/open-preferences "Buka Preferensi"
+ :plugin/open-logseq-dir "Buka"
+ :plugin/remote-error "Kesalahan jarak jauh: "
+ :plugin/checking-for-updates "Memeriksa pembaruan plugin ..."
+ :plugin/list-of-updates "Pembaruan Plugin: "
+ :plugin/auto-check-for-updates "Periksa otomatis pembaruan"
+ :plugin.install-from-file/menu-title "Instal dari plugins.edn"
+ :plugin.install-from-file/title "Instal plugin dari plugins.edn"
+ :plugin.install-from-file/notice "Plugin berikut akan menggantikan plugin Anda:"
+ :plugin.install-from-file/success "Semua plugin terinstal!"
+ :pdf/copy-ref "Salin ref"
+ :pdf/copy-text "Salin teks"
+ :pdf/linked-ref "Referensi terkait"
+ :pdf/toggle-dashed "Gaya putus-putus untuk sorotan area"
+ :pdf/hl-block-colored "Label berwarna untuk blok sorotan"
+ :pdf/doc-metadata "Metadata Dokumen"
+ 
+ :updater/new-version-install "Versi baru telah diunduh."
+ :updater/quit-and-install "Mulai ulang untuk menginstal"
+ 
+ :paginates/pages "Total {1} halaman"
+ :paginates/prev "Sebelumnya"
+ :paginates/next "Berikutnya"
+ 
+ :tips/all-done "Semua Selesai!"
+ 
+ :command-palette/prompt "Ketik perintah"
+ :select/default-prompt "Pilih salah satu"
+ :select/default-select-multiple "Pilih satu atau beberapa"
+ :select.graph/prompt "Pilih grafik"
+ :select.graph/empty-placeholder-description "Tidak ada grafik yang cocok. Apakah Anda ingin menambahkan yang lain?"
+ :select.graph/add-graph "Ya, tambahkan grafik lain"
+ 
+ :file-sync/other-user-graph "Grafik lokal saat ini terikat ke grafik jarak jauh pengguna lain. Jadi tidak dapat memulai sinkronisasi."
+ :file-sync/graph-deleted "Grafik jarak jauh saat ini telah dihapus"
+ :file-sync/rsapi-cannot-upload-err "Tidak dapat memulai sinkronisasi, harap periksa apakah waktu lokal sudah benar."
+ :file-sync/connectivity-testing-failed "Pengujian koneksi jaringan gagal. Harap periksa pengaturan jaringan Anda. URL pengujian: "
+ 
+ :notification/clear-all "Hapus semua"
+ 
+ :shortcut.category/basics "Dasar"
+ :shortcut.category/formatting "Pemformatan"
+ :shortcut.category/navigating "Navigasi"
+ :shortcut.category/block-editing "Penyuntingan Blok Umum"
+ :shortcut.category/block-command-editing "Penyuntingan Perintah Blok"
+ :shortcut.category/block-selection "Pemilihan Blok (tekan Esc untuk keluar dari pemilihan)"
+ :shortcut.category/toggle "Beralih"
+ :shortcut.category/others "Lainnya"
+ :shortcut.category/plugins "Plugin"
+ :shortcut.category/whiteboard "Papan Tulis"
+ 
+ :keymap/all "Semua"
+ :keymap/disabled "Nonaktifkan"
+ :keymap/unset "Tidak diatur"
+ :keymap/custom "Kustom"
+ :keymap/search "Cari"
+ :keymap/total "Total pintasan"
+ :keymap/keystroke-filter "Filter Pekerjaan Tombol"
+ :keymap/keystroke-record-desc "Tekan urutan tombol apa saja untuk memfilter pintasan"
+ :keymap/keystroke-record-setup-label "Tekan urutan tombol apa saja untuk mengatur pintasan"
+ :keymap/restore-to-default "Kembalikan ke default sistem"
+ :keymap/customize-for-label "Sesuaikan pintasan"
+ :keymap/conflicts-for-label "Konflik Keymap untuk"
+
+ :window/minimize "Minimalkan"
+ :window/maximize "Maksimalkan"
+ :window/restore "Pulihkan"
+ :window/close "Tutup"
+ :window/exit-fullscreen "Keluar dari layar penuh"
+ 
+ :header/toggle-left-sidebar "Alihkan bilah sisi kiri"
+ :header/search "Cari"
+ :header/more "Lainnya"
+ :header/go-back "Kembali"
+ :header/go-forward "Maju"
+
+ :command.auto-complete/complete "Pelengkapan otomatis: Pilih item yang dipilih"
+ :command.auto-complete/next "Pelengkapan otomatis: Pilih item berikutnya"
+ :command.auto-complete/open-link "Pelengkapan otomatis: Buka item yang dipilih di browser"
+ :command.auto-complete/prev "Pelengkapan otomatis: Pilih item sebelumnya"
+ :command.auto-complete/shift-complete "APelengkapan otomatis: Buka item yang dipilih di sidebar"
+ :command.cards/forgotten "Kartu: terlupakan"
+ :command.cards/next-card "Kartu: kartu berikutnya"
+ :command.cards/recall "Kartu: ingatlah sejenak"
+ :command.cards/remembered "Kartu: diingatkan"
+ :command.cards/toggle-answers "Kartu: tampilkan / sembunyikan jawaban/clozes"
+ :command.command/run "Jalankan perintah git"
+ :command.command/toggle-favorite "Tambahkan/hapus dari favorit"
+ :command.command-palette/toggle "Aktifkan/matikan palet perintah"
+ :command.date-picker/complete "Date picker: Pilih hari yang dipilih"
+ :command.date-picker/next-day "Date picker: Pilih hari berikutnya"
+ :command.date-picker/next-week "Date picker: Pilih minggu berikutnya"
+ :command.date-picker/prev-day "Date picker: Pilih hari sebelumnya"
+ :command.date-picker/prev-week "Date picker: Pilih minggu sebelumnya"
+ :command.dev/show-block-ast "(Dev) Tampilkan AST blok"
+ :command.dev/show-block-data "(Dev) Tampilkan data blok"
+ :command.dev/show-page-ast "(Dev) Tampilkan AST halaman"
+ :command.dev/show-page-data "(Dev) Tampilkan data halaman"
+ :command.editor/backspace "Backspace / Hapus ke belakang"
+ :command.editor/backward-kill-word "Hapus kata ke belakang"
+ :command.editor/backward-word "Geser kursor ke belakang satu kata"
+ :command.editor/beginning-of-block "Geser kursor ke awal blok"
+ :command.editor/bold "Tebal"
+ :command.editor/clear-block "Hapus seluruh isi blok"
+ :command.editor/collapse-block-children "Kecilkan"
+ :command.editor/copy "Salin (menyalin seleksi atau referensi blok)"
+ :command.editor/copy-current-file "Salin berkas saat ini"
+ :command.editor/copy-embed "Salin referensi blok yang mengarah ke blok saat ini"
+ :command.editor/copy-page-url "Salin URL halaman"
+ :command.editor/copy-text "Salin seleksi sebagai teks"
+ :command.editor/cut "Potong"
+ :command.editor/cycle-todo "Putar status TODO item saat ini"
+ :command.editor/delete "Hapus / Hapus ke depan"
+ :command.editor/delete-selection "Hapus blok yang dipilih"
+ :command.editor/down "Geser kursor ke bawah / Pilih ke bawah"
+ :command.editor/end-of-block "Geser kursor ke akhir blok"
+ :command.editor/escape-editing "Keluar dari mode penyuntingan"
+ :command.editor/expand-block-children "Perluas"
+ :command.editor/follow-link "Ikuti tautan di bawah kursor"
+ :command.editor/forward-kill-word "Hapus kata ke depan"
+ :command.editor/forward-word "Geser kursor maju satu kata"
+ :command.editor/highlight "Sorot"
+ :command.editor/indent "Sisipkan blok"
+ :command.editor/insert-link "Tautan HTML"
+ :command.editor/insert-youtube-timestamp "Sisipkan penanda waktu YouTube"
+ :command.editor/italics "Miring"
+ :command.editor/kill-line-after "Hapus baris setelah posisi kursor"
+ :command.editor/kill-line-before "Hapus baris sebelum posisi kursor"
+ :command.editor/left "Geser kursor ke kiri / Buka blok yang dipilih di awal"
+ :command.editor/move-block-down "Geser blok ke bawah"
+ :command.editor/move-block-up "Geser blok ke atas"
+ :command.editor/new-block "Buat blok baru"
+ :command.editor/new-line "Baris baru dalam blok saat ini"
+ :command.editor/new-whiteboard "Papan tulis baru"
+ :command.editor/open-edit "Sunting blok yang dipilih"
+ :command.editor/open-file-in-default-app "Buka berkas dalam aplikasi default"
+ :command.editor/open-file-in-directory "Buka berkas di direktori induk"
+ :command.editor/open-link-in-sidebar "Buka tautan di sidebar"
+ :command.editor/outdent "Kurangi sisipan blok"
+ :command.editor/paste-text-in-one-block-at-point "Tempelkan teks ke dalam satu blok pada posisi kursor"
+ :command.editor/redo "Lakukan lagi"
+ :command.editor/replace-block-reference-at-point "Ganti referensi blok dengan isinya pada posisi kursor"
+ :command.editor/right "Geser kursor ke kanan / Buka blok yang dipilih di akhir"
+ :command.editor/select-all-blocks "Pilih semua blok"
+ :command.editor/select-block-down "Pilih blok di bawah"
+ :command.editor/select-block-up "Pilih blok di atas"
+ :command.editor/select-down "Pilih konten di bawah"
+ :command.editor/select-parent "Pilih blok induk"
+ :command.editor/select-up "Pilih konten di atas"
+ :command.editor/strike-through "Coret"
+ :command.editor/toggle-number-list "Hidupkan/matikan daftar angka"
+ :command.editor/toggle-open-blocks "Hidupkan/matikan blok terbuka (perluas atau perkecil semua blok)"
+ :command.editor/toggle-undo-redo-mode "Hidupkan/matikan mode undo redo (global atau hanya halaman)"
+ :command.editor/undo "Batal"
+ :command.editor/up "Geser kursor ke atas / Pilih ke atas"
+ :command.editor/zoom-in "Perbesar blok yang sedang disunting / Maju sebaliknya"
+ :command.editor/zoom-out "Perkecil blok yang sedang disunting / Mundur sebaliknya"
+ :command.git/commit "Buat commit git dengan pesan"
+ :command.go/all-graphs "Buka semua grafik"
+ :command.go/all-pages "Buka semua halaman"
+ :command.go/backward "Mundur"
+ :command.go/electron-find-in-page "Cari teks di halaman"
+ :command.go/electron-jump-to-the-next "Lompat ke pencocokan berikutnya dengan pencarian di bilah Temukan Anda"
+ :command.go/electron-jump-to-the-previous "Lompat ke pencocokan sebelumnya dengan pencarian di bilah Temukan Anda"
+ :command.go/flashcards "Alihkan kartu flash"
+ :command.go/forward "Maju"
+ :command.go/graph-view "Buka tampilan grafik"
+ :command.go/home "Buka beranda"
+ :command.go/journals "Buka jurnal"
+ :command.go/keyboard-shortcuts "Buka pintasan keyboard"
+ :command.go/next-journal "Buka jurnal berikutnya"
+ :command.go/prev-journal "Buka jurnal sebelumnya"
+ :command.go/search "Cari halaman dan blok"
+ :command.go/search-in-page "Cari blok di halaman saat ini"
+ :command.go/tomorrow "Buka ke esok hari"
+ :command.go/whiteboards "Buka papan tulis"
+ :command.graph/add "Tambahkan grafik"
+ :command.graph/export-as-html "Ekspor halaman grafik sebagai HTML"
+ :command.graph/open "Pilih grafik untuk dibuka"
+ :command.graph/re-index "Reindeks grafik saat ini"
+ :command.graph/remove "Hapus grafik"
+ :command.graph/save "Simpan grafik saat ini ke disk"
+ :command.misc/copy "Salin"
+ :command.pdf/close "Pdf: Tutup dokumen pdf saat ini"
+ :command.pdf/find "Pdf: Cari teks dokumen pdf saat ini"
+ :command.pdf/next-page "Pdf: Halaman berikutnya dokumen pdf saat ini"
+ :command.pdf/previous-page "Pdf: Halaman sebelumnya dokumen pdf saat ini"
+ :command.search/re-index "Bangun ulang indeks pencarian"
+ :command.sidebar/clear "Hapus semua di bilah sisi kanan"
+ :command.sidebar/close-top "Tutup item teratas di bilah sisi kanan"
+ :command.sidebar/open-today-page "Buka halaman hari ini di bilah sisi kanan"
+ :command.ui/clear-all-notifications "Hapus semua pemberitahuan"
+ :command.ui/goto-plugins "Buka dasbor plugin"
+ :command.ui/install-plugins-from-file "Pasang plugin dari plugins.edn"
+ :command.ui/select-theme-color "Pilih warna tema yang tersedia"
+ :command.ui/toggle-brackets "Alihkan tampilan tanda kurung"
+ :command.ui/toggle-cards "Alihkan kartu"
+ :command.ui/toggle-contents "Alihkan Konten di bilah samping"
+ :command.ui/toggle-document-mode "Alihkan mode dokumen"
+ :command.ui/toggle-help "Alihkan bantuan"
+ :command.ui/toggle-left-sidebar "Alihkan bilah sisi kiri"
+ :command.ui/toggle-right-sidebar "Alihkan bilah sisi kanan"
+ :command.ui/toggle-settings "Alihkan pengaturan"
+ :command.ui/toggle-theme "Alihkan antara tema gelap/cerah"
+ :command.ui/toggle-wide-mode "Alihkan mode lebar"
+ :command.whiteboard/bring-forward "Pindahkan ke depan"
+ :command.whiteboard/bring-to-front "Pindahkan ke depan sekali"
+ :command.whiteboard/connector "Alat konektor"
+ :command.whiteboard/ellipse "Alat elips"
+ :command.whiteboard/eraser "Alat penghapus"
+ :command.whiteboard/group "Pilihan grup"
+ :command.whiteboard/highlighter "Alat penggaris"
+ :command.whiteboard/lock "Kunci pilihan"
+ :command.whiteboard/pan "Alat geser"
+ :command.whiteboard/pencil "Alat pensil"
+ :command.whiteboard/portal "Alat portal"
+ :command.whiteboard/rectangle "Alat persegi panjang"
+ :command.whiteboard/reset-zoom "Atur ulang zoom"
+ :command.whiteboard/select "Alat pilihan"
+ :command.whiteboard/send-backward "Pindahkan ke belakang"
+ :command.whiteboard/send-to-back "Pindahkan ke belakang sekali"
+ :command.whiteboard/text "Alat teks"
+ :command.whiteboard/toggle-grid "Alihkan tampilan grid kanvas"
+ :command.whiteboard/ungroup "Pilihan ungroup"
+ :command.whiteboard/unlock "Buka kunci pilihan"
+ :command.whiteboard/zoom-in "Perbesar"
+ :command.whiteboard/zoom-out "Perkecil"
+ :command.whiteboard/zoom-to-fit "Zoom ke gambar"
+ :command.whiteboard/zoom-to-selection "Zoom ke seleksi"
+}

+ 0 - 2
src/resources/dicts/it.edn

@@ -102,8 +102,6 @@
  :settings-page/enable-tooltip "Suggerimenti"
  :settings-page/enable-journals "Diario"
  :settings-page/enable-all-pages-public "Tutte le pagine pubbliche durante la pubblicazione"
- :settings-page/customize-shortcuts "Scorciatoie da tastiera"
- :settings-page/shortcut-settings "Personalizza scorciatoie"
  :settings-page/home-default-page "Imposta la home page predefinita"
  :settings-page/clear-cache "Pulisci cache"
  :settings-page/clear "Pulisci"

+ 0 - 2
src/resources/dicts/ja.edn

@@ -308,8 +308,6 @@
  :settings-page/enable-tooltip "ツールチップ"
  :settings-page/enable-journals "日誌"
  :settings-page/enable-all-pages-public "パブリッシュ時には全てのページを公開する"
- :settings-page/customize-shortcuts "キーボードショートカット"
- :settings-page/shortcut-settings "ショートカットをカスタマイズ"
  :settings-page/home-default-page "デフォルトのホームページを設定"
  :settings-page/clear-cache "キャッシュをクリア"
  :settings-page/clear "クリア"

+ 0 - 2
src/resources/dicts/ko.edn

@@ -104,8 +104,6 @@
  :settings-page/enable-tooltip "툴팁 활성화"
  :settings-page/enable-journals "일지 활성화"
  :settings-page/enable-all-pages-public "출판할 때 모든 페이지 공개로 설정"
- :settings-page/customize-shortcuts "키보드 단축키"
- :settings-page/shortcut-settings "단축키 설정"
  :settings-page/home-default-page "기본 홈 페이지 설정"
  :settings-page/clear-cache "캐시 지우기"
  :settings-page/clear "지우기"

+ 0 - 2
src/resources/dicts/nb-no.edn

@@ -102,8 +102,6 @@
  :settings-page/enable-tooltip "Aktiver verktøytips"
  :settings-page/enable-journals "Aktiver dagbøker"
  :settings-page/enable-all-pages-public "Aktiver alle sider som offentlige ved publisering"
- :settings-page/customize-shortcuts "Tastatursnarveier"
- :settings-page/shortcut-settings "Tilpass snarveier"
  :settings-page/home-default-page "Angi standard hjemmeside"
  :settings-page/clear-cache "Slett hurtigbuffer"
  :settings-page/clear "Slett"

+ 0 - 2
src/resources/dicts/nl.edn

@@ -192,7 +192,6 @@
  :settings-page/custom-configuration "Aangepaste configuratie"
  :settings-page/custom-date-format "Gewenst datumformaat"
  :settings-page/custom-theme "Aangepast thema"
- :settings-page/customize-shortcuts "Toetsenbord snelkoppelingen"
  :settings-page/developer-mode "Ontwikkelaar modus"
  :settings-page/developer-mode-desc "De ontwikkelaarsmodus helpt bijdragers en extensie-ontwikkelaars om hun integratie met Logseq efficiënter te testen."
  :settings-page/disable-sentry "Gebruiksgegevens en diagnostiek naar Logseq sturen"
@@ -215,7 +214,6 @@
  :settings-page/preferred-file-format "Gewenst bestandsformaat"
  :settings-page/preferred-outdenting "Logische uitdenting"
  :settings-page/preferred-workflow "Voorkeur voor workflow"
- :settings-page/shortcut-settings "Snelkoppelingen aanpassen"
  :settings-page/show-brackets "Toon beugels"
  :settings-page/spell-checker "Spellingcontrole"
  :settings-page/tab-advanced "Geavanceerd"

+ 0 - 2
src/resources/dicts/pl.edn

@@ -107,8 +107,6 @@
  :settings-page/enable-tooltip "Podpowiedzi"
  :settings-page/enable-journals "Dzienniki"
  :settings-page/enable-all-pages-public "Publikuj wszystkie strony"
- :settings-page/customize-shortcuts "Skróty klawiszowe"
- :settings-page/shortcut-settings "Zmień skróty"
  :settings-page/home-default-page "Ustaw domyślną stronę startową"
  :settings-page/clear-cache "Wyczyść cache"
  :settings-page/clear "Wyczyść"

Some files were not shown because too many files changed in this diff