1
0
Эх сурвалжийг харах

Merge branch 'master' into feat/crdt-rtc

Tienson Qin 3 жил өмнө
parent
commit
eca9716831
100 өөрчлөгдсөн 4076 нэмэгдсэн , 1780 устгасан
  1. 3 1
      .carve/config.edn
  2. 10 2
      .carve/ignore
  3. 21 2
      .clj-kondo/config.edn
  4. 2 2
      .github/workflows/build-android.yml
  5. 13 2
      .github/workflows/build.yml
  6. 3 0
      .gitignore
  7. 7 5
      CODEBASE_OVERVIEW.md
  8. 2 2
      android/app/build.gradle
  9. 3 0
      android/app/capacitor.build.gradle
  10. 3 0
      android/app/src/main/assets/capacitor.config.json
  11. 12 0
      android/app/src/main/assets/capacitor.plugins.json
  12. 33 25
      android/app/src/main/java/com/logseq/app/FsWatcher.java
  13. 9 0
      android/capacitor.settings.gradle
  14. 9 2
      bb.edn
  15. 4 0
      capacitor.config.ts
  16. 5 6
      deps.edn
  17. 2 2
      docs/contributing-to-translations.md
  18. 10 3
      docs/dev-practices.md
  19. 3 4
      e2e-tests/code-editing.spec.ts
  20. 3 3
      e2e-tests/sidebar.spec.ts
  21. 3 0
      externs.js
  22. 42 6
      ios/App/App.xcodeproj/project.pbxproj
  23. 3 33
      ios/App/App/AppDelegate.swift
  24. 211 0
      ios/App/App/FileSync/Extensions.swift
  25. 19 0
      ios/App/App/FileSync/FileSync.m
  26. 333 0
      ios/App/App/FileSync/FileSync.swift
  27. 36 0
      ios/App/App/FileSync/Payload.swift
  28. 326 0
      ios/App/App/FileSync/SyncClient.swift
  29. 41 24
      ios/App/App/FsWatcher.swift
  30. 13 0
      ios/App/App/Utils.m
  31. 22 0
      ios/App/App/Utils.swift
  32. 3 0
      ios/App/App/capacitor.config.json
  33. 4 0
      ios/App/Podfile
  34. 1 1
      ios/App/ShareViewController/ShareViewController.swift
  35. 112 51
      libs/src/LSPlugin.core.ts
  36. 40 19
      libs/src/LSPlugin.ts
  37. 4 3
      libs/src/LSPlugin.user.ts
  38. 5 10
      libs/src/helpers.ts
  39. 9 2
      package.json
  40. 1 0
      public/index.html
  41. 6 0
      resources/css/animation.css
  42. 13 3
      resources/css/common.css
  43. 9 0
      resources/css/shepherd.css
  44. 1 0
      resources/electron.html
  45. 1 0
      resources/index.html
  46. 0 0
      resources/js/lsplugin.core.js
  47. 117 0
      resources/js/shepherd.min.js
  48. 4 6
      resources/package.json
  49. 17 0
      scripts/src/logseq/tasks/dev.clj
  50. 30 0
      scripts/src/logseq/tasks/nbb.clj
  51. 12 9
      shadow-cljs.edn
  52. 51 0
      src/electron/electron/backup_file.cljs
  53. 18 17
      src/electron/electron/core.cljs
  54. 1 1
      src/electron/electron/file_sync_rsapi.cljs
  55. 11 9
      src/electron/electron/fs_watcher.cljs
  56. 12 34
      src/electron/electron/handler.cljs
  57. 14 14
      src/electron/electron/utils.cljs
  58. 9 13
      src/main/frontend/commands.cljs
  59. 413 258
      src/main/frontend/components/block.cljs
  60. 68 8
      src/main/frontend/components/block.css
  61. 0 1
      src/main/frontend/components/command_palette.css
  62. 11 12
      src/main/frontend/components/content.cljs
  63. 12 111
      src/main/frontend/components/editor.cljs
  64. 6 33
      src/main/frontend/components/editor.css
  65. 176 0
      src/main/frontend/components/encryption.cljs
  66. 6 6
      src/main/frontend/components/file.cljs
  67. 30 24
      src/main/frontend/components/header.cljs
  68. 120 99
      src/main/frontend/components/header.css
  69. 1 1
      src/main/frontend/components/hierarchy.cljs
  70. 10 6
      src/main/frontend/components/journal.cljs
  71. 553 338
      src/main/frontend/components/onboarding/index.css
  72. 158 0
      src/main/frontend/components/onboarding/quick_tour.cljs
  73. 8 7
      src/main/frontend/components/onboarding/setups.cljs
  74. 31 27
      src/main/frontend/components/page.cljs
  75. 9 3
      src/main/frontend/components/page.css
  76. 8 8
      src/main/frontend/components/page_menu.cljs
  77. 163 59
      src/main/frontend/components/plugins.cljs
  78. 3 8
      src/main/frontend/components/plugins.css
  79. 1 2
      src/main/frontend/components/query_table.cljs
  80. 28 28
      src/main/frontend/components/reference.cljs
  81. 14 24
      src/main/frontend/components/repo.cljs
  82. 30 40
      src/main/frontend/components/right_sidebar.cljs
  83. 9 12
      src/main/frontend/components/search.cljs
  84. 1 1
      src/main/frontend/components/select.cljs
  85. 36 42
      src/main/frontend/components/settings.cljs
  86. 159 88
      src/main/frontend/components/sidebar.cljs
  87. 73 37
      src/main/frontend/components/sidebar.css
  88. 0 0
      src/main/frontend/components/svg.cljs
  89. 1 0
      src/main/frontend/components/theme.cljs
  90. 2 2
      src/main/frontend/components/widgets.cljs
  91. 33 22
      src/main/frontend/config.cljs
  92. 17 36
      src/main/frontend/date.cljs
  93. 4 10
      src/main/frontend/db.cljs
  94. 4 3
      src/main/frontend/db/conn.cljs
  95. 2 3
      src/main/frontend/db/debug.cljs
  96. 1 1
      src/main/frontend/db/default.cljs
  97. 134 87
      src/main/frontend/db/model.cljs
  98. 8 7
      src/main/frontend/db/query_dsl.cljs
  99. 8 5
      src/main/frontend/db/query_react.cljs
  100. 14 5
      src/main/frontend/db/react.cljs

+ 3 - 1
.carve/config.edn

@@ -5,5 +5,7 @@
                   ;; Ignore b/c too many false positives
                   frontend.db
                   ;; Used for debugging
-                  frontend.db.debug]
+                  frontend.db.debug
+                  ;; carve doesn't detect nbb only usage
+                  logseq.graph-parser.log]
  :report {:format :ignore}}

+ 10 - 2
.carve/ignore

@@ -15,6 +15,12 @@ frontend.debug/print
 ;; Lazily loaded
 frontend.extensions.code/editor
 ;; Lazily loaded
+frontend.extensions.age-encryption/keygen
+frontend.extensions.age-encryption/encrypt-with-x25519
+frontend.extensions.age-encryption/decrypt-with-x25519
+frontend.extensions.age-encryption/encrypt-with-user-passphrase
+frontend.extensions.age-encryption/decrypt-with-user-passphrase
+;; Lazily loaded
 frontend.extensions.excalidraw/draw
 ;; Referenced in commented TODO
 frontend.extensions.pdf.utils/get-page-bounding
@@ -23,7 +29,7 @@ frontend.extensions.zotero.api/item
 ;; For repl
 frontend.external.roam/reset-state!
 ;; For repl
-frontend.format.mldoc/ast-export-markdown
+logseq.graph-parser.mldoc/ast-export-markdown
 ;; Protocol fn wrapper that could be used
 frontend.fs/readdir
 ;; Referenced in TODO
@@ -66,5 +72,7 @@ frontend.util/trace!
 frontend.util.pool/terminate-pool!
 ;; Repl fn
 frontend.util.property/add-page-properties
-;; Used by shadow
+;; Test runner used by shadow
 frontend.test.node-test-runner/main
+;; Test runner for nbb
+logseq.graph-parser.nbb-test-runner/run-tests

+ 21 - 2
.clj-kondo/config.edn

@@ -2,7 +2,13 @@
  {:unresolved-symbol {:exclude [goog.DEBUG
                                 goog.string.unescapeEntities
                                 ;; TODO:lint: Fix when fixing all type hints
-                                object]}
+                                object
+                                ;; TODO: Remove parse-* and update-* when https://github.com/clj-kondo/clj-kondo/issues/1694 is done
+                                parse-long
+                                parse-double
+                                parse-uuid
+                                update-keys
+                                update-vals]}
   ;; TODO:lint: Remove node-path excludes once we have a cleaner api
   :unresolved-var {:exclude [frontend.util/node-path.basename
                              frontend.util/node-path.dirname
@@ -17,7 +23,20 @@
              medley.core medley
              frontend.db.query-dsl query-dsl
              frontend.db.react react
-             frontend.db.query-react query-react}}}
+             frontend.db.query-react query-react
+             frontend.util util
+             frontend.util.property property
+             frontend.config config
+             frontend.format.mldoc mldoc
+             frontend.format.block block
+             frontend.handler.extract extract
+             logseq.graph-parser.text text
+             logseq.graph-parser.block gp-block
+             logseq.graph-parser.mldoc gp-mldoc
+             logseq.graph-parser.util gp-util
+             logseq.graph-parser.property gp-property
+             logseq.graph-parser.config gp-config
+             logseq.graph-parser.date-time-util date-time-util}}}
 
  :hooks {:analyze-call {rum.core/defc hooks.rum/defc
                          rum.core/defcs hooks.rum/defcs}}

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

@@ -89,8 +89,8 @@ jobs:
           sed -i 's/defonce version ".*"/defonce version "${{ steps.ref.outputs.version }}"/g' src/main/frontend/version.cljs
           sed -i 's/versionName ".*"/versionName "${{ steps.ref.outputs.version }}"/g' android/app/build.gradle
 
-      - name: Compile CLJS
-        run: yarn install && yarn release
+      - name: Compile CLJS - android variant, use es6 instead of es-next
+        run: yarn install && yarn release-android-app
 
       - name: Upload Sentry Sourcemaps (beta only)
         if: ${{ inputs.build-target == 'beta' || github.event.inputs.build-target == 'beta' }}

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

@@ -17,7 +17,7 @@ env:
   JAVA_VERSION: '8'
   # This is the latest node version we can run.
   NODE_VERSION: '16'
-  BABASHKA_VERSION: '0.7.7'
+  BABASHKA_VERSION: '0.8.1'
 
 jobs:
 
@@ -52,6 +52,11 @@ jobs:
         with:
           cli: ${{ env.CLOJURE_VERSION }}
 
+      - name: Setup Babashka
+        uses: turtlequeue/[email protected]
+        with:
+          babashka-version: ${{ env.BABASHKA_VERSION }}
+
       - name: Clojure cache
         uses: actions/cache@v2
         id: clojure-deps
@@ -69,11 +74,17 @@ jobs:
       - name: Fetch yarn deps
         run: yarn install --frozen-lockfile
 
-      - name: Run ClojureScript test
+      - name: Run ClojureScript tests
         run: |
           yarn cljs:test
           node static/tests.js
 
+      - name: Run nbb tests for graph-parser
+        run: yarn nbb-logseq -cp src/main:src/test -m logseq.graph-parser.nbb-test-runner/run-tests
+      # In this job because it depends on an npm package
+      - name: Load nbb compatible namespaces
+        run: bb test:load-nbb-compatible-namespaces
+
   lint:
     runs-on: ubuntu-latest
 

+ 3 - 0
.gitignore

@@ -42,3 +42,6 @@ android/app/src/main/assets/capacitor.plugin.json
 ios/App/App/capacitor.config.json
 
 startup.png
+
+/src/test/docs
+~*~

+ 7 - 5
CODEBASE_OVERVIEW.md

@@ -36,15 +36,17 @@ After cloning the [Logseq repository](https://github.com/logseq/logseq), there a
 
 - Config files are located at the root directory. `package.json` contains the JavaScript dependencies while `deps.edn` contains their Clojure counterparts. `shadow-cljs.edn` and `gulpfile.js` contain all the build scripts.
 
-- `/public` and `/resources` contain all the static assets
+- `public/` and `resources/` contain all the static assets
 
-- `/src` is where most of the code locates.
+- `src/` is where most of the code is located.
 
-  - `/src/electron` and `/src/main/electron` contains code specific to the desktop app.
+  - `src/electron/` and `src/main/electron/` contains code specific to the desktop app.
 
-  - `/src/test` contains all the test and `/src/dev-cljs` contains some development utilities.
+  - `src/test/` contains all the tests and `src/dev-cljs/` contains some development utilities.
 
-  - `/src/main/frontend` contains code that powers the Logseq editor. Folders and files inside are organized by features or functions. For example, `components` contains all the UI components and `handler` contains all the event-handling code. You can explore on your own interest.
+  - `src/main/frontend/` contains code that powers the Logseq editor. Folders and files inside are organized by features or functions. For example, `components` contains all the UI components and `handler` contains all the event-handling code. You can explore on your own interest.
+
+  - `src/main/logseq/` contains the api used by plugins and the graph-parser.
 
 ## Data Flow
 

+ 2 - 2
android/app/build.gradle

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

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

@@ -11,8 +11,11 @@ apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
 dependencies {
     implementation project(':capacitor-app')
     implementation project(':capacitor-camera')
+    implementation project(':capacitor-clipboard')
     implementation project(':capacitor-filesystem')
+    implementation project(':capacitor-haptics')
     implementation project(':capacitor-keyboard')
+    implementation project(':capacitor-share')
     implementation project(':capacitor-splash-screen')
     implementation project(':capacitor-status-bar')
     implementation project(':capacitor-voice-recorder')

+ 3 - 0
android/app/src/main/assets/capacitor.config.json

@@ -10,6 +10,9 @@
 			"androidScaleType": "CENTER_CROP",
 			"splashImmersive": false,
 			"backgroundColor": "#002b36"
+		},
+		"Keyboard": {
+			"resize": "none"
 		}
 	},
 	"ios": {

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

@@ -7,14 +7,26 @@
 		"pkg": "@capacitor/camera",
 		"classpath": "com.capacitorjs.plugins.camera.CameraPlugin"
 	},
+	{
+		"pkg": "@capacitor/clipboard",
+		"classpath": "com.capacitorjs.plugins.clipboard.ClipboardPlugin"
+	},
 	{
 		"pkg": "@capacitor/filesystem",
 		"classpath": "com.capacitorjs.plugins.filesystem.FilesystemPlugin"
 	},
+	{
+		"pkg": "@capacitor/haptics",
+		"classpath": "com.capacitorjs.plugins.haptics.HapticsPlugin"
+	},
 	{
 		"pkg": "@capacitor/keyboard",
 		"classpath": "com.capacitorjs.plugins.keyboard.KeyboardPlugin"
 	},
+	{
+		"pkg": "@capacitor/share",
+		"classpath": "com.capacitorjs.plugins.share.SharePlugin"
+	},
 	{
 		"pkg": "@capacitor/splash-screen",
 		"classpath": "com.capacitorjs.plugins.splashscreen.SplashScreenPlugin"

+ 33 - 25
android/app/src/main/java/com/logseq/app/FsWatcher.java

@@ -1,5 +1,7 @@
 package com.logseq.app;
 
+import android.annotation.SuppressLint;
+import android.os.Build;
 import android.system.ErrnoException;
 import android.system.Os;
 import android.system.StructStat;
@@ -26,7 +28,6 @@ public class FsWatcher extends Plugin {
 
     List<SingleFileObserver> observers;
     private String mPath;
-    private Uri mPathUri;
 
     @Override
     public void load() {
@@ -35,17 +36,23 @@ public class FsWatcher extends Plugin {
 
     @PluginMethod()
     public void watch(PluginCall call) {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
+            call.reject("Android version not supported");
+            return;
+        }
         String pathParam = call.getString("path");
         // check file:// or no scheme uris
         Uri u = Uri.parse(pathParam);
         Log.i("FsWatcher", "watching " + u);
         if (u.getScheme() == null || u.getScheme().equals("file")) {
-            File pathObj = new File(u.getPath());
-            if (pathObj == null) {
+            File pathObj;
+            try {
+                pathObj = new File(u.getPath());
+            } catch (Exception e) {
                 call.reject("invalid watch path: " + pathParam);
                 return;
             }
-            mPathUri = Uri.fromFile(pathObj);
+
             mPath = pathObj.getAbsolutePath();
 
             int mask = FileObserver.CLOSE_WRITE |
@@ -56,15 +63,16 @@ public class FsWatcher extends Plugin {
                 call.reject("already watching");
                 return;
             }
-            observers = new ArrayList<SingleFileObserver>();
+            observers = new ArrayList<>();
             observers.add(new SingleFileObserver(pathObj, mask));
 
             // NOTE: only watch first level of directory
             File[] files = pathObj.listFiles();
             if (files != null) {
-                for (int i = 0; i < files.length; ++i) {
-                    if (files[i].isDirectory() && !files[i].getName().startsWith(".")) {
-                        observers.add(new SingleFileObserver(files[i], mask));
+                for (File file : files) {
+                    String filename = file.getName();
+                    if (file.isDirectory() && !filename.startsWith(".") && !filename.equals("bak") && !filename.equals("node_modules")) {
+                        observers.add(new SingleFileObserver(file, mask));
                     }
                 }
             }
@@ -103,13 +111,14 @@ public class FsWatcher extends Plugin {
         }
         File[] files = pathObj.listFiles();
         if (files != null) {
-            for (int i = 0; i < files.length; ++i) {
-                if (files[i].isDirectory() && !files[i].getName().startsWith(".") && !files[i].getName().equals("bak")) {
-                    this.initialNotify(files[i], maxDepth - 1);
-                } else if (files[i].isFile()
-                        && Pattern.matches("[^.].*?\\.(md|org|css|edn|text|markdown|yml|yaml|json|js)$",
-                                files[i].getName())) {
-                    this.onObserverEvent(FileObserver.CREATE, files[i].getAbsolutePath());
+            for (File file : files) {
+                String filename = file.getName();
+                if (file.isDirectory() && !filename.startsWith(".") && !filename.equals("bak") && !filename.equals("version-files") && !filename.equals("node_modules")) {
+                    this.initialNotify(file, maxDepth - 1);
+                } else if (file.isFile()
+                        && Pattern.matches("(?i)[^.].*?\\.(md|org|css|edn|js|markdown)$",
+                        file.getName())) {
+                    this.onObserverEvent(FileObserver.CREATE, file.getAbsolutePath());
                 }
             }
         }
@@ -132,9 +141,7 @@ public class FsWatcher extends Plugin {
                 try {
                     obj.put("stat", getFileStat(path));
                     content = getFileContents(f);
-                } catch (IOException e) {
-                    e.printStackTrace();
-                } catch (ErrnoException e) {
+                } catch (IOException | ErrnoException e) {
                     e.printStackTrace();
                 }
                 obj.put("content", content);
@@ -145,9 +152,7 @@ public class FsWatcher extends Plugin {
                 try {
                     obj.put("stat", getFileStat(path));
                     content = getFileContents(f);
-                } catch (IOException e) {
-                    e.printStackTrace();
-                } catch (ErrnoException e) {
+                } catch (IOException | ErrnoException e) {
                     e.printStackTrace();
                 }
                 obj.put("content", content);
@@ -172,7 +177,7 @@ public class FsWatcher extends Plugin {
         ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
 
         byte[] buffer = new byte[1024];
-        int length = 0;
+        int length;
 
         while ((length = inputStream.read(buffer)) != -1) {
             outputStream.write(buffer, 0, length);
@@ -183,22 +188,25 @@ public class FsWatcher extends Plugin {
     }
 
     public static JSObject getFileStat(final String path) throws ErrnoException {
+        File file = new File(path);
         StructStat stat = Os.stat(path);
         JSObject obj = new JSObject();
         obj.put("atime", stat.st_atime);
         obj.put("mtime", stat.st_mtime);
         obj.put("ctime", stat.st_ctime);
+        obj.put("size", file.length());
         return obj;
     }
 
     private class SingleFileObserver extends FileObserver {
-        private String mPath;
+        private final String mPath;
 
         public SingleFileObserver(String path, int mask) {
             super(path, mask);
             mPath = path;
         }
 
+        @SuppressLint("NewApi")
         public SingleFileObserver(File path, int mask) {
             super(path, mask);
             mPath = path.getAbsolutePath();
@@ -206,9 +214,9 @@ public class FsWatcher extends Plugin {
 
         @Override
         public void onEvent(int event, String path) {
-            if (path != null) {
+            if (path != null && !path.equals("graphs-txid.edn") && !path.equals("broken-config.edn")) {
                 Log.d("FsWatcher", "got path=" + path + " event=" + event);
-                if (Pattern.matches("[^.].*?\\.(md|org|css|edn|text|markdown|yml|yaml|json|js)$", path)) {
+                if (Pattern.matches("(?i)[^.].*?\\.(md|org|css|edn|js|markdown)$", path)) {
                     String fullPath = mPath + "/" + path;
                     FsWatcher.this.onObserverEvent(event, fullPath);
                 }

+ 9 - 0
android/capacitor.settings.gradle

@@ -8,12 +8,21 @@ project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/
 include ':capacitor-camera'
 project(':capacitor-camera').projectDir = new File('../node_modules/@capacitor/camera/android')
 
+include ':capacitor-clipboard'
+project(':capacitor-clipboard').projectDir = new File('../node_modules/@capacitor/clipboard/android')
+
 include ':capacitor-filesystem'
 project(':capacitor-filesystem').projectDir = new File('../node_modules/@capacitor/filesystem/android')
 
+include ':capacitor-haptics'
+project(':capacitor-haptics').projectDir = new File('../node_modules/@capacitor/haptics/android')
+
 include ':capacitor-keyboard'
 project(':capacitor-keyboard').projectDir = new File('../node_modules/@capacitor/keyboard/android')
 
+include ':capacitor-share'
+project(':capacitor-share').projectDir = new File('../node_modules/@capacitor/share/android')
+
 include ':capacitor-splash-screen'
 project(':capacitor-splash-screen').projectDir = new File('../node_modules/@capacitor/splash-screen/android')
 

+ 9 - 2
bb.edn

@@ -2,8 +2,9 @@
  :deps
  {org.babashka/spec.alpha
   {:git/url "https://github.com/babashka/spec.alpha"
-   :sha "1a841c4cc1d4f6dab7505a98ed2d532dd9d56b78"}
-  medley/medley {:mvn/version "1.3.0"}}
+   :sha "1a841c4cc1d4f6dab7505a98ed2d532dd9d56b78"}}
+ :pods
+ {clj-kondo/clj-kondo {:version "2022.02.09"}}
  :tasks
  {dev:watch
   logseq.tasks.dev/watch
@@ -22,6 +23,12 @@
   dev:validate-local-storage
   logseq.tasks.spec/validate-local-storage
 
+  dev:lint
+  logseq.tasks.dev/lint
+
+  test:load-nbb-compatible-namespaces
+  logseq.tasks.nbb/load-compatible-namespaces
+
   lang:list
   logseq.tasks.lang/list-langs
 

+ 4 - 0
capacitor.config.ts

@@ -13,6 +13,10 @@ const config: CapacitorConfig = {
             splashImmersive: false,
             backgroundColor: "#002b36"
         },
+
+        Keyboard: {
+            resize: "none"
+        }
     },
     ios: {
         scheme: "Logseq"

+ 5 - 6
deps.edn

@@ -1,14 +1,13 @@
 {:paths ["src/main" "src/electron" "src/workspaces" "templates"]
  :deps
  {org.clojure/clojure                   {:mvn/version "1.10.0"}
-  cheshire/cheshire                     {:mvn/version "5.10.0"}
   rum/rum                               {:mvn/version "0.12.9"}
   datascript/datascript                 {:mvn/version "1.3.8"}
   datascript-transit/datascript-transit {:mvn/version "0.3.0"}
   borkdude/rewrite-edn                  {:git/url "https://github.com/borkdude/rewrite-edn"
                                          :sha     "edd87dc7f045f28d7afcbfc44bc0f0a2683dde62"}
   funcool/promesa                       {:mvn/version "4.0.2"}
-  medley/medley                         {:mvn/version "1.2.0"}
+  medley/medley                         {:mvn/version "1.4.0"}
   metosin/reitit-frontend               {:mvn/version "0.3.10"}
   cljs-bean/cljs-bean                   {:mvn/version "1.5.0"}
   prismatic/dommy                       {:mvn/version "1.1.0"}
@@ -21,9 +20,9 @@
   hickory/hickory                       {:git/url "https://github.com/logseq/hickory"
                                          :sha     "9c2c2f1fc2c45efaad906e0faabc3201278deeaa"}
   hiccups/hiccups                       {:mvn/version "0.3.0"}
-  tongue/tongue                         {:mvn/version "0.2.9"}
+  tongue/tongue                         {:mvn/version "0.4.4"}
   org.clojure/core.async                {:mvn/version "1.3.610"}
-  thheller/shadow-cljs                  {:mvn/version "2.17.5"}
+  thheller/shadow-cljs                  {:mvn/version "2.19.0"}
   expound/expound                       {:mvn/version "0.8.6"}
   com.lambdaisland/glogi                {:mvn/version "1.1.144"}
   binaryage/devtools                    {:mvn/version "1.0.5"}
@@ -37,14 +36,14 @@
                                          :sha "5ac8f9a51792b879ee860965c46e48cc5690f789"}}
 
  :aliases {:cljs {:extra-paths ["src/dev-cljs/" "src/test/" "src/electron/"]
-                  :extra-deps  {org.clojure/clojurescript        {:mvn/version "1.10.891"}
+                  :extra-deps  {org.clojure/clojurescript        {:mvn/version "1.11.54"}
                                 org.clojure/tools.namespace      {:mvn/version "0.2.11"}
                                 cider/cider-nrepl                {:mvn/version "0.26.0"}
                                 org.clojars.knubie/cljs-run-test {:mvn/version "1.0.1"}}
                   :main-opts   ["-m" "shadow.cljs.devtools.cli"]}
 
            :test {:extra-paths ["src/test/"]
-                  :extra-deps  {org.clojure/clojurescript        {:mvn/version "1.10.891"}
+                  :extra-deps  {org.clojure/clojurescript        {:mvn/version "1.11.54"}
                                 org.clojure/test.check           {:mvn/version "1.1.1"}
                                 pjstadig/humane-test-output      {:mvn/version "0.11.0"}
                                 org.clojars.knubie/cljs-run-test {:mvn/version "1.0.1"}}

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

@@ -14,9 +14,9 @@ In order to run the commands in this doc, you will need to install
 ## Where to Contribute
 
 Language translations are in two files,
-[frontend/dicts.cljs](https://github.com/logseq/logseq/blob/master/src/main/frontend/dicts.cljs)
+[frontend/dicts.cljc](https://github.com/logseq/logseq/blob/master/src/main/frontend/dicts.cljc)
 and
-[shortcut/dict.cljs](https://github.com/logseq/logseq/blob/master/src/main/frontend/modules/shortcut/dicts.cljs).
+[shortcut/dict.cljc](https://github.com/logseq/logseq/blob/master/src/main/frontend/modules/shortcut/dicts.cljc).
 
 ## Language Overview
 

+ 10 - 3
docs/dev-practices.md

@@ -58,6 +58,13 @@ We use [datascript](https://github.com/tonsky/datascript)'s datalog to power our
 scripts/lint_rules.clj
 ```
 
+### Nbb compatible
+
+Namespaces have the metadata flag `^:nbb-compatible` indicate they are compatible with https://github.com/logseq/nbb-logseq. This compatibility is necessary in order for namespaces to be reused by the frontend and CLIs. To confirm these compatibilities, run:
+```
+bb test:load-nbb-compatible-namespaces
+```
+
 ## Testing
 
 We have unit and end to end tests.
@@ -94,9 +101,9 @@ For this workflow:
   1. Add `^:focus` metadata flags to tests e.g. `(deftest ^:focus test-name ...)`.
   2. In another shell, run `node static/tests.js -i focus` to only run those
   tests. To run all tests except those tests run `node static/tests.js -e focus`.
-3. Or focus namespaces: Using the regex option `-r`, run tests for `frontend.text-test` with `node static/tests.js -r text`.
+3. Or focus namespaces: Using the regex option `-r`, run tests for `frontend.util.page-property-test` with `node static/tests.js -r page-property`.
 
-Multiple options can be specified to AND selections. For example, to run all `frontend.text-test` tests except for the focused one: `node static/tests.js -r text -e focus`
+Multiple options can be specified to AND selections. For example, to run all `frontend.util.page-property-test` tests except for the focused one: `node static/tests.js -r page-property -e focus`
 
 For help on more options, run `node static/tests.js -h`.
 
@@ -107,7 +114,7 @@ shadow-cljs watch test --config-merge '{:autorun true}'`. The test output may
 appear where shadow-cljs was first invoked e.g. where `yarn watch` is running.
 Specific namespace(s) can be auto run with the `:ns-regexp` option e.g. `npx
 shadow-cljs watch test --config-merge '{:autorun true :ns-regexp
-"frontend.text-test"}'`.
+"frontend.util.page-property-test"}'`.
 
 ## Logging
 

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

@@ -164,7 +164,7 @@ test('multiple code block', async ({ page }) => {
   await page.waitForSelector('.CodeMirror pre', { state: 'visible' })
 
   await page.waitForTimeout(500)
-  await page.click('.CodeMirror pre >> nth=1')
+  await page.click('.CodeMirror >> nth=1 >> pre')
   await page.waitForTimeout(500)
 
   await page.type('.CodeMirror textarea >> nth=1', '\n  :key-test 日本語\n', { strict: true })
@@ -191,11 +191,10 @@ test('click outside to exit', async ({ page }) => {
   expect(await page.inputValue('.block-editor textarea')).toBe('Header ``Click``\n```\n  ABC  DEF\n  GHI\n```')
 })
 
-test('click language label to exit #3463', async ({ page }) => {
+test('click language label to exit #3463', async ({ page, block }) => {
   await createRandomPage(page)
 
-  await page.press('.block-editor textarea', 'Enter')
-  await page.waitForTimeout(200)
+  await block.enterNext();
 
   await page.fill('.block-editor textarea', '```cpp\n```')
   await page.waitForTimeout(200)

+ 3 - 3
e2e-tests/sidebar.spec.ts

@@ -16,12 +16,12 @@ test('favorite item and recent item test', async ({ page }) => {
   await page.locator("text=Add to Favorites").click()
   // click from another page
   const another_page_name = await createRandomPage(page)
-  expect(await page.innerText(':nth-match(.favorite-item a, 1)')).toBe('◦' + fav_page_name)
+  expect(await page.innerText(':nth-match(.favorite-item a, 1)')).toBe(fav_page_name)
   await page.click(":nth-match(.favorite-item, 1)")
   expect(await page.innerText('.page-title .title')).toBe(fav_page_name)
 
-  expect(await page.innerText(':nth-match(.recent-item a, 1)')).toBe('◦' + fav_page_name)
-  expect(await page.innerText(':nth-match(.recent-item a, 2)')).toBe('◦' + another_page_name)
+  expect(await page.innerText(':nth-match(.recent-item a, 1)')).toBe(fav_page_name)
+  expect(await page.innerText(':nth-match(.recent-item a, 2)')).toBe(another_page_name)
 
   // remove fav
   await page.click('.ui__dropdown-trigger')

+ 3 - 0
externs.js

@@ -127,6 +127,9 @@ dummy.getNodesObjects = function() {};
 dummy.getEdgesObjects = function() {};
 dummy.alphaTarget = function() {};
 dummy.restart = function() {};
+dummy.observe = function() {};
+dummy.contentRect = function() {};
+dummy.height = function() {};
 
 /**
  * @typedef {{

+ 42 - 6
ios/App/App.xcodeproj/project.pbxproj

@@ -16,6 +16,8 @@
 		50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
 		5FD5BB71278579F5008E6875 /* DownloadiCloudFiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FD5BB70278579F5008E6875 /* DownloadiCloudFiles.swift */; };
 		5FD5BB73278579FF008E6875 /* DownloadiCloudFiles.m in Sources */ = {isa = PBXBuildFile; fileRef = 5FD5BB72278579FF008E6875 /* DownloadiCloudFiles.m */; };
+		5FF8632A283B5ADB0047731B /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FF86329283B5ADB0047731B /* Utils.swift */; };
+		5FF8632C283B5BFD0047731B /* Utils.m in Sources */ = {isa = PBXBuildFile; fileRef = 5FF8632B283B5BFD0047731B /* Utils.m */; };
 		5FFF7D6D27E343FA00B00DA8 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FFF7D6C27E343FA00B00DA8 /* ShareViewController.swift */; };
 		5FFF7D7027E343FA00B00DA8 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5FFF7D6E27E343FA00B00DA8 /* MainInterface.storyboard */; };
 		5FFF7D7427E343FA00B00DA8 /* ShareViewController.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 5FFF7D6A27E343FA00B00DA8 /* ShareViewController.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
@@ -25,8 +27,13 @@
 		D32752BE275496C60039291C /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D32752BD275496C60039291C /* CloudKit.framework */; };
 		D3D62A0A275C92880003FBDC /* FileContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3D62A09275C92880003FBDC /* FileContainer.swift */; };
 		D3D62A0C275C928F0003FBDC /* FileContainer.m in Sources */ = {isa = PBXBuildFile; fileRef = D3D62A0B275C928F0003FBDC /* FileContainer.m */; };
+		FE443F1C27FF5420007ECE65 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE443F1B27FF5420007ECE65 /* Extensions.swift */; };
+		FE443F1E27FF54AA007ECE65 /* Payload.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE443F1D27FF54AA007ECE65 /* Payload.swift */; };
+		FE443F2027FF54C9007ECE65 /* SyncClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE443F1F27FF54C9007ECE65 /* SyncClient.swift */; };
 		FE647FF427BDFEDE00F3206B /* FsWatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE647FF327BDFEDE00F3206B /* FsWatcher.swift */; };
 		FE647FF627BDFEF500F3206B /* FsWatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = FE647FF527BDFEF500F3206B /* FsWatcher.m */; };
+		FE8C946B27FD762700C8017B /* FileSync.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE8C946927FD762700C8017B /* FileSync.swift */; };
+		FE8C946C27FD762700C8017B /* FileSync.m in Sources */ = {isa = PBXBuildFile; fileRef = FE8C946A27FD762700C8017B /* FileSync.m */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXContainerItemProxy section */
@@ -65,6 +72,8 @@
 		50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
 		5FD5BB70278579F5008E6875 /* DownloadiCloudFiles.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadiCloudFiles.swift; sourceTree = "<group>"; };
 		5FD5BB72278579FF008E6875 /* DownloadiCloudFiles.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DownloadiCloudFiles.m; sourceTree = "<group>"; };
+		5FF86329283B5ADB0047731B /* Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = "<group>"; };
+		5FF8632B283B5BFD0047731B /* Utils.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Utils.m; sourceTree = "<group>"; };
 		5FFF7D6A27E343FA00B00DA8 /* ShareViewController.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareViewController.appex; sourceTree = BUILT_PRODUCTS_DIR; };
 		5FFF7D6C27E343FA00B00DA8 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
 		5FFF7D6F27E343FA00B00DA8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
@@ -81,8 +90,13 @@
 		D3D62A09275C92880003FBDC /* FileContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileContainer.swift; sourceTree = "<group>"; };
 		D3D62A0B275C928F0003FBDC /* FileContainer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FileContainer.m; sourceTree = "<group>"; };
 		DE5650F4AD4E2242AB9C012D /* Pods-Logseq.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Logseq.debug.xcconfig"; path = "Target Support Files/Pods-Logseq/Pods-Logseq.debug.xcconfig"; sourceTree = "<group>"; };
+		FE443F1B27FF5420007ECE65 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = "<group>"; };
+		FE443F1D27FF54AA007ECE65 /* Payload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Payload.swift; sourceTree = "<group>"; };
+		FE443F1F27FF54C9007ECE65 /* SyncClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncClient.swift; sourceTree = "<group>"; };
 		FE647FF327BDFEDE00F3206B /* FsWatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FsWatcher.swift; sourceTree = "<group>"; };
 		FE647FF527BDFEF500F3206B /* FsWatcher.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FsWatcher.m; sourceTree = "<group>"; };
+		FE8C946927FD762700C8017B /* FileSync.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileSync.swift; sourceTree = "<group>"; };
+		FE8C946A27FD762700C8017B /* FileSync.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FileSync.m; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
@@ -128,6 +142,8 @@
 		504EC3061FED79650016851F /* App */ = {
 			isa = PBXGroup;
 			children = (
+				5FF86329283B5ADB0047731B /* Utils.swift */,
+				5FF8632B283B5BFD0047731B /* Utils.m */,
 				5FD5BB72278579FF008E6875 /* DownloadiCloudFiles.m */,
 				5FD5BB70278579F5008E6875 /* DownloadiCloudFiles.swift */,
 				D32752BF2754C5AB0039291C /* AppDebug.entitlements */,
@@ -142,6 +158,7 @@
 				50B271D01FEDC1A000F3C39B /* public */,
 				7435D10B2704659F00AB88E0 /* FolderPicker.swift */,
 				FE647FF327BDFEDE00F3206B /* FsWatcher.swift */,
+				FE443F1A27FF53A2007ECE65 /* FileSync */,
 				FE647FF527BDFEF500F3206B /* FsWatcher.m */,
 				7435D10E2704660B00AB88E0 /* FolderPicker.m */,
 				D3D62A09275C92880003FBDC /* FileContainer.swift */,
@@ -180,6 +197,18 @@
 			path = Pods;
 			sourceTree = "<group>";
 		};
+		FE443F1A27FF53A2007ECE65 /* FileSync */ = {
+			isa = PBXGroup;
+			children = (
+				FE8C946927FD762700C8017B /* FileSync.swift */,
+				FE443F1F27FF54C9007ECE65 /* SyncClient.swift */,
+				FE443F1D27FF54AA007ECE65 /* Payload.swift */,
+				FE443F1B27FF5420007ECE65 /* Extensions.swift */,
+				FE8C946A27FD762700C8017B /* FileSync.m */,
+			);
+			path = FileSync;
+			sourceTree = "<group>";
+		};
 /* End PBXGroup section */
 
 /* Begin PBXNativeTarget section */
@@ -331,13 +360,20 @@
 			files = (
 				504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
 				5FD5BB71278579F5008E6875 /* DownloadiCloudFiles.swift in Sources */,
+				FE443F1E27FF54AA007ECE65 /* Payload.swift in Sources */,
+				5FF8632C283B5BFD0047731B /* Utils.m in Sources */,
+				FE8C946B27FD762700C8017B /* FileSync.swift in Sources */,
 				FE647FF427BDFEDE00F3206B /* FsWatcher.swift in Sources */,
+				5FF8632A283B5ADB0047731B /* Utils.swift in Sources */,
 				5FD5BB73278579FF008E6875 /* DownloadiCloudFiles.m in Sources */,
 				D3D62A0A275C92880003FBDC /* FileContainer.swift in Sources */,
 				D3D62A0C275C928F0003FBDC /* FileContainer.m in Sources */,
+				FE443F1C27FF5420007ECE65 /* Extensions.swift in Sources */,
+				FE8C946C27FD762700C8017B /* FileSync.m in Sources */,
 				7435D10F2704660B00AB88E0 /* FolderPicker.m in Sources */,
 				7435D10C2704659F00AB88E0 /* FolderPicker.swift in Sources */,
 				FE647FF627BDFEF500F3206B /* FsWatcher.m in Sources */,
+				FE443F2027FF54C9007ECE65 /* SyncClient.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -438,7 +474,7 @@
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
-				IPHONEOS_DEPLOYMENT_TARGET = 12.0;
+				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				MTL_ENABLE_DEBUG_INFO = YES;
 				ONLY_ACTIVE_ARCH = YES;
 				SDKROOT = iphoneos;
@@ -492,7 +528,7 @@
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
-				IPHONEOS_DEPLOYMENT_TARGET = 12.0;
+				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				MTL_ENABLE_DEBUG_INFO = NO;
 				SDKROOT = iphoneos;
 				SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
@@ -514,7 +550,7 @@
 				INFOPLIST_FILE = App/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
-				MARKETING_VERSION = 0.6.6;
+				MARKETING_VERSION = 0.7.0;
 				OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq;
 				PRODUCT_NAME = "$(TARGET_NAME)";
@@ -540,7 +576,7 @@
 				INFOPLIST_FILE = App/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
-				MARKETING_VERSION = 0.6.6;
+				MARKETING_VERSION = 0.7.0;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
@@ -565,7 +601,7 @@
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
-				MARKETING_VERSION = 0.6.6;
+				MARKETING_VERSION = 0.7.0;
 				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
 				MTL_FAST_MATH = YES;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
@@ -592,7 +628,7 @@
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
-				MARKETING_VERSION = 0.6.6;
+				MARKETING_VERSION = 0.7.0;
 				MTL_FAST_MATH = YES;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
 				PRODUCT_NAME = "$(TARGET_NAME)";

+ 3 - 33
ios/App/App/AppDelegate.swift

@@ -1,19 +1,14 @@
 import UIKit
 import Capacitor
-import SendIntent
 
 @UIApplicationMain
 class AppDelegate: UIResponder, UIApplicationDelegate {
 
     var window: UIWindow?
-    let store = ShareStore.store
     
     func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
         // Override point for customization after application launch.
-        DispatchQueue.global().asyncAfter(deadline: .now() + 3) {
-            NotificationCenter.default
-                .post(name: Notification.Name("triggerSendIntent"), object: nil )
-        }
+        
         return true
     }
     
@@ -33,6 +28,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
 
     func applicationDidBecomeActive(_ application: UIApplication) {
         // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
+        
+        
     }
 
     func applicationWillTerminate(_ application: UIApplication) {
@@ -45,33 +42,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
             if CAPBridge.handleOpenUrl(url, options) {
                 success = ApplicationDelegateProxy.shared.application(app, open: url, options: options)
             }
-            
-            guard let components = NSURLComponents(url: url, resolvingAgainstBaseURL: true),
-                  let params = components.queryItems else {
-                      return false
-                  }
-            let titles = params.filter { $0.name == "title" }
-            let descriptions = params.filter { $0.name == "description" }
-            let types = params.filter { $0.name == "type" }
-            let urls = params.filter { $0.name == "url" }
-            
-            store.shareItems.removeAll()
-        
-            if(titles.count > 0){
-                for index in 0...titles.count-1 {
-                    var shareItem: JSObject = JSObject()
-                    shareItem["title"] = titles[index].value!
-                    shareItem["description"] = descriptions[index].value!
-                    shareItem["type"] = types[index].value!
-                    shareItem["url"] = urls[index].value!
-                    store.shareItems.append(shareItem)
-                }
-            }
-            
-            store.processed = false
-            
-            NotificationCenter.default.post(name: Notification.Name("triggerSendIntent"), object: nil )
-            
             return success
         }
 

+ 211 - 0
ios/App/App/FileSync/Extensions.swift

@@ -0,0 +1,211 @@
+//
+//  Extensions.swift
+//  Logseq
+//
+//  Created by Mono Wang on 4/8/R4.
+//
+
+import Foundation
+import CryptoKit
+
+// via https://github.com/krzyzanowskim/CryptoSwift
+extension Array where Element == UInt8 {
+  public init(hex: String) {
+      self = Array.init()
+      self.reserveCapacity(hex.unicodeScalars.lazy.underestimatedCount)
+      var buffer: UInt8?
+      var skip = hex.hasPrefix("0x") ? 2 : 0
+      for char in hex.unicodeScalars.lazy {
+          guard skip == 0 else {
+              skip -= 1
+              continue
+          }
+          guard char.value >= 48 && char.value <= 102 else {
+              removeAll()
+              return
+          }
+          let v: UInt8
+          let c: UInt8 = UInt8(char.value)
+          switch c {
+          case let c where c <= 57:
+              v = c - 48
+          case let c where c >= 65 && c <= 70:
+              v = c - 55
+          case let c where c >= 97:
+              v = c - 87
+          default:
+              removeAll()
+              return
+          }
+          if let b = buffer {
+              append(b << 4 | v)
+              buffer = nil
+          } else {
+              buffer = v
+          }
+      }
+      if let b = buffer {
+          append(b)
+      }
+  }
+}
+
+@available(iOS 13.0, *)
+extension SymmetricKey {
+    public init(passwordString keyString: String) throws {
+        let size = SymmetricKeySize.bits256
+        guard var keyData = keyString.data(using: .utf8) else {
+            print("Could not create raw Data from String.")
+            throw CryptoKitError.incorrectParameterSize
+        }
+            
+        let keySizeBytes = size.bitCount / 8
+        keyData = keyData.subdata(in: 0..<keySizeBytes)
+        guard keyData.count >= keySizeBytes else { throw CryptoKitError.incorrectKeySize }
+        
+        print("debug key \(keyData) \(keyData.hexDescription)")
+        
+        self.init(data: keyData)
+    }
+}
+
+extension Data {
+    public init?(hexEncoded: String) {
+        self.init(Array<UInt8>(hex: hexEncoded))
+    }
+    
+    var hexDescription: String {
+        return map { String(format: "%02hhx", $0) }.joined()
+    }
+    
+    @available(iOS 13.0, *)
+    func aesEncrypt(keyString: String) throws -> Data {
+        let key = try? SymmetricKey(passwordString: keyString)
+        
+        let nonce = Data(hexEncoded: "131348c0987c7eece60fc0bc") // = initialization vector
+        let tag = Data(hexEncoded: "5baa85ff3e7eda3204744ec74b71d523")
+        
+        print("debug tag \(tag?.hexDescription) nonce \(nonce?.hexDescription)")
+        let sealedData = try! AES.GCM.seal(self, using: key!, nonce: AES.GCM.Nonce(data: nonce!), authenticating: tag!)
+            
+        print("debug encrypted \(sealedData)")
+        guard let encryptedContent = sealedData.combined else {
+            throw CryptoKitError.underlyingCoreCryptoError(error: 2)
+        }
+        print("debug encrypted \(encryptedContent)")
+        print("debug encrypted \(encryptedContent.hexDescription)")
+        print("debug tag \(sealedData.tag.hexDescription)")
+        return encryptedContent
+    }
+    
+    @available(iOS 13.0, *)
+    func aesDecrypt(keyString: String) throws -> Data {
+        let key = try! SymmetricKey(passwordString: keyString)
+        let tag = Data(hexEncoded: "5baa85ff3e7eda3204744ec74b71d523")
+        
+        guard let sealedBox = try? AES.GCM.SealedBox(combined: self) else {
+            throw CryptoKitError.authenticationFailure
+        }
+        guard let decryptedData = try? AES.GCM.open(sealedBox, using: key, authenticating: tag!) else {
+            throw CryptoKitError.authenticationFailure
+        }
+        return decryptedData
+    }
+}
+
+extension String {
+    var MD5: String {
+        let computed = Insecure.MD5.hash(data: self.data(using: .utf8)!)
+        return computed.map { String(format: "%02hhx", $0) }.joined()
+    }
+    
+    func encodeAsFname() -> String {
+        var allowed = NSMutableCharacterSet.urlPathAllowed
+        allowed.remove(charactersIn: "&$@=;:+ ,?%#")
+        return self.addingPercentEncoding(withAllowedCharacters: allowed) ?? self
+    }
+    
+    func decodeFromFname() -> String {
+        return self.removingPercentEncoding ?? self
+    }
+    
+    static func random(length: Int) -> String {
+        let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
+        return String((0..<length).map{ _ in letters.randomElement()! })
+    }
+}
+
+extension URL {
+    func relativePath(from base: URL) -> String? {
+        // Ensure that both URLs represent files:
+        guard self.isFileURL && base.isFileURL else {
+            return nil
+        }
+        
+        // Remove/replace "." and "..", make paths absolute:
+        let destComponents = self.standardized.pathComponents
+        let baseComponents = base.standardized.pathComponents
+        
+        // Find number of common path components:
+        var i = 0
+        while i < destComponents.count && i < baseComponents.count
+                && destComponents[i] == baseComponents[i] {
+            i += 1
+        }
+        
+        // Build relative path:
+        var relComponents = Array(repeating: "..", count: baseComponents.count - i)
+        relComponents.append(contentsOf: destComponents[i...])
+        return relComponents.joined(separator: "/")
+    }
+    
+    func download(toFile file: URL, completion: @escaping (Error?) -> Void) {
+        // Download the remote URL to a file
+        let task = URLSession.shared.downloadTask(with: self) {
+            (tempURL, response, error) in
+            // Early exit on error
+            guard let tempURL = tempURL else {
+                completion(error)
+                return
+            }
+            
+            if let response = response! as? HTTPURLResponse {
+                if response.statusCode == 404 {
+                    completion(NSError(domain: "",
+                                       code: response.statusCode,
+                                       userInfo: [NSLocalizedDescriptionKey: "remote file not found"]))
+                    return
+                }
+                if response.statusCode != 200 {
+                    completion(NSError(domain: "",
+                                       code: response.statusCode,
+                                       userInfo: [NSLocalizedDescriptionKey: "invalid http status code"]))
+                    return
+                }
+            }
+            
+            do {
+                // Remove any existing document at file
+                if FileManager.default.fileExists(atPath: file.path) {
+                    try FileManager.default.removeItem(at: file)
+                }
+                
+                // Copy the tempURL to file
+                try FileManager.default.copyItem(
+                    at: tempURL,
+                    to: file
+                )
+                
+                completion(nil)
+            }
+            
+            // Handle potential file system errors
+            catch {
+                completion(error)
+            }
+        }
+        
+        // Start the download
+        task.resume()
+    }
+}

+ 19 - 0
ios/App/App/FileSync/FileSync.m

@@ -0,0 +1,19 @@
+//
+//  FileSync.m
+//  Logseq
+//
+//  Created by Mono Wang on 2/24/R4.
+//
+
+#import <Capacitor/Capacitor.h>
+
+CAP_PLUGIN(FileSync, "FileSync",
+           CAP_PLUGIN_METHOD(setEnv, CAPPluginReturnPromise);
+           CAP_PLUGIN_METHOD(getLocalFilesMeta, CAPPluginReturnPromise);
+           CAP_PLUGIN_METHOD(getLocalAllFilesMeta, CAPPluginReturnPromise);
+           CAP_PLUGIN_METHOD(renameLocalFile, CAPPluginReturnPromise);
+           CAP_PLUGIN_METHOD(deleteLocalFiles, CAPPluginReturnPromise);
+           CAP_PLUGIN_METHOD(updateLocalFiles, CAPPluginReturnPromise);
+           CAP_PLUGIN_METHOD(deleteRemoteFiles, CAPPluginReturnPromise);
+           CAP_PLUGIN_METHOD(updateRemoteFiles, CAPPluginReturnPromise);
+)

+ 333 - 0
ios/App/App/FileSync/FileSync.swift

@@ -0,0 +1,333 @@
+//
+//  FileSync.swift
+//  Logseq
+//
+//  Created by Mono Wang on 2/24/R4.
+//
+
+import Capacitor
+import Foundation
+import AWSMobileClient
+import CryptoKit
+
+// MARK: Global variables
+
+// Defualts to dev
+var URL_BASE = URL(string: "https://api.logseq.com/file-sync/")!
+var BUCKET: String = "logseq-file-sync-bucket"
+var REGION: String = "us-east-2"
+
+
+public struct SyncMetadata: CustomStringConvertible, Equatable {
+    var md5: String
+    var size: Int
+
+    public init?(of fileURL: URL) {
+        do {
+            let fileAttributes = try fileURL.resourceValues(forKeys:[.isRegularFileKey, .fileSizeKey])
+            guard fileAttributes.isRegularFile! else {
+                return nil
+            }
+            size = fileAttributes.fileSize ?? 0
+            
+            // incremental MD5sum
+            let bufferSize = 1024 * 1024
+            let file = try FileHandle(forReadingFrom: fileURL)
+            defer {
+                file.closeFile()
+            }
+            var ctx = Insecure.MD5.init()
+            while autoreleasepool(invoking: {
+                let data = file.readData(ofLength: bufferSize)
+                if data.count > 0 {
+                    ctx.update(data: data)
+                    return true // continue
+                } else {
+                    return false // eof
+                }
+            }) {}
+            
+            let computed = ctx.finalize()
+            md5 = computed.map { String(format: "%02hhx", $0) }.joined()
+        } catch {
+            return nil
+        }
+    }
+
+    public var description: String {
+        return "SyncMetadata(md5=\(md5), size=\(size))"
+    }
+}
+
+
+// MARK: FileSync Plugin
+
+@objc(FileSync)
+public class FileSync: CAPPlugin, SyncDebugDelegate {
+    override public func load() {
+        print("debug File sync iOS plugin loaded!")
+        AWSMobileClient.default().initialize { (userState, error) in
+            guard error == nil else {
+                print("error initializing AWSMobileClient. Error: \(error!.localizedDescription)")
+                return
+            }
+        }
+    }
+    
+    // NOTE: for debug, or an activity indicator
+    public func debugNotification(_ message: [String: Any]) {
+        self.notifyListeners("debug", data: message)
+    }
+    
+    @objc func setEnv(_ call: CAPPluginCall) {
+        guard let env = call.getString("env") else {
+            call.reject("required parameter: env")
+            return
+        }
+        switch env {
+        case "production", "product", "prod":
+            URL_BASE = URL(string: "https://api-prod.logseq.com/file-sync/")!
+            BUCKET = "logseq-file-sync-bucket-prod"
+            REGION = "us-east-1"
+        case "development", "develop", "dev":
+            URL_BASE = URL(string: "https://api.logseq.com/file-sync/")!
+            BUCKET = "logseq-file-sync-bucket"
+            REGION = "us-east-2"
+        default:
+            call.reject("invalid env: \(env)")
+            return
+        }
+        self.debugNotification(["event": "setenv:\(env)"])
+        call.resolve(["ok": true])
+    }
+    
+    @objc func getLocalFilesMeta(_ call: CAPPluginCall) {
+        guard let basePath = call.getString("basePath"),
+              let filePaths = call.getArray("filePaths") as? [String] else {
+                  call.reject("required paremeters: basePath, filePaths")
+                  return
+              }
+        guard let baseURL = URL(string: basePath) else {
+            call.reject("invalid basePath")
+            return
+        }
+        
+        var fileMetadataDict: [String: [String: Any]] = [:]
+        for filePath in filePaths {
+            let url = baseURL.appendingPathComponent(filePath)
+            if let meta = SyncMetadata(of: url) {
+                fileMetadataDict[filePath] = ["md5": meta.md5,
+                                              "size": meta.size]
+            }
+        }
+        
+        call.resolve(["result": fileMetadataDict])
+    }
+    
+    @objc func getLocalAllFilesMeta(_ call: CAPPluginCall) {
+        guard let basePath = call.getString("basePath"),
+              let baseURL = URL(string: basePath) else {
+                  call.reject("invalid basePath")
+                  return
+              }
+        
+        var fileMetadataDict: [String: [String: Any]] = [:]
+        if let enumerator = FileManager.default.enumerator(at: baseURL, includingPropertiesForKeys: [.isRegularFileKey], options: [.skipsPackageDescendants, .skipsHiddenFiles]) {
+            
+            for case let fileURL as URL in enumerator {
+                if !fileURL.isSkipped() {
+                    if let meta = SyncMetadata(of: fileURL) {
+                        fileMetadataDict[fileURL.relativePath(from: baseURL)!] = ["md5": meta.md5,
+                                                                                  "size": meta.size]
+                    }
+                } else if fileURL.isICloudPlaceholder() {
+                    try? FileManager.default.startDownloadingUbiquitousItem(at: fileURL)
+                }
+            }
+        }
+        call.resolve(["result": fileMetadataDict])
+    }
+    
+    
+    @objc func renameLocalFile(_ call: CAPPluginCall) {
+        guard let basePath = call.getString("basePath"),
+              let baseURL = URL(string: basePath) else {
+                  call.reject("invalid basePath")
+                  return
+              }
+        guard let from = call.getString("from") else {
+            call.reject("invalid from file")
+            return
+        }
+        guard let to = call.getString("to") else {
+            call.reject("invalid to file")
+            return
+        }
+        
+        let fromUrl = baseURL.appendingPathComponent(from)
+        let toUrl = baseURL.appendingPathComponent(to)
+        
+        do {
+            try FileManager.default.moveItem(at: fromUrl, to: toUrl)
+        } catch {
+            call.reject("can not rename file: \(error.localizedDescription)")
+            return
+        }
+        call.resolve(["ok": true])
+        
+    }
+    
+    @objc func deleteLocalFiles(_ call: CAPPluginCall) {
+        guard let baseURL = call.getString("basePath").flatMap({path in URL(string: path)}),
+              let filePaths = call.getArray("filePaths") as? [String] else {
+                  call.reject("required paremeters: basePath, filePaths")
+                  return
+              }
+        
+        for filePath in filePaths {
+            let fileUrl = baseURL.appendingPathComponent(filePath)
+            try? FileManager.default.removeItem(at: fileUrl) // ignore any delete errors
+        }
+        call.resolve(["ok": true])
+    }
+    
+    /// remote -> local
+    @objc func updateLocalFiles(_ call: CAPPluginCall) {
+        guard let baseURL = call.getString("basePath").flatMap({path in URL(string: path)}),
+              let filePaths = call.getArray("filePaths") as? [String],
+              let graphUUID = call.getString("graphUUID") ,
+              let token = call.getString("token") else {
+                  call.reject("required paremeters: basePath, filePaths, graphUUID, token")
+                  return
+              }
+        
+        let client = SyncClient(token: token, graphUUID: graphUUID)
+        client.delegate = self // receives notification
+        
+        client.getFiles(at: filePaths) {  (fileURLs, error) in
+            if let error = error {
+                print("debug getFiles error \(error)")
+                self.debugNotification(["event": "download:error", "data": ["message": "error while getting files \(filePaths)"]])
+                call.reject(error.localizedDescription)
+            } else {
+                // handle multiple completionHandlers
+                let group = DispatchGroup()
+                
+                var downloaded: [String] = []
+                
+                for (filePath, remoteFileURL) in fileURLs {
+                    group.enter()
+
+                    // NOTE: fileURLs from getFiles API is percent-encoded
+                    let localFileURL = baseURL.appendingPathComponent(filePath.decodeFromFname())
+                    remoteFileURL.download(toFile: localFileURL) {error in
+                        if let error = error {
+                            self.debugNotification(["event": "download:error", "data": ["message": "error while downloading \(filePath): \(error)"]])
+                            print("debug download \(error) in \(filePath)")
+                        } else {
+                            self.debugNotification(["event": "download:file", "data": ["file": filePath]])
+                            downloaded.append(filePath)
+                        }
+                        group.leave()
+                    }
+                }
+                group.notify(queue: .main) {
+                    self.debugNotification(["event": "download:done"])
+                    call.resolve(["ok": true, "data": downloaded])
+                }
+                
+            }
+        }
+    }
+    
+    @objc func deleteRemoteFiles(_ call: CAPPluginCall) {
+        guard let filePaths = call.getArray("filePaths") as? [String],
+              let graphUUID = call.getString("graphUUID"),
+              let token = call.getString("token"),
+              let txid = call.getInt("txid") else {
+                  call.reject("required paremeters: filePaths, graphUUID, token, txid")
+                  return
+              }
+        guard !filePaths.isEmpty else {
+            call.reject("empty filePaths")
+            return
+        }
+        
+        let client = SyncClient(token: token, graphUUID: graphUUID, txid: txid)
+        client.deleteFiles(filePaths) { txid, error in
+            guard error == nil else {
+                call.reject("delete \(error!)")
+                return
+            }
+            guard let txid = txid else {
+                call.reject("missing txid")
+                return
+            }
+            call.resolve(["ok": true, "txid": txid])
+        }
+    }
+    
+    /// local -> remote
+    @objc func updateRemoteFiles(_ call: CAPPluginCall) {
+        guard let baseURL = call.getString("basePath").flatMap({path in URL(string: path)}),
+              let filePaths = call.getArray("filePaths") as? [String],
+              let graphUUID = call.getString("graphUUID"),
+              let token = call.getString("token"),
+              let txid = call.getInt("txid") else {
+                  call.reject("required paremeters: basePath, filePaths, graphUUID, token, txid")
+                  return
+              }
+        guard !filePaths.isEmpty else {
+            return call.reject("empty filePaths")
+        }
+        
+        print("debug begin updateRemoteFiles \(filePaths)")
+        
+        let client = SyncClient(token: token, graphUUID: graphUUID, txid: txid)
+        client.delegate = self
+        
+        // 1. refresh_temp_credential
+        client.getTempCredential() { (credentials, error) in
+            guard error == nil else {
+                self.debugNotification(["event": "upload:error", "data": ["message": "error while refreshing credential: \(error!)"]])
+                call.reject("error(getTempCredential): \(error!)")
+                return
+            }
+            
+            var files: [String: URL] = [:]
+            for filePath in filePaths {
+                // NOTE: filePath from js may contain spaces
+                let fileURL = baseURL.appendingPathComponent(filePath)
+                files[filePath.encodeAsFname()] = fileURL
+            }
+            
+            // 2. upload_temp_file
+            client.uploadTempFiles(files, credentials: credentials!) { (uploadedFileKeyDict, error) in
+                guard error == nil else {
+                    self.debugNotification(["event": "upload:error", "data": ["message": "error while uploading temp files: \(error!)"]])
+                    call.reject("error(uploadTempFiles): \(error!)")
+                    return
+                }
+                // 3. update_files
+                guard !uploadedFileKeyDict.isEmpty else {
+                    self.debugNotification(["event": "upload:error", "data": ["message": "no file to update"]])
+                    call.reject("no file to update")
+                    return
+                }
+                client.updateFiles(uploadedFileKeyDict) { (txid, error) in
+                    guard error == nil else {
+                        self.debugNotification(["event": "upload:error", "data": ["message": "error while updating files: \(error!)"]])
+                        call.reject("error updateFiles: \(error!)")
+                        return
+                    }
+                    guard let txid = txid else {
+                        call.reject("error: missing txid")
+                        return
+                    }
+                    self.debugNotification(["event": "upload:done", "data": ["files": filePaths, "txid": txid]])
+                    call.resolve(["ok": true, "files": uploadedFileKeyDict, "txid": txid])
+                }
+            }
+        }
+    }
+}

+ 36 - 0
ios/App/App/FileSync/Payload.swift

@@ -0,0 +1,36 @@
+//
+//  Payload.swift
+//  Logseq
+//
+//  Created by Mono Wang on 4/8/R4.
+//
+
+import Foundation
+
+struct GetFilesResponse: Decodable {
+    let PresignedFileUrls: [String: String]
+}
+
+struct DeleteFilesResponse: Decodable {
+    let TXId: Int
+    let DeleteSuccFiles: [String]
+    let DeleteFailedFiles: [String: String]
+}
+
+public struct S3Credential: Decodable {
+    let AccessKeyId: String
+    let Expiration: String
+    let SecretKey: String
+    let SessionToken: String
+}
+
+struct GetTempCredentialResponse: Decodable {
+    let Credentials: S3Credential
+    let S3Prefix: String
+}
+
+struct UpdateFilesResponse: Decodable {
+    let TXId: Int
+    let UpdateSuccFiles: [String]
+    let UpdateFailedFiles: [String: String]
+}

+ 326 - 0
ios/App/App/FileSync/SyncClient.swift

@@ -0,0 +1,326 @@
+//
+//  SyncClient.swift
+//  Logseq
+//
+//  Created by Mono Wang on 4/8/R4.
+//
+
+import Foundation
+import AWSMobileClient
+import AWSS3
+
+public protocol SyncDebugDelegate {
+    func debugNotification(_ message: [String: Any])
+}
+
+
+public class SyncClient {
+    private var token: String
+    private var graphUUID: String?
+    private var txid: Int = 0
+    private var s3prefix: String?
+    
+    public var delegate: SyncDebugDelegate? = nil
+    
+    public init(token: String) {
+        self.token = token
+    }
+    
+    public init(token: String, graphUUID: String) {
+        self.token = token
+        self.graphUUID = graphUUID
+    }
+    
+    public init(token: String, graphUUID: String, txid: Int) {
+        self.token = token
+        self.graphUUID = graphUUID
+        self.txid = txid
+    }
+
+    // get_files
+    // => file_path, file_url
+    public func getFiles(at filePaths: [String], completionHandler: @escaping ([String: URL], Error?) -> Void) {
+        let url = URL_BASE.appendingPathComponent("get_files")
+        
+        var request = URLRequest(url: url)
+        request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
+        request.setValue("Logseq-sync/0.1", forHTTPHeaderField: "User-Agent")
+        request.setValue("Bearer \(self.token)", forHTTPHeaderField: "Authorization")
+        
+        let payload = [
+            "GraphUUID": self.graphUUID ?? "",
+            "Files": filePaths.map { filePath in filePath.encodeAsFname()}
+        ] as [String : Any]
+        let bodyData = try? JSONSerialization.data(
+            withJSONObject: payload,
+            options: []
+        )
+        request.httpMethod = "POST"
+        request.httpBody = bodyData
+        
+        let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
+            guard error == nil else {
+                completionHandler([:], error)
+                return
+            }
+            
+            if (response as? HTTPURLResponse)?.statusCode != 200 {
+                let body = String(data: data!, encoding: .utf8) ?? "";
+                completionHandler([:], NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "http error \(body)"]))
+                return
+            }
+            
+            if let data = data {
+                let resp = try? JSONDecoder().decode([String:[String:String]].self, from: data)
+                let files = resp?["PresignedFileUrls"] ?? [:]
+                self.delegate?.debugNotification(["event": "download:prepare"])
+                completionHandler(files.mapValues({ url in URL(string: url)!}), nil)
+            } else {
+                // Handle unexpected error
+                completionHandler([:], NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "unexpected error"]))
+            }
+        }
+        task.resume()
+    }
+    
+    
+    public func deleteFiles(_ filePaths: [String], completionHandler: @escaping  (Int?, Error?) -> Void) {
+        let url = URL_BASE.appendingPathComponent("delete_files")
+        
+        var request = URLRequest(url: url)
+        request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
+        request.setValue("Logseq-sync/0.1", forHTTPHeaderField: "User-Agent")
+        request.setValue("Bearer \(self.token)", forHTTPHeaderField: "Authorization")
+        
+        let payload = [
+            "GraphUUID": self.graphUUID ?? "",
+            "Files": filePaths.map { filePath in filePath.encodeAsFname()},
+            "TXId": self.txid,
+        ] as [String : Any]
+        let bodyData = try? JSONSerialization.data(
+            withJSONObject: payload,
+            options: []
+        )
+        request.httpMethod = "POST"
+        request.httpBody = bodyData
+        
+        let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
+            guard error == nil else {
+                completionHandler(nil, error)
+                return
+            }
+            
+            if let response = response as? HTTPURLResponse {
+                let body = String(data: data!, encoding: .utf8) ?? ""
+                
+                if response.statusCode == 409 {
+                    if body.contains("txid_to_validate") {
+                        completionHandler(nil, NSError(domain: "",
+                                                       code: 409,
+                                                       userInfo: [NSLocalizedDescriptionKey: "invalid txid: \(body)"]))
+                        return
+                    }
+                    // fallthrough
+                }
+                if response.statusCode != 200 {
+                    completionHandler(nil, NSError(domain: "",
+                                                   code: response.statusCode,
+                                                   userInfo: [NSLocalizedDescriptionKey: "invalid http status \(response.statusCode): \(body)"]))
+                    return
+                }
+            }
+            
+            if let data = data {
+                do {
+                    let resp = try JSONDecoder().decode(DeleteFilesResponse.self, from: data)
+                    // TODO: handle api resp?
+                    self.delegate?.debugNotification(["event": "delete"])
+                    completionHandler(resp.TXId, nil)
+                } catch {
+                    completionHandler(nil, error)
+                }
+            } else {
+                // Handle unexpected error
+                completionHandler(nil, NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "unexpected error"]))
+            }
+        }
+        task.resume()
+    }
+    
+    // (txid, error)
+    public func updateFiles(_ fileKeyDict: [String: String], completionHandler: @escaping  (Int?, Error?) -> Void) {
+        let url = URL_BASE.appendingPathComponent("update_files")
+        
+        var request = URLRequest(url: url)
+        request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
+        request.setValue("Logseq-sync/0.1", forHTTPHeaderField: "User-Agent")
+        request.setValue("Bearer \(self.token)", forHTTPHeaderField: "Authorization")
+        
+        let payload = [
+            "GraphUUID": self.graphUUID ?? "",
+            "Files": Dictionary(uniqueKeysWithValues: fileKeyDict.map { ($0, $1) }) as [String: String] as Any,
+            "TXId": self.txid,
+        ] as [String : Any]
+        let bodyData = try? JSONSerialization.data(
+            withJSONObject: payload,
+            options: []
+        )
+        request.httpMethod = "POST"
+        request.httpBody = bodyData
+        
+        let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
+            guard error == nil else {
+                completionHandler(nil, error)
+                return
+            }
+            
+            if let response = response as? HTTPURLResponse {
+                let body = String(data: data!, encoding: .utf8) ?? ""
+                
+                if response.statusCode == 409 {
+                    if body.contains("txid_to_validate") {
+                        completionHandler(nil, NSError(domain: "",
+                                                       code: 409,
+                                                       userInfo: [NSLocalizedDescriptionKey: "invalid txid: \(body)"]))
+                        return
+                    }
+                    // fallthrough
+                }
+                if response.statusCode != 200 {
+                    completionHandler(nil, NSError(domain: "",
+                                                   code: response.statusCode,
+                                                   userInfo: [NSLocalizedDescriptionKey: "invalid http status \(response.statusCode): \(body)"]))
+                    return
+                }
+            }
+            
+            if let data = data {
+                let resp = try? JSONDecoder().decode(UpdateFilesResponse.self, from: data)
+                if resp?.UpdateFailedFiles.isEmpty ?? true {
+                    completionHandler(resp?.TXId, nil)
+                } else {
+                    completionHandler(nil, NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "update fail for some files: \(resp?.UpdateFailedFiles.debugDescription)"]))
+                }
+            } else {
+                // Handle unexpected error
+                completionHandler(nil, NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "unexpected error"]))
+            }
+        }
+        task.resume()
+    }
+    
+    public func getTempCredential(completionHandler: @escaping (S3Credential?, Error?) -> Void) {
+        let url = URL_BASE.appendingPathComponent("get_temp_credential")
+        
+        var request = URLRequest(url: url)
+        request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
+        request.setValue("Logseq-sync/0.1", forHTTPHeaderField: "User-Agent")
+        request.setValue("Bearer \(self.token)", forHTTPHeaderField: "Authorization")
+        request.httpMethod = "POST"
+        request.httpBody = Data()
+        
+        let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
+            guard error == nil else {
+                completionHandler(nil, error)
+                return
+            }
+            if let response = response as? HTTPURLResponse {
+                let body = String(data: data!, encoding: .utf8) ?? ""
+                if response.statusCode == 401 {
+                    completionHandler(nil, NSError(domain: "", code: 401, userInfo: [NSLocalizedDescriptionKey: "unauthorized"]))
+                    return
+                }
+                if response.statusCode != 200 {
+                    completionHandler(nil, NSError(domain: "",
+                                                   code: response.statusCode,
+                                                   userInfo: [NSLocalizedDescriptionKey: "invalid http status \(response.statusCode): \(body)"]))
+                    return
+                }
+            }
+            if let data = data {
+                let resp = try? JSONDecoder().decode(GetTempCredentialResponse.self, from: data)
+                // NOTE: remove BUCKET prefix here.
+                self.s3prefix = resp?.S3Prefix.replacingOccurrences(of: "\(BUCKET)/", with: "")
+                self.delegate?.debugNotification(["event": "upload:prepare"])
+                completionHandler(resp?.Credentials, nil)
+            } else {
+                // Handle unexpected error
+                completionHandler(nil, NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "unexpected error"]))
+            }
+        }
+        task.resume()
+    }
+    
+    // [filePath, Key]
+    public func uploadTempFiles(_ files: [String: URL], credentials: S3Credential, completionHandler: @escaping ([String: String], Error?) -> Void) {
+        let credentialsProvider = AWSBasicSessionCredentialsProvider(
+            accessKey: credentials.AccessKeyId, secretKey: credentials.SecretKey, sessionToken: credentials.SessionToken)
+        let configuration = AWSServiceConfiguration(region: .USEast2, credentialsProvider: credentialsProvider)
+        configuration?.timeoutIntervalForRequest = 5.0
+        configuration?.timeoutIntervalForResource = 5.0
+        
+        let tuConf = AWSS3TransferUtilityConfiguration()
+        tuConf.bucket = BUCKET
+        //x tuConf.isAccelerateModeEnabled = true
+        
+        let transferKey = String.random(length: 10)
+        AWSS3TransferUtility.register(
+            with: configuration!,
+            transferUtilityConfiguration: tuConf,
+            forKey: transferKey
+        ) { (error) in
+            if let error = error {
+                print("error while register tu \(error)")
+            }
+        }
+        
+        let transferUtility = AWSS3TransferUtility.s3TransferUtility(forKey: transferKey)
+        let uploadExpression = AWSS3TransferUtilityUploadExpression()
+        
+        let group = DispatchGroup()
+        var keyFileDict: [String: String] = [:]
+        var fileKeyDict: [String: String] = [:]
+        
+        let uploadCompletionHandler = { (task: AWSS3TransferUtilityUploadTask, error: Error?) -> Void in
+            // ignore any errors in first level of handler
+            if let error = error {
+                self.delegate?.debugNotification(["event": "upload:error", "data": ["key": task.key, "error": error.localizedDescription]])
+            }
+            if let HTTPResponse = task.response {
+                if HTTPResponse.statusCode != 200 || task.status != .completed {
+                    print("debug uploading error \(HTTPResponse)")
+                }
+            }
+            
+            // only save successful keys
+            let filePath = keyFileDict[task.key]!
+            fileKeyDict[filePath] = task.key
+            keyFileDict.removeValue(forKey: task.key)
+            self.delegate?.debugNotification(["event": "upload:file", "data": ["file": filePath, "key": task.key]])
+            group.leave() // notify finish upload
+        }
+        
+        for (filePath, fileLocalURL) in files {
+            print("debug, upload temp \(fileLocalURL) \(filePath)")
+            guard let rawData = try? Data(contentsOf: fileLocalURL) else { continue }
+            group.enter()
+            
+            let randFileName = String.random(length: 15).appending(".").appending(fileLocalURL.pathExtension)
+            let key = "\(self.s3prefix!)/ios\(randFileName)"
+
+            keyFileDict[key] = filePath
+            transferUtility?.uploadData(rawData, key: key, contentType: "application/octet-stream", expression: uploadExpression, completionHandler: uploadCompletionHandler)
+                .continueWith(block: { (task) in
+                    if let error = task.error {
+                        completionHandler([:], error)
+                    }
+                    return nil
+                })
+        }
+        
+        group.notify(queue: .main) {
+            AWSS3TransferUtility.remove(forKey: transferKey)
+            completionHandler(fileKeyDict, nil)
+        }
+    }
+}

+ 41 - 24
ios/App/App/FsWatcher.swift

@@ -52,23 +52,20 @@ public class FsWatcher: CAPPlugin, PollingWatcherDelegate {
                                                    "dir": baseUrl?.description as Any,
                                                    "path": url.description,
                                                   ])
-        case .Add:
-            let content = try? String(contentsOf: url, encoding: .utf8)
-            self.notifyListeners("watcher", data: ["event": "add",
+        case .Add, .Change:
+            var content: String? = nil
+            if url.shouldNotifyWithContent() {
+                content = try? String(contentsOf: url, encoding: .utf8)
+            }
+            self.notifyListeners("watcher", data: ["event": event.description,
                                                    "dir": baseUrl?.description as Any,
                                                    "path": url.description,
                                                    "content": content as Any,
-                                                   "stat": ["mtime": metadata?.contentModificationTimestamp,
-                                                            "ctime": metadata?.creationTimestamp]
+                                                   "stat": ["mtime": metadata?.contentModificationTimestamp ?? 0,
+                                                            "ctime": metadata?.creationTimestamp ?? 0,
+                                                            "size": metadata?.fileSize as Any]
                                                   ])
-        case .Change:
-            let content = try? String(contentsOf: url, encoding: .utf8)
-            self.notifyListeners("watcher", data: ["event": "change",
-                                                   "dir": baseUrl?.description as Any,
-                                                   "path": url.description,
-                                                   "content": content as Any,
-                                                   "stat": ["mtime": metadata?.contentModificationTimestamp,
-                                                            "ctime": metadata?.creationTimestamp]])
+            
         case .Error:
             // TODO: handle error?
             break
@@ -84,12 +81,18 @@ extension URL {
         if self.lastPathComponent.starts(with: ".") {
             return true
         }
-        let allowedPathExtensions: Set = ["md", "markdown", "org", "css", "edn", "excalidraw"]
+        if self.lastPathComponent == "graphs-txid.edn" || self.lastPathComponent == "broken-config.edn" {
+            return true
+        }
+        return false
+    }
+    
+    func shouldNotifyWithContent() -> Bool {
+        let allowedPathExtensions: Set = ["md", "markdown", "org", "js", "edn", "css", "excalidraw"]
         if allowedPathExtensions.contains(self.pathExtension.lowercased()) {
-            return false
+            return true
         }
-        // skip for other file types
-        return true
+        return false
     }
     
     func isICloudPlaceholder() -> Bool {
@@ -106,13 +109,27 @@ public protocol PollingWatcherDelegate {
     func recevedNotification(_ url: URL, _ event: PollingWatcherEvent, _ metadata: SimpleFileMetadata?)
 }
 
-public enum PollingWatcherEvent: String {
+public enum PollingWatcherEvent {
     case Add
     case Change
     case Unlink
     case Error
+    
+    var description: String {
+        switch self {
+        case .Add:
+            return "add"
+        case .Change:
+            return "change"
+        case .Unlink:
+            return "unlink"
+        case .Error:
+            return "error"
+        }
+    }
 }
 
+
 public struct SimpleFileMetadata: CustomStringConvertible, Equatable {
     var contentModificationTimestamp: Double
     var creationTimestamp: Double
@@ -167,7 +184,7 @@ public class PollingWatcher {
     }
     
     private func tick() {
-        let startTime = DispatchTime.now()
+        // let startTime = DispatchTime.now()
         
         if let enumerator = FileManager.default.enumerator(
             at: url,
@@ -188,11 +205,11 @@ public class PollingWatcher {
                 
                 if isDirectory {
                     // NOTE: URL.path won't end with a `/`
-                    if fileURL.path.hasSuffix("/logseq/bak") || name == ".recycle" || name.hasPrefix(".") || name == "node_modules" {
+                    if fileURL.path.hasSuffix("/logseq/bak") || fileURL.path.hasSuffix("/logseq/version-files") || name == ".recycle" || name.hasPrefix(".") || name == "node_modules" {
                         enumerator.skipDescendants()
                     }
                 }
-            
+                
                 if isRegularFile && !fileURL.isSkipped() {
                     if let meta = SimpleFileMetadata(of: fileURL) {
                         newMetaDb[fileURL] = meta
@@ -205,9 +222,9 @@ public class PollingWatcher {
             self.updateMetaDb(with: newMetaDb)
         }
         
-        let elapsedNanoseconds = DispatchTime.now().uptimeNanoseconds - startTime.uptimeNanoseconds
-        let elapsedInMs = Double(elapsedNanoseconds) / 1_000_000
-        print("debug ticker elapsed=\(elapsedInMs)ms")
+        // let elapsedNanoseconds = DispatchTime.now().uptimeNanoseconds - startTime.uptimeNanoseconds
+        // let elapsedInMs = Double(elapsedNanoseconds) / 1_000_000
+        // print("debug ticker elapsed=\(elapsedInMs)ms")
         
         if #available(iOS 13.0, *) {
             timer?.schedule(deadline: .now().advanced(by: .seconds(2)), leeway: .milliseconds(100))

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

@@ -0,0 +1,13 @@
+//
+//  Utils.m
+//  Logseq
+//
+//  Created by leizhe on 2022/5/23.
+//
+
+#import <Foundation/Foundation.h>
+#import <Capacitor/Capacitor.h>
+
+CAP_PLUGIN(Utils, "Utils",
+           CAP_PLUGIN_METHOD(isZoomed, CAPPluginReturnPromise);
+           )

+ 22 - 0
ios/App/App/Utils.swift

@@ -0,0 +1,22 @@
+//
+//  Utils.swift
+//  Logseq
+//
+//  Created by leizhe on 2022/5/23.
+//
+
+import Foundation
+import Capacitor
+
+@objc(Utils)
+public class Utils: CAPPlugin  {
+    
+    @objc func isZoomed(_ call: CAPPluginCall) {
+        
+        var isZoomed: Bool {
+            return UIScreen.main.scale < UIScreen.main.nativeScale
+        }
+
+        call.resolve(["isZoomed": isZoomed])
+    }
+ }

+ 3 - 0
ios/App/App/capacitor.config.json

@@ -10,6 +10,9 @@
 			"androidScaleType": "CENTER_CROP",
 			"splashImmersive": false,
 			"backgroundColor": "#002b36"
+		},
+		"Keyboard": {
+			"resize": "none"
 		}
 	},
 	"ios": {

+ 4 - 0
ios/App/Podfile

@@ -13,7 +13,9 @@ def capacitor_pods
   pod 'CapacitorCamera', :path => '../../node_modules/@capacitor/camera'
   pod 'CapacitorClipboard', :path => '../../node_modules/@capacitor/clipboard'
   pod 'CapacitorFilesystem', :path => '../../node_modules/@capacitor/filesystem'
+  pod 'CapacitorHaptics', :path => '../../node_modules/@capacitor/haptics'
   pod 'CapacitorKeyboard', :path => '../../node_modules/@capacitor/keyboard'
+  pod 'CapacitorShare', :path => '../../node_modules/@capacitor/share'
   pod 'CapacitorSplashScreen', :path => '../../node_modules/@capacitor/splash-screen'
   pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
   pod 'CapacitorVoiceRecorder', :path => '../../node_modules/capacitor-voice-recorder'
@@ -23,4 +25,6 @@ end
 target 'Logseq' do
   capacitor_pods
   # Add your Pods here
+  pod 'AWSMobileClient'
+  pod 'AWSS3'
 end

+ 1 - 1
ios/App/ShareViewController/ShareViewController.swift

@@ -44,7 +44,7 @@ class ShareViewController: UIViewController {
                     value: $0.url?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""),
             ]
         }.flatMap({ $0 })
-        var urlComps = URLComponents(string: "logseq://")!
+        var urlComps = URLComponents(string: "logseq://shared?")!
         urlComps.queryItems = queryItems
         openURL(urlComps.url!)
     }

+ 112 - 51
libs/src/LSPlugin.core.ts

@@ -3,7 +3,6 @@ import {
   deepMerge,
   setupInjectedStyle,
   genID,
-  setupInjectedTheme,
   setupInjectedUI,
   deferred,
   invokeHostExportedApi,
@@ -19,6 +18,7 @@ import {
   IS_DEV,
   cleanInjectedScripts,
   safeSnakeCase,
+  injectTheme,
 } from './helpers'
 import * as pluginHelpers from './helpers'
 import Debug from 'debug'
@@ -34,11 +34,13 @@ import {
 } from './LSPlugin.caller'
 import {
   ILSPluginThemeManager,
+  LegacyTheme,
   LSPluginPkgConfig,
   SettingSchemaDesc,
   StyleOptions,
   StyleString,
-  ThemeOptions,
+  Theme,
+  ThemeMode,
   UIContainerAttrs,
   UIOptions,
 } from './LSPlugin'
@@ -173,10 +175,13 @@ class PluginLogger extends EventEmitter<'change'> {
 }
 
 interface UserPreferences {
-  theme: ThemeOptions
+  theme: LegacyTheme
+  themes: {
+    mode: ThemeMode
+    light: Theme
+    dark: Theme
+  }
   externals: string[] // external plugin locations
-
-  [key: string]: any
 }
 
 interface PluginLocalOptions {
@@ -310,7 +315,7 @@ function initProviderHandlers(pluginLocal: PluginLocal) {
   let themed = false
 
   // provider:theme
-  pluginLocal.on(_('theme'), (theme: ThemeOptions) => {
+  pluginLocal.on(_('theme'), (theme: Theme) => {
     pluginLocal.themeMgr.registerTheme(pluginLocal.id, theme)
 
     if (!themed) {
@@ -697,7 +702,7 @@ class PluginLocal extends EventEmitter<
     this._options.entry = entry
   }
 
-  async _loadConfigThemes(themes: ThemeOptions[]) {
+  async _loadConfigThemes(themes: Theme[]) {
     themes.forEach((options) => {
       if (!options.url) return
 
@@ -1123,6 +1128,7 @@ class LSPluginCore
     | 'unregistered'
     | 'theme-changed'
     | 'theme-selected'
+    | 'reset-custom-theme'
     | 'settings-changed'
     | 'unlink-plugin'
     | 'beforereload'
@@ -1133,19 +1139,24 @@ class LSPluginCore
   private _isRegistering = false
   private _readyIndicator?: DeferredActor
   private readonly _hostMountedActor: DeferredActor = deferred()
-  private readonly _userPreferences: Partial<UserPreferences> = {}
-  private readonly _registeredThemes = new Map<
-    PluginLocalIdentity,
-    ThemeOptions[]
-  >()
+  private readonly _userPreferences: UserPreferences = {
+    theme: null,
+    themes: {
+      mode: 'light',
+      light: null,
+      dark: null,
+    },
+    externals: [],
+  }
+  private readonly _registeredThemes = new Map<PluginLocalIdentity, Theme[]>()
   private readonly _registeredPlugins = new Map<
     PluginLocalIdentity,
     PluginLocal
   >()
   private _currentTheme: {
-    dis: () => void
     pid: PluginLocalIdentity
-    opt: ThemeOptions
+    opt: Theme | LegacyTheme
+    eject: () => void
   }
 
   /**
@@ -1182,12 +1193,25 @@ class LSPluginCore
     }
   }
 
+  /**
+   * Activate the user preferences.
+   *
+   * Steps:
+   *
+   * 1. Load the custom theme.
+   *
+   * @memberof LSPluginCore
+   */
   async activateUserPreferences() {
-    const { theme } = this._userPreferences
-
-    // 0. theme
-    if (theme) {
-      await this.selectTheme(theme, false)
+    const { theme: legacyTheme, themes } = this._userPreferences
+    const currentTheme = themes[themes.mode]
+
+    // If there is currently a theme that has been set
+    if (currentTheme) {
+      await this.selectTheme(currentTheme, { effect: false })
+    } else if (legacyTheme) {
+      // Otherwise compatible with older versions
+      await this.selectTheme(legacyTheme, { effect: false })
     }
   }
 
@@ -1238,7 +1262,7 @@ class LSPluginCore
 
       await this.loadUserPreferences()
 
-      const externals = new Set(this._userPreferences.externals || [])
+      const externals = new Set(this._userPreferences.externals)
 
       if (initial) {
         plugins = plugins.concat(
@@ -1349,8 +1373,8 @@ class LSPluginCore
       this.emit('unregistered', identity)
     }
 
-    const externals = this._userPreferences.externals || []
-    if (externals.length > 0 && unregisteredExternals.length > 0) {
+    const externals = this._userPreferences.externals
+    if (externals.length && unregisteredExternals.length) {
       await this.saveUserPreferences({
         externals: externals.filter((it) => {
           return !unregisteredExternals.includes(it)
@@ -1472,18 +1496,15 @@ class LSPluginCore
     return this._isRegistering
   }
 
-  get themes(): Map<PluginLocalIdentity, ThemeOptions[]> {
+  get themes() {
     return this._registeredThemes
   }
 
-  async registerTheme(
-    id: PluginLocalIdentity,
-    opt: ThemeOptions
-  ): Promise<void> {
-    debug('registered Theme #', id, opt)
+  async registerTheme(id: PluginLocalIdentity, opt: Theme): Promise<void> {
+    debug('Register theme #', id, opt)
 
     if (!id) return
-    let themes: ThemeOptions[] = this._registeredThemes.get(id)!
+    let themes: Theme[] = this._registeredThemes.get(id)!
     if (!themes) {
       this._registeredThemes.set(id, (themes = []))
     }
@@ -1492,41 +1513,81 @@ class LSPluginCore
     this.emit('theme-changed', this.themes, { id, ...opt })
   }
 
-  async selectTheme(opt?: ThemeOptions, effect = true): Promise<void> {
-    // clear current
+  async selectTheme(
+    theme: Theme | LegacyTheme,
+    options: {
+      effect?: boolean
+      emit?: boolean
+    } = {}
+  ) {
+    const { effect, emit } = Object.assign(
+      {},
+      { effect: true, emit: true },
+      options
+    )
+
+    // Clear current theme before injecting.
     if (this._currentTheme) {
-      this._currentTheme.dis?.()
+      this._currentTheme.eject()
     }
 
-    const disInjectedTheme = setupInjectedTheme(opt?.url)
-    this.emit('theme-selected', opt)
-    effect && (await this.saveUserPreferences({ theme: opt?.url ? opt : null }))
-    if (opt?.url) {
+    // Detect if it is the default theme (no url).
+    if (!theme.url) {
+      this._currentTheme = null
+    } else {
+      const ejectTheme = injectTheme(theme.url)
+
       this._currentTheme = {
-        dis: () => {
-          disInjectedTheme()
-          effect && this.saveUserPreferences({ theme: null })
-        },
-        opt,
-        pid: opt.pid,
+        pid: theme.pid,
+        opt: theme,
+        eject: ejectTheme,
       }
     }
+
+    if (effect) {
+      await this.saveUserPreferences(
+        theme.mode
+          ? {
+              themes: {
+                ...this._userPreferences.themes,
+                mode: theme.mode,
+                [theme.mode]: theme,
+              },
+            }
+          : { theme: theme }
+      )
+    }
+
+    if (emit) {
+      this.emit('theme-selected', theme)
+    }
   }
 
-  async unregisterTheme(
-    id: PluginLocalIdentity,
-    effect: boolean = true
-  ): Promise<void> {
-    debug('unregistered Theme #', id)
+  async unregisterTheme(id: PluginLocalIdentity, effect = true) {
+    debug('Unregister theme #', id)
+
+    if (!this._registeredThemes.has(id)) {
+      return
+    }
 
-    if (!this._registeredThemes.has(id)) return
     this._registeredThemes.delete(id)
     this.emit('theme-changed', this.themes, { id })
     if (effect && this._currentTheme?.pid === id) {
-      this._currentTheme.dis?.()
+      this._currentTheme.eject()
       this._currentTheme = null
-      // reset current theme
-      this.emit('theme-selected', null)
+
+      const { theme, themes } = this._userPreferences
+      await this.saveUserPreferences({
+        theme: theme?.pid === id ? null : theme,
+        themes: {
+          ...themes,
+          light: themes.light?.pid === id ? null : themes.light,
+          dark: themes.dark?.pid === id ? null : themes.dark,
+        },
+      })
+
+      // Reset current theme if it is unregistered
+      this.emit('reset-custom-theme', this._userPreferences.themes)
     }
   }
 }

+ 40 - 19
libs/src/LSPlugin.ts

@@ -1,18 +1,24 @@
-import EventEmitter from 'eventemitter3'
 import * as CSS from 'csstype'
+
+import EventEmitter from 'eventemitter3'
 import { LSPluginCaller } from './LSPlugin.caller'
-import { LSPluginFileStorage } from './modules/LSPlugin.Storage'
 import { LSPluginExperiments } from './modules/LSPlugin.Experiments'
+import { LSPluginFileStorage } from './modules/LSPlugin.Storage'
 
 export type PluginLocalIdentity = string
 
-export type ThemeOptions = {
+export type ThemeMode = 'light' | 'dark'
+
+export interface LegacyTheme {
   name: string
   url: string
   description?: string
-  mode?: 'dark' | 'light'
+  mode?: ThemeMode
+  pid: PluginLocalIdentity
+}
 
-  [key: string]: any
+export interface Theme extends LegacyTheme {
+  mode: ThemeMode
 }
 
 export type StyleString = string
@@ -64,7 +70,7 @@ export interface LSPluginPkgConfig {
   entry: string // alias of main
   title: string
   mode: 'shadow' | 'iframe'
-  themes: Array<ThemeOptions>
+  themes: Theme[]
   icon: string
 
   [key: string]: any
@@ -122,7 +128,7 @@ export interface AppInfo {
  * User's app configurations
  */
 export interface AppUserConfigs {
-  preferredThemeMode: 'dark' | 'light'
+  preferredThemeMode: ThemeMode
   preferredFormat: 'markdown' | 'org'
   preferredDateFormat: string
   preferredStartOfWeek: string
@@ -308,6 +314,12 @@ export interface IAppProxy {
     action: SimpleCommandCallback
   ) => void
 
+  /**
+   * Supported key names
+   * @link https://gist.github.com/xyhp915/d1a6d151a99f31647a95e59cdfbf4ddc
+   * @param keybinding
+   * @param action
+   */
   registerCommandShortcut: (
     keybinding: SimpleCommandKeybinding,
     action: SimpleCommandCallback
@@ -376,7 +388,7 @@ export interface IAppProxy {
     content: string,
     status?: 'success' | 'warning' | 'error' | string
   ) => void
-  
+
   setZoomFactor: (factor: number) => void
   setFullScreen: (flag: boolean | 'toggle') => void
   setLeftSidebarVisible: (flag: boolean | 'toggle') => void
@@ -608,9 +620,17 @@ export interface IEditorProxy extends Record<string, any> {
 
   getAllPages: (repo?: string) => Promise<any>
 
-  prependBlockInPage: (page: PageIdentity, content: string, opts?: Partial<{ properties: {} }>) => Promise<BlockEntity | null>
+  prependBlockInPage: (
+    page: PageIdentity,
+    content: string,
+    opts?: Partial<{ properties: {} }>
+  ) => Promise<BlockEntity | null>
 
-  appendBlockInPage: (page: PageIdentity, content: string, opts?: Partial<{ properties: {} }>) => Promise<BlockEntity | null>
+  appendBlockInPage: (
+    page: PageIdentity,
+    content: string,
+    opts?: Partial<{ properties: {} }>
+  ) => Promise<BlockEntity | null>
 
   getPreviousSiblingBlock: (
     srcBlock: BlockIdentity
@@ -750,9 +770,7 @@ export interface IAssetsProxy {
    * @added 0.0.2
    * @param exts
    */
-  listFilesOfCurrentGraph(
-    exts: string | string[]
-  ): Promise<{
+  listFilesOfCurrentGraph(exts: string | string[]): Promise<{
     path: string
     size: number
     accessTime: number
@@ -762,14 +780,17 @@ export interface IAssetsProxy {
   }>
 }
 
-export interface ILSPluginThemeManager extends EventEmitter {
-  themes: Map<PluginLocalIdentity, Array<ThemeOptions>>
+export interface ILSPluginThemeManager {
+  get themes(): Map<PluginLocalIdentity, Theme[]>
 
-  registerTheme(id: PluginLocalIdentity, opt: ThemeOptions): Promise<void>
+  registerTheme(id: PluginLocalIdentity, opt: Theme): Promise<void>
 
-  unregisterTheme(id: PluginLocalIdentity): Promise<void>
+  unregisterTheme(id: PluginLocalIdentity, effect?: boolean): Promise<void>
 
-  selectTheme(opt?: ThemeOptions): Promise<void>
+  selectTheme(
+    opt: Theme | LegacyTheme,
+    options: { effect?: boolean; emit?: boolean }
+  ): Promise<void>
 }
 
 export type LSPluginUserEvents = 'ui:visible:changed' | 'settings:changed'
@@ -831,7 +852,7 @@ export interface ILSPluginUser extends EventEmitter<LSPluginUserEvents> {
   /**
    * Set the theme for the main Logseq app
    */
-  provideTheme(theme: ThemeOptions): this
+  provideTheme(theme: Theme): this
 
   /**
    * Inject custom css for the main Logseq app

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

@@ -15,7 +15,7 @@ import {
   SlashCommandAction,
   BlockCommandCallback,
   StyleString,
-  ThemeOptions,
+  Theme,
   UIOptions,
   IHookEvent,
   BlockIdentity,
@@ -318,7 +318,8 @@ const KEY_MAIN_UI = 0
  */
 export class LSPluginUser
   extends EventEmitter<LSPluginUserEvents>
-  implements ILSPluginUser {
+  implements ILSPluginUser
+{
   // @ts-ignore
   private _version: string = LIB_VERSION
   private _debugTag: string = ''
@@ -436,7 +437,7 @@ export class LSPluginUser
     return this
   }
 
-  provideTheme(theme: ThemeOptions) {
+  provideTheme(theme: Theme) {
     this.caller.call('provider:theme', theme)
     return this
   }

+ 5 - 10
libs/src/helpers.ts

@@ -416,26 +416,21 @@ export function transformableEvent(target: HTMLElement, e: Event) {
   return obj
 }
 
-let injectedThemeEffect: any = null
-
-export function setupInjectedTheme(url?: string) {
-  injectedThemeEffect?.call()
-
-  if (!url) return
-
+export function injectTheme(url: string) {
   const link = document.createElement('link')
   link.rel = 'stylesheet'
   link.href = url
   document.head.appendChild(link)
 
-  return (injectedThemeEffect = () => {
+  const ejectTheme = () => {
     try {
       document.head.removeChild(link)
     } catch (e) {
       console.error(e)
     }
-    injectedThemeEffect = null
-  })
+  }
+
+  return ejectTheme
 }
 
 export function mergeSettingsWithSchema(

+ 9 - 2
package.json

@@ -5,6 +5,7 @@
     "main": "static/electron.js",
     "devDependencies": {
         "@capacitor/cli": "3.2.2",
+        "@logseq/nbb-logseq": "^0.5.103",
         "@playwright/test": "^1.19.2",
         "@tailwindcss/ui": "0.7.2",
         "@types/gulp": "^4.0.7",
@@ -33,6 +34,7 @@
         "app-watch": "run-p gulp:watch cljs:app-watch",
         "release": "run-s gulp:build cljs:release",
         "release-app": "run-s gulp:build cljs:release-app",
+        "release-android-app": "run-s gulp:build cljs:release-android-app",
         "dev-release-app": "run-s gulp:build cljs:dev-release-app",
         "dev-electron-app": "gulp electron",
         "release-electron": "run-s gulp:build && gulp electronMaker",
@@ -52,8 +54,9 @@
         "cljs:app-watch": "clojure -M:cljs watch app",
         "cljs:electron-watch": "clojure -M:cljs watch app electron --config-merge '{:asset-path \"./js\"}'",
         "cljs:release": "clojure -M:cljs release app publishing electron",
-        "cljs:release-electron": "clojure -M:cljs release app publishing electron --debug",
+        "cljs:release-electron": "clojure -M:cljs release app electron --debug && clojure -M:cljs release publishing",
         "cljs:release-app": "clojure -M:cljs release app",
+        "cljs:release-android-app": "clojure -M:cljs release app --config-merge '{:compiler-options {:output-feature-set :es6}}'",
         "cljs:test": "clojure -M:test compile test",
         "cljs:run-test": "node static/tests.js",
         "cljs:dev-release-app": "clojure -M:cljs release app --config-merge '{:closure-defines {frontend.config/DEV-RELEASE true}}'",
@@ -69,8 +72,10 @@
         "@capacitor/clipboard": "^1.0.8",
         "@capacitor/core": "3.2.2",
         "@capacitor/filesystem": "1.0.6",
+        "@capacitor/haptics": "^1.1.4",
         "@capacitor/ios": "3.2.2",
         "@capacitor/keyboard": "^1.2.0",
+        "@capacitor/share": "^1.1.2",
         "@capacitor/splash-screen": "1.1.3",
         "@capacitor/status-bar": "1.0.6",
         "@excalidraw/excalidraw": "0.10.0",
@@ -80,6 +85,7 @@
         "@sentry/tracing": "^6.18.2",
         "@tabler/icons": "1.54.0",
         "@tippyjs/react": "4.2.5",
+        "bignumber.js": "^9.0.2",
         "capacitor-voice-recorder": "2.1.0",
         "chokidar": "3.5.1",
         "chrono-node": "2.2.4",
@@ -98,7 +104,7 @@
         "ignore": "5.1.8",
         "is-svg": "4.3.0",
         "jszip": "3.5.0",
-        "mldoc": "1.3.3",
+        "mldoc": "1.3.9",
         "path": "0.12.7",
         "pixi-graph-fork": "0.2.0",
         "pixi.js": "6.2.0",
@@ -112,6 +118,7 @@
         "react-textarea-autosize": "8.3.3",
         "react-tippy": "1.4.0",
         "react-transition-group": "4.3.0",
+        "react-visibility-sensor": "^5.1.1",
         "reakit": "0.11.1",
         "remove-accents": "0.4.2",
         "send-intent": "3.0.11",

+ 1 - 0
public/index.html

@@ -49,6 +49,7 @@
 <script defer src="/static/js/lsplugin.core.js"></script>
 <script defer src="/static/js/main.js"></script>
 <script defer src="/static/js/code-editor.js"></script>
+<script defer src="/static/js/age-encryption.js"></script>
 <script defer src="/static/js/excalidraw.js"></script>
 <script>
   /*!

+ 6 - 0
resources/css/animation.css

@@ -44,6 +44,12 @@
   animation-duration: 1s;
 }
 
+.faster-fade-in {
+  -webkit-animation-duration: 0.3s;
+  -moz-animation-duration: 0.3s;
+  animation-duration: 0.3s;
+}
+
 /* page transition */
 .fade-enter {
   opacity: 0;

+ 13 - 3
resources/css/common.css

@@ -11,7 +11,7 @@
   --ls-border-radius-medium: 8px;
   --ls-headbar-height: 3rem;
   --ls-headbar-inner-top-padding: 0px;
-  --ls-left-sidebar-width: 240px;
+  --ls-left-sidebar-width: 246px;
   --ls-left-sidebar-sm-width: 70%;
   --ls-left-sidebar-nav-btn-size: 38px;
 }
@@ -101,8 +101,8 @@ See: https://github.com/logseq/logseq/pull/4652. */
 html[data-theme='light'] {
   --ls-primary-background-color: #ffffff;
   --ls-secondary-background-color: #f7f7f7;
-  --ls-tertiary-background-color: #f1eee8;
-  --ls-quaternary-background-color: #e8e5de;
+  --ls-tertiary-background-color: #eaeaea;
+  --ls-quaternary-background-color: #dcdcdc;
   --ls-table-tr-even-background-color: #f7f7f7;
   --ls-active-primary-color: rgb(0, 105, 182);
   --ls-active-secondary-color: #00477c;
@@ -715,6 +715,11 @@ li p:last-child,
   background-color: var(--ls-primary-background-color, #fff);
 }
 
+.bg-base-4 {
+  background-color: var(--ls-tertiary-background-color);
+}
+
+
 .pre-white-space {
   white-space: pre;
 }
@@ -1169,3 +1174,8 @@ html[data-theme='dark'] .keyboard-shortcut > code {
     overflow: hidden;
     text-overflow: ellipsis;
 }
+
+.lazy-visibility {
+    min-width: 1px;
+    min-height: 1px;
+}

+ 9 - 0
resources/css/shepherd.css

@@ -0,0 +1,9 @@
+.shepherd-button{background:#3288e6;border:0;border-radius:3px;color:hsla(0,0%,100%,.75);cursor:pointer;margin-right:.5rem;padding:.5rem 1.5rem;transition:all .5s ease}.shepherd-button:not(:disabled):hover{background:#196fcc;color:hsla(0,0%,100%,.75)}.shepherd-button.shepherd-button-secondary{background:#f1f2f3;color:rgba(0,0,0,.75)}.shepherd-button.shepherd-button-secondary:not(:disabled):hover{background:#d6d9db;color:rgba(0,0,0,.75)}.shepherd-button:disabled{cursor:not-allowed}
+.shepherd-footer{border-bottom-left-radius:5px;border-bottom-right-radius:5px;display:flex;justify-content:flex-end;padding:0 .75rem .75rem}.shepherd-footer .shepherd-button:last-child{margin-right:0}
+.shepherd-cancel-icon{background:transparent;border:none;color:hsla(0,0%,50%,.75);cursor:pointer;font-size:2em;font-weight:400;margin:0;padding:0;transition:color .5s ease}.shepherd-cancel-icon:hover{color:rgba(0,0,0,.75)}.shepherd-has-title .shepherd-content .shepherd-cancel-icon{color:hsla(0,0%,50%,.75)}.shepherd-has-title .shepherd-content .shepherd-cancel-icon:hover{color:rgba(0,0,0,.75)}
+.shepherd-title{color:rgba(0,0,0,.75);display:flex;flex:1 0 auto;font-size:1rem;font-weight:400;margin:0;padding:0}
+.shepherd-header{align-items:center;border-top-left-radius:5px;border-top-right-radius:5px;display:flex;justify-content:flex-end;line-height:2em;padding:.75rem .75rem 0}.shepherd-has-title .shepherd-content .shepherd-header{background:#e6e6e6;padding:1em}
+.shepherd-text{color:rgba(0,0,0,.75);font-size:1rem;line-height:1.3em;padding:.75em}.shepherd-text p{margin-top:0}.shepherd-text p:last-child{margin-bottom:0}
+.shepherd-content{border-radius:5px;outline:none;padding:0}
+.shepherd-element{background:#fff;border-radius:5px;box-shadow:0 1px 4px rgba(0,0,0,.2);max-width:400px;opacity:0;outline:none;transition:opacity .3s,visibility .3s;visibility:hidden;width:100%;z-index:9999}.shepherd-enabled.shepherd-element{opacity:1;visibility:visible}.shepherd-element[data-popper-reference-hidden]:not(.shepherd-centered){opacity:0;pointer-events:none;visibility:hidden}.shepherd-element,.shepherd-element *,.shepherd-element :after,.shepherd-element :before{box-sizing:border-box}.shepherd-arrow,.shepherd-arrow:before{height:16px;position:absolute;width:16px;z-index:-1}.shepherd-arrow:before{background:#fff;content:"";transform:rotate(45deg)}.shepherd-element[data-popper-placement^=top]>.shepherd-arrow{bottom:-8px}.shepherd-element[data-popper-placement^=bottom]>.shepherd-arrow{top:-8px}.shepherd-element[data-popper-placement^=left]>.shepherd-arrow{right:-8px}.shepherd-element[data-popper-placement^=right]>.shepherd-arrow{left:-8px}.shepherd-element.shepherd-centered>.shepherd-arrow{opacity:0}.shepherd-element.shepherd-has-title[data-popper-placement^=bottom]>.shepherd-arrow:before{background-color:#e6e6e6}.shepherd-target-click-disabled.shepherd-enabled.shepherd-target,.shepherd-target-click-disabled.shepherd-enabled.shepherd-target *{pointer-events:none}
+.shepherd-modal-overlay-container{height:0;left:0;opacity:0;overflow:hidden;pointer-events:none;position:fixed;top:0;transition:all .3s ease-out,height 0ms .3s,opacity .3s 0ms;width:100vw;z-index:9997}.shepherd-modal-overlay-container.shepherd-modal-is-visible{height:100vh;opacity:.5;transition:all .3s ease-out,height 0s 0s,opacity .3s 0s}.shepherd-modal-overlay-container.shepherd-modal-is-visible path{pointer-events:all}

+ 1 - 0
resources/electron.html

@@ -53,6 +53,7 @@ const portal = new MagicPortal(worker);
 <script defer src="./js/lsplugin.core.js"></script>
 <script defer src="./js/main.js"></script>
 <script defer src="./js/code-editor.js"></script>
+<script defer src="./js/age-encryption.js"></script>
 <script defer src="./js/excalidraw.js"></script>
 </body>
 </html>

+ 1 - 0
resources/index.html

@@ -52,6 +52,7 @@ const portal = new MagicPortal(worker);
 <script defer src="./js/lsplugin.core.js"></script>
 <script defer src="./js/main.js"></script>
 <script defer src="./js/code-editor.js"></script>
+<script defer src="./js/age-encryption.js"></script>
 <script defer src="./js/excalidraw.js"></script>
 </body>
 </html>

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
resources/js/lsplugin.core.js


+ 117 - 0
resources/js/shepherd.min.js

@@ -0,0 +1,117 @@
+/*! shepherd.js 9.1.0 */
+
+'use strict';(function(O,pa){"object"===typeof exports&&"undefined"!==typeof module?module.exports=pa():"function"===typeof define&&define.amd?define(pa):(O="undefined"!==typeof globalThis?globalThis:O||self,O.Shepherd=pa())})(this,function(){function O(a,b){return!1!==b.clone&&b.isMergeableObject(a)?da(Array.isArray(a)?[]:{},a,b):a}function pa(a,b,c){return a.concat(b).map(function(d){return O(d,c)})}function Db(a){return Object.getOwnPropertySymbols?Object.getOwnPropertySymbols(a).filter(function(b){return a.propertyIsEnumerable(b)}):
+[]}function Ta(a){return Object.keys(a).concat(Db(a))}function Ua(a,b){try{return b in a}catch(c){return!1}}function Eb(a,b,c){var d={};c.isMergeableObject(a)&&Ta(a).forEach(function(e){d[e]=O(a[e],c)});Ta(b).forEach(function(e){if(!Ua(a,e)||Object.hasOwnProperty.call(a,e)&&Object.propertyIsEnumerable.call(a,e))if(Ua(a,e)&&c.isMergeableObject(b[e])){if(c.customMerge){var f=c.customMerge(e);f="function"===typeof f?f:da}else f=da;d[e]=f(a[e],b[e],c)}else d[e]=O(b[e],c)});return d}function da(a,b,c){c=
+c||{};c.arrayMerge=c.arrayMerge||pa;c.isMergeableObject=c.isMergeableObject||Fb;c.cloneUnlessOtherwiseSpecified=O;var d=Array.isArray(b),e=Array.isArray(a);return d!==e?O(b,c):d?c.arrayMerge(a,b,c):Eb(a,b,c)}function ea(a){return"function"===typeof a}function qa(a){return"string"===typeof a}function Va(a){let b=Object.getOwnPropertyNames(a.constructor.prototype);for(let c=0;c<b.length;c++){let d=b[c],e=a[d];"constructor"!==d&&"function"===typeof e&&(a[d]=e.bind(a))}return a}function Gb(a,b){return c=>
+{if(b.isOpen()){let d=b.el&&c.currentTarget===b.el;(void 0!==a&&c.currentTarget.matches(a)||d)&&b.tour.next()}}}function Hb(a){let {event:b,selector:c}=a.options.advanceOn||{};if(b){let d=Gb(c,a),e;try{e=document.querySelector(c)}catch(f){}if(void 0===c||e)e?(e.addEventListener(b,d),a.on("destroy",()=>e.removeEventListener(b,d))):(document.body.addEventListener(b,d,!0),a.on("destroy",()=>document.body.removeEventListener(b,d,!0)));else return console.error(`No element was found for the selector supplied to advanceOn: ${c}`)}else return console.error("advanceOn was defined, but no event name was passed.")}
+function M(a){return a?(a.nodeName||"").toLowerCase():null}function K(a){return null==a?window:"[object Window]"!==a.toString()?(a=a.ownerDocument)?a.defaultView||window:window:a}function fa(a){var b=K(a).Element;return a instanceof b||a instanceof Element}function F(a){var b=K(a).HTMLElement;return a instanceof b||a instanceof HTMLElement}function Ea(a){if("undefined"===typeof ShadowRoot)return!1;var b=K(a).ShadowRoot;return a instanceof b||a instanceof ShadowRoot}function N(a){return a.split("-")[0]}
+function ha(a,b){void 0===b&&(b=!1);var c=a.getBoundingClientRect(),d=1,e=1;F(a)&&b&&(b=a.offsetHeight,a=a.offsetWidth,0<a&&(d=ia(c.width)/a||1),0<b&&(e=ia(c.height)/b||1));return{width:c.width/d,height:c.height/e,top:c.top/e,right:c.right/d,bottom:c.bottom/e,left:c.left/d,x:c.left/d,y:c.top/e}}function Fa(a){var b=ha(a),c=a.offsetWidth,d=a.offsetHeight;1>=Math.abs(b.width-c)&&(c=b.width);1>=Math.abs(b.height-d)&&(d=b.height);return{x:a.offsetLeft,y:a.offsetTop,width:c,height:d}}function Wa(a,b){var c=
+b.getRootNode&&b.getRootNode();if(a.contains(b))return!0;if(c&&Ea(c)){do{if(b&&a.isSameNode(b))return!0;b=b.parentNode||b.host}while(b)}return!1}function P(a){return K(a).getComputedStyle(a)}function U(a){return((fa(a)?a.ownerDocument:a.document)||window.document).documentElement}function wa(a){return"html"===M(a)?a:a.assignedSlot||a.parentNode||(Ea(a)?a.host:null)||U(a)}function Xa(a){return F(a)&&"fixed"!==P(a).position?a.offsetParent:null}function ra(a){for(var b=K(a),c=Xa(a);c&&0<=["table","td",
+"th"].indexOf(M(c))&&"static"===P(c).position;)c=Xa(c);if(c&&("html"===M(c)||"body"===M(c)&&"static"===P(c).position))return b;if(!c)a:{c=-1!==navigator.userAgent.toLowerCase().indexOf("firefox");if(-1===navigator.userAgent.indexOf("Trident")||!F(a)||"fixed"!==P(a).position)for(a=wa(a),Ea(a)&&(a=a.host);F(a)&&0>["html","body"].indexOf(M(a));){var d=P(a);if("none"!==d.transform||"none"!==d.perspective||"paint"===d.contain||-1!==["transform","perspective"].indexOf(d.willChange)||c&&"filter"===d.willChange||
+c&&d.filter&&"none"!==d.filter){c=a;break a}else a=a.parentNode}c=null}return c||b}function Ga(a){return 0<=["top","bottom"].indexOf(a)?"x":"y"}function Ya(a){return Object.assign({},{top:0,right:0,bottom:0,left:0},a)}function Za(a,b){return b.reduce(function(c,d){c[d]=a;return c},{})}function ja(a){return a.split("-")[1]}function $a(a){var b,c=a.popper,d=a.popperRect,e=a.placement,f=a.variation,g=a.offsets,l=a.position,m=a.gpuAcceleration,k=a.adaptive,p=a.roundOffsets,q=a.isFixed;a=g.x;a=void 0===
+a?0:a;var n=g.y,r=void 0===n?0:n;n="function"===typeof p?p({x:a,y:r}):{x:a,y:r};a=n.x;r=n.y;n=g.hasOwnProperty("x");g=g.hasOwnProperty("y");var x="left",h="top",t=window;if(k){var v=ra(c),A="clientHeight",u="clientWidth";v===K(c)&&(v=U(c),"static"!==P(v).position&&"absolute"===l&&(A="scrollHeight",u="scrollWidth"));if("top"===e||("left"===e||"right"===e)&&"end"===f)h="bottom",r-=(q&&v===t&&t.visualViewport?t.visualViewport.height:v[A])-d.height,r*=m?1:-1;if("left"===e||("top"===e||"bottom"===e)&&
+"end"===f)x="right",a-=(q&&v===t&&t.visualViewport?t.visualViewport.width:v[u])-d.width,a*=m?1:-1}c=Object.assign({position:l},k&&Ib);!0===p?(p=r,d=window.devicePixelRatio||1,a={x:ia(a*d)/d||0,y:ia(p*d)/d||0}):a={x:a,y:r};p=a;a=p.x;r=p.y;if(m){var w;return Object.assign({},c,(w={},w[h]=g?"0":"",w[x]=n?"0":"",w.transform=1>=(t.devicePixelRatio||1)?"translate("+a+"px, "+r+"px)":"translate3d("+a+"px, "+r+"px, 0)",w))}return Object.assign({},c,(b={},b[h]=g?r+"px":"",b[x]=n?a+"px":"",b.transform="",b))}
+function xa(a){return a.replace(/left|right|bottom|top/g,function(b){return Jb[b]})}function ab(a){return a.replace(/start|end/g,function(b){return Kb[b]})}function Ha(a){a=K(a);return{scrollLeft:a.pageXOffset,scrollTop:a.pageYOffset}}function Ia(a){return ha(U(a)).left+Ha(a).scrollLeft}function Ja(a){a=P(a);return/auto|scroll|overlay|hidden/.test(a.overflow+a.overflowY+a.overflowX)}function bb(a){return 0<=["html","body","#document"].indexOf(M(a))?a.ownerDocument.body:F(a)&&Ja(a)?a:bb(wa(a))}function sa(a,
+b){var c;void 0===b&&(b=[]);var d=bb(a);a=d===(null==(c=a.ownerDocument)?void 0:c.body);c=K(d);d=a?[c].concat(c.visualViewport||[],Ja(d)?d:[]):d;b=b.concat(d);return a?b:b.concat(sa(wa(d)))}function Ka(a){return Object.assign({},a,{left:a.x,top:a.y,right:a.x+a.width,bottom:a.y+a.height})}function cb(a,b){if("viewport"===b){b=K(a);var c=U(a);b=b.visualViewport;var d=c.clientWidth;c=c.clientHeight;var e=0,f=0;b&&(d=b.width,c=b.height,/^((?!chrome|android).)*safari/i.test(navigator.userAgent)||(e=b.offsetLeft,
+f=b.offsetTop));a={width:d,height:c,x:e+Ia(a),y:f};a=Ka(a)}else fa(b)?(a=ha(b),a.top+=b.clientTop,a.left+=b.clientLeft,a.bottom=a.top+b.clientHeight,a.right=a.left+b.clientWidth,a.width=b.clientWidth,a.height=b.clientHeight,a.x=a.left,a.y=a.top):(f=U(a),a=U(f),d=Ha(f),b=null==(c=f.ownerDocument)?void 0:c.body,c=L(a.scrollWidth,a.clientWidth,b?b.scrollWidth:0,b?b.clientWidth:0),e=L(a.scrollHeight,a.clientHeight,b?b.scrollHeight:0,b?b.clientHeight:0),f=-d.scrollLeft+Ia(f),d=-d.scrollTop,"rtl"===P(b||
+a).direction&&(f+=L(a.clientWidth,b?b.clientWidth:0)-c),a=Ka({width:c,height:e,x:f,y:d}));return a}function Lb(a){var b=sa(wa(a)),c=0<=["absolute","fixed"].indexOf(P(a).position)&&F(a)?ra(a):a;return fa(c)?b.filter(function(d){return fa(d)&&Wa(d,c)&&"body"!==M(d)}):[]}function Mb(a,b,c){b="clippingParents"===b?Lb(a):[].concat(b);c=[].concat(b,[c]);c=c.reduce(function(d,e){e=cb(a,e);d.top=L(e.top,d.top);d.right=V(e.right,d.right);d.bottom=V(e.bottom,d.bottom);d.left=L(e.left,d.left);return d},cb(a,
+c[0]));c.width=c.right-c.left;c.height=c.bottom-c.top;c.x=c.left;c.y=c.top;return c}function db(a){var b=a.reference,c=a.element,d=(a=a.placement)?N(a):null;a=a?ja(a):null;var e=b.x+b.width/2-c.width/2,f=b.y+b.height/2-c.height/2;switch(d){case "top":e={x:e,y:b.y-c.height};break;case "bottom":e={x:e,y:b.y+b.height};break;case "right":e={x:b.x+b.width,y:f};break;case "left":e={x:b.x-c.width,y:f};break;default:e={x:b.x,y:b.y}}d=d?Ga(d):null;if(null!=d)switch(f="y"===d?"height":"width",a){case "start":e[d]-=
+b[f]/2-c[f]/2;break;case "end":e[d]+=b[f]/2-c[f]/2}return e}function ta(a,b){void 0===b&&(b={});var c=b;b=c.placement;b=void 0===b?a.placement:b;var d=c.boundary,e=void 0===d?"clippingParents":d;d=c.rootBoundary;var f=void 0===d?"viewport":d;d=c.elementContext;d=void 0===d?"popper":d;var g=c.altBoundary,l=void 0===g?!1:g;c=c.padding;c=void 0===c?0:c;c=Ya("number"!==typeof c?c:Za(c,ua));g=a.rects.popper;l=a.elements[l?"popper"===d?"reference":"popper":d];e=Mb(fa(l)?l:l.contextElement||U(a.elements.popper),
+e,f);f=ha(a.elements.reference);l=db({reference:f,element:g,strategy:"absolute",placement:b});g=Ka(Object.assign({},g,l));f="popper"===d?g:f;var m={top:e.top-f.top+c.top,bottom:f.bottom-e.bottom+c.bottom,left:e.left-f.left+c.left,right:f.right-e.right+c.right};a=a.modifiersData.offset;if("popper"===d&&a){var k=a[b];Object.keys(m).forEach(function(p){var q=0<=["right","bottom"].indexOf(p)?1:-1,n=0<=["top","bottom"].indexOf(p)?"y":"x";m[p]+=k[n]*q})}return m}function Nb(a,b){void 0===b&&(b={});var c=
+b.boundary,d=b.rootBoundary,e=b.padding,f=b.flipVariations,g=b.allowedAutoPlacements,l=void 0===g?eb:g,m=ja(b.placement);b=m?f?fb:fb.filter(function(p){return ja(p)===m}):ua;f=b.filter(function(p){return 0<=l.indexOf(p)});0===f.length&&(f=b);var k=f.reduce(function(p,q){p[q]=ta(a,{placement:q,boundary:c,rootBoundary:d,padding:e})[N(q)];return p},{});return Object.keys(k).sort(function(p,q){return k[p]-k[q]})}function Ob(a){if("auto"===N(a))return[];var b=xa(a);return[ab(a),b,ab(b)]}function gb(a,
+b,c){void 0===c&&(c={x:0,y:0});return{top:a.top-b.height-c.y,right:a.right-b.width+c.x,bottom:a.bottom-b.height+c.y,left:a.left-b.width-c.x}}function hb(a){return["top","right","bottom","left"].some(function(b){return 0<=a[b]})}function Pb(a,b,c){void 0===c&&(c=!1);var d=F(b),e;if(e=F(b)){var f=b.getBoundingClientRect();e=ia(f.width)/b.offsetWidth||1;f=ia(f.height)/b.offsetHeight||1;e=1!==e||1!==f}f=e;e=U(b);a=ha(a,f);f={scrollLeft:0,scrollTop:0};var g={x:0,y:0};if(d||!d&&!c){if("body"!==M(b)||Ja(e))f=
+b!==K(b)&&F(b)?{scrollLeft:b.scrollLeft,scrollTop:b.scrollTop}:Ha(b);F(b)?(g=ha(b,!0),g.x+=b.clientLeft,g.y+=b.clientTop):e&&(g.x=Ia(e))}return{x:a.left+f.scrollLeft-g.x,y:a.top+f.scrollTop-g.y,width:a.width,height:a.height}}function Qb(a){function b(f){d.add(f.name);[].concat(f.requires||[],f.requiresIfExists||[]).forEach(function(g){d.has(g)||(g=c.get(g))&&b(g)});e.push(f)}var c=new Map,d=new Set,e=[];a.forEach(function(f){c.set(f.name,f)});a.forEach(function(f){d.has(f.name)||b(f)});return e}function Rb(a){var b=
+Qb(a);return Sb.reduce(function(c,d){return c.concat(b.filter(function(e){return e.phase===d}))},[])}function Tb(a){var b;return function(){b||(b=new Promise(function(c){Promise.resolve().then(function(){b=void 0;c(a())})}));return b}}function Ub(a){var b=a.reduce(function(c,d){var e=c[d.name];c[d.name]=e?Object.assign({},e,d,{options:Object.assign({},e.options,d.options),data:Object.assign({},e.data,d.data)}):d;return c},{});return Object.keys(b).map(function(c){return b[c]})}function ib(){for(var a=
+arguments.length,b=Array(a),c=0;c<a;c++)b[c]=arguments[c];return!b.some(function(d){return!(d&&"function"===typeof d.getBoundingClientRect)})}function La(){La=Object.assign||function(a){for(var b=1;b<arguments.length;b++){var c=arguments[b],d;for(d in c)Object.prototype.hasOwnProperty.call(c,d)&&(a[d]=c[d])}return a};return La.apply(this,arguments)}function Vb(){return[{name:"applyStyles",fn(a){let {state:b}=a;Object.keys(b.elements).forEach(c=>{if("popper"===c){var d=b.attributes[c]||{},e=b.elements[c];
+Object.assign(e.style,{position:"fixed",left:"50%",top:"50%",transform:"translate(-50%, -50%)"});Object.keys(d).forEach(f=>{let g=d[f];!1===g?e.removeAttribute(f):e.setAttribute(f,!0===g?"":g)})}})}},{name:"computeStyles",options:{adaptive:!1}}]}function Wb(a){let b=Vb(),c={placement:"top",strategy:"fixed",modifiers:[{name:"focusAfterRender",enabled:!0,phase:"afterWrite",fn(){setTimeout(()=>{a.el&&a.el.focus()},300)}}]};return c=La({},c,{modifiers:Array.from(new Set([...c.modifiers,...b]))})}function jb(a){return qa(a)&&
+""!==a?"-"!==a.charAt(a.length-1)?`${a}-`:a:""}function Ma(a){a=a.options.attachTo||{};let b=Object.assign({},a);if(qa(a.element)){try{b.element=document.querySelector(a.element)}catch(c){}b.element||console.error(`The element for this Shepherd step was not found ${a.element}`)}return b}function Na(){let a=Date.now();return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,b=>{let c=(a+16*Math.random())%16|0;a=Math.floor(a/16);return("x"==b?c:c&3|8).toString(16)})}function Xb(a,b){let c={modifiers:[{name:"preventOverflow",
+options:{altAxis:!0,tether:!1}},{name:"focusAfterRender",enabled:!0,phase:"afterWrite",fn(){setTimeout(()=>{b.el&&b.el.focus()},300)}}],strategy:"absolute"};b.isCentered()?c=Wb(b):c.placement=a.on;(a=b.tour&&b.tour.options&&b.tour.options.defaultStepOptions)&&(c=kb(a,c));return c=kb(b.options,c)}function kb(a,b){if(a.popperOptions){let c=Object.assign({},b,a.popperOptions);if(a.popperOptions.modifiers&&0<a.popperOptions.modifiers.length){let d=a.popperOptions.modifiers.map(e=>e.name);b=b.modifiers.filter(e=>
+!d.includes(e.name));c.modifiers=Array.from(new Set([...b,...a.popperOptions.modifiers]))}return c}return b}function G(){}function Yb(a,b){for(let c in b)a[c]=b[c];return a}function ka(a){return a()}function lb(a){return"function"===typeof a}function Q(a,b){return a!=a?b==b:a!==b||a&&"object"===typeof a||"function"===typeof a}function H(a){a.parentNode.removeChild(a)}function mb(a){return document.createElementNS("http://www.w3.org/2000/svg",a)}function ya(a,b,c,d){a.addEventListener(b,c,d);return()=>
+a.removeEventListener(b,c,d)}function B(a,b,c){null==c?a.removeAttribute(b):a.getAttribute(b)!==c&&a.setAttribute(b,c)}function nb(a,b){let c=Object.getOwnPropertyDescriptors(a.__proto__);for(let d in b)null==b[d]?a.removeAttribute(d):"style"===d?a.style.cssText=b[d]:"__value"===d?a.value=a[d]=b[d]:c[d]&&c[d].set?a[d]=b[d]:B(a,d,b[d])}function la(a,b,c){a.classList[c?"add":"remove"](b)}function za(){if(!R)throw Error("Function called outside component initialization");return R}function Oa(a){Aa.push(a)}
+function ob(){let a=R;do{for(;Ba<va.length;){var b=va[Ba];Ba++;R=b;b=b.$$;if(null!==b.fragment){b.update();b.before_update.forEach(ka);var c=b.dirty;b.dirty=[-1];b.fragment&&b.fragment.p(b.ctx,c);b.after_update.forEach(Oa)}}R=null;for(Ba=va.length=0;ma.length;)ma.pop()();for(b=0;b<Aa.length;b+=1)c=Aa[b],Pa.has(c)||(Pa.add(c),c());Aa.length=0}while(va.length);for(;pb.length;)pb.pop()();Qa=!1;Pa.clear();R=a}function Z(){aa={r:0,c:[],p:aa}}function ba(){aa.r||aa.c.forEach(ka);aa=aa.p}function z(a,b){a&&
+a.i&&(Ca.delete(a),a.i(b))}function C(a,b,c,d){a&&a.o&&!Ca.has(a)&&(Ca.add(a),aa.c.push(()=>{Ca.delete(a);d&&(c&&a.d(1),d())}),a.o(b))}function ca(a){a&&a.c()}function W(a,b,c,d){let {fragment:e,on_mount:f,on_destroy:g,after_update:l}=a.$$;e&&e.m(b,c);d||Oa(()=>{let m=f.map(ka).filter(lb);g?g.push(...m):m.forEach(ka);a.$$.on_mount=[]});l.forEach(Oa)}function X(a,b){a=a.$$;null!==a.fragment&&(a.on_destroy.forEach(ka),a.fragment&&a.fragment.d(b),a.on_destroy=a.fragment=null,a.ctx=[])}function S(a,b,
+c,d,e,f,g,l){void 0===l&&(l=[-1]);let m=R;R=a;let k=a.$$={fragment:null,ctx:null,props:f,update:G,not_equal:e,bound:Object.create(null),on_mount:[],on_destroy:[],on_disconnect:[],before_update:[],after_update:[],context:new Map(b.context||(m?m.$$.context:[])),callbacks:Object.create(null),dirty:l,skip_bound:!1,root:b.target||m.$$.root};g&&g(k.root);let p=!1;k.ctx=c?c(a,b.props||{},function(q,n){let r=(2>=arguments.length?0:arguments.length-2)?2>=arguments.length?void 0:arguments[2]:n;if(k.ctx&&e(k.ctx[q],
+k.ctx[q]=r)){if(!k.skip_bound&&k.bound[q])k.bound[q](r);p&&(-1===a.$$.dirty[0]&&(va.push(a),Qa||(Qa=!0,Zb.then(ob)),a.$$.dirty.fill(0)),a.$$.dirty[q/31|0]|=1<<q%31)}return n}):[];k.update();p=!0;k.before_update.forEach(ka);k.fragment=d?d(k.ctx):!1;b.target&&(b.hydrate?(c=Array.from(b.target.childNodes),k.fragment&&k.fragment.l(c),c.forEach(H)):k.fragment&&k.fragment.c(),b.intro&&z(a.$$.fragment),W(a,b.target,b.anchor,b.customElement),ob());R=m}function $b(a){let b,c,d,e,f;return{c(){b=document.createElement("button");
+B(b,"aria-label",c=a[3]?a[3]:null);B(b,"class",d=`${a[1]||""} shepherd-button ${a[4]?"shepherd-button-secondary":""}`);b.disabled=a[2];B(b,"tabindex","0")},m(g,l){g.insertBefore(b,l||null);b.innerHTML=a[5];e||(f=ya(b,"click",function(){lb(a[0])&&a[0].apply(this,arguments)}),e=!0)},p(g,l){[l]=l;a=g;l&32&&(b.innerHTML=a[5]);l&8&&c!==(c=a[3]?a[3]:null)&&B(b,"aria-label",c);l&18&&d!==(d=`${a[1]||""} shepherd-button ${a[4]?"shepherd-button-secondary":""}`)&&B(b,"class",d);l&4&&(b.disabled=a[2])},i:G,o:G,
+d(g){g&&H(b);e=!1;f()}}}function ac(a,b,c){function d(n){return ea(n)?n.call(f):n}let {config:e,step:f}=b,g,l,m,k,p,q;a.$$set=n=>{"config"in n&&c(6,e=n.config);"step"in n&&c(7,f=n.step)};a.$$.update=()=>{a.$$.dirty&192&&(c(0,g=e.action?e.action.bind(f.tour):null),c(1,l=e.classes),c(2,m=e.disabled?d(e.disabled):!1),c(3,k=e.label?d(e.label):null),c(4,p=e.secondary),c(5,q=e.text?d(e.text):null))};return[g,l,m,k,p,q,e,f]}function qb(a,b,c){a=a.slice();a[2]=b[c];return a}function rb(a){let b,c,d=a[1],
+e=[];for(let g=0;g<d.length;g+=1)e[g]=sb(qb(a,d,g));let f=g=>C(e[g],1,1,()=>{e[g]=null});return{c(){for(let g=0;g<e.length;g+=1)e[g].c();b=document.createTextNode("")},m(g,l){for(let m=0;m<e.length;m+=1)e[m].m(g,l);g.insertBefore(b,l||null);c=!0},p(g,l){if(l&3){d=g[1];let m;for(m=0;m<d.length;m+=1){let k=qb(g,d,m);e[m]?(e[m].p(k,l),z(e[m],1)):(e[m]=sb(k),e[m].c(),z(e[m],1),e[m].m(b.parentNode,b))}Z();for(m=d.length;m<e.length;m+=1)f(m);ba()}},i(g){if(!c){for(g=0;g<d.length;g+=1)z(e[g]);c=!0}},o(g){e=
+e.filter(Boolean);for(g=0;g<e.length;g+=1)C(e[g]);c=!1},d(g){var l=e;for(let m=0;m<l.length;m+=1)l[m]&&l[m].d(g);g&&H(b)}}}function sb(a){let b,c;b=new bc({props:{config:a[2],step:a[0]}});return{c(){ca(b.$$.fragment)},m(d,e){W(b,d,e);c=!0},p(d,e){let f={};e&2&&(f.config=d[2]);e&1&&(f.step=d[0]);b.$set(f)},i(d){c||(z(b.$$.fragment,d),c=!0)},o(d){C(b.$$.fragment,d);c=!1},d(d){X(b,d)}}}function cc(a){let b,c,d=a[1]&&rb(a);return{c(){b=document.createElement("footer");d&&d.c();B(b,"class","shepherd-footer")},
+m(e,f){e.insertBefore(b,f||null);d&&d.m(b,null);c=!0},p(e,f){[f]=f;e[1]?d?(d.p(e,f),f&2&&z(d,1)):(d=rb(e),d.c(),z(d,1),d.m(b,null)):d&&(Z(),C(d,1,1,()=>{d=null}),ba())},i(e){c||(z(d),c=!0)},o(e){C(d);c=!1},d(e){e&&H(b);d&&d.d()}}}function dc(a,b,c){let d,{step:e}=b;a.$$set=f=>{"step"in f&&c(0,e=f.step)};a.$$.update=()=>{a.$$.dirty&1&&c(1,d=e.options.buttons)};return[e,d]}function ec(a){let b,c,d,e,f;return{c(){b=document.createElement("button");c=document.createElement("span");c.textContent="\u00d7";
+B(c,"aria-hidden","true");B(b,"aria-label",d=a[0].label?a[0].label:"Close Tour");B(b,"class","shepherd-cancel-icon");B(b,"type","button")},m(g,l){g.insertBefore(b,l||null);b.appendChild(c);e||(f=ya(b,"click",a[1]),e=!0)},p(g,l){[l]=l;l&1&&d!==(d=g[0].label?g[0].label:"Close Tour")&&B(b,"aria-label",d)},i:G,o:G,d(g){g&&H(b);e=!1;f()}}}function fc(a,b,c){let {cancelIcon:d,step:e}=b;a.$$set=f=>{"cancelIcon"in f&&c(0,d=f.cancelIcon);"step"in f&&c(2,e=f.step)};return[d,f=>{f.preventDefault();e.cancel()},
+e]}function gc(a){let b;return{c(){b=document.createElement("h3");B(b,"id",a[1]);B(b,"class","shepherd-title")},m(c,d){c.insertBefore(b,d||null);a[3](b)},p(c,d){[d]=d;d&2&&B(b,"id",c[1])},i:G,o:G,d(c){c&&H(b);a[3](null)}}}function hc(a,b,c){let {labelId:d,element:e,title:f}=b;za().$$.after_update.push(()=>{ea(f)&&c(2,f=f());c(0,e.innerHTML=f,e)});a.$$set=g=>{"labelId"in g&&c(1,d=g.labelId);"element"in g&&c(0,e=g.element);"title"in g&&c(2,f=g.title)};return[e,d,f,function(g){ma[g?"unshift":"push"](()=>
+{e=g;c(0,e)})}]}function tb(a){let b,c;b=new ic({props:{labelId:a[0],title:a[2]}});return{c(){ca(b.$$.fragment)},m(d,e){W(b,d,e);c=!0},p(d,e){let f={};e&1&&(f.labelId=d[0]);e&4&&(f.title=d[2]);b.$set(f)},i(d){c||(z(b.$$.fragment,d),c=!0)},o(d){C(b.$$.fragment,d);c=!1},d(d){X(b,d)}}}function ub(a){let b,c;b=new jc({props:{cancelIcon:a[3],step:a[1]}});return{c(){ca(b.$$.fragment)},m(d,e){W(b,d,e);c=!0},p(d,e){let f={};e&8&&(f.cancelIcon=d[3]);e&2&&(f.step=d[1]);b.$set(f)},i(d){c||(z(b.$$.fragment,d),
+c=!0)},o(d){C(b.$$.fragment,d);c=!1},d(d){X(b,d)}}}function kc(a){let b,c,d,e=a[2]&&tb(a),f=a[3]&&a[3].enabled&&ub(a);return{c(){b=document.createElement("header");e&&e.c();c=document.createTextNode(" ");f&&f.c();B(b,"class","shepherd-header")},m(g,l){g.insertBefore(b,l||null);e&&e.m(b,null);b.appendChild(c);f&&f.m(b,null);d=!0},p(g,l){[l]=l;g[2]?e?(e.p(g,l),l&4&&z(e,1)):(e=tb(g),e.c(),z(e,1),e.m(b,c)):e&&(Z(),C(e,1,1,()=>{e=null}),ba());g[3]&&g[3].enabled?f?(f.p(g,l),l&8&&z(f,1)):(f=ub(g),f.c(),
+z(f,1),f.m(b,null)):f&&(Z(),C(f,1,1,()=>{f=null}),ba())},i(g){d||(z(e),z(f),d=!0)},o(g){C(e);C(f);d=!1},d(g){g&&H(b);e&&e.d();f&&f.d()}}}function lc(a,b,c){let {labelId:d,step:e}=b,f,g;a.$$set=l=>{"labelId"in l&&c(0,d=l.labelId);"step"in l&&c(1,e=l.step)};a.$$.update=()=>{a.$$.dirty&2&&(c(2,f=e.options.title),c(3,g=e.options.cancelIcon))};return[d,e,f,g]}function mc(a){let b;return{c(){b=document.createElement("div");B(b,"class","shepherd-text");B(b,"id",a[1])},m(c,d){c.insertBefore(b,d||null);a[3](b)},
+p(c,d){[d]=d;d&2&&B(b,"id",c[1])},i:G,o:G,d(c){c&&H(b);a[3](null)}}}function nc(a,b,c){let {descriptionId:d,element:e,step:f}=b;za().$$.after_update.push(()=>{let {text:g}=f.options;ea(g)&&(g=g.call(f));g instanceof HTMLElement?e.appendChild(g):c(0,e.innerHTML=g,e)});a.$$set=g=>{"descriptionId"in g&&c(1,d=g.descriptionId);"element"in g&&c(0,e=g.element);"step"in g&&c(2,f=g.step)};return[e,d,f,function(g){ma[g?"unshift":"push"](()=>{e=g;c(0,e)})}]}function vb(a){let b,c;b=new oc({props:{labelId:a[1],
+step:a[2]}});return{c(){ca(b.$$.fragment)},m(d,e){W(b,d,e);c=!0},p(d,e){let f={};e&2&&(f.labelId=d[1]);e&4&&(f.step=d[2]);b.$set(f)},i(d){c||(z(b.$$.fragment,d),c=!0)},o(d){C(b.$$.fragment,d);c=!1},d(d){X(b,d)}}}function wb(a){let b,c;b=new pc({props:{descriptionId:a[0],step:a[2]}});return{c(){ca(b.$$.fragment)},m(d,e){W(b,d,e);c=!0},p(d,e){let f={};e&1&&(f.descriptionId=d[0]);e&4&&(f.step=d[2]);b.$set(f)},i(d){c||(z(b.$$.fragment,d),c=!0)},o(d){C(b.$$.fragment,d);c=!1},d(d){X(b,d)}}}function xb(a){let b,
+c;b=new qc({props:{step:a[2]}});return{c(){ca(b.$$.fragment)},m(d,e){W(b,d,e);c=!0},p(d,e){let f={};e&4&&(f.step=d[2]);b.$set(f)},i(d){c||(z(b.$$.fragment,d),c=!0)},o(d){C(b.$$.fragment,d);c=!1},d(d){X(b,d)}}}function rc(a){let b,c=void 0!==a[2].options.title||a[2].options.cancelIcon&&a[2].options.cancelIcon.enabled,d,e=void 0!==a[2].options.text,f,g=Array.isArray(a[2].options.buttons)&&a[2].options.buttons.length,l,m=c&&vb(a),k=e&&wb(a),p=g&&xb(a);return{c(){b=document.createElement("div");m&&m.c();
+d=document.createTextNode(" ");k&&k.c();f=document.createTextNode(" ");p&&p.c();B(b,"class","shepherd-content")},m(q,n){q.insertBefore(b,n||null);m&&m.m(b,null);b.appendChild(d);k&&k.m(b,null);b.appendChild(f);p&&p.m(b,null);l=!0},p(q,n){[n]=n;n&4&&(c=void 0!==q[2].options.title||q[2].options.cancelIcon&&q[2].options.cancelIcon.enabled);c?m?(m.p(q,n),n&4&&z(m,1)):(m=vb(q),m.c(),z(m,1),m.m(b,d)):m&&(Z(),C(m,1,1,()=>{m=null}),ba());n&4&&(e=void 0!==q[2].options.text);e?k?(k.p(q,n),n&4&&z(k,1)):(k=wb(q),
+k.c(),z(k,1),k.m(b,f)):k&&(Z(),C(k,1,1,()=>{k=null}),ba());n&4&&(g=Array.isArray(q[2].options.buttons)&&q[2].options.buttons.length);g?p?(p.p(q,n),n&4&&z(p,1)):(p=xb(q),p.c(),z(p,1),p.m(b,null)):p&&(Z(),C(p,1,1,()=>{p=null}),ba())},i(q){l||(z(m),z(k),z(p),l=!0)},o(q){C(m);C(k);C(p);l=!1},d(q){q&&H(b);m&&m.d();k&&k.d();p&&p.d()}}}function sc(a,b,c){let {descriptionId:d,labelId:e,step:f}=b;a.$$set=g=>{"descriptionId"in g&&c(0,d=g.descriptionId);"labelId"in g&&c(1,e=g.labelId);"step"in g&&c(2,f=g.step)};
+return[d,e,f]}function yb(a){let b;return{c(){b=document.createElement("div");B(b,"class","shepherd-arrow");B(b,"data-popper-arrow","")},m(c,d){c.insertBefore(b,d||null)},d(c){c&&H(b)}}}function tc(a){let b,c,d,e,f,g,l,m,k=a[4].options.arrow&&a[4].options.attachTo&&a[4].options.attachTo.element&&a[4].options.attachTo.on&&yb();d=new uc({props:{descriptionId:a[2],labelId:a[3],step:a[4]}});let p=[{"aria-describedby":e=void 0!==a[4].options.text?a[2]:null},{"aria-labelledby":f=a[4].options.title?a[3]:
+null},a[1],{role:"dialog"},{tabindex:"0"}],q={};for(let n=0;n<p.length;n+=1)q=Yb(q,p[n]);return{c(){b=document.createElement("div");k&&k.c();c=document.createTextNode(" ");ca(d.$$.fragment);nb(b,q);la(b,"shepherd-has-cancel-icon",a[5]);la(b,"shepherd-has-title",a[6]);la(b,"shepherd-element",!0)},m(n,r){n.insertBefore(b,r||null);k&&k.m(b,null);b.appendChild(c);W(d,b,null);a[13](b);g=!0;l||(m=ya(b,"keydown",a[7]),l=!0)},p(n,r){var [x]=r;n[4].options.arrow&&n[4].options.attachTo&&n[4].options.attachTo.element&&
+n[4].options.attachTo.on?k||(k=yb(),k.c(),k.m(b,c)):k&&(k.d(1),k=null);r={};x&4&&(r.descriptionId=n[2]);x&8&&(r.labelId=n[3]);x&16&&(r.step=n[4]);d.$set(r);r=b;x=[(!g||x&20&&e!==(e=void 0!==n[4].options.text?n[2]:null))&&{"aria-describedby":e},(!g||x&24&&f!==(f=n[4].options.title?n[3]:null))&&{"aria-labelledby":f},x&2&&n[1],{role:"dialog"},{tabindex:"0"}];let h={},t={},v={$$scope:1},A=p.length;for(;A--;){let u=p[A],w=x[A];if(w){for(let y in u)y in w||(t[y]=1);for(let y in w)v[y]||(h[y]=w[y],v[y]=
+1);p[A]=w}else for(let y in u)v[y]=1}for(let u in t)u in h||(h[u]=void 0);nb(r,q=h);la(b,"shepherd-has-cancel-icon",n[5]);la(b,"shepherd-has-title",n[6]);la(b,"shepherd-element",!0)},i(n){g||(z(d.$$.fragment,n),g=!0)},o(n){C(d.$$.fragment,n);g=!1},d(n){n&&H(b);k&&k.d();X(d);a[13](null);l=!1;m()}}}function zb(a){return a.split(" ").filter(b=>!!b.length)}function vc(a,b,c){let {classPrefix:d,element:e,descriptionId:f,firstFocusableElement:g,focusableElements:l,labelId:m,lastFocusableElement:k,step:p,
+dataStepId:q}=b,n,r,x;za().$$.on_mount.push(()=>{c(1,q={[`data-${d}shepherd-step-id`]:p.id});c(9,l=e.querySelectorAll('a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex="0"]'));c(8,g=l[0]);c(10,k=l[l.length-1])});za().$$.after_update.push(()=>{if(x!==p.options.classes){var h=x;qa(h)&&(h=zb(h),h.length&&e.classList.remove(...h));h=x=p.options.classes;qa(h)&&(h=zb(h),h.length&&e.classList.add(...h))}});a.$$set=h=>{"classPrefix"in
+h&&c(11,d=h.classPrefix);"element"in h&&c(0,e=h.element);"descriptionId"in h&&c(2,f=h.descriptionId);"firstFocusableElement"in h&&c(8,g=h.firstFocusableElement);"focusableElements"in h&&c(9,l=h.focusableElements);"labelId"in h&&c(3,m=h.labelId);"lastFocusableElement"in h&&c(10,k=h.lastFocusableElement);"step"in h&&c(4,p=h.step);"dataStepId"in h&&c(1,q=h.dataStepId)};a.$$.update=()=>{a.$$.dirty&16&&(c(5,n=p.options&&p.options.cancelIcon&&p.options.cancelIcon.enabled),c(6,r=p.options&&p.options.title))};
+return[e,q,f,m,p,n,r,h=>{const {tour:t}=p;switch(h.keyCode){case 9:if(0===l.length){h.preventDefault();break}if(h.shiftKey){if(document.activeElement===g||document.activeElement.classList.contains("shepherd-element"))h.preventDefault(),k.focus()}else document.activeElement===k&&(h.preventDefault(),g.focus());break;case 27:t.options.exitOnEsc&&p.cancel();break;case 37:t.options.keyboardNavigation&&t.back();break;case 39:t.options.keyboardNavigation&&t.next()}},g,l,k,d,()=>e,function(h){ma[h?"unshift":
+"push"](()=>{e=h;c(0,e)})}]}function wc(a){a&&({steps:a}=a,a.forEach(b=>{b.options&&!1===b.options.canClickTarget&&b.options.attachTo&&b.target instanceof HTMLElement&&b.target.classList.remove("shepherd-target-click-disabled")}))}function xc(a){let b,c,d,e,f;return{c(){b=mb("svg");c=mb("path");B(c,"d",a[2]);B(b,"class",d=`${a[1]?"shepherd-modal-is-visible":""} shepherd-modal-overlay-container`)},m(g,l){g.insertBefore(b,l||null);b.appendChild(c);a[11](b);e||(f=ya(b,"touchmove",a[3]),e=!0)},p(g,l){[l]=
+l;l&4&&B(c,"d",g[2]);l&2&&d!==(d=`${g[1]?"shepherd-modal-is-visible":""} shepherd-modal-overlay-container`)&&B(b,"class",d)},i:G,o:G,d(g){g&&H(b);a[11](null);e=!1;f()}}}function Ab(a){if(!a)return null;let b=a instanceof HTMLElement&&window.getComputedStyle(a).overflowY;return"hidden"!==b&&"visible"!==b&&a.scrollHeight>=a.clientHeight?a:Ab(a.parentElement)}function yc(a,b,c){function d(){c(4,p={width:0,height:0,x:0,y:0,r:0})}function e(){c(1,q=!1);l()}function f(h,t,v,A){void 0===h&&(h=0);void 0===
+t&&(t=0);if(A){var u=A.getBoundingClientRect();let y=u.y||u.top;u=u.bottom||y+u.height;if(v){var w=v.getBoundingClientRect();v=w.y||w.top;w=w.bottom||v+w.height;y=Math.max(y,v);u=Math.min(u,w)}let {y:Y,height:E}={y,height:Math.max(u-y,0)},{x:I,width:D,left:na}=A.getBoundingClientRect();c(4,p={width:D+2*h,height:E+2*h,x:(I||na)-h,y:Y-h,r:t})}else d()}function g(){c(1,q=!0)}function l(){n&&(cancelAnimationFrame(n),n=void 0);window.removeEventListener("touchmove",x,{passive:!1})}function m(h){let {modalOverlayOpeningPadding:t,
+modalOverlayOpeningRadius:v}=h.options,A=Ab(h.target),u=()=>{n=void 0;f(t,v,A,h.target);n=requestAnimationFrame(u)};u();window.addEventListener("touchmove",x,{passive:!1})}let {element:k,openingProperties:p}=b;Na();let q=!1,n=void 0,r;d();let x=h=>{h.preventDefault()};a.$$set=h=>{"element"in h&&c(0,k=h.element);"openingProperties"in h&&c(4,p=h.openingProperties)};a.$$.update=()=>{if(a.$$.dirty&16){let {width:h,height:t,x:v=0,y:A=0,r:u=0}=p,{innerWidth:w,innerHeight:y}=window;c(2,r=`M${w},${y}\
+H0\
+V0\
+H${w}\
+V${y}\
+Z\
+M${v+u},${A}\
+a${u},${u},0,0,0-${u},${u}\
+V${t+A-u}\
+a${u},${u},0,0,0,${u},${u}\
+H${h+v-u}\
+a${u},${u},0,0,0,${u}-${u}\
+V${A+u}\
+a${u},${u},0,0,0-${u}-${u}\
+Z`)}};return[k,q,r,h=>{h.stopPropagation()},p,()=>k,d,e,f,function(h){l();h.tour.options.useModalOverlay?(m(h),g()):e()},g,function(h){ma[h?"unshift":"push"](()=>{k=h;c(0,k)})}]}var Fb=function(a){var b;if(b=!!a&&"object"===typeof a)b=Object.prototype.toString.call(a),b=!("[object RegExp]"===b||"[object Date]"===b||a.$$typeof===zc);return b},zc="function"===typeof Symbol&&Symbol.for?Symbol.for("react.element"):60103;da.all=function(a,b){if(!Array.isArray(a))throw Error("first argument should be an array");
+return a.reduce(function(c,d){return da(c,d,b)},{})};var Ac=da;class Ra{on(a,b,c,d){void 0===d&&(d=!1);void 0===this.bindings&&(this.bindings={});void 0===this.bindings[a]&&(this.bindings[a]=[]);this.bindings[a].push({handler:b,ctx:c,once:d});return this}once(a,b,c){return this.on(a,b,c,!0)}off(a,b){if(void 0===this.bindings||void 0===this.bindings[a])return this;void 0===b?delete this.bindings[a]:this.bindings[a].forEach((c,d)=>{c.handler===b&&this.bindings[a].splice(d,1)});return this}trigger(a){for(var b=
+arguments.length,c=Array(1<b?b-1:0),d=1;d<b;d++)c[d-1]=arguments[d];void 0!==this.bindings&&this.bindings[a]&&this.bindings[a].forEach((e,f)=>{let {ctx:g,handler:l,once:m}=e;l.apply(g||this,c);m&&this.bindings[a].splice(f,1)});return this}}var ua=["top","bottom","right","left"],fb=ua.reduce(function(a,b){return a.concat([b+"-start",b+"-end"])},[]),eb=[].concat(ua,["auto"]).reduce(function(a,b){return a.concat([b,b+"-start",b+"-end"])},[]),Sb="beforeRead read afterRead beforeMain main afterMain beforeWrite write afterWrite".split(" "),
+L=Math.max,V=Math.min,ia=Math.round,Ib={top:"auto",right:"auto",bottom:"auto",left:"auto"},Da={passive:!0},Jb={left:"right",right:"left",bottom:"top",top:"bottom"},Kb={start:"end",end:"start"},Bb={placement:"bottom",modifiers:[],strategy:"absolute"},Bc=function(a){void 0===a&&(a={});var b=a.defaultModifiers,c=void 0===b?[]:b;a=a.defaultOptions;var d=void 0===a?Bb:a;return function(e,f,g){function l(){k.orderedModifiers.forEach(function(r){var x=r.name,h=r.options;h=void 0===h?{}:h;r=r.effect;"function"===
+typeof r&&(x=r({state:k,name:x,instance:n,options:h}),p.push(x||function(){}))})}function m(){p.forEach(function(r){return r()});p=[]}void 0===g&&(g=d);var k={placement:"bottom",orderedModifiers:[],options:Object.assign({},Bb,d),modifiersData:{},elements:{reference:e,popper:f},attributes:{},styles:{}},p=[],q=!1,n={state:k,setOptions:function(r){r="function"===typeof r?r(k.options):r;m();k.options=Object.assign({},d,k.options,r);k.scrollParents={reference:fa(e)?sa(e):e.contextElement?sa(e.contextElement):
+[],popper:sa(f)};r=Rb(Ub([].concat(c,k.options.modifiers)));k.orderedModifiers=r.filter(function(x){return x.enabled});l();return n.update()},forceUpdate:function(){if(!q){var r=k.elements,x=r.reference;r=r.popper;if(ib(x,r))for(k.rects={reference:Pb(x,ra(r),"fixed"===k.options.strategy),popper:Fa(r)},k.reset=!1,k.placement=k.options.placement,k.orderedModifiers.forEach(function(v){return k.modifiersData[v.name]=Object.assign({},v.data)}),x=0;x<k.orderedModifiers.length;x++)if(!0===k.reset)k.reset=
+!1,x=-1;else{var h=k.orderedModifiers[x];r=h.fn;var t=h.options;t=void 0===t?{}:t;h=h.name;"function"===typeof r&&(k=r({state:k,options:t,name:h,instance:n})||k)}}},update:Tb(function(){return new Promise(function(r){n.forceUpdate();r(k)})}),destroy:function(){m();q=!0}};if(!ib(e,f))return n;n.setOptions(g).then(function(r){if(!q&&g.onFirstUpdate)g.onFirstUpdate(r)});return n}}({defaultModifiers:[{name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(a){var b=a.state,c=a.instance;
+a=a.options;var d=a.scroll,e=void 0===d?!0:d;a=a.resize;var f=void 0===a?!0:a,g=K(b.elements.popper),l=[].concat(b.scrollParents.reference,b.scrollParents.popper);e&&l.forEach(function(m){m.addEventListener("scroll",c.update,Da)});f&&g.addEventListener("resize",c.update,Da);return function(){e&&l.forEach(function(m){m.removeEventListener("scroll",c.update,Da)});f&&g.removeEventListener("resize",c.update,Da)}},data:{}},{name:"popperOffsets",enabled:!0,phase:"read",fn:function(a){var b=a.state;b.modifiersData[a.name]=
+db({reference:b.rects.reference,element:b.rects.popper,strategy:"absolute",placement:b.placement})},data:{}},{name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(a){var b=a.state,c=a.options;a=c.gpuAcceleration;a=void 0===a?!0:a;var d=c.adaptive;d=void 0===d?!0:d;c=c.roundOffsets;c=void 0===c?!0:c;a={placement:N(b.placement),variation:ja(b.placement),popper:b.elements.popper,popperRect:b.rects.popper,gpuAcceleration:a,isFixed:"fixed"===b.options.strategy};null!=b.modifiersData.popperOffsets&&
+(b.styles.popper=Object.assign({},b.styles.popper,$a(Object.assign({},a,{offsets:b.modifiersData.popperOffsets,position:b.options.strategy,adaptive:d,roundOffsets:c}))));null!=b.modifiersData.arrow&&(b.styles.arrow=Object.assign({},b.styles.arrow,$a(Object.assign({},a,{offsets:b.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:c}))));b.attributes.popper=Object.assign({},b.attributes.popper,{"data-popper-placement":b.placement})},data:{}},{name:"applyStyles",enabled:!0,phase:"write",
+fn:function(a){var b=a.state;Object.keys(b.elements).forEach(function(c){var d=b.styles[c]||{},e=b.attributes[c]||{},f=b.elements[c];F(f)&&M(f)&&(Object.assign(f.style,d),Object.keys(e).forEach(function(g){var l=e[g];!1===l?f.removeAttribute(g):f.setAttribute(g,!0===l?"":l)}))})},effect:function(a){var b=a.state,c={popper:{position:b.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};Object.assign(b.elements.popper.style,c.popper);b.styles=c;b.elements.arrow&&
+Object.assign(b.elements.arrow.style,c.arrow);return function(){Object.keys(b.elements).forEach(function(d){var e=b.elements[d],f=b.attributes[d]||{};d=Object.keys(b.styles.hasOwnProperty(d)?b.styles[d]:c[d]).reduce(function(g,l){g[l]="";return g},{});F(e)&&M(e)&&(Object.assign(e.style,d),Object.keys(f).forEach(function(g){e.removeAttribute(g)}))})}},requires:["computeStyles"]},{name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(a){var b=a.state,c=a.name;a=a.options.offset;
+var d=void 0===a?[0,0]:a;a=eb.reduce(function(g,l){var m=b.rects;var k=N(l);var p=0<=["left","top"].indexOf(k)?-1:1,q="function"===typeof d?d(Object.assign({},m,{placement:l})):d;m=q[0];q=q[1];m=m||0;q=(q||0)*p;k=0<=["left","right"].indexOf(k)?{x:q,y:m}:{x:m,y:q};g[l]=k;return g},{});var e=a[b.placement],f=e.x;e=e.y;null!=b.modifiersData.popperOffsets&&(b.modifiersData.popperOffsets.x+=f,b.modifiersData.popperOffsets.y+=e);b.modifiersData[c]=a}},{name:"flip",enabled:!0,phase:"main",fn:function(a){var b=
+a.state,c=a.options;a=a.name;if(!b.modifiersData[a]._skip){var d=c.mainAxis;d=void 0===d?!0:d;var e=c.altAxis;e=void 0===e?!0:e;var f=c.fallbackPlacements,g=c.padding,l=c.boundary,m=c.rootBoundary,k=c.altBoundary,p=c.flipVariations,q=void 0===p?!0:p,n=c.allowedAutoPlacements;c=b.options.placement;p=N(c);f=f||(p!==c&&q?Ob(c):[xa(c)]);var r=[c].concat(f).reduce(function(E,I){return E.concat("auto"===N(I)?Nb(b,{placement:I,boundary:l,rootBoundary:m,padding:g,flipVariations:q,allowedAutoPlacements:n}):
+I)},[]);c=b.rects.reference;f=b.rects.popper;var x=new Map;p=!0;for(var h=r[0],t=0;t<r.length;t++){var v=r[t],A=N(v),u="start"===ja(v),w=0<=["top","bottom"].indexOf(A),y=w?"width":"height",Y=ta(b,{placement:v,boundary:l,rootBoundary:m,altBoundary:k,padding:g});u=w?u?"right":"left":u?"bottom":"top";c[y]>f[y]&&(u=xa(u));y=xa(u);w=[];d&&w.push(0>=Y[A]);e&&w.push(0>=Y[u],0>=Y[y]);if(w.every(function(E){return E})){h=v;p=!1;break}x.set(v,w)}if(p)for(d=function(E){var I=r.find(function(D){if(D=x.get(D))return D.slice(0,
+E).every(function(na){return na})});if(I)return h=I,"break"},e=q?3:1;0<e&&"break"!==d(e);e--);b.placement!==h&&(b.modifiersData[a]._skip=!0,b.placement=h,b.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}},{name:"preventOverflow",enabled:!0,phase:"main",fn:function(a){var b=a.state,c=a.options;a=a.name;var d=c.mainAxis,e=void 0===d?!0:d;d=c.altAxis;var f=void 0===d?!1:d;d=c.tether;var g=void 0===d?!0:d;d=c.tetherOffset;var l=void 0===d?0:d,m=ta(b,{boundary:c.boundary,rootBoundary:c.rootBoundary,
+padding:c.padding,altBoundary:c.altBoundary}),k=N(b.placement),p=ja(b.placement),q=!p,n=Ga(k);c="x"===n?"y":"x";d=b.modifiersData.popperOffsets;var r=b.rects.reference,x=b.rects.popper;l="function"===typeof l?l(Object.assign({},b.rects,{placement:b.placement})):l;var h="number"===typeof l?{mainAxis:l,altAxis:l}:Object.assign({mainAxis:0,altAxis:0},l),t=b.modifiersData.offset?b.modifiersData.offset[b.placement]:null;l={x:0,y:0};if(d){if(e){var v,A="y"===n?"top":"left",u="y"===n?"bottom":"right",w=
+"y"===n?"height":"width";e=d[n];var y=e+m[A],Y=e-m[u],E=g?-x[w]/2:0,I="start"===p?r[w]:x[w];p="start"===p?-x[w]:-r[w];var D=b.elements.arrow;D=g&&D?Fa(D):{width:0,height:0};var na=b.modifiersData["arrow#persistent"]?b.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0};A=na[A];u=na[u];D=L(0,V(r[w],D[w]));I=q?r[w]/2-E-D-A-h.mainAxis:I-D-A-h.mainAxis;q=q?-r[w]/2+E+D+u+h.mainAxis:p+D+u+h.mainAxis;w=(w=b.elements.arrow&&ra(b.elements.arrow))?"y"===n?w.clientTop||0:w.clientLeft||
+0:0;E=null!=(v=null==t?void 0:t[n])?v:0;v=e+q-E;y=g?V(y,e+I-E-w):y;v=g?L(Y,v):Y;v=L(y,V(e,v));d[n]=v;l[n]=v-e}if(f){var J;f=d[c];e="y"===c?"height":"width";v=f+m["x"===n?"top":"left"];m=f-m["x"===n?"bottom":"right"];k=-1!==["top","left"].indexOf(k);n=null!=(J=null==t?void 0:t[c])?J:0;J=k?v:f-r[e]-x[e]-n+h.altAxis;r=k?f+r[e]+x[e]-n-h.altAxis:m;g&&k?(J=L(J,V(f,r)),J=J>r?r:J):J=L(g?J:v,V(f,g?r:m));d[c]=J;l[c]=J-f}b.modifiersData[a]=l}},requiresIfExists:["offset"]},{name:"arrow",enabled:!0,phase:"main",
+fn:function(a){var b,c=a.state,d=a.name,e=a.options,f=c.elements.arrow,g=c.modifiersData.popperOffsets,l=N(c.placement);a=Ga(l);l=0<=["left","right"].indexOf(l)?"height":"width";if(f&&g){e=e.padding;e="function"===typeof e?e(Object.assign({},c.rects,{placement:c.placement})):e;e=Ya("number"!==typeof e?e:Za(e,ua));var m=Fa(f),k="y"===a?"top":"left",p="y"===a?"bottom":"right",q=c.rects.reference[l]+c.rects.reference[a]-g[a]-c.rects.popper[l];g=g[a]-c.rects.reference[a];f=(f=ra(f))?"y"===a?f.clientHeight||
+0:f.clientWidth||0:0;g=f/2-m[l]/2+(q/2-g/2);l=L(e[k],V(g,f-m[l]-e[p]));c.modifiersData[d]=(b={},b[a]=l,b.centerOffset=l-g,b)}},effect:function(a){var b=a.state;a=a.options.element;a=void 0===a?"[data-popper-arrow]":a;if(null!=a){if("string"===typeof a&&(a=b.elements.popper.querySelector(a),!a))return;Wa(b.elements.popper,a)&&(b.elements.arrow=a)}},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]},{name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(a){var b=
+a.state;a=a.name;var c=b.rects.reference,d=b.rects.popper,e=b.modifiersData.preventOverflow,f=ta(b,{elementContext:"reference"}),g=ta(b,{altBoundary:!0});c=gb(f,c);d=gb(g,d,e);e=hb(c);g=hb(d);b.modifiersData[a]={referenceClippingOffsets:c,popperEscapeOffsets:d,isReferenceHidden:e,hasPopperEscaped:g};b.attributes.popper=Object.assign({},b.attributes.popper,{"data-popper-reference-hidden":e,"data-popper-escaped":g})}}]});let R,va=[],ma=[],Aa=[],pb=[],Zb=Promise.resolve(),Qa=!1,Pa=new Set,Ba=0,Ca=new Set,
+aa;class T{$destroy(){X(this,1);this.$destroy=G}$on(a,b){let c=this.$$.callbacks[a]||(this.$$.callbacks[a]=[]);c.push(b);return()=>{let d=c.indexOf(b);-1!==d&&c.splice(d,1)}}$set(a){this.$$set&&0!==Object.keys(a).length&&(this.$$.skip_bound=!0,this.$$set(a),this.$$.skip_bound=!1)}}class bc extends T{constructor(a){super();S(this,a,ac,$b,Q,{config:6,step:7})}}class qc extends T{constructor(a){super();S(this,a,dc,cc,Q,{step:0})}}class jc extends T{constructor(a){super();S(this,a,fc,ec,Q,{cancelIcon:0,
+step:2})}}class ic extends T{constructor(a){super();S(this,a,hc,gc,Q,{labelId:1,element:0,title:2})}}class oc extends T{constructor(a){super();S(this,a,lc,kc,Q,{labelId:0,step:1})}}class pc extends T{constructor(a){super();S(this,a,nc,mc,Q,{descriptionId:1,element:0,step:2})}}class uc extends T{constructor(a){super();S(this,a,sc,rc,Q,{descriptionId:0,labelId:1,step:2})}}class Cc extends T{constructor(a){super();S(this,a,vc,tc,Q,{classPrefix:11,element:0,descriptionId:2,firstFocusableElement:8,focusableElements:9,
+labelId:3,lastFocusableElement:10,step:4,dataStepId:1,getElement:12})}get getElement(){return this.$$.ctx[12]}}var Cb=function(a,b){return b={exports:{}},a(b,b.exports),b.exports}(function(a,b){(function(){a.exports={polyfill:function(){function c(h,t){this.scrollLeft=h;this.scrollTop=t}function d(h){if(null===h||"object"!==typeof h||void 0===h.behavior||"auto"===h.behavior||"instant"===h.behavior)return!0;if("object"===typeof h&&"smooth"===h.behavior)return!1;throw new TypeError("behavior member of ScrollOptions "+
+h.behavior+" is not a valid value for enumeration ScrollBehavior.");}function e(h,t){if("Y"===t)return h.clientHeight+x<h.scrollHeight;if("X"===t)return h.clientWidth+x<h.scrollWidth}function f(h,t){h=k.getComputedStyle(h,null)["overflow"+t];return"auto"===h||"scroll"===h}function g(h){var t=e(h,"Y")&&f(h,"Y");h=e(h,"X")&&f(h,"X");return t||h}function l(h){var t=(r()-h.startTime)/468;var v=.5*(1-Math.cos(Math.PI*(1<t?1:t)));t=h.startX+(h.x-h.startX)*v;v=h.startY+(h.y-h.startY)*v;h.method.call(h.scrollable,
+t,v);t===h.x&&v===h.y||k.requestAnimationFrame(l.bind(k,h))}function m(h,t,v){var A=r();if(h===p.body){var u=k;var w=k.scrollX||k.pageXOffset;h=k.scrollY||k.pageYOffset;var y=n.scroll}else u=h,w=h.scrollLeft,h=h.scrollTop,y=c;l({scrollable:u,method:y,startTime:A,startX:w,startY:h,x:t,y:v})}var k=window,p=document;if(!("scrollBehavior"in p.documentElement.style&&!0!==k.__forceSmoothScrollPolyfill__)){var q=k.HTMLElement||k.Element,n={scroll:k.scroll||k.scrollTo,scrollBy:k.scrollBy,elementScroll:q.prototype.scroll||
+c,scrollIntoView:q.prototype.scrollIntoView},r=k.performance&&k.performance.now?k.performance.now.bind(k.performance):Date.now,x=/MSIE |Trident\/|Edge\//.test(k.navigator.userAgent)?1:0;k.scroll=k.scrollTo=function(h,t){void 0!==h&&(!0===d(h)?n.scroll.call(k,void 0!==h.left?h.left:"object"!==typeof h?h:k.scrollX||k.pageXOffset,void 0!==h.top?h.top:void 0!==t?t:k.scrollY||k.pageYOffset):m.call(k,p.body,void 0!==h.left?~~h.left:k.scrollX||k.pageXOffset,void 0!==h.top?~~h.top:k.scrollY||k.pageYOffset))};
+k.scrollBy=function(h,t){void 0!==h&&(d(h)?n.scrollBy.call(k,void 0!==h.left?h.left:"object"!==typeof h?h:0,void 0!==h.top?h.top:void 0!==t?t:0):m.call(k,p.body,~~h.left+(k.scrollX||k.pageXOffset),~~h.top+(k.scrollY||k.pageYOffset)))};q.prototype.scroll=q.prototype.scrollTo=function(h,t){if(void 0!==h)if(!0===d(h)){if("number"===typeof h&&void 0===t)throw new SyntaxError("Value could not be converted");n.elementScroll.call(this,void 0!==h.left?~~h.left:"object"!==typeof h?~~h:this.scrollLeft,void 0!==
+h.top?~~h.top:void 0!==t?~~t:this.scrollTop)}else t=h.left,h=h.top,m.call(this,this,"undefined"===typeof t?this.scrollLeft:~~t,"undefined"===typeof h?this.scrollTop:~~h)};q.prototype.scrollBy=function(h,t){void 0!==h&&(!0===d(h)?n.elementScroll.call(this,void 0!==h.left?~~h.left+this.scrollLeft:~~h+this.scrollLeft,void 0!==h.top?~~h.top+this.scrollTop:~~t+this.scrollTop):this.scroll({left:~~h.left+this.scrollLeft,top:~~h.top+this.scrollTop,behavior:h.behavior}))};q.prototype.scrollIntoView=function(h){if(!0===
+d(h))n.scrollIntoView.call(this,void 0===h?!0:h);else{for(h=this;h!==p.body&&!1===g(h);)h=h.parentNode||h.host;var t=h.getBoundingClientRect(),v=this.getBoundingClientRect();h!==p.body?(m.call(this,h,h.scrollLeft+v.left-t.left,h.scrollTop+v.top-t.top),"fixed"!==k.getComputedStyle(h).position&&k.scrollBy({left:t.left,top:t.top,behavior:"smooth"})):k.scrollBy({left:v.left,top:v.top,behavior:"smooth"})}}}}}})()});Cb.polyfill;Cb.polyfill();class Sa extends Ra{constructor(a,b){void 0===b&&(b={});super(a,
+b);this.tour=a;this.classPrefix=this.tour.options?jb(this.tour.options.classPrefix):"";this.styles=a.styles;Va(this);this._setOptions(b);return this}cancel(){this.tour.cancel();this.trigger("cancel")}complete(){this.tour.complete();this.trigger("complete")}destroy(){this.tooltip&&(this.tooltip.destroy(),this.tooltip=null);this.el instanceof HTMLElement&&this.el.parentNode&&(this.el.parentNode.removeChild(this.el),this.el=null);this._updateStepTargetOnHide();this.trigger("destroy")}getTour(){return this.tour}hide(){this.tour.modal.hide();
+this.trigger("before-hide");this.el&&(this.el.hidden=!0);this._updateStepTargetOnHide();this.trigger("hide")}isCentered(){let a=Ma(this);return!a.element||!a.on}isOpen(){return!(!this.el||this.el.hidden)}show(){if(ea(this.options.beforeShowPromise)){let a=this.options.beforeShowPromise();if(void 0!==a)return a.then(()=>this._show())}this._show()}updateStepOptions(a){Object.assign(this.options,a);this.shepherdElementComponent&&this.shepherdElementComponent.$set({step:this})}getElement(){return this.el}getTarget(){return this.target}_createTooltipContent(){this.shepherdElementComponent=
+new Cc({target:this.tour.options.stepsContainer||document.body,props:{classPrefix:this.classPrefix,descriptionId:`${this.id}-description`,labelId:`${this.id}-label`,step:this,styles:this.styles}});return this.shepherdElementComponent.getElement()}_scrollTo(a){let {element:b}=Ma(this);ea(this.options.scrollToHandler)?this.options.scrollToHandler(b):b instanceof Element&&"function"===typeof b.scrollIntoView&&b.scrollIntoView(a)}_getClassOptions(a){var b=this.tour&&this.tour.options&&this.tour.options.defaultStepOptions;
+b=b&&b.classes?b.classes:"";a=[...(a.classes?a.classes:"").split(" "),...b.split(" ")];a=new Set(a);return Array.from(a).join(" ").trim()}_setOptions(a){void 0===a&&(a={});let b=this.tour&&this.tour.options&&this.tour.options.defaultStepOptions;b=Ac({},b||{});this.options=Object.assign({arrow:!0},b,a);let {when:c}=this.options;this.options.classes=this._getClassOptions(a);this.destroy();this.id=this.options.id||`step-${Na()}`;c&&Object.keys(c).forEach(d=>{this.on(d,c[d],this)})}_setupElements(){void 0!==
+this.el&&this.destroy();this.el=this._createTooltipContent();this.options.advanceOn&&Hb(this);this.tooltip&&this.tooltip.destroy();let a=Ma(this),b=a.element,c=Xb(a,this);this.isCentered()&&(b=document.body,this.shepherdElementComponent.getElement().classList.add("shepherd-centered"));this.tooltip=Bc(b,this.el,c);this.target=a.element}_show(){this.trigger("before-show");this._setupElements();this.tour.modal||this.tour._setupModal();this.tour.modal.setupForStep(this);this._styleTargetElementForStep(this);
+this.el.hidden=!1;this.options.scrollTo&&setTimeout(()=>{this._scrollTo(this.options.scrollTo)});this.el.hidden=!1;let a=this.shepherdElementComponent.getElement(),b=this.target||document.body;b.classList.add(`${this.classPrefix}shepherd-enabled`);b.classList.add(`${this.classPrefix}shepherd-target`);a.classList.add("shepherd-enabled");this.trigger("show")}_styleTargetElementForStep(a){let b=a.target;b&&(a.options.highlightClass&&b.classList.add(a.options.highlightClass),b.classList.remove("shepherd-target-click-disabled"),
+!1===a.options.canClickTarget&&b.classList.add("shepherd-target-click-disabled"))}_updateStepTargetOnHide(){let a=this.target||document.body;this.options.highlightClass&&a.classList.remove(this.options.highlightClass);a.classList.remove("shepherd-target-click-disabled",`${this.classPrefix}shepherd-enabled`,`${this.classPrefix}shepherd-target`)}}class Dc extends T{constructor(a){super();S(this,a,yc,xc,Q,{element:0,openingProperties:4,getElement:5,closeModalOpening:6,hide:7,positionModal:8,setupForStep:9,
+show:10})}get getElement(){return this.$$.ctx[5]}get closeModalOpening(){return this.$$.ctx[6]}get hide(){return this.$$.ctx[7]}get positionModal(){return this.$$.ctx[8]}get setupForStep(){return this.$$.ctx[9]}get show(){return this.$$.ctx[10]}}let oa=new Ra;class Ec extends Ra{constructor(a){void 0===a&&(a={});super(a);Va(this);this.options=Object.assign({},{exitOnEsc:!0,keyboardNavigation:!0},a);this.classPrefix=jb(this.options.classPrefix);this.steps=[];this.addSteps(this.options.steps);"active cancel complete inactive show start".split(" ").map(b=>
+{(c=>{this.on(c,d=>{d=d||{};d.tour=this;oa.trigger(c,d)})})(b)});this._setTourID();return this}addStep(a,b){a instanceof Sa?a.tour=this:a=new Sa(this,a);void 0!==b?this.steps.splice(b,0,a):this.steps.push(a);return a}addSteps(a){Array.isArray(a)&&a.forEach(b=>{this.addStep(b)});return this}back(){let a=this.steps.indexOf(this.currentStep);this.show(a-1,!1)}cancel(){this.options.confirmCancel?window.confirm(this.options.confirmCancelMessage||"Are you sure you want to stop the tour?")&&this._done("cancel"):
+this._done("cancel")}complete(){this._done("complete")}getById(a){return this.steps.find(b=>b.id===a)}getCurrentStep(){return this.currentStep}hide(){let a=this.getCurrentStep();if(a)return a.hide()}isActive(){return oa.activeTour===this}next(){let a=this.steps.indexOf(this.currentStep);a===this.steps.length-1?this.complete():this.show(a+1,!0)}removeStep(a){let b=this.getCurrentStep();this.steps.some((c,d)=>{if(c.id===a)return c.isOpen()&&c.hide(),c.destroy(),this.steps.splice(d,1),!0});b&&b.id===
+a&&(this.currentStep=void 0,this.steps.length?this.show(0):this.cancel())}show(a,b){void 0===a&&(a=0);void 0===b&&(b=!0);if(a=qa(a)?this.getById(a):this.steps[a])this._updateStateBeforeShow(),ea(a.options.showOn)&&!a.options.showOn()?this._skipStep(a,b):(this.trigger("show",{step:a,previous:this.currentStep}),this.currentStep=a,a.show())}start(){this.trigger("start");this.focusedElBeforeOpen=document.activeElement;this.currentStep=null;this._setupModal();this._setupActiveTour();this.next()}_done(a){let b=
+this.steps.indexOf(this.currentStep);Array.isArray(this.steps)&&this.steps.forEach(c=>c.destroy());wc(this);this.trigger(a,{index:b});oa.activeTour=null;this.trigger("inactive",{tour:this});this.modal&&this.modal.hide();"cancel"!==a&&"complete"!==a||!this.modal||(a=document.querySelector(".shepherd-modal-overlay-container"))&&a.remove();this.focusedElBeforeOpen instanceof HTMLElement&&this.focusedElBeforeOpen.focus()}_setupActiveTour(){this.trigger("active",{tour:this});oa.activeTour=this}_setupModal(){this.modal=
+new Dc({target:this.options.modalContainer||document.body,props:{classPrefix:this.classPrefix,styles:this.styles}})}_skipStep(a,b){a=this.steps.indexOf(a);a=b?a+1:a-1;a===this.steps.length-1?this.complete():this.show(a,b)}_updateStateBeforeShow(){this.currentStep&&this.currentStep.hide();this.isActive()||this._setupActiveTour()}_setTourID(){this.id=`${this.options.tourName||"tour"}--${Na()}`}}Object.assign(oa,{Tour:Ec,Step:Sa});return oa})

+ 4 - 6
resources/package.json

@@ -1,6 +1,6 @@
 {
   "name": "Logseq",
-  "version": "0.6.7",
+  "version": "0.7.0",
   "main": "electron.js",
   "author": "Logseq",
   "license": "AGPL-3.0",
@@ -36,8 +36,8 @@
     "https-proxy-agent": "5.0.0",
     "@sentry/electron": "2.5.1",
     "posthog-js": "1.10.2",
-    "@andelf/rsapi": "0.0.7",
-    "electron-deeplink": "1.0.9"
+    "@logseq/rsapi": "0.0.16",
+    "electron-deeplink": "1.0.10"
   },
   "devDependencies": {
     "@electron-forge/cli": "^6.0.0-beta.57",
@@ -52,8 +52,6 @@
     "electron-rebuild": "3.2.5"
   },
   "resolutions": {
-    "@electron-forge/cli/**/colors": "1.4.0",
-    "electron-rebuild/**/colors": "1.4.0",
-    "electron-builder/**/colors": "1.4.0"
+    "**/electron": "15.1.2"
   }
 }

+ 17 - 0
scripts/src/logseq/tasks/dev.clj

@@ -24,3 +24,20 @@
                (shell "yarn dev-electron-app")
                (println "Waiting for app to build..."))
              (Thread/sleep 1000))))
+
+
+(defn lint
+  "Run all lint tasks
+  - clj-kondo lint
+  - carve lint for unused vars
+  - lint for vars that are too large
+  - lint invalid translation entries
+  - Lint datalog rules"
+  []
+  (doseq [cmd ["clojure -M:clj-kondo --parallel --lint src"
+               "scripts/carve.clj"
+               "scripts/large_vars.clj"
+               "bb lang:invalid-translations"
+               "scripts/lint_rules.clj"]]
+    (println cmd)
+    (shell cmd)))

+ 30 - 0
scripts/src/logseq/tasks/nbb.clj

@@ -0,0 +1,30 @@
+(ns logseq.tasks.nbb
+  (:require [pod.borkdude.clj-kondo :as clj-kondo]
+            [babashka.tasks :refer [shell]]))
+
+(defn- fetch-meta-namespaces
+  "Return namespaces with metadata"
+  [paths]
+  (let [paths (or (seq paths) ["src"])
+        {{:keys [namespace-definitions]} :analysis}
+        (clj-kondo/run!
+         {:lint paths
+          :config {:output {:analysis {:namespace-definitions {:meta true}}}}})
+        matches (keep (fn [m]
+                        (when (:meta m)
+                          {:ns   (:name m)
+                           :meta (:meta m)}))
+                      namespace-definitions)]
+    matches))
+
+(defn load-compatible-namespaces
+  "Check nbb-compatible namespaces can be required by nbb-logseq"
+  []
+  (let [namespaces (map :ns
+                        (filter #(get-in % [:meta :nbb-compatible])
+                                (fetch-meta-namespaces ["src/main"])))]
+    (assert (seq namespaces) "There must be some nbb namespaces to check")
+    (doseq [n namespaces]
+      (println "Requiring" n "...")
+      (shell "yarn nbb-logseq -cp src/main -e" (format "(require '[%s])" n)))
+    (println "Success!")))

+ 12 - 9
shadow-cljs.edn

@@ -12,12 +12,12 @@
         :js-options    {:ignore-asset-requires true} ;; handle `require(xxx.css)`
         :modules       {:main
                         {:init-fn    frontend.core/init}
-                        ;; :graph
-                        ;; {:entries [frontend.extensions.graph.force]
-                        ;;  :depends-on #{:main}}
                         :code-editor
                         {:entries    [frontend.extensions.code]
                          :depends-on #{:main}}
+                        :age-encryption
+                        {:entries    [frontend.extensions.age-encryption]
+                         :depends-on #{:main}}
                         :excalidraw
                         {:entries    [frontend.extensions.excalidraw]
                          :depends-on #{:main}}}
@@ -29,7 +29,8 @@
                            :source-map         true
                            :externs            ["datascript/externs.js"
                                                 "externs.js"]
-                           :warnings           {:fn-deprecated false}}
+                           :warnings           {:fn-deprecated false
+                                                :redef false}}
         :closure-defines  {goog.debug.LOGGING_ENABLED      true}
 
         ;; NOTE: electron, browser/mobile-app use different asset-paths.
@@ -54,7 +55,8 @@
 
                                 :externs  ["datascript/externs.js"
                                            "externs.js"]
-                                :warnings {:fn-deprecated false}}}
+                                :warnings {:fn-deprecated false
+                                           :redef false}}}
 
   :test {:target          :node-test
          :output-to       "static/tests.js"
@@ -69,12 +71,12 @@
                :js-options    {:ignore-asset-requires true}
                :modules       {:main
                                {:init-fn    frontend.publishing/init}
-                               ;; :graph
-                               ;; {:entries [frontend.extensions.graph.force]
-                               ;;  :depends-on #{:main}}
                                :code-editor
                                {:entries    [frontend.extensions.code]
                                 :depends-on #{:main}}
+                               :age-encryption
+                               {:entries    [frontend.extensions.age-encryption]
+                                :depends-on #{:main}}
                                :excalidraw
                                {:entries    [frontend.extensions.excalidraw]
                                 :depends-on #{:main}}}
@@ -86,7 +88,8 @@
                                   :output-feature-set :es-next
                                   :externs            ["datascript/externs.js"
                                                        "externs.js"]
-                                  :warnings           {:fn-deprecated false}}
+                                  :warnings           {:fn-deprecated false
+                                                       :redef false}}
                :devtools         {:before-load frontend.core/stop
                                   :after-load  frontend.core/start
                                   :preloads    [devtools.preload]}}

+ 51 - 0
src/electron/electron/backup_file.cljs

@@ -0,0 +1,51 @@
+(ns electron.backup-file
+  (:require [clojure.string :as string]
+            ["path" :as path]
+            ["fs" :as fs]
+            ["fs-extra" :as fs-extra]))
+
+(def backup-dir "logseq/bak")
+(def version-file-dir "version-files/local")
+
+(defn- get-backup-dir*
+  [repo relative-path bak-dir]
+  (let [relative-path* (string/replace relative-path repo "")
+        bak-dir (path/join repo bak-dir)
+        path (path/join bak-dir relative-path*)
+        parsed-path (path/parse path)]
+    (path/join (.-dir parsed-path)
+               (.-name parsed-path))))
+
+(defn get-backup-dir
+  [repo relative-path]
+  (get-backup-dir* repo relative-path backup-dir))
+
+(defn get-version-file-dir
+  [repo relative-path]
+  (get-backup-dir* repo relative-path version-file-dir))
+
+(defn- truncate-old-versioned-files!
+  "reserve the latest 3 version files"
+  [dir]
+  (let [files (fs/readdirSync dir (clj->js {:withFileTypes true}))
+        files (mapv #(.-name %) files)
+        old-versioned-files (drop 3 (reverse (sort files)))]
+    (doseq [file old-versioned-files]
+      (fs-extra/removeSync (path/join dir file)))))
+
+(defn backup-file
+  "backup CONTENT under DIR :backup-dir or :version-file-dir
+  :backup-dir = `backup-dir`
+  :version-file-dir = `version-file-dir`"
+  [repo dir relative-path ext content]
+  {:pre [(contains? #{:backup-dir :version-file-dir} dir)]}
+  (let [dir* (case dir
+               :backup-dir (get-backup-dir repo relative-path)
+               :version-file-dir (get-version-file-dir repo relative-path))
+        new-path (path/join dir*
+                            (str (string/replace (.toISOString (js/Date.)) ":" "_")
+                                 ext))]
+    (fs-extra/ensureDirSync dir*)
+    (fs/writeFileSync new-path content)
+    (fs/statSync new-path)
+    (truncate-old-versioned-files! dir*)))

+ 18 - 17
src/electron/electron/core.cljs

@@ -81,7 +81,7 @@
      (.unregisterProtocol protocol FILE_LSP_SCHEME)
      (.unregisterProtocol protocol "assets")))
 
-(defn- handle-export-publish-assets [_event html custom-css-path repo-path asset-filenames output-path]
+(defn- handle-export-publish-assets [_event html custom-css-path export-css-path repo-path asset-filenames output-path]
   (p/let [app-path (. app getAppPath)
           asset-filenames (js->clj asset-filenames)
           root-dir (or output-path (handler/open-dir-dialog))]
@@ -89,7 +89,8 @@
       (let [static-dir (path/join root-dir "static")
             assets-from-dir (path/join repo-path "assets")
             assets-to-dir (path/join root-dir "assets")
-            index-html-path (path/join root-dir "index.html")]
+            index-html-path (path/join root-dir "index.html")
+            export-or-custom-css-path (if (fs/existsSync export-css-path) export-css-path custom-css-path)]
         (p/let [_ (. fs ensureDir static-dir)
                 _ (. fs ensureDir assets-to-dir)
                 _ (p/all (concat
@@ -99,35 +100,35 @@
                            (. fs copy (path/join app-path "404.html") (path/join root-dir "404.html"))]
 
                           (map
-                            (fn [filename]
-                              (-> (. fs copy (path/join assets-from-dir filename) (path/join assets-to-dir filename))
-                                  (p/catch
-                                      (fn [e]
-                                        (println (str "Failed to copy " (path/join assets-from-dir filename) " to " (path/join assets-to-dir filename)))
-                                        (js/console.error e)))))
-                            asset-filenames)
+                           (fn [filename]
+                             (-> (. fs copy (path/join assets-from-dir filename) (path/join assets-to-dir filename))
+                                 (p/catch
+                                  (fn [e]
+                                    (println (str "Failed to copy " (path/join assets-from-dir filename) " to " (path/join assets-to-dir filename)))
+                                    (js/console.error e)))))
+                           asset-filenames)
 
                           (map
-                            (fn [part]
-                              (. fs copy (path/join app-path part) (path/join static-dir part)))
-                            ["css" "fonts" "icons" "img" "js"])))
-                custom-css (. fs readFile custom-css-path)
-                _ (. fs writeFile (path/join static-dir "css" "custom.css") custom-css)
+                           (fn [part]
+                             (. fs copy (path/join app-path part) (path/join static-dir part)))
+                           ["css" "fonts" "icons" "img" "js"])))
+                export-css (. fs readFile export-or-custom-css-path)
+                _ (. fs writeFile (path/join static-dir "css" "export.css") export-css)
                 js-files ["main.js" "code-editor.js" "excalidraw.js"]
                 _ (p/all (map (fn [file]
                                 (. fs removeSync (path/join static-dir "js" file)))
-                           js-files))
+                              js-files))
                 _ (p/all (map (fn [file]
                                 (. fs moveSync
                                    (path/join static-dir "js" "publishing" file)
                                    (path/join static-dir "js" file)))
-                           js-files))
+                              js-files))
                 _ (. fs removeSync (path/join static-dir "js" "publishing"))
                 ;; remove source map files
                 ;; TODO: ugly, replace with ls-files and filter with ".map"
                 _ (p/all (map (fn [file]
                                 (. fs removeSync (path/join static-dir "js" (str file ".map"))))
-                           ["main.js" "code-editor.js" "excalidraw.js"]))]
+                              ["main.js" "code-editor.js" "excalidraw.js" "age-encryption.js"]))]
           (. dialog showMessageBox (clj->js {:message (str "Export public pages and publish assets to " root-dir " successfully")})))))))
 
 (defn setup-app-manager!

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

@@ -1,5 +1,5 @@
 (ns electron.file-sync-rsapi
-  (:require ["@andelf/rsapi" :as rsapi]))
+  (:require ["@logseq/rsapi" :as rsapi]))
 
 (defn set-env [env] (rsapi/setEnv env))
 

+ 11 - 9
src/electron/electron/fs_watcher.cljs

@@ -4,7 +4,6 @@
             ["chokidar" :as watcher]
             [electron.utils :as utils]
             ["electron" :refer [app]]
-            [frontend.util.fs :as util-fs]
             [electron.window :as window]))
 
 ;; TODO: explore different solutions for different platforms
@@ -30,10 +29,15 @@
 
 (defn- publish-file-event!
   [dir path event]
-  (send-file-watcher! dir event {:dir (utils/fix-win-path! dir)
-                                 :path (utils/fix-win-path! path)
-                                 :content (utils/read-file path)
-                                 :stat (fs/statSync path)}))
+  (let [content (when (and (not= event "unlink")
+                           (utils/should-read-content? path))
+                  (utils/read-file path))
+        stat (when (not= event "unlink")
+               (fs/statSync path))]
+    (send-file-watcher! dir event {:dir (utils/fix-win-path! dir)
+                                   :path (utils/fix-win-path! path)
+                                   :content content
+                                   :stat stat})))
 
 (defn watch-dir!
   "Watch a directory if no such file watcher exists"
@@ -43,7 +47,7 @@
     (let [watcher (.watch watcher dir
                           (clj->js
                            {:ignored (fn [path]
-                                       (util-fs/ignored-path? dir path))
+                                       (utils/ignored-path? dir path))
                             :ignoreInitial false
                             :ignorePermissionErrors true
                             :interval polling-interval
@@ -63,9 +67,7 @@
              (publish-file-event! dir path "change")))
       (.on watcher "unlink"
            (fn [path]
-             (send-file-watcher! dir "unlink"
-                                 {:dir (utils/fix-win-path! dir)
-                                  :path (utils/fix-win-path! path)})))
+             (publish-file-event! dir path "unlink")))
       (.on watcher "error"
            (fn [path]
              (println "Watch error happened: "

+ 12 - 34
src/electron/electron/handler.cljs

@@ -19,7 +19,8 @@
             [electron.git :as git]
             [electron.plugin :as plugin]
             [electron.window :as win]
-            [electron.file-sync-rsapi :as rsapi]))
+            [electron.file-sync-rsapi :as rsapi]
+            [electron.backup-file :as backup-file]))
 
 (defmulti handle (fn [_window args] (keyword (first args))))
 
@@ -65,45 +66,18 @@
   (let [result (.diff_main Diff old new)]
     (some (fn [a] (= -1 (first a))) result)))
 
-(defn- truncate-old-versioned-files!
-  [dir]
-  (let [files (fs/readdirSync dir (clj->js {:withFileTypes true}))
-        files (map #(.-name %) files)
-        old-versioned-files (drop 3 (reverse (sort files)))]
-    (doseq [file old-versioned-files]
-      (fs-extra/removeSync (path/join dir file)))))
-
-(defn- get-backup-dir
-  [repo path]
-  (let [path (string/replace path repo "")
-        bak-dir (str repo "/logseq/bak")
-        path (str bak-dir path)
-        parsed-path (path/parse path)]
-    (path/join (.-dir parsed-path)
-               (.-name parsed-path))))
-
-(defn backup-file
-  [repo path content]
-  (let [path-dir (get-backup-dir repo path)
-        ext (path/extname path)
-        new-path (path/join path-dir
-                            (str (string/replace (.toISOString (js/Date.)) ":" "_")
-                                 ext))]
-    (fs-extra/ensureDirSync path-dir)
-    (fs/writeFileSync new-path content)
-    (fs/statSync new-path)
-    (truncate-old-versioned-files! path-dir)
-    new-path))
-
 (defmethod handle :backupDbFile [_window [_ repo path db-content new-content]]
   (when (and (string? db-content)
              (string? new-content)
              (string-some-deleted? db-content new-content))
-    (backup-file repo path db-content)))
+    (backup-file/backup-file repo :backup-dir path (path/extname path) db-content)))
+
+(defmethod handle :addVersionFile [_window [_ repo path content]]
+  (backup-file/backup-file repo :version-file-dir path (path/extname path) content))
 
 (defmethod handle :openFileBackupDir [_window [_ repo path]]
   (when (string? path)
-    (let [dir (get-backup-dir repo path)]
+    (let [dir (backup-file/get-backup-dir repo path)]
       (.openPath shell dir))))
 
 (defmethod handle :readFile [_window [_ path]]
@@ -129,7 +103,7 @@
       (fs/statSync path)
       (catch :default e
         (let [backup-path (try
-                            (backup-file repo path content)
+                            (backup-file/backup-file repo :backup-dir path (path/extname path) content)
                             (catch :default e
                               (println "Backup file failed")
                               (js/console.dir e)))]
@@ -314,6 +288,10 @@
 (defmethod handle :getLogseqDotDirRoot []
   (utils/get-ls-dotdir-root))
 
+(defmethod handle :testProxyUrl [win [_ url]]
+  (p/let [_ (utils/fetch url)]
+    (utils/send-to-renderer win :notification {:type "success" :payload (str "Successfully: " url)})))
+
 (defmethod handle :getUserDefaultPlugins []
   (utils/get-ls-default-plugins))
 

+ 14 - 14
src/electron/electron/utils.cljs

@@ -62,27 +62,27 @@
   (when-let [agent (cfgs/get-item :settings/agent)]
     (set-fetch-agent agent)))
 
-;; keep same as ignored-path? in src/main/frontend/util/fs.cljs
-;; TODO: merge them
 (defn ignored-path?
+  "Ignore given path from file-watcher notification"
   [dir path]
   (when (string? path)
     (or
      (some #(string/starts-with? path (str dir "/" %))
-           ["." ".recycle" "assets" "node_modules" "logseq/bak"])
+           ["." ".recycle" "node_modules" "logseq/bak" "version-files"])
      (some #(string/includes? path (str "/" % "/"))
-           ["." ".recycle" "assets" "node_modules" "logseq/bak"])
-     (string/ends-with? path ".DS_Store")
+           ["." ".recycle" "node_modules" "logseq/bak" "version-files"])
+     (some #(string/ends-with? path %)
+           [".DS_Store" "logseq/graphs-txid.edn" "logseq/broken-config.edn"])
      ;; hidden directory or file
      (let [relpath (path/relative dir path)]
        (or (re-find #"/\.[^.]+" relpath)
-           (re-find #"^\.[^.]+" relpath)))
-     (let [path (string/lower-case path)]
-       (and
-        (not (string/blank? (path/extname path)))
-        (not
-         (some #(string/ends-with? path %)
-               [".md" ".markdown" ".org" ".js" ".edn" ".css"])))))))
+           (re-find #"^\.[^.]+" relpath))))))
+
+(defn should-read-content?
+  "Skip reading content of file while using file-watcher"
+  [path]
+  (let [ext (string/lower-case (path/extname path))]
+    (contains? #{".md" ".markdown" ".org" ".js" ".edn" ".css"} ext)))
 
 (defn fix-win-path!
   [path]
@@ -118,7 +118,7 @@
   ([window kind payload]
    (when window
      (.. ^js window -webContents
-         (send kind (bean/->js payload))))))
+         (send (name kind) (bean/->js payload))))))
 
 (defn get-graph-dir
   [graph-name]
@@ -131,4 +131,4 @@
 
 (defn normalize-lc
   [s]
-  (normalize (string/lower-case s)))
+  (normalize (string/lower-case s)))

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

@@ -15,6 +15,8 @@
             [frontend.util.marker :as marker]
             [frontend.util.priority :as priority]
             [frontend.util.property :as property]
+            [logseq.graph-parser.util :as gp-util]
+            [logseq.graph-parser.config :as gp-config]
             [goog.dom :as gdom]
             [goog.object :as gobj]
             [promesa.core :as p]))
@@ -274,25 +276,19 @@
                     [:codemirror/focus]] "Insert a calculator"]
      ["Draw" (fn []
                (let [file (draw/file-name)
-                     path (str config/default-draw-directory "/" file)
+                     path (str gp-config/default-draw-directory "/" file)
                      text (util/format "[[%s]]" path)]
                  (p/let [_ (draw/create-draw-with-default-content path)]
                    (println "draw file created, " path))
                  text)) "Draw a graph with Excalidraw"]
-
-     (when (util/zh-CN-supported?)
-       ["Embed Bilibili video" [[:editor/input "{{bilibili }}" {:last-pattern (state/get-editor-command-trigger)
-                                                                :backward-pos 2}]]])
+     
      ["Embed HTML " (->inline "html")]
 
-     ["Embed Youtube video" [[:editor/input "{{youtube }}" {:last-pattern (state/get-editor-command-trigger)
-                                                            :backward-pos 2}]]]
+     ["Embed Video URL" [[:editor/input "{{video }}" {:last-pattern (state/get-editor-command-trigger)
+                                                    :backward-pos 2}]]]
 
      ["Embed Youtube timestamp" [[:youtube/insert-timestamp]]]
 
-     ["Embed Vimeo video" [[:editor/input "{{vimeo }}" {:last-pattern (state/get-editor-command-trigger)
-                                                        :backward-pos 2}]]]
-
      ["Embed Twitter tweet" [[:editor/input "{{tweet }}" {:last-pattern (state/get-editor-command-trigger)
                                                           :backward-pos 2}]]]]
 
@@ -333,13 +329,13 @@
           current-pos (cursor/pos input)
           current-pos (or
                        (when (and end-pattern (string? end-pattern))
-                         (when-let [i (string/index-of (util/safe-subs edit-content current-pos) end-pattern)]
+                         (when-let [i (string/index-of (gp-util/safe-subs edit-content current-pos) end-pattern)]
                            (+ current-pos i)))
                        current-pos)
           orig-prefix (subs edit-content 0 current-pos)
           space? (when (and last-pattern orig-prefix)
                    (let [s (when-let [last-index (string/last-index-of orig-prefix last-pattern)]
-                             (util/safe-subs orig-prefix 0 last-index))]
+                             (gp-util/safe-subs orig-prefix 0 last-index))]
                      (not
                       (or
                        (and s
@@ -352,7 +348,7 @@
                    space?)
           prefix (cond
                    (and backward-truncate-number (integer? backward-truncate-number))
-                   (str (util/safe-subs orig-prefix 0 (- (count orig-prefix) backward-truncate-number))
+                   (str (gp-util/safe-subs orig-prefix 0 (- (count orig-prefix) backward-truncate-number))
                         (when-not (zero? backward-truncate-number)
                           value))
 

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 413 - 258
src/main/frontend/components/block.cljs


+ 68 - 8
src/main/frontend/components/block.css

@@ -4,7 +4,7 @@
 
   @screen sm {
     width: calc(100% - 33px);
-    overflow-x: auto;
+    overflow-x: visible;
   }
 }
 
@@ -185,12 +185,42 @@
   }
 }
 
-html.is-mobile,
-html.is-native-iphone,
-html.is-native-android {
-  .references .block-control {
-    margin-left: -20px;
-  }
+.block-left-menu {
+    background-color: var(--ls-secondary-background-color);
+    background: linear-gradient(90deg, var(--ls-primary-background-color) 0%, var(--ls-secondary-background-color) 100%);
+    
+    .commands-button {
+        overflow: hidden;
+        max-width: 50px;
+        text-align: center;
+        margin: auto 0;
+
+        .indent {
+            opacity: 30%;
+        }
+    }
+}
+
+.block-right-menu {
+    background-color: var(--ls-secondary-background-color);
+    /* background: linear-gradient(-90deg, var(--ls-primary-background-color) 0%, var(--ls-secondary-background-color) 100%); */
+
+    .commands-button {
+        overflow: hidden;
+        max-width: 80px;
+        text-align: center;
+        margin: auto 0;
+
+        .outdent {
+            margin: 0 12px;
+            opacity: 30%;
+        }
+
+        .more {
+            margin: 0 12px;
+            opacity: 30%;
+        }
+    }
 }
 
 .block-ref {
@@ -240,6 +270,19 @@ html.is-native-android {
       align-items: center;
     }
   }
+
+  &.is-doc {
+      &:before {
+          content: "[[📜";
+          opacity: .7;
+          margin-right: 4px;
+      }
+
+      &:after {
+          content: "]]";
+          opacity: .7;
+      }
+  }
 }
 
 .embed-page {
@@ -581,6 +624,23 @@ a.cloze-revealed {
 
 html.is-native-ios {
     audio {
-        width: 300px;
+        width: 100%;
+        max-width: 350px;
+    }
+}
+
+html:not(.is-mac) {
+    body[data-active-keystroke="Control"] {
+        .block-content {
+            cursor: pointer;
+        }
+    }
+}
+
+html.is-mac {
+    body[data-active-keystroke="Meta"] {
+        .block-content {
+            cursor: pointer;
+        }
     }
 }

+ 0 - 1
src/main/frontend/components/command_palette.css

@@ -16,7 +16,6 @@
     }
 
     .menu-link {
-      background-color: transparent;
       transition: none;
       border: none;
       border-radius: unset !important;

+ 11 - 12
src/main/frontend/components/content.cljs

@@ -21,6 +21,7 @@
             [frontend.state :as state]
             [frontend.ui :as ui]
             [frontend.util :as util]
+            [logseq.graph-parser.util :as gp-util]
             [frontend.util.url :as url-util]
             [goog.dom :as gdom]
             [goog.object :as gobj]
@@ -35,7 +36,7 @@
 
 (defn- lazy-load
   [format]
-  (let [format (format/normalize format)]
+  (let [format (gp-util/normalize-format format)]
     (when-let [record (format/get-format-record format)]
       (when-not (protocol/loaded? record)
         (set-format-js-loading! format true)
@@ -214,7 +215,7 @@
             :on-click (fn [_e]
                         (editor-handler/copy-block-ref! block-id #(util/format "{{embed ((%s))}}" %)))}
            "Copy block embed")
-          
+
           ;; TODO Logseq protocol mobile support
           (when (util/electron?)
             (ui/menu-link
@@ -295,12 +296,10 @@
       (ui/menu-link
        {:key "open-in-sidebar"
         :on-click (fn []
-                    (let [block (db/pull [:block/uuid block-ref-id])]
-                      (state/sidebar-add-block!
-                       (state/get-current-repo)
-                       block-ref-id
-                       :block-ref
-                       {:block block}))                    )}
+                    (state/sidebar-add-block!
+                     (state/get-current-repo)
+                     block-ref-id
+                     :block-ref))}
        "Open in sidebar")
       (ui/menu-link
        {:key "copy"
@@ -364,13 +363,13 @@
                            e
                            (custom-context-menu-content))
 
-                          (and block-id (util/uuid-string? block-id))
+                          (and block-id (parse-uuid block-id))
                           (let [block (.closest target ".ls-block")]
                             (when block
                               (util/select-highlight! [block]))
                             (common-handler/show-custom-context-menu!
                             e
-                            (block-context-menu-content target (cljs.core/uuid block-id))))
+                            (block-context-menu-content target (uuid block-id))))
 
                           :else
                           nil))))))
@@ -389,7 +388,7 @@
                    :format format}
                   id
                   config)
-      (let [format (format/normalize format)
+      (let [format (gp-util/normalize-format format)
             loading? (get loading format)
             markup? (contains? config/html-render-formats format)
             on-click (fn [e]
@@ -447,5 +446,5 @@
   (if hiccup
     [:div
      (hiccup-content id option)]
-    (let [format (format/normalize format)]
+    (let [format (gp-util/normalize-format format)]
       (non-hiccup-content id content on-click on-hide config format))))

+ 12 - 111
src/main/frontend/components/editor.cljs

@@ -7,9 +7,6 @@
             [frontend.components.datetime :as datetime-comp]
             [frontend.components.search :as search]
             [frontend.components.svg :as svg]
-            [frontend.mobile.camera :as mobile-camera]
-            [frontend.mobile.util :as mobile-util]
-            [frontend.config :as config]
             [frontend.db :as db]
             [frontend.db.model :as db-model]
             [frontend.extensions.zotero :as zotero]
@@ -23,12 +20,11 @@
             [frontend.util :as util]
             [frontend.util.cursor :as cursor]
             [frontend.util.keycode :as keycode]
+            [logseq.graph-parser.util :as gp-util]
             [goog.dom :as gdom]
             [promesa.core :as p]
-            [rum.core :as rum]
-            [frontend.handler.history :as history]
-            [frontend.mobile.footer :as footer]
-            [frontend.handler.config :as config-handler]))
+            [react-draggable]
+            [rum.core :as rum]))
 
 (rum/defc commands < rum/reactive
   [id format]
@@ -112,9 +108,9 @@
               q (or
                  @editor-handler/*selected-text
                  (when (state/sub :editor/show-page-search-hashtag?)
-                   (util/safe-subs edit-content pos current-pos))
+                   (gp-util/safe-subs edit-content pos current-pos))
                  (when (> (count edit-content) current-pos)
-                   (util/safe-subs edit-content pos current-pos))
+                   (gp-util/safe-subs edit-content pos current-pos))
                  "")
               matched-pages (when-not (string/blank? q)
                               (editor-handler/get-matched-pages q))
@@ -231,88 +227,6 @@
                            template)
             :class       "black"}))))))
 
-(rum/defc mobile-bar-indent-outdent [indent? icon]
-  [:div
-   [:button.bottom-action
-    {:on-mouse-down (fn [e]
-                      (util/stop e)
-                      (editor-handler/indent-outdent indent?))}
-    (ui/icon icon {:style {:fontSize ui/icon-size}})]])
-
-(def ^:private mobile-bar-icons-keywords
-  [:checkbox :brackets :parentheses :command :tag :a-b :list :camera
-   :brand-youtube :link :rotate :rotate-clockwise :code :bold :italic :strikethrough :paint])
-
-(def ^:private mobile-bar-commands-stats
-  (atom (into {}
-              (mapv (fn [name] [name {:counts 0}])
-                    mobile-bar-icons-keywords))))
-
-(defn set-command-stats [icon]
-  (let [key (keyword icon)
-        counts (get-in @mobile-bar-commands-stats [key :counts])]
-    (swap! mobile-bar-commands-stats
-           assoc-in [key :counts] (inc counts))
-    (config-handler/set-config!
-     :mobile/toolbar-stats @mobile-bar-commands-stats)))
-
-(rum/defc mobile-bar-command
-  [command-handler icon & [count? event?]]
-  [:div
-   [:button.bottom-action
-    {:on-mouse-down (fn [e]
-                      (util/stop e)
-                      (when count?
-                        (set-command-stats icon))
-                      (if event?
-                        (command-handler e)
-                        (command-handler)))}
-    (ui/icon icon {:style {:fontSize ui/icon-size}})]])
-
-(defn mobile-bar-commands
-  [_parent-state parent-id]
-  (let [viewport-fn (fn [] (when-let [input (gdom/getElement parent-id)]
-                             (util/make-el-cursor-position-into-center-viewport input)
-                             (.focus input)))]
-    (zipmap mobile-bar-icons-keywords
-     [(mobile-bar-command editor-handler/cycle-todo! "checkbox" true)
-      (mobile-bar-command #(editor-handler/toggle-page-reference-embed parent-id) "brackets" true)
-      (mobile-bar-command #(editor-handler/toggle-block-reference-embed parent-id) "parentheses" true)
-      (mobile-bar-command #(do (viewport-fn) (commands/simple-insert! parent-id "/" {})) "command" true)
-      (mobile-bar-command #(do (viewport-fn) (commands/simple-insert! parent-id "#" {})) "tag" true)
-      (mobile-bar-command editor-handler/cycle-priority! "a-b" true)
-      (mobile-bar-command editor-handler/toggle-list! "list" true)
-      (mobile-bar-command #(mobile-camera/embed-photo parent-id) "camera" true)
-      (mobile-bar-command commands/insert-youtube-timestamp "brand-youtube" true)
-      (mobile-bar-command editor-handler/html-link-format! "link" true)
-      (mobile-bar-command history/undo! "rotate" true true)
-      (mobile-bar-command history/redo! "rotate-clockwise" true true)
-      (mobile-bar-command #(do (viewport-fn) (commands/simple-insert! parent-id "<" {})) "code" true)
-      (mobile-bar-command editor-handler/bold-format! "bold" true)
-      (mobile-bar-command editor-handler/italics-format! "italic" true)
-      (mobile-bar-command editor-handler/strike-through-format! "strikethrough" true)
-      (mobile-bar-command editor-handler/highlight-format! "paint" true)])))
-
-(rum/defc mobile-bar < rum/reactive
-  [parent-state parent-id]
-  (when-let [config-toolbar-stats (:mobile/toolbar-stats (state/get-config))]
-   (reset! mobile-bar-commands-stats config-toolbar-stats))
-  (let [commands (mobile-bar-commands parent-state parent-id)
-        sorted-commands (sort-by (comp :counts second) > @mobile-bar-commands-stats)]
-    [:div#mobile-editor-toolbar.bg-base-2
-     [:div.toolbar-commands
-      (mobile-bar-indent-outdent false "arrow-bar-left")
-      (mobile-bar-indent-outdent true "arrow-bar-right")
-      (mobile-bar-command (editor-handler/move-up-down true) "arrow-bar-to-up")
-      (mobile-bar-command (editor-handler/move-up-down false) "arrow-bar-to-down")
-      (mobile-bar-command #(if (state/sub :document/mode?)
-                             (editor-handler/insert-new-block! nil)
-                             (commands/simple-insert! parent-id "\n" {})) "arrow-back")
-      (for [command sorted-commands]
-        ((first command) commands))]
-     [:div.toolbar-hide-keyboard
-      (mobile-bar-command #(state/clear-edit!) "keyboard-show")]]))
-
 (rum/defcs input < rum/reactive
   (rum/local {} ::input-value)
   (mixins/event-mixin
@@ -367,7 +281,6 @@
         max-width 300
         offset-top 24
         vw-height js/window.innerHeight
-        vw-width js/window.innerWidth
         to-max-height (if (and (seq rect) (> vw-height max-height))
                         (let [delta-height (- vw-height (+ (:top rect) top offset-top))]
                           (if (< delta-height max-height)
@@ -387,15 +300,14 @@
                                    (when (> ofx 0)
                                      (set! (.-transform (.-style el)) (str "translateX(-" (+ ofx 20) "px)")))))))
                            [right-sidebar? editing-key])
-        x-overflow-vw? (when (and (seq rect) (> vw-width max-width))
-                         (let [delta-width (- vw-width (+ (:left rect) left))]
-                           (< delta-width (* max-width 0.5))))
+        y-overflow-vh? (< to-max-height 130)
+        to-max-height (if y-overflow-vh? max-height to-max-height)
         pos-rect (when (and (seq rect) editing-key)
                    (:rect (cursor/get-caret-pos (state/get-input))))
         y-diff (when pos-rect (- (:height pos-rect) (:height rect)))]
     [:div.absolute.rounded-md.shadow-lg.absolute-modal
      {:ref *el
-      :class (if x-overflow-vw? "is-overflow-vw-x" "")
+      :class (if y-overflow-vh? "is-overflow-vh-y" "")
       :on-mouse-down (fn [e]
                        (.stopPropagation e))
       :style (merge
@@ -407,9 +319,9 @@
                :z-index    11}
               (when set-default-width?
                 {:width max-width})
-              (let [^js/HTMLElement textarea
-                    (js/document.querySelector "textarea.ls-textarea")]
-                (if (<= (.-clientWidth textarea) (+ left (if set-default-width? max-width 500)))
+              (let [^js/HTMLElement editor
+                    (js/document.querySelector ".editor-wrapper")]
+                (if (<= (.-clientWidth editor) (+ left (if set-default-width? max-width 500)))
                   {:right 0}
                   {:left (if (and y-diff (= y-diff 0)) left 0)})))}
      cp]))
@@ -476,7 +388,6 @@
   (let [content (if content (str content) "")]
     ;; as the function is binding to the editor content, optimization is welcome
     (str
-     "ls-textarea "
      (if (or (> (.-length content) 1000)
              (string/includes? content "\n"))
        "multiline-block"
@@ -607,20 +518,11 @@
   (mixins/event-mixin setup-key-listener!)
   (shortcut/mixin :shortcut.handler/block-editing-only)
   lifecycle/lifecycle
-  [state {:keys [format block]} id config]
+  [state {:keys [format block]} id _config]
   (let [content (state/sub-edit-content)
         heading-class (get-editor-style-class content format)]
     [:div.editor-inner {:class (if block "block-editor" "non-block-editor")}
 
-     (when (= (state/sub :editor/record-status) "RECORDING")
-       [:div#audio-record-toolbar
-        (footer/audio-record-cp)])
-
-     (when (and (or (mobile-util/is-native-platform?)
-                    config/mobile?)
-                (not (:review-cards? config)))
-       (mobile-bar state id))
-     
      (ui/ls-textarea
       {:id                id
        :cacheMeasurements (editor-row-height-unchanged?) ;; check when content updated (as the content variable is binded)
@@ -629,7 +531,6 @@
        :on-click          (editor-handler/editor-on-click! id)
        :on-change         (editor-handler/editor-on-change! block id search-timeout)
        :on-paste          (editor-handler/editor-on-paste! id)
-       :on-height-change  (editor-handler/editor-on-height-change! id)
        :auto-focus        false
        :class             heading-class})
 

+ 6 - 33
src/main/frontend/components/editor.css

@@ -1,43 +1,12 @@
-#mobile-editor-toolbar {
-  position: fixed;
-  bottom: 0;
-  left: 0;
-  width: 100%;
-  /* height: 2.5rem; */
-  z-index: 9999;
-  transition: none;
-  display: flex;
-  justify-content: space-between;
-
-  button {
-    padding: 7px 10px;
-  }
-  
-  .toolbar-commands {
-    justify-content: space-between;
-    display: flex;
-    align-items: center;
-    overflow-x: overlay;
-    overflow-y: hidden;
-    width: 95%;
-  }
-
-  .toolbar-hide-keyboard {
-    border-left: 1px solid;
-    border-color: var(--ls-quaternary-background-color);
-  }
-}
-
 #audio-record-toolbar {
     position: fixed;
     background-color: var(--ls-secondary-background-color);
-    bottom: 45px;
-    width: 88px;
+    width: 90px;
     justify-content: left;
     left: 5px;
     transition: none;
     z-index: 9999;
-    padding: 5px 5px 0px 5px;
+    padding: 5px 5px 5px 8px;
     border-radius: 5px;
 }
 
@@ -66,6 +35,10 @@
   &.is-overflow-vw-x {
     transform: translateX(calc(-100% + 1rem));
   }
+
+  &.is-overflow-vh-y {
+    transform: translateY(calc(-100% - 2rem));
+  }
 }
 
 .is-mobile {

+ 176 - 0
src/main/frontend/components/encryption.cljs

@@ -0,0 +1,176 @@
+(ns frontend.components.encryption
+  (:require [clojure.string :as string]
+            [frontend.context.i18n :refer [t]]
+            [frontend.encrypt :as e]
+            [frontend.handler.metadata :as metadata-handler]
+            [frontend.handler.notification :as notification]
+            [frontend.state :as state]
+            [frontend.ui :as ui]
+            [frontend.util :as util]
+            [promesa.core :as p]
+            [rum.core :as rum]))
+
+(rum/defcs encryption-dialog-inner <
+  (rum/local false ::reveal-secret-phrase?)
+  [state repo-url close-fn]
+  (let [reveal-secret-phrase? (get state ::reveal-secret-phrase?)
+        public-key (e/get-public-key repo-url)
+        private-key (e/get-secret-key repo-url)]
+    [:div
+     [:div.sm:flex.sm:items-start
+      [:div.mt-3.text-center.sm:mt-0.sm:text-left
+       [:h3#modal-headline.text-lg.leading-6.font-medium
+        "This graph is encrypted with " [:a {:href "https://age-encryption.org/" :target "_blank" :rel "noopener"} "age-encryption.org/v1"]]]]
+
+     [:div.mt-1
+      [:div.max-w-2xl.rounded-md.shadow-sm.sm:max-w-xl
+       [:div.cursor-pointer.block.w-full.rounded-sm.p-2
+        {:on-click (fn []
+                     (when (not @reveal-secret-phrase?)
+                       (reset! reveal-secret-phrase? true)))}
+        [:div.font-medium "Public Key:"]
+        [:div.font-mono.select-all.break-all public-key]
+        (if @reveal-secret-phrase?
+          [:div
+           [:div.mt-1.font-medium "Private Key:"]
+           [:div.font-mono.select-all.break-all private-key]]
+          [:div.underline "click to view the private key"])]]]
+
+     [:div.mt-5.sm:mt-4.sm:flex.sm:flex-row-reverse
+      [:span.mt-3.flex.w-full.rounded-md.shadow-sm.sm:mt-0.sm:w-auto
+       [:button.inline-flex.justify-center.w-full.rounded-md.border.border-gray-300.px-4.py-2.bg-white.text-base.leading-6.font-medium.text-gray-700.shadow-sm.hover:text-gray-500.focus:outline-none.focus:border-blue-300.focus:shadow-outline-blue.transition.ease-in-out.duration-150.sm:text-sm.sm:leading-5
+        {:type "button"
+         :on-click close-fn}
+        (t :close)]]]]))
+
+(defn encryption-dialog
+  [repo-url]
+  (fn [close-fn]
+    (encryption-dialog-inner repo-url close-fn)))
+
+(rum/defcs input-password-inner <
+  (rum/local "" ::password)
+  (rum/local "" ::password-confirm)
+  [state repo-url close-fn]
+  (let [password (get state ::password)
+        password-confirm (get state ::password-confirm)]
+    [:div
+     [:div.sm:flex.sm:items-start
+      [:div.mt-3.text-center.sm:mt-0.sm:text-left
+       [:h3#modal-headline.text-lg.leading-6.font-medium.font-bold
+        "Enter a password"]]]
+
+     (ui/admonition
+      :warning
+      [:div.opacity-70
+       "Choose a strong and hard to guess password.\nIf you lose your password, all the data can't be decrypted!! Please make sure you remember the password you have set, or you can keep a secure backup of the password."])
+     [:input.form-input.block.w-full.sm:text-sm.sm:leading-5.my-2
+      {:type "password"
+       :placeholder "Password"
+       :auto-focus true
+       :on-change (fn [e]
+                    (reset! password (util/evalue e)))}]
+     [:input.form-input.block.w-full.sm:text-sm.sm:leading-5.my-2
+      {:type "password"
+       :placeholder "Re-enter the password"
+       :on-change (fn [e]
+                    (reset! password-confirm (util/evalue e)))}]
+
+     [:div.mt-5.sm:mt-4.sm:flex.sm:flex-row-reverse
+      [:span.flex.w-full.rounded-md.shadow-sm.sm:ml-3.sm:w-auto
+       [:button.inline-flex.justify-center.w-full.rounded-md.border.border-transparent.px-4.py-2.bg-indigo-600.text-base.leading-6.font-medium.text-white.shadow-sm.hover:bg-indigo-500.focus:outline-none.focus:border-indigo-700.focus:shadow-outline-indigo.transition.ease-in-out.duration-150.sm:text-sm.sm:leading-5
+        {:type "button"
+         :on-click (fn []
+                     (let [value @password]
+                       (cond
+                         (string/blank? value)
+                         nil
+
+                         (not= @password @password-confirm)
+                         (notification/show! "The passwords are not matched." :error)
+
+                         :else
+                         (p/let [keys (e/generate-key-pair-and-save! repo-url)
+                                 db-encrypted-secret (e/encrypt-with-passphrase value keys)]
+                           (metadata-handler/set-db-encrypted-secret! db-encrypted-secret)
+                           (close-fn true)))))}
+        "Submit"]]]]))
+
+(defn input-password
+  [repo-url close-fn]
+  (fn [_close-fn]
+    (input-password-inner repo-url close-fn)))
+
+(rum/defcs encryption-setup-dialog-inner
+  [state repo-url close-fn]
+  [:div
+   [:div.sm:flex.sm:items-start
+    [:div.mt-3.text-center.sm:mt-0.sm:text-left
+     [:h3#modal-headline.text-lg.leading-6.font-medium
+      "Do you want to create an encrypted graph?"]]]
+
+   [:div.mt-5.sm:mt-4.sm:flex.sm:flex-row-reverse
+    [:span.flex.w-full.rounded-md.shadow-sm.sm:ml-3.sm:w-auto
+     [:button.inline-flex.justify-center.w-full.rounded-md.border.border-transparent.px-4.py-2.bg-indigo-600.text-base.leading-6.font-medium.text-white.shadow-sm.hover:bg-indigo-500.focus:outline-none.focus:border-indigo-700.focus:shadow-outline-indigo.transition.ease-in-out.duration-150.sm:text-sm.sm:leading-5
+      {:type "button"
+       :on-click (fn []
+                   (state/set-modal! (input-password repo-url close-fn)))}
+      (t :yes)]]
+    [:span.mt-3.flex.w-full.rounded-md.shadow-sm.sm:mt-0.sm:w-auto
+     [:button.inline-flex.justify-center.w-full.rounded-md.border.border-gray-300.px-4.py-2.bg-white.text-base.leading-6.font-medium.text-gray-700.shadow-sm.hover:text-gray-500.focus:outline-none.focus:border-blue-300.focus:shadow-outline-blue.transition.ease-in-out.duration-150.sm:text-sm.sm:leading-5
+      {:type "button"
+       :on-click (fn [] (close-fn false))}
+      (t :no)]]]])
+
+(defn encryption-setup-dialog
+  [repo-url close-fn]
+  (fn [close-modal-fn]
+    (let [close-fn (fn [encrypted?]
+                     (close-fn encrypted?)
+                     (close-modal-fn))]
+      (encryption-setup-dialog-inner repo-url close-fn))))
+
+(rum/defcs encryption-input-secret-inner <
+  (rum/local "" ::secret)
+  (rum/local false ::loading)
+  [state _repo-url db-encrypted-secret close-fn]
+  (let [secret (::secret state)
+        loading (::loading state)]
+    [:div
+     [:div.sm:flex.sm:items-start
+      [:div.mt-3.text-center.sm:mt-0.sm:text-left
+       [:h3#modal-headline.text-lg.leading-6.font-medium
+        "Enter your password"]]]
+
+     [:input.form-input.block.w-full.sm:text-sm.sm:leading-5.my-2
+      {:type "password"
+       :auto-focus true
+       :on-change (fn [e]
+                    (reset! secret (util/evalue e)))}]
+
+     [:div.mt-5.sm:mt-4.sm:flex.sm:flex-row-reverse
+      [:span.flex.w-full.rounded-md.shadow-sm.sm:ml-3.sm:w-auto
+       [:button.inline-flex.justify-center.w-full.rounded-md.border.border-transparent.px-4.py-2.bg-indigo-600.text-base.leading-6.font-medium.text-white.shadow-sm.hover:bg-indigo-500.focus:outline-none.focus:border-indigo-700.focus:shadow-outline-indigo.transition.ease-in-out.duration-150.sm:text-sm.sm:leading-5
+        {:type "button"
+         :on-click (fn []
+                     (reset! loading true)
+                     (let [value @secret]
+                       (when-not (string/blank? value) ; TODO: length or other checks
+                         (let [repo (state/get-current-repo)]
+                           (p/do!
+                            (-> (e/decrypt-with-passphrase value db-encrypted-secret)
+                                (p/then (fn [keys]
+                                          (e/save-key-pair! repo keys)
+                                          (close-fn true)
+                                          (state/set-state! :encryption/graph-parsing? false)))
+                                (p/catch #(notification/show! "The password is not matched." :warning true))
+                                (p/finally #(reset! loading false))))))))}
+        (if @loading (ui/loading "Decrypting") "Decrypt")]]]]))
+
+(defn encryption-input-secret-dialog
+  [repo-url db-encrypted-secret close-fn]
+  (fn [close-modal-fn]
+    (let [close-fn (fn [encrypted?]
+                     (close-fn encrypted?)
+                     (close-modal-fn))]
+      (encryption-input-secret-inner repo-url db-encrypted-secret close-fn))))

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

@@ -9,10 +9,11 @@
             [frontend.context.i18n :refer [t]]
             [frontend.date :as date]
             [frontend.db :as db]
-            [frontend.format :as format]
             [frontend.handler.export :as export-handler]
             [frontend.state :as state]
             [frontend.util :as util]
+            [logseq.graph-parser.config :as gp-config]
+            [logseq.graph-parser.util :as gp-util]
             [goog.object :as gobj]
             [reitit.frontend.easy :as rfe]
             [rum.core :as rum]))
@@ -43,8 +44,8 @@
            (let [file-id file]
              [:tr {:key file-id}
               [:td
-               (let [href (if (config/draw? file)
-                            (rfe/href :draw nil {:file (string/replace file (str config/default-draw-directory "/") "")})
+               (let [href (if (gp-config/draw? file)
+                            (rfe/href :draw nil {:file (string/replace file (str gp-config/default-draw-directory "/") "")})
                             (rfe/href :file {:path file-id}))]
                  [:a {:href href}
                   file])]
@@ -70,7 +71,7 @@
                    state)}
   [state]
   (let [path (get-path state)
-        format (format/get-format path)
+        format (gp-util/get-format path)
         original-name (db/get-file-page path)
         random-id (str (d/squuid))]
     [:div.file {:id (str "file-edit-wrapper-" random-id)}
@@ -86,8 +87,7 @@
                                                 (state/sidebar-add-block!
                                                  (state/get-current-repo)
                                                  (:db/id page)
-                                                 :page
-                                                 {:page page}))
+                                                 :page))
                                               (util/stop e)))}
          original-name]])
 

+ 30 - 24
src/main/frontend/components/header.cljs

@@ -41,7 +41,7 @@
         (ui/dropdown-with-links
          (fn [{:keys [toggle-fn]}]
            [:a.button
-              {:on-click toggle-fn}
+            {:on-click toggle-fn}
             [:span.text-sm.font-medium (user-handler/email)]])
          [{:title (t :logout)
            :options {:on-click user-handler/logout}}]
@@ -133,8 +133,9 @@
   (ui/with-shortcut :ui/toggle-left-sidebar "bottom"
     [:a#left-menu.cp__header-left-menu.button
      {:on-click on-click
-      :style {:margin-left 12}}
-     (ui/icon "menu-2" {:style {:fontSize ui/icon-size}})]))
+      :style    {:margin-left 12}}
+     [:span.inner
+      (ui/icon "menu-2" {:style {:fontSize ui/icon-size}})]]))
 
 (rum/defc dropdown-menu < rum/reactive
   [{:keys [current-repo t]}]
@@ -224,12 +225,15 @@
   (let [repos (->> (state/sub [:me :repos])
                    (remove #(= (:url %) config/local-repo)))
         electron-mac? (and util/mac? (util/electron?))
-        vw-state (state/sub :ui/visual-viewport-state)
         show-open-folder? (and (nfs/supported?)
                                (or (empty? repos)
                                    (nil? (state/sub :git/current-repo)))
-                               (not (mobile-util/is-native-platform?))
-                               (not config/publishing?))]
+                               (not (mobile-util/native-platform?))
+                               (not config/publishing?))
+        left-menu (left-menu-button {:on-click (fn []
+                                       (open-fn)
+                                       (state/set-left-sidebar-open!
+                                        (not (:ui/left-sidebar-open? @state/state))))})]
     [:div.cp__header#head
      {:class           (util/classnames [{:electron-mac   electron-mac?
                                           :native-ios     (mobile-util/native-ios?)
@@ -239,22 +243,25 @@
                            (when (and (util/electron?)
                                       (.. target -classList (contains "cp__header")))
                              (js/window.apis.toggleMaxOrMinActiveWindow))))
-      :style           {:fontSize  50
-                        :transform (str "translateY(" (or (:offset-top vw-state) 0) "px)")}}
+      :style           {:fontSize  50}}
      [:div.l.flex
-      (left-menu-button {:on-click (fn []
-                                     (open-fn)
-                                     (state/set-left-sidebar-open!
-                                      (not (:ui/left-sidebar-open? @state/state))))})
-
-      (when current-repo ;; this is for the Search button
-        (ui/with-shortcut :go/search "right"
-          [:a.button#search-button
-           {:on-click #(do (when (or (mobile-util/native-android?)
-                                     (mobile-util/native-iphone?))
-                             (state/set-left-sidebar-open! false))
-                           (state/pub-event! [:go/search]))}
-           (ui/icon "search" {:style {:fontSize ui/icon-size}})]))]
+      (when-not (mobile-util/native-platform?)
+        [left-menu
+         (when current-repo ;; this is for the Search button
+           (ui/with-shortcut :go/search "right"
+             [:a.button#search-button
+              {:on-click #(do (when (or (mobile-util/native-android?)
+                                        (mobile-util/native-iphone?))
+                                (state/set-left-sidebar-open! false))
+                              (state/pub-event! [:go/search]))}
+              (ui/icon "search" {:style {:fontSize ui/icon-size}})]))])
+      (when (mobile-util/native-platform?)
+        (if (state/home?)
+          left-menu
+          (ui/with-shortcut :go/backward "bottom"
+            [:a.it.navigation.nav-left.button
+             {:title "Go back" :on-click #(js/window.history.back)}
+             (ui/icon "chevron-left" {:style {:fontSize 25}})])))]
 
      [:div.r.flex
       (when-not file-sync-handler/hiding-login&file-sync
@@ -267,11 +274,10 @@
       (when (not= (state/get-current-route) :home)
         (home-button))
 
-      (when (or (util/electron?)
-                (mobile-util/native-ios?))
+      (when (util/electron?)
         (back-and-forward))
 
-      (when-not (mobile-util/is-native-platform?)
+      (when-not (mobile-util/native-platform?)
         (new-block-mode))
 
       (when show-open-folder?

+ 120 - 99
src/main/frontend/components/header.css

@@ -1,7 +1,7 @@
 .cp__header {
-  @apply shadow z-10;
+  @apply z-10;  
   -webkit-app-region: drag;
-  
+
   padding-top: var(--ls-headbar-inner-top-padding);
   height: calc(var(--ls-headbar-height) + var(--ls-headbar-inner-top-padding));
   display: flex;
@@ -9,7 +9,6 @@
   justify-content: space-between;
   flex: 0 0 auto;
   position: sticky;
-  position: -webkit-sticky;
   top: 0;
   left: 0;
   right: 0;
@@ -32,9 +31,10 @@
   }
 
   /* To prevent header glitch on Safari */
+
   > .l, > .r {
-      -webkit-transform: translate3d(0, 0, 0);
-      transform: translate3d(0, 0, 0);
+    -webkit-transform: translate3d(0, 0, 0);
+    transform: translate3d(0, 0, 0);
   }
 
   .it svg {
@@ -120,6 +120,18 @@
       top: 1px;
     }
   }
+
+  &-left-menu {
+    &.button {
+      margin: 0;
+      padding: 0;
+    }
+
+    > .inner {
+      line-height: 0;
+      padding: 3px;
+    }
+  }
 }
 
 .is-electron.is-mac .cp__header {
@@ -192,7 +204,7 @@ a.button {
     background: none;
 
     @screen md {
-        background: var(--ls-tertiary-background-color);
+      background: var(--ls-tertiary-background-color);
     }
   }
 
@@ -206,7 +218,7 @@ a.button {
 }
 
 html.is-ios.is-safari {
-    
+  
   .cp__header {
     background-color: var(--ls-primary-background-color);
   }
@@ -220,121 +232,130 @@ html.is-native-iphone,
 html.is-native-iphone-without-notch,
 html.is-native-ipad {
 
-     #main-container {
-        padding-top: 0px;
-    }
+  #main-container {
+    padding-top: 0px;
+    display: flex;
+    flex-direction: column;
+  }
 
-     #main-content-container {
-         padding-left: 22px;
-         padding-right: 14px;
-         padding-top: 0px;
-         height: calc(100vh - var(--ls-headbar-inner-top-padding) - var(--ls-headbar-height));
+  #main-content-container {
+    padding-left: 22px;
+    padding-right: 14px;
+    padding-top: 0px;
+    height: calc(100vh - var(--ls-headbar-inner-top-padding) - var(--ls-headbar-height));
 
-         @screen sm {
-             padding-left: 2rem;
-         }
+    @screen sm {
+      padding-left: 2rem;
+    }
 
-         .page {
-             margin-top: 24px;
-         }
+    .page {
+      margin-top: 24px;
     }
-    
-    .cp__header > .r {
-        display: flex;
+  }
+
+  .cp__header {
+    > .r {
+      display: flex;
+    }
+
+    a.button {
+      opacity: 1;
     }
+  }
 }
 
 html.is-native-ipad {
-    --ls-headbar-inner-top-padding: 0px;
-    --ls-headbar-height: 4rem;
-    
-    .cp__header {
-      background-color: transparent !important;
-      display: flex;
-      
-      > .l {
-        /* background-color: var(--ls-primary-background-color); */
-        padding-top: 20px;
-      }
+  --ls-headbar-inner-top-padding: 0px;
+  --ls-headbar-height: 4rem;
 
-      > .r {
-        flex: 1;
-        background-color: var(--ls-primary-background-color);
-        height: 100%;
-        padding-top: 20px;
-        justify-content: flex-end;
-        align-items: center;
-      }
+  .cp__header {
+    background-color: transparent !important;
+    display: flex;
+
+    > .l {
+      /* background-color: var(--ls-primary-background-color); */
+      padding-top: 20px;
     }
-    
-    .left-sidebar-inner  {
-        > .wrap {
-            padding-top: 20px;
-      }
+
+    > .r {
+      flex: 1;
+      background-color: var(--ls-primary-background-color);
+      height: 100%;
+      padding-top: 20px;
+      justify-content: flex-end;
+      align-items: center;
     }
+  }
 
-    .cp__right-sidebar {
-        &-settings {
-            margin-top: -4px;
-        }
-
-        &-topbar {
-            padding-top: 37px;
-        }
-
-        &-inner {
-            .resizer {
-                top: 30vh;
-                width: 12px;
-                height: 40vh;
-            }
-            
-            .resizer:hover {
-                background-color: var(--ls-guideline-color, #ddd);
-            }
-        }
+  .left-sidebar-inner  {
+    > .wrap {
+      padding-top: 20px;
     }
-}
+  }
 
-html.is-native-iphone {
-    --ls-headbar-inner-top-padding: 36px;
+  .cp__right-sidebar {
+    &-settings {
+      margin-top: -4px;
+    }
+
+    &-topbar {
+      padding-top: 37px;
+    }
 
-    .left-sidebar-inner {
-        > .wrap {
-            padding-top: 12px;
-        }
+    &-inner {
+      .resizer {
+        top: 30vh;
+        width: 12px;
+        height: 40vh;
+      }
 
-        .new-page {
-            padding-bottom: 12px;
-        }
+      .resizer:hover {
+        background-color: var(--ls-guideline-color, #ddd);
+      }
     }
-    
-    .ui__notifications {
-        top: calc(var(--ls-headbar-height) + var(--ls-headbar-inner-top-padding) - 0.3rem);
+  }
+}
+
+html.is-native-iphone {
+  --ls-headbar-inner-top-padding: 36px;
+
+  .left-sidebar-inner {
+    > .wrap {
+      padding-top: 12px;
     }
-    
-    @media (orientation: landscape) {
-        --ls-headbar-inner-top-padding: 8px;
-        --ls-headbar-height: 2.5rem;
-
-        .cp__header {
-            @apply shadow z-10;
-        }
+
+    .new-page {
+      padding-bottom: 12px;
     }
+  }
+
+  .ui__notifications {
+    top: calc(var(--ls-headbar-height) + var(--ls-headbar-inner-top-padding) - 0.3rem);
+  }
+
+  @media (orientation: landscape) {
+    --ls-headbar-inner-top-padding: 8px;
+    --ls-headbar-height: 2.5rem;
+  }
 }
 
 html.is-native-iphone-without-notch {
-    
-    --ls-headbar-inner-top-padding: 15px;
+
+  --ls-headbar-inner-top-padding: 15px;
+  --ls-headbar-height: 2.5rem;
+
+  @media (orientation: landscape) {
+
+    --ls-headbar-inner-top-padding: 0px;
     --ls-headbar-height: 2.5rem;
+  }
+}
 
-    @media (orientation: landscape) {
+html.is-zoomed-native-ios {
+  --ls-headbar-inner-top-padding: 30px;
 
-        --ls-headbar-inner-top-padding: 0px;
-        --ls-headbar-height: 2.5rem;
-     
-        .cp__header {
-            @apply shadow z-10;
-        }
-    }
+  @media (orientation: landscape) {
+    --ls-headbar-inner-top-padding: 8px;
+    --ls-headbar-height: 2.5rem;
+  }
 }

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

@@ -4,7 +4,7 @@
             [frontend.db :as db]
             [frontend.db.model :as db-model]
             [frontend.state :as state]
-            [frontend.text :as text]
+            [logseq.graph-parser.text :as text]
             [frontend.ui :as ui]
             [medley.core :as medley]
             [rum.core :as rum]

+ 10 - 6
src/main/frontend/components/journal.cljs

@@ -8,7 +8,8 @@
             [frontend.db.model :as model]
             [frontend.handler.page :as page-handler]
             [frontend.state :as state]
-            [frontend.text :as text]
+            [logseq.graph-parser.text :as text]
+            [logseq.graph-parser.util :as gp-util]
             [frontend.ui :as ui]
             [frontend.util :as util]
             [goog.object :as gobj]
@@ -48,14 +49,17 @@
                         (state/sidebar-add-block!
                          (state/get-current-repo)
                          (:db/id page)
-                         :page
-                         {:page     page
-                          :journal? true}))
+                         :page))
                       (.preventDefault e)))}
        [:h1.title
-        (util/capitalize-all title)]]
+        (gp-util/capitalize-all title)]]
 
-      (blocks-cp repo page format)
+      (if today?
+        (blocks-cp repo page format)
+        (ui/lazy-visible (fn []
+                           (blocks-cp repo page format))
+                         nil
+                         {}))
 
       {})
 

+ 553 - 338
src/main/frontend/components/onboarding/index.css

@@ -1,401 +1,616 @@
 body[data-page=repo-add],
 body[data-page=import] {
-    .cp__header .add-graph-btn {
-        display: none;
-    }
+  .cp__header .add-graph-btn {
+    display: none;
+  }
 }
 
 .cp__onboarding {
-    &-setups {
-        background-color: var(--ls-primary-background-color);
-        z-index: 1;
+  &-setups {
+    background-color: var(--ls-primary-background-color);
+    z-index: 1;
+
+    .as-flex-center {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+    }
+
+    .inner-card {
+      background-color: var(--ls-secondary-background-color);
+      padding-top: 55px;
 
-        .as-flex-center {
+      > h1 {
+        font-size: 30px;
+
+        strong {
+          color: var(--ls-active-primary-color);
+          display: inline-block;
+          padding-right: 4px;
+          font-weight: normal;
+
+          &:first-child {
+            display: block;
+            text-align: center;
+            padding-bottom: 15px;
+          }
+        }
+      }
+
+      > h2 {
+        font-size: 14px;
+        margin-top: 20px;
+        margin-bottom: 40px;
+        padding: 0 30px;
+        text-align: center;
+      }
+
+      > article {
+        background-color: var(--ls-secondary-background-color);
+        border-radius: 20px;
+        flex: 1;
+
+        > section {
+          flex: 1;
+
+          &.a {
+            background-color: var(--ls-tertiary-background-color);
             display: flex;
+            flex-direction: column;
             align-items: center;
-            justify-content: center;
-        }
+            padding-top: 75px;
+            padding-bottom: 100px;
+            border-radius: 20px 20px 0 0;
+            min-width: 50%;
+
+            > strong {
+              font-size: 28px;
+              padding-bottom: 15px;
+              font-weight: 600;
+            }
 
-        .inner-card {
-            background-color: var(--ls-secondary-background-color);
-            padding-top: 55px;
+            > small {
+              font-size: 18px;
+              line-height: 30px;
+              padding: 0 40px;
+              text-align: center;
+            }
 
-            > h1 {
-                font-size: 30px;
+            > .choose {
+              padding-top: 48px;
+              width: 100%;
+              cursor: pointer;
 
-                strong {
-                    color: var(--ls-active-primary-color);
-                    display: inline-block;
-                    padding-right: 4px;
-                    font-weight: normal;
-
-                    &:first-child {
-                        display: block;
-                        text-align: center;
-                        padding-bottom: 15px;
-                    }
-                }
-            }
+              &:active {
+                opacity: .8;
+              }
+
+              i {
+                width: 180px;
+                height: 120px;
+                background-image: url("../img/folder-logo.png");
+                background-size: contain;
+              }
 
-            > h2 {
-                font-size: 14px;
-                margin-top: 20px;
-                margin-bottom: 40px;
-                padding: 0 30px;
-                text-align: center;
+              .control {
+                padding: 20px 20px 0 20px;
+                width: 100%;
+              }
             }
+          }
 
-            > article {
-                background-color: var(--ls-secondary-background-color);
-                border-radius: 20px;
-                flex: 1;
+          &.b {
+            display: none;
+            padding: 50px 40px;
 
-                > section {
-                    flex: 1;
-
-                    &.a {
-                        background-color: var(--ls-tertiary-background-color);
-                        display: flex;
-                        flex-direction: column;
-                        align-items: center;
-                        padding-top: 75px;
-                        padding-bottom: 100px;
-                        border-radius: 20px 20px 0 0;
-                        min-width: 50%;
-
-                        > strong {
-                            font-size: 28px;
-                            padding-bottom: 15px;
-                            font-weight: 600;
-                        }
-
-                        > small {
-                            font-size: 18px;
-                            line-height: 30px;
-                            padding: 0 40px;
-                            text-align: center;
-                        }
-
-                        > .choose {
-                            padding-top: 48px;
-                            width: 100%;
-                            cursor: pointer;
-
-                            &:active {
-                                opacity: .8;
-                            }
-
-                            i {
-                                width: 180px;
-                                height: 120px;
-                                background-image: url("../img/folder-logo.png");
-                                background-size: contain;
-                            }
-
-                            .control {
-                                padding: 20px 20px 0 20px;
-                                width: 100%;
-                            }
-                        }
-                    }
+            > p {
+              width: 100%;
 
-                    &.b {
-                        display: none;
-                        padding: 50px 40px;
-
-                        > p {
-                            width: 100%;
-
-                            &:first-child {
-                                i {
-                                    width: 42px;
-                                }
-
-                                strong {
-                                    font-size: 19px;
-                                    line-height: 1.5em;
-                                }
-                            }
-                        }
-
-                        ul {
-                            margin: 0;
-                            padding-top: 20px;
-                            width: 100%;
-
-                            > li {
-                                list-style: none;
-                                margin-bottom: 8px;
-                                height: 40px;
-                                display: flex;
-
-                                &.hr {
-                                    height: 1px;
-                                    border-bottom: 1px solid var(--ls-border-color);
-                                    margin: 20px 0px;
-                                    opacity: 0.6;
-                                }
-
-                                > i {
-                                    width: 48px;
-                                    background-image: url(../img/folder.png);
-                                    background-size: contain;
-                                    background-repeat: no-repeat;
-                                    background-position-y: 2px;
-                                    position: relative;
-                                    right: -2px;
-
-                                    &.is-file {
-                                        background-image: url("../img/file-edn.png");
-                                        background-size: 50%;
-                                        background-position-x: center;
-                                    }
-
-                                    > .ti {
-                                        opacity: .25;
-                                        color: black;
-                                    }
-                                }
-
-                                > span {
-                                    flex: 1;
-                                    padding-left: 15px;
-                                    display: flex;
-                                    flex-direction: column;
-
-                                    strong {
-                                        font-size: 11px;
-                                    }
-                                }
-                            }
-                        }
-                    }
+              &:first-child {
+                i {
+                  width: 42px;
                 }
 
-                &.importer {
-                    background-color: var(--ls-tertiary-background-color);
-                    padding-top: 80px;
-                    position: relative;
-
-                    > section {
-                        flex: unset;
-                        padding: 0 30px;
-
-                        h1 {
-                            font-size: 26px;
-                            font-weight: 500;
-                        }
-
-                        h2 {
-                            font-size: 14px;
-                            padding-top: 15px;
-                            padding-bottom: 15px;
-                        }
-
-                        &.d {
-                            width: 100%;
-                            padding: 20px 0;
-
-                            > label {
-                                width: unset;
-                                height: 80px;
-                                flex: 1;
-                                margin: 0 15px;
-                                margin-bottom: 10px;
-
-                                > span {
-                                    &:first-child {
-                                        padding: 16px;
-
-                                        i {
-                                            height: 30px;
-                                            width: 30px;
-                                            display: flex;
-                                            align-items: center;
-                                            justify-content: center;
-                                        }
-                                    }
-                                }
-
-                                strong {
-                                    font-size: 18px;
-                                }
-
-                                small {
-                                    font-size: 11px;
-                                }
-                            }
-                        }
-                    }
+                strong {
+                  font-size: 19px;
+                  line-height: 1.5em;
                 }
+              }
             }
 
+            ul {
+              margin: 0;
+              padding-top: 20px;
+              width: 100%;
+
+              > li {
+                list-style: none;
+                margin-bottom: 8px;
+                height: 40px;
+                display: flex;
+
+                &.hr {
+                  height: 1px;
+                  border-bottom: 1px solid var(--ls-border-color);
+                  margin: 20px 0px;
+                  opacity: 0.6;
+                }
 
-            @screen lg {
-                background-color: unset;
+                > i {
+                  width: 48px;
+                  background-image: url(../img/folder.png);
+                  background-size: contain;
+                  background-repeat: no-repeat;
+                  background-position-y: 2px;
+                  position: relative;
+                  right: -2px;
+
+                  &.is-file {
+                    background-image: url("../img/file-edn.png");
+                    background-size: 50%;
+                    background-position-x: center;
+                  }
+
+                  > .ti {
+                    opacity: .25;
+                    color: black;
+                  }
+                }
 
-                > h1 {
-                    font-size: 36px;
+                > span {
+                  flex: 1;
+                  padding-left: 15px;
+                  display: flex;
+                  flex-direction: column;
 
-                    strong {
-                        &:first-child {
-                            display: inline-block;
-                        }
-                    }
+                  strong {
+                    font-size: 11px;
+                  }
                 }
+              }
+            }
+          }
+        }
 
-                > h2 {
-                    font-size: 20px;
-                    padding: 0;
-                }
+        &.importer {
+          background-color: var(--ls-tertiary-background-color);
+          position: relative;
 
-                > article {
-                    max-width: 100vw;
-                    flex: unset;
+          > section {
+            flex: unset;
+            padding: 0 30px;
 
-                    > section {
-                        &.a {
-                            border-radius: 20px 0 0 20px;
+            h1 {
+              font-size: 26px;
+              font-weight: 500;
+            }
 
-                            > small {
-                                padding: 0 95px;
-                            }
+            h2 {
+              font-size: 14px;
+              padding-top: 15px;
+              padding-bottom: 15px;
+            }
 
-                            > .choose {
-                                .control {
-                                    padding: 30px 70px 0 70px;
-                                }
-                            }
-                        }
+            &.d {
+              width: 100%;
+              padding: 20px 0;
 
-                        &.b {
-                            display: flex;
-                        }
+              > label {
+                width: unset;
+                height: 80px;
+                flex: 1;
+                margin-bottom: 10px;
+
+                > span {
+                  &:first-child {
+                    padding: 16px;
+
+                    i {
+                      height: 30px;
+                      width: 30px;
+                      display: flex;
+                      align-items: center;
+                      justify-content: center;
                     }
+                  }
+                }
 
-                    &.importer {
-                        padding-top: 150px;
-
-                        > section {
-                            padding: 0;
+                strong {
+                  font-size: 18px;
+                }
 
-                            h1 {
-                                font-size: 28px;
-                            }
+                small {
+                  font-size: 11px;
+                }
+              }
+            }
+          }
+        }
+      }
 
-                            h2 {
-                                font-size: 19px;
-                                line-height: 3em;
-                                padding: unset;
-                            }
 
-                            &.d {
-                                padding: 40px 150px;
-                            }
+      @screen lg {
+        background-color: unset;
 
-                            &.e {
-                                position: absolute;
-                                bottom: -50px;
-                                right: 15px;
+        > h1 {
+          font-size: 36px;
 
-                                a.button {
-                                    padding: 4px 10px;
-                                }
-                            }
-                        }
-                    }
-                }
+          strong {
+            &:first-child {
+              display: inline-block;
             }
+          }
+        }
+
+        > h2 {
+          font-size: 20px;
+          padding: 0;
         }
 
-        label.action-input {
-            transition: none;
-            color: var(--ls-active-primary-color);
-            background-color: var(--ls-quaternary-background-color);
-            height: 68px;
-            width: 100%;
-            opacity: .8;
-            user-select: none;
-            border-radius: 12px;
-            overflow: hidden;
-            cursor: pointer;
-
-            small {
-                font-size: 11px;
-                text-align: center;
+        > article {
+          max-width: 100vw;
+          flex: unset;
+
+          > section {
+            &.a {
+              border-radius: 20px 0 0 20px;
+
+              > small {
+                padding: 0 95px;
+              }
+
+              > .choose {
+                .control {
+                  padding: 30px 70px 0 70px;
+                }
+              }
             }
 
-            &:hover {
-                opacity: 1;
+            &.b {
+              display: flex;
             }
+          }
 
-            &:active {
-                opacity: .5;
+          &.importer {
+            > section {
+              padding: 0;
+
+              h1 {
+                font-size: 28px;
+              }
+
+              h2 {
+                font-size: 19px;
+                line-height: 3em;
+                padding: unset;
+              }
+
+              &.e {
+                position: absolute;
+                bottom: -50px;
+                right: 15px;
+
+                a.button {
+                  padding: 4px 10px;
+                }
+              }
             }
+          }
+        }
+      }
+    }
 
-            &[disabled] {
-                pointer-events: none;
+    label.action-input {
+      transition: none;
+      color: var(--ls-active-primary-color);
+      background-color: var(--ls-quaternary-background-color);
+      height: 68px;
+      width: 100%;
+      opacity: .8;
+      user-select: none;
+      border-radius: 12px;
+      overflow: hidden;
+      cursor: pointer;
+
+      small {
+        font-size: 11px;
+        text-align: center;
+      }
+
+      &:hover {
+        opacity: 1;
+      }
+
+      &:active {
+        opacity: .5;
+      }
+
+      &[disabled] {
+        pointer-events: none;
+      }
+    }
+
+    @screen lg {
+      justify-content: center;
+      align-items: center;
+    }
+  }
+
+  &-quick-tour {
+    &.shepherd-element, .shepherd-arrow:before {
+      background-color: var(--ls-quaternary-background-color);
+      font-size: 14px;
+    }
+
+    &.shepherd-element {
+      max-width: 320px;
+      border-radius: 8px;
+      box-shadow: none;
+
+      &[data-shepherd-step-id=nav-journal-page] {
+        .shepherd-arrow {
+          left: 30px !important;
+        }
+      }
+    }
+
+    .shepherd-content {
+      display: flex;
+      flex-direction: column;
+      -webkit-font-smoothing: antialiased;
+    }
+
+    .shepherd-text {
+      font-size: 14px;
+      color: var(--ls-secondary-text-color);
+      flex: 1;
+      padding: 8px 10px;
+      position: relative;
+
+      h2 {
+        font-size: 18px;
+        line-height: 28px;
+        font-weight: 700;
+        padding-top: 5px;
+        padding-bottom: 5px;
+      }
+
+      p {
+        color: var(--ls-secondary-text-color);
+      }
+
+      .steps {
+        position: absolute;
+        bottom: -28px;
+        left: 10px;
+        font-size: 12px;
+
+        strong {
+          opacity: .5;
+          font-size: 11px;
+        }
+
+        ul {
+          display: flex;
+          list-style: none;
+          align-items: center;
+          margin: 0;
+          padding-left: 1px;
+          padding-top: 2px;
+
+          li {
+            padding: 0;
+            width: 5px;
+            height: 5px;
+            margin: 0 5px 0 0;
+            background-color: rgba(0, 0, 0, .2);
+            border-radius: 100%;
+            font-size: 0;
+
+            &.active {
+              background-color: var(--ls-primary-text-color);
             }
+          }
         }
+      }
+    }
 
-        @screen lg {
-            justify-content: center;
-            align-items: center;
+    .shepherd-footer {
+      button {
+        @apply bg-indigo-600 hover:bg-indigo-700;
+
+        padding: 4px 8px;
+        border-radius: 6px;
+        overflow: hidden;
+        color: white;
+        font-size: 13px;
+        font-weight: 500;
+
+        &.back {
+          background-color: rgba(0, 0, 0, .3);
         }
+      }
+    }
+  }
+
+  &-skip-quick-tour {
+    -webkit-font-smoothing: antialiased;
+    position: fixed;
+    bottom: 20px;
+    left: 50%;
+    transform: translateX(-60px);
+    z-index: 10001;
+    background-color: #4B5563;
+    padding: 12px 20px;
+    border-radius: 8px;
+    color: white;
+    box-shadow: 0 1px 2px rgba(16, 24, 40, 0.05);
+    font-weight: 700;
+
+    .ti {
+      padding-right: 6px;
+      font-size: 18px;
+      position: relative;
+      top: 2px;
+      font-weight: normal;
+    }
+
+    &:hover {
+      opacity: .8;
+    }
+
+    &:active {
+      opacity: 1;
     }
+  }
+}
+
+.shepherd-modal-overlay-container.shepherd-modal-is-visible {
+  transition: none;
+  opacity: .4;
+}
+
+.shepherd-target {
+  &.nav-content-item {
+    background-color: rgba(0, 0, 0, .4);
+    padding-left: 15px;
+    padding-right: 15px;
+  }
+
+  .nav-content-item-inner {
+    background-color: var(--ls-secondary-background-color);
+    overflow: hidden;
+
+    .header {
+      padding: 4px 10px !important;
+    }
+
+    .bd {
+      ul a {
+        padding: 2px 4px !important;
+      }
+    }
+  }
+
+  &.page-title {
+    background-color: rgba(0, 0, 0, .4);
+
+    > h1 {
+      background-color: var(--ls-primary-background-color);
+      border-radius: 12px;
+    }
+  }
+
+  &.cp__sidebar-help-btn {
+    background-color: rgba(0, 0, 0, .4);
+
+    > .inner {
+      opacity: 1;
+    }
+  }
+
+  &.cp__header-left-menu {
+    background-color: rgba(0, 0, 0, .4);
+    border-radius: 0;
+    opacity: 1 !important;
+    transition: none;
+
+    &:hover {
+      background-color: rgba(0, 0, 0, .4);
+    }
+
+    > .inner {
+      border-radius: 100%;
+      background-color: var(--ls-primary-background-color);
+    }
+  }
+}
+
+.shepherd-element {
+  &[data-popper-placement^=right] > .shepherd-arrow {
+    left: -4px;
+  }
+
+  &[data-popper-placement^=top] > .shepherd-arrow {
+    bottom: -4px !important;
+  }
+
+  &[data-popper-placement^=left] > .shepherd-arrow {
+    right: -4px !important;
+  }
+
+  &[data-popper-placement^=bottom] > .shepherd-arrow {
+    top: -4px !important;
+  }
+}
+
+.shepherd-arrow, .shepherd-arrow:before {
+  width: 8px;
+  height: 8px;
+}
+
+html[data-theme=light] {
+  .cp__onboarding {
+    &-quick-tour {
+      &.shepherd-element, .shepherd-arrow:before {
+        background-color: #E8E5DE;
+      }
+    }
+  }
 }
 
 html.is-native-android,
 html.is-native-ipad,
 html.is-native-iphone,
 html.is-native-iphone-without-notch {
-    .cp__onboarding {
-        &-setups {
-            position: absolute;
-            width: 100%;
-            top: 0;
-            left: 0;
-            height: 100vh;
-            overflow-y: auto;
-
-            .inner-card {
-                padding-top: 30px;
-                min-height: 100vh;
-                width: 100%;
-
-                > h2 {
-                    margin-bottom: 25px;
-                }
-
-                > article {
-                    > section {
-                        &.a {
-                            padding-top: 25px;
-
-                            small {
-                                text-align: left;
-                                padding: 0 40px;
-                            }
-                            .mobile-intro {
-                                margin-top: 10px;
-                                line-height: 1.5rem;
-                                font-size: 1rem;
-                            }
-
-                            > .choose {
-                                padding-top: 25px;
-                            }
-                        }
-                    }
-                }
+  .cp__onboarding {
+    &-setups {
+      position: absolute;
+      width: 100%;
+      top: 0;
+      left: 0;
+      height: 100vh;
+      overflow-y: auto;
+
+      .inner-card {
+        padding-top: 30px;
+        min-height: 100vh;
+        width: 100%;
+
+        > h2 {
+          margin-bottom: 25px;
+        }
 
-                @screen lg {
-                    > article {
-                        width: 1040px;
-                        height: 510px;
-                    }
-                }
+        > article {
+          > section {
+            &.a {
+              padding-top: 25px;
+
+              small {
+                text-align: left;
+                padding: 0 40px;
+              }
+
+              .mobile-intro {
+                margin-top: 10px;
+                line-height: 1.5rem;
+                font-size: 1rem;
+              }
+
+              > .choose {
+                padding-top: 25px;
+              }
             }
+          }
+        }
+
+        @screen lg {
+          > article {
+            width: 1040px;
+            height: 510px;
+          }
         }
+      }
     }
+  }
 }

+ 158 - 0
src/main/frontend/components/onboarding/quick_tour.cljs

@@ -0,0 +1,158 @@
+(ns frontend.components.onboarding.quick-tour
+  (:require [promesa.core :as p]
+            [cljs-bean.core :as bean]
+            [frontend.loader :refer [load]]
+            [frontend.state :as state]
+            [frontend.date :as date]
+            [frontend.util :as util]
+            [frontend.handler.route :as router-handler]
+            [frontend.handler.command-palette :as command-palette]
+            [hiccups.runtime :as h]
+            [dommy.core :as d]))
+
+(defn js-load$
+  [url]
+  (p/create
+    (fn [resolve]
+      (load url resolve))))
+
+(def JS_ROOT
+  (if (= js/location.protocol "file:")
+    "./js"
+    "./static/js"))
+
+(defn- load-base-assets$
+  []
+  (js-load$ (str JS_ROOT "/shepherd.min.js")))
+
+(defn- make-skip-fns
+  [^js jsTour]
+  (let [^js el (js/document.createElement "button")]
+    (.add (.-classList el) "cp__onboarding-skip-quick-tour")
+    (set! (.-innerHTML el) (h/render-html [:span [:i.ti.ti-player-skip-forward] "Skip Quick Tour"]))
+    (.addEventListener el "click" #(.cancel jsTour))
+    [#(.appendChild js/document.body el)
+     #(.removeChild js/document.body el)]))
+
+(defn- wait-target
+  [fn-or-selector time]
+  (p/let [action (if (string? fn-or-selector)
+                   #(d/sel1 fn-or-selector)
+                   fn-or-selector)
+          _      (action)
+          _      (p/delay time)]))
+
+(defn- inject-steps-indicator
+  [current total]
+
+  (h/render-html
+    [:div.steps
+     [:strong (str "STEP " current)]
+     [:ul (for [i (range total)] [:li {:class (when (= current (inc i)) "active")} i])]]))
+
+(defn- create-steps! [^js jsTour]
+  [
+   ;; step 1
+   {:id                "nav-help"
+    :text              (h/render-html [:section [:h2 "❓ Help"]
+                                       [:p "You can always click here for help and other information about Logseq."]])
+    :attachTo          {:element ".cp__sidebar-help-btn" :on "top"}
+    :beforeShowPromise #(if (state/sub :ui/sidebar-open?)
+                          (wait-target state/hide-right-sidebar! 700)
+                          (p/resolved true))
+    :canClickTarget    true
+    :buttons           [{:text "Next" :action (.-next jsTour)}]
+    :popperOptions     {:modifiers [{:name    "preventOverflow"
+                                     :options {:padding 20}}
+                                    {:name    "offset"
+                                     :options {:offset [0, 10]}}]}}
+
+   ;; step 2
+   {:id                "nav-journal-page"
+    :text              (h/render-html [:section [:h2 "📆 Daily Journal Page"]
+                                       [:p
+                                        [:span "This is today’s daily journal page. Here yo can dump your thoughts, learnings and ideas. Don’t worry about organizing. Just write and"]
+                                        [:a "[[link]]"]
+                                        [:span "your thoughts."]]])
+
+    :attachTo          {:element ".page.is-journals .page-title" :on "top-end"}
+    :beforeShowPromise #(if-not (= (util/safe-lower-case (state/get-current-page))
+                                  (util/safe-lower-case (date/today)))
+                          (wait-target (fn []
+                                         (router-handler/redirect-to-page! (date/today))
+                                         (util/scroll-to-top)) 200)
+                          (p/resolved true))
+    :buttons           [{:text "Back" :classes "back" :action (.-back jsTour)}
+                        {:text "Next" :action (.-next jsTour)}]
+    :popperOptions     {:modifiers [{:name    "preventOverflow"
+                                     :options {:padding 63}}
+                                    {:name    "offset"
+                                     :options {:offset [10, 10]}}]}}
+
+   ;; step 3
+   {:id                "nav-left-sidebar"
+    :text              (h/render-html [:section [:h2 "👀 Left Sidebar"]
+                                       [:p [:span "Open the left sidebar to explore important menu items in Logseq."]]])
+
+    :attachTo          {:element "#left-menu" :on "top"}
+    :beforeShowPromise #(p/resolved true)
+    :buttons           [{:text "Back" :classes "back" :action (.-back jsTour)}
+                        {:text "Next" :action (.-next jsTour)}]
+    :popperOptions     {:modifiers [{:name    "preventOverflow"
+                                     :options {:padding 20}}
+                                    {:name    "offset"
+                                     :options {:offset [10, 10]}}]}}
+
+   ;; step 4
+   {:id                "nav-favorites"
+    :text              (h/render-html [:section [:h2 "⭐️ Favorites"]
+                                       [:p "Pin your favorite pages via the `... `menu on any page."]
+                                       [:p "We’ve also added some template pages here to help you get started. You can remove these once you start writing your own notes."]])
+    :beforeShowPromise #(if-not (state/sub :ui/left-sidebar-open?)
+                          (wait-target state/toggle-left-sidebar! 500)
+                          (p/resolved true))
+    :attachTo          {:element ".nav-content-item.favorites" :on "right"}
+    :buttons           [{:text "Back" :classes "back" :action (.-back jsTour)}
+                        {:text "Finish" :action (.-complete jsTour)}]}
+   ])
+
+(defn start
+  []
+  (let [^js jsTour (js/Shepherd.Tour.
+                     (bean/->js
+                       {:useModalOverlay    true
+                        :defaultStepOptions {:classes  "cp__onboarding-quick-tour"
+                                             :scrollTo false}}))
+        steps      (create-steps! jsTour)
+        steps      (map-indexed #(assoc %2 :text (str (:text %2) (inject-steps-indicator (inc %1) (count steps)))) steps)
+        [show-skip! hide-skip!] (make-skip-fns jsTour)]
+
+    ;; events
+    (doto jsTour
+      (.on "show" show-skip!)
+      (.on "hide" hide-skip!)
+      (.on "complete" hide-skip!)
+      (.on "cancel" hide-skip!))
+
+    (doseq [step steps]
+      (.addStep jsTour (bean/->js step)))
+
+    (.start jsTour)))
+
+(defn- ready
+  [callback]
+  (p/then
+    (if (nil? js/window.Shepherd)
+      (load-base-assets$) (p/resolved true))
+    callback))
+
+(def should-guide? false)
+
+(defn init []
+  (command-palette/register {:id     :document/quick-tour
+                             :desc   "Quick tour for onboarding"
+                             :action #(ready start)})
+
+  ;; TODO: fix logic
+  (when should-guide?
+    (ready start)))

+ 8 - 7
src/main/frontend/components/onboarding/setups.cljs

@@ -71,10 +71,10 @@
       [:section.a
        [:strong "Let’s get you set up."]
        [:small (str "Where on your " DEVICE " do you want to save your work?")
-        (when (mobile-util/is-native-platform?)
+        (when (mobile-util/native-platform?)
           (mobile-intro))]
 
-       (if (or (nfs/supported?) (mobile-util/is-native-platform?))
+       (if (or (nfs/supported?) (mobile-util/native-platform?))
          [:div.choose.flex.flex-col.items-center
           {:on-click #(page-handler/ls-dir-files!
                        (fn []
@@ -140,18 +140,19 @@
 
     (setups-container
      :importer
-     [:article.flex.flex-col.items-center.importer
+     [:article.flex.flex-col.items-center.importer.py-16.px-8
       [:section.c.text-center
        [:h1 "Do you already have notes that you want to import?"]
        [:h2 "If they are in a JSON or Markdown format Logseq can work with them."]]
       [:section.d.md:flex
-       [:label.action-input.flex.items-center
+       [:label.action-input.flex.items-center.mx-2.my-2
         {:disabled (or roam-importing? opml-importing?)}
         [:span.as-flex-center [:i (svg/roam-research 28)]]
-        [:span.flex.flex-col
+        [:div.flex.flex-col
          (if roam-importing?
            (ui/loading "Importing ...")
-           [[:strong "RoamResearch"]
+           [
+            [:strong "RoamResearch"]
             [:small "Import a JSON Export of your Roam graph"]])]
         [:input.absolute.hidden
          {:id        "import-roam"
@@ -172,7 +173,7 @@
                            (notification/show! "Please choose a JSON file."
                                                :error))))}]]
 
-       [:label.action-input.flex.items-center
+       [:label.action-input.flex.items-center.mx-2.my-2
         {:disabled (or roam-importing? opml-importing?)}
         [:span.as-flex-center (ui/icon "sitemap" {:style {:fontSize "26px"}})]
         [:span.flex.flex-col

+ 31 - 27
src/main/frontend/components/page.cljs

@@ -1,6 +1,6 @@
 (ns frontend.components.page
   (:require [clojure.string :as string]
-            [frontend.components.block :as block]
+            [frontend.components.block :as component-block]
             [frontend.components.content :as content]
             [frontend.components.editor :as editor]
             [frontend.components.hierarchy :as hierarchy]
@@ -15,7 +15,6 @@
             [frontend.db.model :as model]
             [frontend.extensions.graph :as graph]
             [frontend.extensions.pdf.assets :as pdf-assets]
-            [frontend.format.block :as format-block]
             [frontend.handler.common :as common-handler]
             [frontend.handler.config :as config-handler]
             [frontend.handler.editor :as editor-handler]
@@ -26,7 +25,7 @@
             [frontend.handler.route :as route-handler]
             [frontend.mixins :as mixins]
             [frontend.state :as state]
-            [frontend.text :as text]
+            [logseq.graph-parser.text :as text]
             [frontend.search :as search]
             [frontend.ui :as ui]
             [frontend.util :as util]
@@ -34,6 +33,8 @@
             [reitit.frontend.easy :as rfe]
             [medley.core :as medley]
             [rum.core :as rum]
+            [logseq.graph-parser.util :as gp-util]
+            [frontend.format.block :as block]
             [frontend.mobile.util :as mobile-util]))
 
 (defn- get-page-name
@@ -108,14 +109,21 @@
      [:a.add-button-link.block
       (ui/icon "circle-plus")]]]])
 
-(rum/defc page-blocks-cp < rum/reactive
-  db-mixins/query
+(rum/defc page-blocks-cp < rum/reactive db-mixins/query
+  {:will-mount (fn [state]
+                 (let [page-e (second (:rum/args state))
+                       page-name (:block/name page-e)]
+                   (when (and (db/journal-page? page-name)
+                              (>= (date/journal-title->int page-name)
+                                  (date/journal-title->int (date/today))))
+                     (state/pub-event! [:journal/insert-template page-name])))
+                 state)}
   [repo page-e {:keys [sidebar?] :as config}]
   (when page-e
     (let [page-name (or (:block/name page-e)
                         (str (:block/uuid page-e)))
-          block? (util/uuid-string? page-name)
-          block-id (and block? (uuid page-name))
+          block-id (parse-uuid page-name)
+          block? (boolean block-id)
           page-blocks (get-blocks repo page-name block-id)]
       (if (empty? page-blocks)
         (dummy-block page-name)
@@ -131,7 +139,7 @@
                               :document/mode? document-mode?}
                              config)
               hiccup-config (common-handler/config-with-document-mode hiccup-config)
-              hiccup (block/->hiccup page-blocks hiccup-config {})]
+              hiccup (component-block/->hiccup page-blocks hiccup-config {})]
           [:div
            (page-blocks-inner page-name page-blocks hiccup sidebar? block-id)
            (when-not config/publishing?
@@ -155,9 +163,9 @@
            (rum/with-key
              (ui/catch-error
               (ui/component-error "Failed default query:" {:content (pr-str query)})
-              (block/custom-query {:attr {:class "mt-10"}
-                                   :editor-box editor/box
-                                   :page page} query))
+              (component-block/custom-query {:attr {:class "mt-10"}
+                                             :editor-box editor/box
+                                             :page page} query))
              (str repo "-custom-query-" (:query query))))]))))
 
 (defn tagged-pages
@@ -169,7 +177,7 @@
         (ui/foldable
          [:h2.font-bold.opacity-50 (util/format "Pages tagged with \"%s\"" tag)]
          [:ul.mt-2
-          (for [[original-name name] (sort pages)]
+          (for [[original-name name] (sort-by last pages)]
             [:li {:key (str "tagged-page-" name)}
              [:a {:href (rfe/href :page {:name name})}
               original-name]])]
@@ -214,10 +222,9 @@
                          (reset! *edit? false)
                          (notification/show! "Illegal page name, can not rename!" :warning))
           blur-fn (fn [e]
-                    (when (util/wrapped-by-quotes? @*title-value)
-                      (swap! *title-value util/unquote-string)
+                    (when (gp-util/wrapped-by-quotes? @*title-value)
+                      (swap! *title-value gp-util/unquote-string)
                       (gobj/set (rum/deref input-ref) "value" @*title-value))
-                    (state/set-state! :editor/editing-page-title? false)
                     (cond
                       (= old-name @*title-value)
                       (reset! *edit? false)
@@ -252,7 +259,6 @@
                               (reset! *title-value old-name)
                               (reset! *edit? false)))}]]
         [:a.page-title {:on-mouse-down (fn [e]
-                                         (state/set-state! :editor/editing-page-title? true)
                                          (when (util/right-click? e)
                                            (state/set-state! :page-title/context {:page page-name})))
                         :on-click (fn [e]
@@ -262,8 +268,7 @@
                                         (state/sidebar-add-block!
                                          repo
                                          (:db/id page)
-                                         :page
-                                         {:page page}))
+                                         :page))
                                       (when (and (not hls-file?) (not fmt-journal?))
                                         (reset! *edit? true))))}
          [:h1.title.ls-page-title {:data-ref page-name}
@@ -310,8 +315,8 @@
     (let [current-repo (state/sub :git/current-repo)
           repo (or repo current-repo)
           page-name (util/page-name-sanity-lc path-page-name)
-          block? (util/uuid-string? page-name)
-          block-id (and block? (uuid page-name))
+          block-id (parse-uuid page-name)
+          block? (boolean block-id)
           format (let [page (if block-id
                               (:block/name (:block/page (db/entity [:block/uuid block-id])))
                               page-name)]
@@ -325,7 +330,7 @@
                       (db/entity repo))
                  (do
                    (when-not (db/entity repo [:block/name page-name])
-                     (let [m (format-block/page-name->map path-page-name true)]
+                     (let [m (block/page-name->map path-page-name true)]
                        (db/transact! repo [m])))
                    (db/pull [:block/name page-name])))
           {:keys [icon]} (:block/properties page)
@@ -350,7 +355,7 @@
        [:div.relative
         (when (and (not sidebar?) (not block?))
           [:div.flex.flex-row.space-between
-           (when (or (mobile-util/is-native-platform?) (util/mobile?))
+           (when (or (mobile-util/native-platform?) (util/mobile?))
              [:div.flex.flex-row.pr-2
               {:style {:margin-left -15}
                :on-mouse-over (fn [e]
@@ -370,7 +375,7 @@
            (let [config {:id "block-parent"
                          :block? true}]
              [:div.mb-4
-              (block/block-parents config repo block-id {:level-limit 3})]))
+              (component-block/breadcrumb config repo block-id {:level-limit 3})]))
 
          ;; blocks
          (let [page (if block?
@@ -705,7 +710,7 @@
          [:tr {:key name}
           [:td.n.w-12 [:span.opacity-70 (str (inc n) ".")]]
           [:td.name [:a {:href     (rfe/href :page {:name (:block/name page)})}
-                     (block/page-cp {} page)]]
+                     (component-block/page-cp {} page)]]
           [:td.backlinks [:span (or backlinks "0")]]
           (when-not orphaned-pages? [:td.created-at [:span (if created-at (date/int->local-time-2 created-at) "Unknown")]])
           (when-not orphaned-pages? [:td.updated-at [:span (if updated-at (date/int->local-time-2 updated-at) "Unknown")]])])]]
@@ -945,10 +950,9 @@
                                               (state/sidebar-add-block!
                                                repo
                                                (:db/id page)
-                                               :page
-                                               {:page (:block/name page)}))))
+                                               :page))))
                               :href     (rfe/href :page {:name (:block/name page)})}
-                          (block/page-cp {} page)]]
+                          (component-block/page-cp {} page)]]
 
                (when-not mobile?
                  [:td.backlinks [:span backlinks]])

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

@@ -257,10 +257,10 @@
 
 .ls-page-title {
   @apply rounded-sm;
-  
+
   padding: 5px 8px;
   margin: 0 -6px;
-  
+
   &.title {
     margin-bottom: 12px;
   }
@@ -274,11 +274,17 @@
   }
 }
 
+a.page-title {
+  padding: 0 8px;
+  margin-left: -12px;
+  transition: none;
+}
+
 html.is-native-android,
 html.is-native-ipad,
 html.is-native-iphone,
 html.is-native-iphone-without-notch {
-    
+
     .ls-page-title {
         margin: 0px 0px 24px -15px;
         padding: 0px;

+ 8 - 8
src/main/frontend/components/page_menu.cljs

@@ -84,14 +84,13 @@
                          (page-handler/unfavorite-page! page-original-name)
                          (page-handler/favorite-page! page-original-name)))}}
 
-          (when-not (mobile-util/is-native-platform?)
+          (when-not (mobile-util/native-platform?)
             {:title (t :page/presentation-mode)
              :options {:on-click (fn []
                                    (state/sidebar-add-block!
                                     repo
                                     (:db/id page)
-                                    :page-presentation
-                                    {:page page}))}})
+                                    :page-presentation))}})
 
           ;; TODO: In the future, we'd like to extract file-related actions
           ;; (such as open-in-finder & open-with-default-app) into a sub-menu of
@@ -102,11 +101,12 @@
               :options {:on-click #(js/window.apis.showItemInFolder file-path)}}
              {:title   (t :page/open-with-default-app)
               :options {:on-click #(js/window.apis.openPath file-path)}}])
-          
-          (when (util/electron?)
+
+          (when (or (util/electron?)
+                    (mobile-util/native-platform?))
             {:title   (t :page/copy-page-url)
-              :options {:on-click #(util/copy-to-clipboard!
-                                    (url-util/get-logseq-graph-page-url nil repo page-original-name))}})
+             :options {:on-click #(util/copy-to-clipboard!
+                                   (url-util/get-logseq-graph-page-url nil repo page-original-name))}})
 
           (when-not contents?
             {:title   (t :page/delete)
@@ -147,7 +147,7 @@
             (for [[_ {:keys [label] :as cmd} action pid] (state/get-plugins-commands-with-type :page-menu-item)]
               {:title label
                :options {:on-click #(commands/exec-plugin-simple-command!
-                                     pid (assoc cmd :page (state/get-current-page)) action)}}))
+                                     pid (assoc cmd :page page-name) action)}}))
 
           (when developer-mode?
             {:title   "(Dev) Show page data"

+ 163 - 59
src/main/frontend/components/plugins.cljs

@@ -8,6 +8,7 @@
             [frontend.search :as search]
             [frontend.util :as util]
             [frontend.mixins :as mixins]
+            [electron.ipc :as ipc]
             [promesa.core :as p]
             [frontend.components.svg :as svg]
             [frontend.components.plugins-settings :as plugins-settings]
@@ -16,62 +17,87 @@
             [clojure.string :as string]))
 
 (rum/defcs installed-themes
-  < rum/reactive
-    (rum/local 0 ::cursor)
-    (rum/local 0 ::total)
-    (mixins/event-mixin
-      (fn [state]
-        (let [*cursor (::cursor state)
-              *total (::total state)
-              ^js target (rum/dom-node state)]
-          (.focus target)
-          (mixins/on-key-down
-            state {38                                       ;; up
-                   (fn [^js _e]
-                     (reset! *cursor
-                             (if (zero? @*cursor)
-                               (dec @*total) (dec @*cursor))))
-                   40                                       ;; down
-                   (fn [^js _e]
-                     (reset! *cursor
-                             (if (= @*cursor (dec @*total))
-                               0 (inc @*cursor))))
-
-                   13                                       ;; enter
-                   #(when-let [^js active (.querySelector target ".is-active")]
-                      (.click active))
-                   }))))
+  <
+  (rum/local [] ::themes)
+  (rum/local 0 ::cursor)
+  (rum/local 0 ::total)
+  {:did-mount (fn [state] (let [*themes        (::themes state)
+                                *cursor        (::cursor state)
+                                *total         (::total state)
+                                mode           (state/sub :ui/theme)
+                                all-themes     (state/sub :plugin/installed-themes)
+                                themes         (->> all-themes
+                                                    (filter #(= (:mode %) mode))
+                                                    (sort-by #(:name %)))
+                                no-mode-themes (->> all-themes
+                                                    (filter #(= (:mode %) nil))
+                                                    (sort-by #(:name %))
+                                                    (map-indexed (fn [idx opt] (assoc opt :group-first (zero? idx) :group-desc (if (zero? idx) "light & dark themes" nil)))))
+                                selected       (state/sub :plugin/selected-theme)
+                                themes         (map-indexed (fn [idx opt]
+                                                              (let [selected? (= (:url opt) selected)]
+                                                                (when selected? (reset! *cursor (+ idx 1)))
+                                                                (assoc opt :mode mode :selected selected?))) (concat themes no-mode-themes))
+                                themes         (cons {:name        (string/join " " ["Default" (string/capitalize mode) "Theme"])
+                                                      :url         nil
+                                                      :description (string/join " " ["Logseq default" mode "theme."])
+                                                      :mode        mode
+                                                      :selected    (nil? selected)
+                                                      :group-first true
+                                                      :group-desc  (str mode " themes")} themes)]
+                            (reset! *themes themes)
+                            (reset! *total (count themes))
+                            state))}
+  (mixins/event-mixin
+   (fn [state]
+     (let [*cursor    (::cursor state)
+           *total     (::total state)
+           ^js target (rum/dom-node state)]
+       (.focus target)
+       (mixins/on-key-down
+        state {38                                       ;; up
+               (fn [^js _e]
+                 (reset! *cursor
+                         (if (zero? @*cursor)
+                           (dec @*total) (dec @*cursor))))
+               40                                       ;; down
+               (fn [^js _e]
+                 (reset! *cursor
+                         (if (= @*cursor (dec @*total))
+                           0 (inc @*cursor))))
+
+               13                                       ;; enter
+               #(when-let [^js active (.querySelector target ".is-active")]
+                  (.click active))}))))
   [state]
   (let [*cursor (::cursor state)
-        *total (::total state)
-        themes (state/sub :plugin/installed-themes)
-        selected (state/sub :plugin/selected-theme)
-        themes (cons {:name "Default Theme" :url nil :description "Logseq default light/dark theme."} themes)
-        themes (sort #(:selected %) (map #(assoc % :selected (= (:url %) selected)) themes))
-        _ (reset! *total (count themes))]
-
+        *themes (::themes state)]
     [:div.cp__themes-installed
      {:tab-index -1}
      [:h1.mb-4.text-2xl.p-1 (t :themes)]
      (map-indexed
-       (fn [idx opt]
-         (let [current-selected (:selected opt)
-               plg (get (:plugin/installed-plugins @state/state) (keyword (:pid opt)))]
+      (fn [idx opt]
+        (let [current-selected? (:selected opt)
+              group-first?      (:group-first opt)
+              plg               (get (:plugin/installed-plugins @state/state) (keyword (:pid opt)))]
+          [:div
+           {:key (str idx (:name opt))}
+           (when (and group-first? (not= idx 0)) [:hr.my-2])
            [:div.it.flex.px-3.py-1.5.rounded-sm.justify-between
-            {:key      (str idx (:url opt))
-             :title    (when current-selected "Cancel selected theme")
+            {:title    (:description opt)
              :class    (util/classnames
-                         [{:is-selected current-selected
-                           :is-active   (= idx @*cursor)}])
-             :on-click #(do (js/LSPluginCore.selectTheme (if current-selected nil (clj->js opt)))
+                        [{:is-selected current-selected?
+                          :is-active   (= idx @*cursor)}])
+             :on-click #(do (js/LSPluginCore.selectTheme (bean/->js opt))
                             (state/close-modal!))}
-            [:section
-             [:strong.block
-              [:small.opacity-60 (str (or (:name plg) "Logseq") " • ")]
-              (:name opt)]]
-            [:small.flex-shrink-0.flex.items-center.opacity-10
-             (when current-selected (ui/icon "check"))]]))
-       themes)]))
+            [:div.flex.items-center.text-xs
+             [:div.opacity-60 (str (or (:name plg) "Logseq") " •")]
+             [:div.name.ml-1 (:name opt)]]
+            (when (or group-first? current-selected?)
+              [:div.flex.items-center
+               (when group-first? [:small.opacity-60 (:group-desc opt)])
+               (when current-selected? [:small.inline-flex.ml-1.opacity-60 (ui/icon "check")])])]]))
+      @*themes)]))
 
 (rum/defc unpacked-plugin-loader
   [unpacked-pkg-path]
@@ -222,7 +248,7 @@
          :on-click #(when-not has-other-pending?
                       (plugin-handler/check-or-update-marketplace-plugin
                         (assoc item :only-check (not new-version))
-                        (fn [e] (notification/show! e :error))))}
+                        (fn [^js e] (notification/show! (.toString e) :error))))}
 
         (if installing-or-updating?
           (t :plugin/updating)
@@ -334,10 +360,69 @@
     :intent "logseq"
     :target "_blank"))
 
+(rum/defc user-proxy-settings-panel
+  [{:keys [protocol] :as agent-opts}]
+  (let [[opts set-opts!] (rum/use-state agent-opts)
+        [testing? set-testing?!] (rum/use-state false)
+        *test-input (rum/create-ref)
+        disabled? (string/blank? (:protocol opts))]
+    [:div.cp__settings-network-proxy-panel
+     [:h1.mb-2.text-2xl.font-bold (t :settings-page/network-proxy)]
+     [:div.p-2
+      [:p [:label [:strong (t :type)]
+           (ui/select [{:label "Disabled" :value "" :selected disabled?}
+                       {:label "http" :value "http" :selected (= protocol "http")}
+                       {:label "https" :value "https" :selected (= protocol "https")}
+                       {:label "socks5" :value "socks5" :selected (= protocol "socks5")}]
+                      #(set-opts!
+                         (assoc opts :protocol (if (= "disabled" (util/safe-lower-case %)) nil %))) nil)]]
+      [:p.flex
+       [:label.pr-4 [:strong (t :host)]
+        [:input.form-input.is-small
+         {:value     (:host opts) :disabled disabled?
+          :on-change #(set-opts!
+                        (assoc opts :host (util/trim-safe (util/evalue %))))}]]
+
+       [:label [:strong (t :port)]
+        [:input.form-input.is-small
+         {:value (:port opts) :type "number" :disabled disabled?
+          :on-change #(set-opts!
+                        (assoc opts :port (util/trim-safe (util/evalue %))))}]]]
+
+      [:hr]
+      [:p.flex.items-center.space-x-2
+       [:span.w-60
+        [:input.form-input.is-small
+         {:ref *test-input
+          :placeholder "http://"
+          :on-change #(set-opts!
+                        (assoc opts :test (util/trim-safe (util/evalue %))))
+          :value (:test opts)}]]
+
+       (ui/button (if testing? (ui/loading "Testing") "Test URL")
+         :intent "logseq" :large? false
+         :style {:margin-top 0 :padding "5px 15px"}
+         :on-click #(let [val (util/trim-safe (.-value (rum/deref *test-input)))]
+                      (when (and (not testing?) (not (string/blank? val)))
+                        (set-testing?! true)
+                        (-> (p/let [_ (ipc/ipc :setHttpsAgent opts)
+                                    _ (ipc/ipc :testProxyUrl val)])
+                          (p/catch (fn [e] (notification/show! (str e) :error)))
+                          (p/finally (fn [] (set-testing?! false)))))
+                      ))]
+
+      [:p.pt-2
+       (ui/button (t :save)
+         :on-click (fn []
+                     (p/let [_ (ipc/ipc :setHttpsAgent opts)]
+                       (state/set-state! [:electron/user-cfgs :settings/agent] opts)
+                       (state/close-sub-modal! :https-proxy-panel))))]]]))
+
 (rum/defc ^:large-vars/cleanup-todo panel-control-tabs < rum/static
   [search-key *search-key category *category
    sort-by *sort-by filter-by *filter-by
-   selected-unpacked-pkg market? develop-mode? reload-market-fn]
+   selected-unpacked-pkg market? develop-mode?
+   reload-market-fn agent-opts]
 
   (let [*search-ref (rum/create-ref)]
     [:div.mb-2.flex.justify-between.control-tabs.relative
@@ -357,6 +442,16 @@
          (unpacked-plugin-loader selected-unpacked-pkg)])]
 
      [:div.flex.items-center.r
+      ;; extra info
+      (let [{:keys [protocol host port]} agent-opts]
+        (when (every? not-empty [protocol host port])
+          (ui/button
+            [:span.flex.items-center.text-indigo-500
+             (ui/icon "world-download") (str protocol "://" host ":" port)]
+            :small? true
+            :intent "link"
+            :on-click #(state/pub-event! [:go/proxy-settings agent-opts]))))
+
       ;; search
       (panel-tab-search search-key *search-key *search-ref)
 
@@ -440,6 +535,10 @@
                     :options {:on-click #(reload-market-fn)}}]
                   [{:title   [:span (ui/icon "rotate-clockwise") (t :plugin/check-all-updates)]
                     :options {:on-click #(plugin-handler/check-enabled-for-updates (not= :plugins category))}}])
+
+                [{:title   [:span (ui/icon "world") (t :settings-page/network-proxy)]
+                  :options {:on-click #(state/pub-event! [:go/proxy-settings agent-opts])}}]
+
                 (when (state/developer-mode?)
                   [{:hr true}
                    {:title   [:span (ui/icon "file-code") "Open Preferences"]
@@ -481,6 +580,7 @@
         installing (state/sub :plugin/installing)
         online? (state/sub :network/online?)
         develop-mode? (state/sub :ui/developer-mode?)
+        agent-opts (state/sub [:electron/user-cfgs :settings/agent])
         *search-key (::search-key state)
         *category (::category state)
         *sort-by (::sort-by state)
@@ -526,20 +626,18 @@
        @*search-key *search-key
        @*category *category
        @*sort-by *sort-by @*filter-by *filter-by
-       nil true develop-mode? (::reload state))
+       nil true develop-mode? (::reload state)
+       agent-opts)
 
      (cond
        (not online?)
-       [:p.flex.justify-center.pt-20.opacity-50
-        (svg/offline 30)]
+       [:p.flex.justify-center.pt-20.opacity-50 (svg/offline 30)]
 
        @*fetching
-       [:p.flex.justify-center.pt-20
-        svg/loading]
+       [:p.flex.justify-center.pt-20 svg/loading]
 
        @*error
-       [:p.flex.justify-center.pt-20.opacity-50
-        "Remote error: " (.-message @*error)]
+       [:p.flex.justify-center.pt-20.opacity-50 "Remote error: " (.-message @*error)]
 
        :else
        [:div.cp__plugins-marketplace-cnt
@@ -568,6 +666,7 @@
         develop-mode? (state/sub :ui/developer-mode?)
         selected-unpacked-pkg (state/sub :plugin/selected-unpacked-pkg)
         coming-updates (state/sub :plugin/updates-coming)
+        agent-opts (state/sub [:electron/user-cfgs :settings/agent])
         *filter-by (::filter-by state)
         *sort-by (::sort-by state)
         *search-key (::search-key state)
@@ -611,7 +710,8 @@
        @*sort-by *sort-by
        @*filter-by *filter-by
        selected-unpacked-pkg
-       false develop-mode? nil)
+       false develop-mode? nil
+       agent-opts)
 
      [:div.cp__plugins-item-lists.grid-cols-1.md:grid-cols-2.lg:grid-cols-3
       (for [item sorted-plugins]
@@ -633,7 +733,7 @@
         updates (state/all-available-coming-updates)]
 
     [:div.cp__plugins-waiting-updates
-     [:h1.mb-4.text-2xl.p-1 (util/format "Found %s updates" (util/safe-parse-int (count updates)))]
+     [:h1.mb-4.text-2xl.p-1 (util/format "Found %s updates" (count updates))]
 
      (if (seq updates)
        ;; lists
@@ -680,7 +780,7 @@
              (if-let [n (state/get-next-selected-coming-update)]
                (plugin-handler/check-or-update-marketplace-plugin
                  (assoc n :only-check false)
-                 (fn [^js e] (notification/show! e :error)))
+                 (fn [^js e] (notification/show! (.toString e) :error)))
                (plugin-handler/close-updates-downloading)))
 
           :disabled
@@ -752,6 +852,10 @@
         market? (= active :marketplace)
         *el-ref (rum/create-ref)]
 
+    (rum/use-effect!
+      #(state/load-app-user-cfgs)
+      [])
+
     [:div.cp__plugins-page
      {:ref       *el-ref
       :tab-index "-1"}

+ 3 - 8
src/main/frontend/components/plugins.css

@@ -590,7 +590,7 @@
     outline: none;
     padding: 1rem;
 
-    > .it {
+    .it {
       user-select: none;
       background-color: var(--ls-secondary-background-color);
       border: 1px solid transparent;
@@ -598,13 +598,8 @@
       cursor: pointer;
       opacity: .8;
 
-      > section {
-        line-height: 1.1em;
-
-        > strong {
-          font-size: 13px;
-          font-weight: 600;
-        }
+      .name {
+        font-weight: 600;
       }
 
       &.is-active {

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

@@ -159,8 +159,7 @@
                                                            (state/sidebar-add-block!
                                                             (state/get-current-repo)
                                                             (:db/id item)
-                                                            :block-ref
-                                                            {:block item})))}
+                                                            :block-ref)))}
                    (when value
                      (if (= :element (first value))
                        (second value)

+ 28 - 28
src/main/frontend/components/reference.cljs

@@ -12,7 +12,6 @@
             [frontend.state :as state]
             [frontend.ui :as ui]
             [frontend.util :as util]
-            [medley.core :as medley]
             [rum.core :as rum]))
 
 (rum/defc filter-dialog-inner < rum/reactive
@@ -48,23 +47,21 @@
   (fn [close-fn]
     (filter-dialog-inner filters-atom close-fn references page-name)))
 
-(defn- block-with-ref-level
-  [block level]
-  (if (:block/children block)
-    (-> (update block :block/children
-                (fn [blocks]
-                  (map (fn [block]
-                         (let [level (inc level)
-                               block (assoc block :ref/level level)]
-                           (block-with-ref-level block level))) blocks)))
-        (assoc :ref/level level))
-    (assoc block :ref/level level)))
-
-(defn- blocks-with-ref-level
-  [page-blocks]
-  (map (fn [[page blocks]]
-         [page (map #(block-with-ref-level % 1) blocks)])
-    page-blocks))
+(rum/defc block-linked-references < rum/reactive db-mixins/query
+  [block-id]
+  (let [refed-blocks-ids (model-db/get-referenced-blocks-ids (str block-id))]
+    (when (seq refed-blocks-ids)
+      (let [ref-blocks (db/get-block-referenced-blocks block-id)
+            ref-hiccup (block/->hiccup ref-blocks
+                                       {:id (str block-id)
+                                        :ref? true
+                                        :breadcrumb-show? true
+                                        :group-by-page? true
+                                        :editor-box editor/box}
+                                       {})]
+        [:div.references-blocks
+         (content/content block-id
+                          {:hiccup ref-hiccup})]))))
 
 (rum/defcs references* < rum/reactive db-mixins/query
   (rum/local nil ::n-ref)
@@ -84,8 +81,7 @@
           default-collapsed? (>= (count refed-blocks-ids) threshold)
           filters-atom (get state ::filters)
           filter-state (rum/react filters-atom)
-          block? (util/uuid-string? page-name)
-          block-id (and block? (uuid page-name))
+          block-id (parse-uuid page-name)
           page-name (string/lower-case page-name)
           journal? (date/valid-journal-title? (string/capitalize page-name))
           scheduled-or-deadlines (when (and journal?
@@ -95,8 +91,8 @@
       (when (or (seq refed-blocks-ids)
                 (seq scheduled-or-deadlines)
                 (seq filter-state))
-        [:div.references.mt-6.flex-1.flex-row
-         [:div.content
+        [:div.references.flex-1.flex-row
+         [:div.content.pt-6
           (when (seq scheduled-or-deadlines)
             (ui/foldable
              [:h2.font-bold.opacity-50 "SCHEDULED AND DEADLINE"]
@@ -127,7 +123,8 @@
                                   references (->> (concat ref-pages references)
                                                   (remove nil?)
                                                   (distinct))]
-                              (state/set-modal! (filter-dialog filters-atom references page-name))))}
+                              (state/set-modal! (filter-dialog filters-atom references page-name)
+                                                {:center? true})))}
                (ui/icon "filter" {:class (cond
                                            (empty? filter-state)
                                            ""
@@ -144,10 +141,9 @@
                                   (db/get-block-referenced-blocks block-id)
                                   (db/get-page-referenced-blocks page-name))
                      filters (when (seq filter-state)
-                               (->> (group-by second filter-state)
-                                    (medley/map-vals #(map first %))))
-                     filtered-ref-blocks (->> (block-handler/filter-blocks repo ref-blocks filters true)
-                                              blocks-with-ref-level)
+                               (-> (group-by second filter-state)
+                                   (update-vals #(map first %))))
+                     filtered-ref-blocks (block-handler/filter-blocks repo ref-blocks filters true)
                      n-ref (apply +
                              (for [[_ rfs] filtered-ref-blocks]
                                (count rfs)))]
@@ -171,7 +167,11 @@
   [page-name]
   (ui/catch-error
    (ui/component-error "Linked References: Unexpected error")
-   (references* page-name)))
+   (ui/lazy-visible
+    (fn []
+      (references* page-name))
+    nil
+    {})))
 
 (rum/defcs unlinked-references-aux
   < rum/reactive db-mixins/query

+ 14 - 24
src/main/frontend/components/repo.cljs

@@ -14,10 +14,12 @@
             [reitit.frontend.easy :as rfe]
             [rum.core :as rum]
             [frontend.mobile.util :as mobile-util]
-            [frontend.text :as text]
+            [logseq.graph-parser.text :as text]
             [promesa.core :as p]
             [electron.ipc :as ipc]
-            [goog.object :as gobj]))
+            [goog.object :as gobj]
+            [frontend.components.encryption :as encryption]
+            [frontend.encrypt :as e]))
 
 (rum/defc add-repo
   [args]
@@ -42,7 +44,7 @@
        [:div.pl-1.content.mt-3
         [:div.flex.flex-row.my-4
          (when (or (nfs-handler/supported?)
-                   (mobile-util/is-native-platform?))
+                   (mobile-util/native-platform?))
            [:div.mr-8
             (ui/button
               (t :open-a-directory)
@@ -60,6 +62,11 @@
                     :href url}
                 (db/get-repo-path url)])
              [:div.controls
+              (when (e/encrypted-db? url)
+                [:a.control {:title "Show encryption information about this graph"
+                             :on-click (fn []
+                                         (state/set-modal! (encryption/encryption-dialog url)))}
+                 "🔐"])
               [:a.text-gray-400.ml-4.font-medium.text-sm
                {:title "No worries, unlink this graph will clear its cache only, it does not remove your files on the disk."
                 :on-click (fn []
@@ -95,7 +102,7 @@
                        (when (and nfs-repo?
                                   (not= current-repo config/local-repo)
                                   (or (nfs-handler/supported?)
-                                      (mobile-util/is-native-platform?)))
+                                      (mobile-util/native-platform?)))
                          {:title (t :sync-from-local-files)
                           :hover-detail (t :sync-from-local-files-detail)
                           :options {:on-click
@@ -116,24 +123,7 @@
                       :options (cond->
                                 {:on-click
                                  (fn []
-                                   (if @*multiple-windows?
-                                     (state/pub-event!
-                                      [:modal/show
-                                       [:div
-                                        [:p (t :re-index-multiple-windows-warning)]]])
-                                     (state/pub-event!
-                                      [:modal/show
-                                       [:div {:style {:max-width 700}}
-                                        [:p (t :re-index-discard-unsaved-changes-warning)]
-                                        (ui/button
-                                         (t :yes)
-                                         :autoFocus "on"
-                                         :large? true
-                                         :on-click (fn []
-                                                     (state/close-modal!)
-                                                     (repo-handler/re-index!
-                                                      nfs-handler/rebuild-index!
-                                                      page-handler/create-today-journal!)))]])))})}
+                                   (state/pub-event! [:graph/ask-for-re-index *multiple-windows?]))})}
         new-window-link (when (util/electron?)
                           {:title        (t :open-new-window)
                            :options {:on-click #(state/pub-event! [:graph/open-new-window nil])}})]
@@ -158,12 +148,12 @@
             render-content (fn [{:keys [toggle-fn]}]
                              (let [repo-path (db/get-repo-name current-repo)
                                    short-repo-name (db/get-short-repo-name repo-path)]
-                               [:a.item.group.flex.items-center.px-2.py-2.text-sm.font-medium.rounded-md
+                               [:a.item.group.flex.items-center.px-1.py-2.text-sm.font-medium.rounded-md
                                 {:on-click (fn []
                                              (check-multiple-windows? state)
                                              (toggle-fn))
                                  :title repo-path} ;; show full path on hover
-                                (ui/icon "database mr-3" {:style {:font-size 20} :id "database-icon"})
+                                (ui/icon "database mr-2" {:style {:font-size 16} :id "database-icon"})
                                 [:div.graphs
                                  [:span#repo-switch.block.pr-2.whitespace-nowrap
                                   [:span [:span#repo-name.font-medium short-repo-name]]

+ 30 - 40
src/main/frontend/components/right_sidebar.cljs

@@ -48,8 +48,18 @@
    (when-let [contents (db/entity [:block/name "contents"])]
      (page/contents-page contents))])
 
+(defn- block-with-breadcrumb
+  [repo block idx sidebar-key ref?]
+  (let [block-id (:block/uuid block)]
+    [[:div.mt-1 {:class (if ref? "ml-8" "ml-1")}
+      (block/breadcrumb {:id     "block-parent"
+                         :block? true
+                         :sidebar-key sidebar-key} repo block-id {})]
+     [:div.ml-2
+      (block-cp repo idx block)]]))
+
 (defn build-sidebar-item
-  [repo idx db-id block-type block-data t]
+  [repo idx db-id block-type]
   (case block-type
     :contents
     [(t :right-side-bar/contents)
@@ -64,33 +74,21 @@
 
     :block-ref
     #_:clj-kondo/ignore
-    (when-let [block (db/entity repo [:block/uuid (:block/uuid (:block block-data))])]
-      [(t :right-side-bar/block-ref)
-       (let [block (:block block-data)
-             block-id (:block/uuid block)
-             format (:block/format block)]
-         [[:div.ml-2.mt-1
-           (block/block-parents {:id     "block-parent"
-                                 :block? true} repo block-id {})]
-          [:div.ml-2
-           (block-cp repo idx block)]])])
+    (let [lookup (if (integer? db-id) db-id [:block/uuid db-id])]
+      (when-let [block (db/entity repo lookup)]
+       [(t :right-side-bar/block-ref)
+        (block-with-breadcrumb repo block idx [repo db-id block-type] true)]))
 
     :block
     #_:clj-kondo/ignore
-    (when-let [block (db/entity repo [:block/uuid (:block/uuid block-data)])]
-      (let [block-id (:block/uuid block-data)
-            format (:block/format block-data)]
-        [(block/block-parents {:id     "block-parent"
-                               :block? true} repo block-id {})
-         [:div.ml-2
-          (block-cp repo idx block-data)]]))
+    (let [lookup (if (integer? db-id) db-id [:block/uuid db-id])]
+      (when-let [block (db/entity repo lookup)]
+        (block-with-breadcrumb repo block idx [repo db-id block-type] false)))
 
     :page
-    (let [page-name (or (:block/name block-data)
-                        db-id)
-          page-name (if (integer? db-id)
-                      (:block/name (db/entity db-id))
-                      page-name)]
+    (when-let [page-name (if (integer? db-id)
+                           (:block/name (db/entity db-id))
+                           db-id)]
       [[:a.page-title {:href     (rfe/href :page {:name page-name})
                        :on-click (fn [e]
                                    (when (gobj/get e "shiftKey")
@@ -100,7 +98,7 @@
         (page-cp repo page-name)]])
 
     :page-presentation
-    (let [page-name (get-in block-data [:page :block/name])]
+    (let [page-name (:block/name (db/entity db-id))]
       [[:a {:href (rfe/href :page {:name page-name})}
         (db-model/get-page-original-name page-name)]
        [:div.ml-2.slide.mt-2
@@ -120,15 +118,8 @@
     svg/close]))
 
 (rum/defc sidebar-item < rum/reactive
-  [repo idx db-id block-type block-data t]
-
-  (let [item
-        (if (= :page block-type)
-          (let [lookup-ref (if (number? db-id) db-id [:block/name (util/page-name-sanity-lc db-id)])
-                page (db/query-entity-in-component lookup-ref)]
-            (when (seq page)
-              (build-sidebar-item repo idx db-id block-type page t)))
-          (build-sidebar-item repo idx db-id block-type block-data t))]
+  [repo idx db-id block-type]
+  (let [item (build-sidebar-item repo idx db-id block-type)]
     (when item
       (let [collapse? (state/sub [:ui/sidebar-collapsed-blocks db-id])]
         [:div.sidebar-item.content.color-level.px-4.shadow-lg
@@ -210,7 +201,7 @@
        [:div.cp__right-sidebar-settings.hide-scrollbar {:key "right-sidebar-settings"}
         [:div.ml-4.text-sm
          [:a.cp__right-sidebar-settings-btn {:on-click (fn [_e]
-                                                         (state/sidebar-add-block! repo "contents" :contents nil))}
+                                                         (state/sidebar-add-block! repo "contents" :contents))}
           (t :right-side-bar/contents)]]
 
         [:div.ml-4.text-sm
@@ -218,14 +209,13 @@
                                                          (when-let [page (get-current-page)]
                                                            (state/sidebar-add-block!
                                                             repo
-                                                            (str "page-graph-" page)
-                                                            :page-graph
-                                                            page)))}
+                                                            page
+                                                            :page-graph)))}
           (t :right-side-bar/page)]]
 
         [:div.ml-4.text-sm
          [:a.cp__right-sidebar-settings-btn {:on-click (fn [_e]
-                                                         (state/sidebar-add-block! repo "help" :help nil))}
+                                                         (state/sidebar-add-block! repo "help" :help))}
           (t :right-side-bar/help)]]]
 
        [:div.flex.align-items {:style {:z-index 999
@@ -234,9 +224,9 @@
 
       [:.sidebar-item-list.flex-1.scrollbar-spacing
        (if @*anim-finished?
-         (for [[idx [repo db-id block-type block-data]] (medley/indexed blocks)]
+         (for [[idx [repo db-id block-type]] (medley/indexed blocks)]
            (rum/with-key
-             (sidebar-item repo idx db-id block-type block-data t)
+             (sidebar-item repo idx db-id block-type)
              (str "sidebar-block-" idx)))
          [:div.p-4
           [:span.font-medium.opacity-50 "Loading ..."]])]]]))

+ 9 - 12
src/main/frontend/components/search.cljs

@@ -74,12 +74,12 @@
     [:div
      (when (not= search-mode :page)
        [:div {:class "mb-1" :key "parents"}
-        (block/block-parents {:id "block-search-block-parent"
-                              :block? true
-                              :search? true}
-                             repo
-                             (clojure.core/uuid uuid)
-                             {:indent? false})])
+        (block/breadcrumb {:id "block-search-block-parent"
+                           :block? true
+                           :search? true}
+                          repo
+                          (clojure.core/uuid uuid)
+                          {:indent? false})])
      [:div {:class "font-medium" :key "content"}
       (highlight-exact-query content q)]]))
 
@@ -157,8 +157,7 @@
         (state/sidebar-add-block!
          repo
          (:db/id page)
-         :page
-         {:page page})))
+         :page)))
 
     :block
     (let [block-uuid (uuid (:block/uuid data))
@@ -166,8 +165,7 @@
       (state/sidebar-add-block!
        repo
        (:db/id block)
-       :block
-       block))
+       :block))
 
     :new-page
     (page-handler/create! search-q)
@@ -325,8 +323,7 @@
                                  (state/sidebar-add-block!
                                   (state/get-current-repo)
                                   (:db/id page)
-                                  :page
-                                  {:page page}))))
+                                  :page))))
 
                             nil))
        :item-render (fn [{:keys [type data]}]

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

@@ -10,7 +10,7 @@
             [frontend.ui :as ui]
             [frontend.util :as util]
             [frontend.db :as db]
-            [frontend.text :as text]
+            [logseq.graph-parser.text :as text]
             [rum.core :as rum]
             [frontend.config :as config]
             [frontend.handler.repo :as repo-handler]

+ 36 - 42
src/main/frontend/components/settings.cljs

@@ -1,6 +1,7 @@
 (ns frontend.components.settings
   (:require [clojure.string :as string]
             [frontend.components.svg :as svg]
+            [frontend.components.plugins :as plugins]
             [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
             [frontend.storage :as storage]
@@ -131,10 +132,9 @@
                               :href     href
                               :on-click on-click))]
     (when-not (or (util/mobile?)
-                  (mobile-util/is-native-platform?))
+                  (mobile-util/native-platform?))
       [:div.text-sm desc])]])
 
-
 (defn edit-config-edn []
   (row-with-button-action
     {:left-label   (t :settings-page/custom-configuration)
@@ -151,6 +151,14 @@
      :on-click     #(js/setTimeout (fn [] (ui-handler/toggle-settings-modal!)))
      :-for         "customize_css"}))
 
+(defn edit-export-css []
+  (row-with-button-action
+   {:left-label   (t :settings-page/export-theme)
+    :button-label (t :settings-page/edit-export-css)
+    :href         (rfe/href :file {:path (config/get-export-css-path)})
+    :on-click     #(js/setTimeout (fn [] (ui-handler/toggle-settings-modal!)))
+    :-for         "customize_css"}))
+
 (defn show-brackets-row [t show-brackets?]
   [:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start
    [:label.block.text-sm.font-medium.leading-5.opacity-70
@@ -161,7 +169,7 @@
      (ui/toggle show-brackets?
                 config-handler/toggle-ui-show-brackets!
                 true)]]
-   (when (not (or (util/mobile?) (mobile-util/is-native-platform?)))
+   (when (not (or (util/mobile?) (mobile-util/native-platform?)))
      [:div {:style {:text-align "right"}}
       (ui/render-keyboard-shortcut (shortcut-helper/gen-shortcut-seq :ui/toggle-brackets))])])
 
@@ -390,6 +398,23 @@
 ;;             (let [value (not enable-block-timestamps?)]
 ;;               (config-handler/set-config! :feature/enable-block-timestamps? value)))))
 
+(defn encryption-row [t enable-encryption?]
+  (toggle "enable_encryption"
+          (t :settings-page/enable-encryption)
+          enable-encryption?
+          #(let [value (not enable-encryption?)]
+             (config-handler/set-config! :feature/enable-encryption? value)
+             (when value
+               (state/close-modal!)
+               (js/setTimeout (fn [] (state/pub-event! [:graph/ask-for-re-index (atom false)]))
+                              100)))
+          [:p.text-sm.opacity-50 "⚠️ This feature is experimental! "
+           [:span "You can use "]
+           [:a {:href "https://github.com/kanru/logseq-encrypt-ui"
+                :target "_blank"}
+            "logseq-encrypt-ui"]
+           [:span " to decrypt your graph."]]))
+
 (rum/defc keyboard-shortcuts-row [t]
   (row-with-button-action
     {:left-label   (t :settings-page/customize-shortcuts)
@@ -468,40 +493,6 @@
                     :on-click #(js/logseq.api.relaunch)
                     :small? true :intent "logseq")]])]))
 
-(rum/defc user-proxy-settings-panel
-  [{:keys [protocol] :as agent-opts}]
-  (let [[opts set-opts!] (rum/use-state agent-opts)
-        disabled? (string/blank? (:protocol opts))]
-    [:div.cp__settings-network-proxy-panel
-     [:h1.mb-2.text-2xl.font-bold (t :settings-page/network-proxy)]
-     [:div.p-2
-      [:p [:label [:strong (t :type)]
-           (ui/select [{:label "Disabled" :value "" :selected disabled?}
-                       {:label "http" :value "http" :selected (= protocol "http")}
-                       {:label "https" :value "https" :selected (= protocol "https")}
-                       {:label "socks5" :value "socks5" :selected (= protocol "socks5")}]
-                      #(set-opts!
-                        (assoc opts :protocol (if (= "disabled" (util/safe-lower-case %)) nil %))) nil)]]
-      [:p.flex
-       [:label.pr-4 [:strong (t :host)]
-        [:input.form-input.is-small
-         {:value     (:host opts) :disabled disabled?
-          :on-change #(set-opts!
-                       (assoc opts :host (util/trim-safe (util/evalue %))))}]]
-
-       [:label [:strong (t :port)]
-        [:input.form-input.is-small
-         {:value (:port opts) :type "number" :disabled disabled?
-          :on-change #(set-opts!
-                       (assoc opts :port (util/trim-safe (util/evalue %))))}]]]
-
-      [:p.pt-2
-       (ui/button (t :save)
-        :on-click (fn []
-                    (p/let [_ (ipc/ipc :setHttpsAgent opts)]
-                      (state/set-state! [:electron/user-cfgs :settings/agent] opts)
-                      (state/close-sub-modal! :https-proxy-panel))))]]]))
-
 (rum/defc user-proxy-settings
   [{:keys [protocol host port] :as agent-opts}]
   (ui/button [:span
@@ -509,7 +500,7 @@
                 [:strong.pr-1 e])
               (ui/icon "edit")]
              :on-click #(state/set-sub-modal!
-                         (fn [_] (user-proxy-settings-panel agent-opts))
+                         (fn [_] (plugins/user-proxy-settings-panel agent-opts))
                          {:id :https-proxy-panel :center? true})))
 
 (defn plugin-system-switcher-row []
@@ -535,6 +526,7 @@
      (theme-modes-row t switch-theme system-theme? dark?)
      (when current-repo (edit-config-edn))
      (when current-repo (edit-custom-css))
+     (when current-repo (edit-export-css))
      (keyboard-shortcuts-row t)]))
 
 (rum/defcs settings-editor < rum/reactive
@@ -544,6 +536,7 @@
         preferred-workflow (state/get-preferred-workflow)
         enable-timetracking? (state/enable-timetracking?)
         enable-journals? (state/enable-journals? current-repo)
+        enable-encryption? (state/enable-encryption? current-repo)
         enable-all-pages-public? (state/all-pages-public?)
         logical-outdenting? (state/logical-outdenting?)
         enable-tooltip? (state/enable-tooltip?)
@@ -559,9 +552,9 @@
      (show-brackets-row t show-brackets?)
      (when (util/electron?) (switch-spell-check-row t))
      (outdenting-row t logical-outdenting?)
-     (when-not (or (util/mobile?) (mobile-util/is-native-platform?))
+     (when-not (or (util/mobile?) (mobile-util/native-platform?))
        (shortcut-tooltip-row t enable-shortcut-tooltip?))
-     (when-not (or (util/mobile?) (mobile-util/is-native-platform?))
+     (when-not (or (util/mobile?) (mobile-util/native-platform?))
        (tooltip-row t enable-tooltip?))
      (timetracking-row t enable-timetracking?)
      (journal-row t enable-journals?)
@@ -578,6 +571,7 @@
             :on-key-press  (fn [e]
                              (when (= "Enter" (util/ekey e))
                                (update-home-page e)))}]]]])
+     (encryption-row t enable-encryption?)
      (enable-all-pages-public-row t enable-all-pages-public?)
      (zotero-settings-row t)
      (auto-push-row t current-repo enable-git-auto-push?)]))
@@ -610,7 +604,7 @@
     [:div.panel-wrap.is-advanced
      (when (and util/mac? (util/electron?)) (app-auto-update-row t))
      (usage-diagnostics-row t instrument-disabled?)
-     (when-not (mobile-util/is-native-platform?) (developer-mode-row t developer-mode?))
+     (when-not (mobile-util/native-platform?) (developer-mode-row t developer-mode?))
      (when (util/electron?) (plugin-system-switcher-row))
      (when (util/electron?) (https-user-agent-row https-agent-opts))
      (clear-cache-row t)
@@ -648,7 +642,7 @@
         (for [[label text icon]
               [[:general (t :settings-page/tab-general) (ui/icon "adjustments" {:style {:font-size 20}})]
                [:editor (t :settings-page/tab-editor) (ui/icon "writing" {:style {:font-size 20}})]
-               (when-not (mobile-util/is-native-platform?)
+               (when-not (mobile-util/native-platform?)
                  [:git (t :settings-page/tab-version-control) (ui/icon "history" {:style {:font-size 20}})])
                [:advanced (t :settings-page/tab-advanced) (ui/icon "bulb" {:style {:font-size 20}})]
                (when plugins-of-settings

+ 159 - 88
src/main/frontend/components/sidebar.cljs

@@ -4,53 +4,59 @@
             [frontend.components.command-palette :as command-palette]
             [frontend.components.header :as header]
             [frontend.components.journal :as journal]
+            [frontend.components.onboarding :as onboarding]
+            [frontend.components.plugins :as plugins]
             [frontend.components.repo :as repo]
             [frontend.components.right-sidebar :as right-sidebar]
+            [frontend.components.select :as select]
+            [frontend.components.svg :as svg]
             [frontend.components.theme :as theme]
             [frontend.components.widgets :as widgets]
-            [frontend.components.plugins :as plugins]
-            [frontend.components.select :as select]
             [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
             [frontend.db :as db]
-            [frontend.db.model :as db-model]
-            [frontend.components.svg :as svg]
             [frontend.db-mixins :as db-mixins]
+            [frontend.db.model :as db-model]
+            [frontend.extensions.pdf.assets :as pdf-assets]
+            [frontend.extensions.srs :as srs]
+            [frontend.handler.common :as common-handler]
             [frontend.handler.editor :as editor-handler]
-            [frontend.handler.route :as route-handler]
+            [frontend.handler.mobile.swipe :as swipe]
             [frontend.handler.page :as page-handler]
+            [frontend.handler.route :as route-handler]
             [frontend.handler.user :as user-handler]
             [frontend.mixins :as mixins]
+            [frontend.mobile.action-bar :as action-bar]
+            [frontend.mobile.footer :as footer]
+            [frontend.mobile.mobile-bar :refer [mobile-bar]]
+            [frontend.mobile.util :as mobile-util]
             [frontend.modules.shortcut.data-helper :as shortcut-dh]
             [frontend.state :as state]
             [frontend.ui :as ui]
             [frontend.util :as util]
-            [reitit.frontend.easy :as rfe]
+            [frontend.util.cursor :as cursor]
             [goog.dom :as gdom]
             [goog.object :as gobj]
-            [rum.core :as rum]
-            [frontend.extensions.srs :as srs]
-            [frontend.extensions.pdf.assets :as pdf-assets]
-            [frontend.mobile.util :as mobile-util]
-            [frontend.handler.mobile.swipe :as swipe]
-            [frontend.components.onboarding :as onboarding]
-            [frontend.mobile.footer :as footer]))
+            [react-draggable]
+            [reitit.frontend.easy :as rfe]
+            [rum.core :as rum]))
 
 (rum/defc nav-content-item
   [name {:keys [class]} child]
 
   [:div.nav-content-item.is-expand
    {:class class}
-   [:div.header.items-center.mb-1
-    {:on-click (fn [^js/MouseEvent e]
-                 (let [^js target (.-target e)
-                       ^js parent (.closest target ".nav-content-item")]
-                   (.toggle (.-classList parent) "is-expand")))}
-
-    [:div.font-medium.fade-link name]
-    [:span
-     [:a.more svg/arrow-down-v2]]]
-   [:div.bd child]])
+   [:div.nav-content-item-inner
+    [:div.header.items-center.mb-1
+     {:on-click (fn [^js/MouseEvent e]
+                  (let [^js target (.-target e)
+                        ^js parent (.closest target ".nav-content-item")]
+                    (.toggle (.-classList parent) "is-expand")))}
+
+     [:div.font-medium.fade-link name]
+     [:span
+      [:a.more svg/arrow-down-v2]]]
+    [:div.bd child]]])
 
 (defn- delta-y
   [e]
@@ -73,8 +79,7 @@
                            (state/sidebar-add-block!
                             (state/get-current-repo)
                             (:db/id page-entity)
-                            :page
-                            {:page page-entity}))
+                            :page))
                          (route-handler/redirect-to-page! name))))}
      [:span.page-icon icon]
      (pdf-assets/fix-local-asset-filename original-name)]))
@@ -95,6 +100,7 @@
         target (state/sub :favorites/dragging)]
     [:li.favorite-item
      {:key name
+      :title name
       :data-ref name
       :class (if (and target @dragging-over (not= target @dragging-over))
                "dragging-target"
@@ -119,10 +125,9 @@
 (rum/defc favorites < rum/reactive
   [t]
   (nav-content-item
-   [:a.flex.items-center.text-sm.font-medium.rounded-md
-    (ui/icon "star mr-1" {:style {:font-size 18}})
-    [:span.flex-1.ml-1 {:style {:padding-top 2}}
-     (t :left-side-bar/nav-favorites)]]
+   [:a.flex.items-center.text-sm.font-medium.rounded-md.wrap-th
+    (ui/icon "star mr-1" {:style {:font-size 16}})
+    [:span.flex-1.ml-1 (string/upper-case (t :left-side-bar/nav-favorites))]]
 
    {:class "favorites"
     :edit-fn
@@ -144,10 +149,10 @@
 (rum/defc recent-pages < rum/reactive db-mixins/query
   [t]
   (nav-content-item
-   [:a.flex.items-center.text-sm.font-medium.rounded-md
-    (ui/icon "history mr-2" {:style {:font-size 18}})
-    [:span.flex-1 {:style {:padding-top 2}}
-     (t :left-side-bar/nav-recent-pages)]]
+   [:a.flex.items-center.text-sm.font-medium.rounded-md.wrap-th
+    (ui/icon "history mr-2" {:style {:font-size 16}})
+    [:span.flex-1
+     (string/upper-case (t :left-side-bar/nav-recent-pages))]]
 
    {:class "recent"}
 
@@ -163,6 +168,7 @@
         (when-let [entity (db/entity [:block/name (util/safe-page-name-sanity-lc name)])]
           [:li.recent-item.select-none
            {:key name
+            :title name
             :data-ref name}
            (page-name name (get-page-icon entity))]))])))
 
@@ -170,10 +176,12 @@
   {:did-mount (fn [state]
                 (srs/update-cards-due-count!)
                 state)}
-  [state]
+  [_state srs-open?]
   (let [num (state/sub :srs/cards-due-count)]
-    [:a.item.group.flex.items-center.px-2.py-2.text-sm.font-medium.rounded-md {:on-click #(state/pub-event! [:modal/show-cards])}
-     (ui/icon "infinity mr-3" {:style {:font-size 20}})
+    [:a.item.group.flex.items-center.px-2.py-2.text-sm.font-medium.rounded-md
+     {:class (util/classnames [{:active srs-open?}])
+      :on-click #(state/pub-event! [:modal/show-cards])}
+     (ui/icon "infinity")
      [:span.flex-1 (t :right-side-bar/flashcards)]
      (when (and num (not (zero? num)))
        [:span.ml-3.inline-block.py-0.5.px-3.text-xs.font-medium.rounded-full.fade-in num])]))
@@ -194,18 +202,21 @@
     class :class
     title :title
     icon :icon
+    active :active
     href :href}]
   [:div
    {:class class}
    [:a.item.group.flex.items-center.px-2.py-2.text-sm.font-medium.rounded-md
     {:on-click on-click-handler
+     :class (when active "active")
      :href href}
-    (ui/icon (str icon " mr-3") {:style {:font-size 20}})
+    (ui/icon (str icon))
     [:span.flex-1 title]]])
 
 (rum/defc sidebar-nav
-  [_route-match close-modal-fn left-sidebar-open?]
-  (let [default-home (get-default-home-if-valid)]
+  [route-match close-modal-fn left-sidebar-open? srs-open?]
+  (let [default-home (get-default-home-if-valid)
+        route-name (get-in route-match [:data :name])]
 
     [:div.left-sidebar-inner.flex-1.flex.flex-col.min-h-0
      {:on-click #(when-let [^js target (and (util/sm-breakpoint?) (.-target %))]
@@ -213,62 +224,87 @@
                                [".favorites .bd" ".recent .bd" ".dropdown-wrapper" ".nav-header"])
                      (close-modal-fn)))}
      [:div.flex.flex-col.pb-4.wrap
-      [:nav.px-2.space-y-1 {:aria-label "Sidebar"}
+      [:nav.px-4.pt-1.space-y-1 {:aria-label "Sidebar"}
        (repo/repos-dropdown)
 
        [:div.nav-header
-        (if (:page default-home)
+        (if-let [page (:page default-home)]
           (sidebar-item
-           {:class            "home-nav"
-            :title            (:page default-home)
-            :on-click-handler route-handler/redirect-to-home!
-            :icon             "home"})
+            {:class            "home-nav"
+             :title            page
+             :on-click-handler route-handler/redirect-to-home!
+             :active           (and (not srs-open?)
+                                    (= route-name :page)
+                                    (= page (get-in route-match [:path-params :name])))
+             :icon             "home"})
           (sidebar-item
-           {:class            "journals-nav"
-            :title            (t :left-side-bar/journals)
-            :on-click-handler route-handler/go-to-journals!
-            :icon             "calendar"}))
+            {:class            "journals-nav"
+             :active           (and (not srs-open?)
+                                 (or (= route-name :all-journals) (= route-name :home)))
+             :title            (t :left-side-bar/journals)
+             :on-click-handler route-handler/go-to-journals!
+             :icon             "calendar"}))
 
         [:div.flashcards-nav
-         (flashcards)]
+         (flashcards srs-open?)]
 
         (sidebar-item
-         {:class "graph-view-nav"
-          :title (t :right-side-bar/graph-view)
-          :href  (rfe/href :graph)
-          :icon  "hierarchy"})
+          {:class  "graph-view-nav"
+           :title  (t :right-side-bar/graph-view)
+           :href   (rfe/href :graph)
+           :active (and (not srs-open?) (= route-name :graph))
+           :icon   "hierarchy"})
 
         (sidebar-item
-         {:class "all-pages-nav"
-          :title (t :right-side-bar/all-pages)
-          :href  (rfe/href :all-pages)
-          :icon  "files"})]]
+          {:class  "all-pages-nav"
+           :title  (t :right-side-bar/all-pages)
+           :href   (rfe/href :all-pages)
+           :active (and (not srs-open?) (= route-name :all-pages))
+           :icon   "files"})]]
 
       (favorites t)
 
       (when (and left-sidebar-open? (not config/publishing?)) (recent-pages t))
 
-      [:nav.px-2 {:aria-label "Sidebar"
-                  :class      "new-page"}
-       (when-not config/publishing?
-         [:a.item.group.flex.items-center.px-2.py-2.text-sm.font-medium.rounded-md
-          {:on-click (fn []
-                       (and (util/sm-breakpoint?)
-                            (state/toggle-left-sidebar!))
-                       (state/pub-event! [:go/search]))}
-          (ui/icon "circle-plus mr-3" {:style {:font-size 20}})
-          [:span.flex-1 (t :right-side-bar/new-page)]])]]]))
+      (when-not (mobile-util/native-platform?)
+       [:nav.px-2 {:aria-label "Sidebar"
+                   :class      "new-page"}
+        (when-not config/publishing?
+          [:a.item.group.flex.items-center.px-2.py-2.text-sm.font-medium.rounded-md.new-page-link
+           {:on-click (fn []
+                        (and (util/sm-breakpoint?)
+                             (state/toggle-left-sidebar!))
+                        (state/pub-event! [:go/search]))}
+           (ui/icon "circle-plus mr-3" {:style {:font-size 20}})
+           [:span.flex-1 (t :right-side-bar/new-page)]])])]]))
 
 (rum/defc left-sidebar < rum/reactive
   [{:keys [left-sidebar-open? route-match]}]
-  (let [close-fn #(state/set-left-sidebar-open! false)]
+  (let [close-fn #(state/set-left-sidebar-open! false)
+        srs-open? (= :srs (state/sub :modal/id))]
     [:div#left-sidebar.cp__sidebar-left-layout
      {:class (util/classnames [{:is-open left-sidebar-open?}])}
 
      ;; sidebar contents
-     (sidebar-nav route-match close-fn left-sidebar-open?)
+     (sidebar-nav route-match close-fn left-sidebar-open? srs-open?)
      [:span.shade-mask {:on-click close-fn}]]))
 
+(rum/defc recording-bar
+  []
+  [:> react-draggable
+   {:onStart (fn [_event]
+               (when-let [pos (some-> (state/get-input) cursor/pos)]
+                 (state/set-editor-last-pos! pos)))
+    :onStop (fn [_event]
+              (when-let [block (get-in @state/state [:editor/block :block/uuid])]
+                (editor-handler/edit-block! block :max (:block/uuid block))
+                (when-let [input (state/get-input)]
+                  (when-let [saved-cursor (state/get-editor-last-pos)]
+                    (cursor/move-cursor-to input saved-cursor)))))}
+   [:div#audio-record-toolbar
+    {:style {:bottom (+ @util/keyboard-height 45)}}
+    (footer/audio-record-cp)]])
+
 (rum/defc main <
   {:did-mount (fn [state]
                 (when-let [element (gdom/getElement "main-content-container")]
@@ -278,9 +314,10 @@
                    {:drop (fn [_e files]
                             (when-let [id (state/get-edit-input-id)]
                               (let [format (:block/format (state/get-edit-block))]
-                                (editor-handler/upload-asset id files format editor-handler/*asset-uploading? true))))}))
+                                (editor-handler/upload-asset id files format editor-handler/*asset-uploading? true))))})
+                  (common-handler/listen-to-scroll! element))
                 state)}
-  [{:keys [route-match global-graph-pages? route-name indexeddb-support? db-restoring? main-content]}]
+  [{:keys [route-match global-graph-pages? route-name indexeddb-support? db-restoring? main-content show-action-bar? show-recording-bar?]}]
   (let [left-sidebar-open? (state/sub :ui/left-sidebar-open?)
         onboarding-and-home? (and (or (nil? (state/get-current-repo)) (config/demo-graph?))
                                   (not config/publishing?)
@@ -294,12 +331,21 @@
 
      [:div#main-content-container.scrollbar-spacing.w-full.flex.justify-center.flex-row
 
+      (when show-action-bar?
+        (action-bar/action-bar))
+
       [:div.cp__sidebar-main-content
        {:data-is-global-graph-pages global-graph-pages?
         :data-is-full-width         (or global-graph-pages?
                                         (contains? #{:all-files :all-pages :my-publishing} route-name))}
 
-       (when (and (not (mobile-util/is-native-platform?))
+       (when show-recording-bar?
+         (recording-bar))
+
+       (mobile-bar)
+       (footer/footer)
+
+       (when (and (not (mobile-util/native-platform?))
                   (contains? #{:page :home} route-name))
          (widgets/demo-graph-alert))
 
@@ -342,6 +388,20 @@
                        (:current-parsing-file state))]]]]
     (ui/progress-bar-with-label width left-label (str finished "/" total))))
 
+(rum/defc file-sync-download-progress < rum/static
+  [state]
+  (let [finished (or (:finished state) 0)
+        total (:total state)
+        width (js/Math.round (* (.toFixed (/ finished total) 2) 100))
+        left-label [:div.flex.flex-row.font-bold
+                    "Downloading"
+                    [:div.hidden.md:flex.flex-row
+                     [:span.mr-1 ": "]
+                     [:ul
+                      (for [file (:downloading-files state)]
+                        [:li file])]]]]
+    (ui/progress-bar-with-label width left-label (str finished "/" total))))
+
 (rum/defc main-content < rum/reactive db-mixins/query
   {:init (fn [state]
            (when-not @sidebar-inited?
@@ -356,8 +416,10 @@
                          [db-id block-type] (if (= page "contents")
                                               ["contents" :contents]
                                               [page :page])]
-                     (state/sidebar-add-block! current-repo db-id block-type nil)))
+                     (state/sidebar-add-block! current-repo db-id block-type)))
                  (reset! sidebar-inited? true))))
+           (when (state/mobile?)
+                  (state/set-state! :mobile/show-tabbar? true))
            state)}
   []
   (let [default-home (get-default-home-if-valid)
@@ -365,8 +427,16 @@
         loading-files? (when current-repo (state/sub [:repo/loading-files? current-repo]))
         journals-length (state/sub :journals-length)
         latest-journals (db/get-latest-journals (state/get-current-repo) journals-length)
-        graph-parsing-state (state/sub [:graph/parsing-state current-repo])]
+        graph-parsing-state (state/sub [:graph/parsing-state current-repo])
+        graph-file-sync-download-init-state (state/sub [:file-sync/download-init-progress current-repo])]
     (cond
+      (or
+       (:downloading? graph-file-sync-download-init-state)
+       (not= (:total graph-file-sync-download-init-state) (:finished graph-file-sync-download-init-state)))
+      [:div.flex.items-center.justify-center.full-height-without-header
+       [:div.flex-1
+        (file-sync-download-progress graph-file-sync-download-init-state)]]
+
       (or
        (:graph-loading? graph-parsing-state)
        (not= (:total graph-parsing-state) (:finished graph-parsing-state)))
@@ -396,7 +466,7 @@
 
          ;; FIXME: why will this happen?
          :else
-         [:div ""])])))
+         [:div])])))
 
 (rum/defc custom-context-menu < rum/reactive
   []
@@ -429,15 +499,17 @@
   []
   (when-not (state/sub :ui/sidebar-open?)
     [:div.cp__sidebar-help-btn
-     {:title (t :help-shortcut-title)
-      :on-click (fn []
-                  (state/sidebar-add-block! (state/get-current-repo) "help" :help nil))}
-     "?"]))
+     [:div.inner
+      {:title    (t :help-shortcut-title)
+       :on-click (fn []
+                   (state/sidebar-add-block! (state/get-current-repo) "help" :help))}
+      "?"]]))
 
 (defn- hide-context-menu-and-clear-selection
   [e]
   (state/hide-custom-context-menu!)
-  (when-not (gobj/get e "shiftKey")
+  (when-not (or (gobj/get e "shiftKey")
+                (util/meta-key? e))
     (editor-handler/clear-selection!)))
 
 (rum/defcs ^:large-vars/cleanup-todo sidebar <
@@ -480,7 +552,9 @@
         home? (= :home route-name)
         edit? (:editor/editing? @state/state)
         default-home (get-default-home-if-valid)
-        logged? (user-handler/logged-in?)]
+        logged? (user-handler/logged-in?)
+        show-action-bar? (state/sub :mobile/show-action-bar?)
+        show-recording-bar? (state/sub :mobile/show-recording-bar?)]
     (theme/container
      {:t             t
       :theme         theme
@@ -522,12 +596,9 @@
                :indexeddb-support?  indexeddb-support?
                :light?              light?
                :db-restoring?       db-restoring?
-               :main-content        main-content})
-
-        (when (and (mobile-util/is-native-platform?)
-                   current-repo
-                   (not (state/sub :modal/show?)))
-          (footer/footer))]
+               :main-content        main-content
+               :show-action-bar?    show-action-bar?
+               :show-recording-bar? show-recording-bar?})]
 
        (right-sidebar/sidebar)
 

+ 73 - 37
src/main/frontend/components/sidebar.css

@@ -30,17 +30,20 @@
 }
 
 #main-container {
+    /* Hack: (overflow-y) to fix sticky header not working */
+    /* To reproduce: quick creating blocks */
+    overflow-y: hidden;
     position: relative;
     height: 100%;
     transition: padding-left .3s;
 
-    &.is-left-sidebar-open {
-        padding-left: 0;
+  &.is-left-sidebar-open {
+    padding-left: 0;
 
-        @screen sm {
-            padding-left: var(--ls-left-sidebar-width);
-        }
+    @screen sm {
+      padding-left: var(--ls-left-sidebar-width);
     }
+  }
 }
 
 #main-content {
@@ -60,11 +63,13 @@
   width: var(--ls-left-sidebar-sm-width);
   overflow-x: hidden;
   overflow-y: auto;
-  background-color: var(--ls-primary-background-color);
+  background-color: var(--ls-secondary-background-color);
   transition: transform .3s;
   transform: translateX(-100%);
   z-index: 3;
 
+  -webkit-font-smoothing: antialiased;
+
   > .wrap {
     height: calc(100vh - var(--ls-headbar-inner-top-padding) - 50px);
     margin-top: 40px;
@@ -77,36 +82,53 @@
   }
 
   .page-icon {
-    margin-right: 8px;
-    width: 1.1em;
+    width: 1em;
     text-align: center;
     display: inline-block;
     color: #aaa;
+    visibility: hidden;
   }
 
   a.item {
     user-select: none;
-    transition: none;
+    transition: background-color .3s;
+    margin-bottom: 2px;
+
+    > .ti {
+      font-size: 16px;
+      margin-right: 8px;
+      opacity: .6;
+      position: relative;
+      top: -1px;
+    }
 
-    > span {
-      margin-top: 3px;
+    &.active, &:active {
+      background-color: var(--ls-quaternary-background-color);
     }
 
     &:hover {
-      background-color: var(--ls-quaternary-background-color);
+      opacity: .8;
+
+      .ti {
+        opacity: .8;
+      }
     }
   }
 
   .nav-content-item {
-    padding: 18px 0 0 0;
+    margin-top: 14px;
 
-    > .header {
+    &-inner {
+      border-radius: 8px;
+    }
+
+    .header {
       display: flex;
       justify-content: space-between;
       align-items: center;
       user-select: none;
       cursor: pointer;
-      padding: 4px 18px;
+      padding: 4px 25px;
 
       > span {
         > a {
@@ -149,9 +171,17 @@
       &:active {
         opacity: .8;
       }
+
+      .wrap-th {
+        > span {
+          font-size: 11px;
+          font-weight: 600;
+          padding-top: 2px;
+        }
+      }
     }
 
-    > .bd {
+    .bd {
       display: none;
 
       ul {
@@ -161,12 +191,13 @@
 
         a {
           width: 100%;
-          padding: 2px 20px;
+          padding: 2px 18px;
           display: block;
           text-overflow: ellipsis;
           overflow: hidden;
           white-space: nowrap;
           color: var(--ls-primary-text-color);
+          transition: background-color .3s;
 
           &:hover {
             background-color: var(--ls-quaternary-background-color);
@@ -180,13 +211,13 @@
     }
 
     &.is-expand {
-      > .header > span > a {
+      .header > span > a {
         &:last-child {
           transform: translateY(2px) translateX(-3px);
         }
       }
 
-      > .bd {
+      .bd {
         display: block;
       }
     }
@@ -194,16 +225,18 @@
 
   .new-page {
     position: absolute;
-    background-color: var(--ls-primary-background-color);
     bottom: 0;
     left: 0;
     width: 100%;
-    padding-bottom: 8px;
-    padding-top: 8px;
+    padding: 14px;
+
+    &-link {
+      background-color: var(--ls-primary-background-color);
+      box-shadow: 0 1px 2px rgba(16, 24, 40, 0.05);
+    }
   }
 
   @screen sm {
-    background-color: var(--ls-secondary-background-color);
     width: var(--ls-left-sidebar-width);
     padding-top: 0;
 
@@ -212,7 +245,9 @@
     }
 
     .new-page {
-      background-color: var(--ls-secondary-background-color);
+      &-link {
+        background-color: var(--ls-primary-background-color);
+      }
     }
   }
 }
@@ -299,9 +334,9 @@
 }
 
 .ls-wide-mode {
- .cp__sidebar-main-content {
-   max-width: var(--ls-main-content-max-width-wide);
- }
+  .cp__sidebar-main-content {
+    max-width: var(--ls-main-content-max-width-wide);
+  }
 }
 
 html[data-theme='dark'] {
@@ -344,14 +379,15 @@ html[data-theme='dark'] {
   }
 
   &-btn {
-    @apply font-bold fixed bottom-4
-    rounded-full h-8 w-8 flex items-center justify-center font-bold
-    opacity-70 hover:opacity-100;
+    @apply fixed bottom-4 right-8;
 
-    user-select: none;
-    cursor: help;
-    right: 24px;
-    background-color: var(--ls-secondary-background-color);
+    > .inner {
+      @apply font-bold
+      rounded-full h-8 w-8 flex items-center justify-center font-bold
+      opacity-70 hover:opacity-100 select-none cursor-help;
+
+      background-color: var(--ls-secondary-background-color);
+    }
   }
 }
 
@@ -365,8 +401,8 @@ html[data-theme='dark'] {
   }
 
   &.open {
-      width: var(--ls-right-sidebar-width);
-      max-width: 60vw;
+    width: var(--ls-right-sidebar-width);
+    max-width: 60vw;
   }
 
   &-scollable {
@@ -476,5 +512,5 @@ html[data-theme='dark'] {
 }
 
 .full-height-without-header {
-    height: calc(100vh - var(--ls-headbar-height) - 4rem);
+  height: calc(100vh - var(--ls-headbar-height) - 4rem);
 }

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
src/main/frontend/components/svg.cljs


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

@@ -24,6 +24,7 @@
         (if (= theme "dark") ;; for tailwind dark mode
           (.add cls "dark")
           (.remove cls "dark"))
+        (ui/apply-custom-theme-effect! theme)
         (plugin-handler/hook-plugin-app :theme-mode-changed {:mode theme} nil))
      [theme])
 

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

@@ -12,8 +12,8 @@
   []
   [:div.flex.flex-col
    [:h1.title (t :on-boarding/add-graph)]
-   (let [nfs-supported? (or (nfs/supported?) (mobile-util/is-native-platform?))]
-     (if (mobile-util/is-native-platform?)
+   (let [nfs-supported? (or (nfs/supported?) (mobile-util/native-platform?))]
+     (if (mobile-util/native-platform?)
        [:div.text-sm
         (ui/button "Open a local directory"
           :on-click #(page-handler/ls-dir-files! shortcut/refresh!))

+ 33 - 22
src/main/frontend/config.cljs

@@ -4,6 +4,7 @@
             [frontend.state :as state]
             [frontend.util :as util]
             [shadow.resource :as rc]
+            [logseq.graph-parser.util :as gp-util]
             [frontend.mobile.util :as mobile-util]))
 
 (goog-define DEV-RELEASE false)
@@ -22,11 +23,12 @@
 ;; (goog-define LOGIN-URL
 ;;              "https://logseq.auth.us-east-1.amazoncognito.com/login?client_id=7ns5v1pu8nrbs04rvdg67u4a7c&response_type=code&scope=email+openid+phone&redirect_uri=logseq%3A%2F%2Fauth-callback")
 ;; (goog-define API-DOMAIN "api-prod.logseq.com")
+;; (goog-define WS-URL "wss://b2rp13onu2.execute-api.us-east-1.amazonaws.com/production?graphuuid=%s")
 
 ;; dev env
 (goog-define FILE-SYNC-PROD? false)
 (goog-define LOGIN-URL
-             "https://logseq-test.auth.us-east-2.amazoncognito.com/login?client_id=4fi79en9aurclkb92e25hmu9ts&response_type=code&scope=email+openid+phone&redirect_uri=logseq%3A%2F%2Fauth-callback")
+             "https://logseq-test2.auth.us-east-2.amazoncognito.com/login?client_id=3ji1a0059hspovjq5fhed3uil8&response_type=code&scope=email+openid+phone&redirect_uri=logseq%3A%2F%2Fauth-callback")
 (goog-define API-DOMAIN "api.logseq.com")
 (goog-define WS-URL "wss://og96xf1si7.execute-api.us-east-2.amazonaws.com/production?graphuuid=%s")
 
@@ -83,6 +85,15 @@
      config-formats
      #{:gif :svg :jpeg :ico :png :jpg :bmp :webp})))
 
+(defn doc-formats
+  []
+  (let [config-formats (some->> (get-in @state/state [:config :document-formats])
+                                (map :keyword)
+                                (set))]
+    (set/union
+     config-formats
+     #{:doc :docx :xls :xlsx :ppt :pptx :one :pdf :epub})))
+
 (def audio-formats #{:mp3 :ogg :mpeg :wav :m4a :flac :wma :aac})
 
 (def media-formats (set/union (img-formats) audio-formats))
@@ -95,14 +106,6 @@
   (set/union (text-formats)
              (img-formats)))
 
-;; TODO: rename
-(defonce mldoc-support-formats
-  #{:org :markdown :md})
-
-(defn mldoc-support?
-  [format]
-  (contains? mldoc-support-formats (keyword format)))
-
 (def mobile?
   (when-not util/node-test?
     (util/safe-re-find #"Mobi" js/navigator.userAgent)))
@@ -254,7 +257,6 @@
 
 (defonce default-journals-directory "journals")
 (defonce default-pages-directory "pages")
-(defonce default-draw-directory "draws")
 
 (defn get-pages-directory
   []
@@ -264,10 +266,6 @@
   []
   (or (state/get-journals-directory) default-journals-directory))
 
-(defn draw?
-  [path]
-  (util/starts-with? path default-draw-directory))
-
 (defonce local-repo "local")
 
 (defn demo-graph?
@@ -276,11 +274,12 @@
   ([graph]
    (= graph local-repo)))
 
-(defonce local-assets-dir "assets")
 (defonce recycle-dir ".recycle")
 (def config-file "config.edn")
 (def custom-css-file "custom.css")
+(def export-css-file "export.css")
 (def custom-js-file "custom.js")
+(def metadata-file "metadata.edn")
 (def pages-metadata-file "pages-metadata.edn")
 
 (def config-default-content (rc/inline "config.edn"))
@@ -299,10 +298,6 @@
   (and (string? s)
        (string/starts-with? s local-db-prefix)))
 
-(defn local-asset?
-  [s]
-  (util/safe-re-find (re-pattern (str "^[./]*" local-assets-dir)) s))
-
 (defn get-local-asset-absolute-path
   [s]
   (str "/" (string/replace s #"^[./]*" "")))
@@ -321,7 +316,7 @@
     (and (util/electron?) (local-db? repo-url))
     (get-local-dir repo-url)
 
-    (and (mobile-util/is-native-platform?) (local-db? repo-url))
+    (and (mobile-util/native-platform?) (local-db? repo-url))
     (let [dir (get-local-dir repo-url)]
       (if (string/starts-with? dir "file:")
         dir
@@ -334,7 +329,7 @@
 
 (defn get-repo-path
   [repo-url path]
-  (if (and (or (util/electron?) (mobile-util/is-native-platform?))
+  (if (and (or (util/electron?) (mobile-util/native-platform?))
            (local-db? repo-url))
     path
     (util/node-path.join (get-repo-dir repo-url) path)))
@@ -368,7 +363,7 @@
 
                  :else
                  relative-path)]
-      (util/path-normalize path))))
+      (gp-util/path-normalize path))))
 
 (defn get-config-path
   ([]
@@ -377,6 +372,13 @@
    (when repo
      (get-file-path repo (str app-name "/" config-file)))))
 
+(defn get-metadata-path
+  ([]
+   (get-metadata-path (state/get-current-repo)))
+  ([repo]
+   (when repo
+     (get-file-path repo (str app-name "/" metadata-file)))))
+
 (defn get-pages-metadata-path
   ([]
    (get-pages-metadata-path (state/get-current-repo)))
@@ -392,6 +394,15 @@
      (get-file-path repo
                     (str app-name "/" custom-css-file)))))
 
+(defn get-export-css-path
+  ([]
+   (get-export-css-path (state/get-current-repo)))
+  ([repo]
+   (when repo
+     (get-file-path repo
+                    (str app-name "/" export-css-file)))))
+
+
 (defn get-custom-js-path
   ([]
    (get-custom-js-path (state/get-current-repo)))

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

@@ -5,9 +5,10 @@
             [cljs-time.core :as t]
             [cljs-time.format :as tf]
             [cljs-time.local :as tl]
-            [clojure.string :as string]
             [frontend.state :as state]
             [frontend.util :as util]
+            [logseq.graph-parser.util :as gp-util]
+            [logseq.graph-parser.date-time-util :as date-time-util]
             [goog.object :as gobj]
             [lambdaisland.glogi :as log]))
 
@@ -16,11 +17,6 @@
   (when (string? s)
     ((gobj/get chrono "parseDate") s)))
 
-(defn format
-  [date]
-  (when-let [formatter-string (state/get-date-formatter)]
-    (tf/unparse (tf/formatter formatter-string) date)))
-
 (def custom-formatter (tf/formatter "yyyy-MM-dd'T'HH:mm:ssZZ"))
 
 (defn journal-title-formatters
@@ -55,13 +51,6 @@
      "yyyy年MM月dd日"}
    (state/get-date-formatter)))
 
-;; (tf/parse (tf/formatter "dd.MM.yyyy") "2021Q4") => 20040120T000000
-(defn safe-journal-title-formatters
-  []
-  (->> [(state/get-date-formatter) "yyyy-MM-dd" "yyyy_MM_dd"]
-       (remove string/blank?)
-       distinct))
-
 (defn get-date-time-string
   ([]
    (get-date-time-string (t/now)))
@@ -115,7 +104,7 @@
   ([]
    (journal-name (tl/local-now)))
   ([date]
-   (format date)))
+   (date-time-util/format date (state/get-date-formatter))))
 
 (defn journal-name-s [s]
   (try
@@ -183,34 +172,19 @@
 (defn valid-journal-title?
   [title]
   (and title
-       (valid? (util/capitalize-all title))))
+       (valid? (gp-util/capitalize-all title))))
 
 (defn journal-title->
   ([journal-title then-fn]
-   (journal-title-> journal-title then-fn (safe-journal-title-formatters)))
+   (journal-title-> journal-title then-fn (date-time-util/safe-journal-title-formatters (state/get-date-formatter))))
   ([journal-title then-fn formatters]
-   (when-not (string/blank? journal-title)
-     (when-let [time (->> (map
-                            (fn [formatter]
-                              (try
-                                (tf/parse (tf/formatter formatter) (util/capitalize-all journal-title))
-                                (catch js/Error _e
-                                  nil)))
-                            formatters)
-                          (filter some?)
-                          first)]
-       (then-fn time)))))
+   (date-time-util/journal-title-> journal-title then-fn formatters)))
 
 (defn journal-title->int
   [journal-title]
-  (when journal-title
-    (let [journal-title (util/capitalize-all journal-title)]
-      (journal-title-> journal-title #(util/parse-int (tf/unparse (tf/formatter "yyyyMMdd") %))))))
-
-(defn int->journal-title
-  [day]
-  (when day
-    (format (tf/parse (tf/formatter "yyyyMMdd") (str day)))))
+  (date-time-util/journal-title->int
+   journal-title
+   (date-time-util/safe-journal-title-formatters (state/get-date-formatter))))
 
 (defn journal-day->ts
   [day]
@@ -231,9 +205,16 @@
                     default-journal-title-formatter)]
     (journal-title-> journal-title #(tf/unparse formatter %))))
 
+(defn date->file-name
+  [date]
+  (let [formatter (if-let [format (state/get-journal-file-name-format)]
+                    (tf/formatter format)
+                    default-journal-title-formatter)]
+    (tf/unparse formatter date)))
+
 (defn journal-title->custom-format
   [journal-title]
-  (journal-title-> journal-title format))
+  (journal-title-> journal-title #(date-time-util/format % (state/get-date-formatter))))
 
 (defn int->local-time-2
   [n]

+ 4 - 10
src/main/frontend/db.cljs

@@ -115,8 +115,8 @@
 
 ;; only save when user's idle
 
-;; TODO: pass as a parameter
-(defonce *sync-search-indice-f (atom nil))
+(def *db-listener (atom nil))
+
 (defn- repo-listen-to-tx!
   [repo conn]
   (d/listen! conn :persistence
@@ -137,14 +137,8 @@
                      (state/set-last-transact-time! repo (util/time-ms))
                      (persist-if-idle! repo)))
 
-                 ;; rebuild search indices
-                 (let [data (:tx-data tx-report)
-                       datoms (filter
-                               (fn [datom]
-                                 (contains? #{:block/name :block/content} (:a datom)))
-                               data)]
-                   (when-let [f @*sync-search-indice-f]
-                     (f datoms)))))))
+                 (when-let [db-listener @*db-listener]
+                   (db-listener repo tx-report))))))
 
 (defn listen-and-persist!
   [repo]

+ 4 - 3
src/main/frontend/db/conn.cljs

@@ -7,7 +7,8 @@
             [frontend.mobile.util :as mobile-util]
             [frontend.state :as state]
             [frontend.config :as config]
-            [frontend.text :as text]
+            [logseq.graph-parser.text :as text]
+            [logseq.graph-parser.util :as gp-util]
             [datascript.core :as d]))
 
 (defonce conns (atom {}))
@@ -22,7 +23,7 @@
 (defn get-repo-name
   [repo]
   (cond
-    (mobile-util/is-native-platform?)
+    (mobile-util/native-platform?)
     (text/get-graph-name-from-path repo)
 
     (config/local-db? repo)
@@ -35,7 +36,7 @@
   "repo-path: output of `get-repo-name`"
   [repo-path]
   (if (or (util/electron?)
-          (mobile-util/is-native-platform?))
+          (mobile-util/native-platform?))
     (text/get-file-basename repo-path)
     repo-path))
 

+ 2 - 3
src/main/frontend/db/debug.cljs

@@ -1,6 +1,5 @@
 (ns frontend.db.debug
-  (:require [medley.core :as medley]
-            [frontend.db.utils :as db-utils]
+  (:require [frontend.db.utils :as db-utils]
             [frontend.db :as db]
             [datascript.core :as d]
             [frontend.util :as util]))
@@ -8,7 +7,7 @@
 ;; shortcut for query a block with string ref
 (defn qb
   [string-id]
-  (db-utils/pull [:block/uuid (medley/uuid string-id)]))
+  (db-utils/pull [:block/uuid (uuid string-id)]))
 
 (defn check-left-id-conflicts
   []

+ 1 - 1
src/main/frontend/db/default.cljs

@@ -1,4 +1,4 @@
-(ns frontend.db.default
+(ns ^:nbb-compatible frontend.db.default
   (:require [clojure.string :as string]))
 
 (defonce built-in-pages-names

+ 134 - 87
src/main/frontend/db/model.cljs

@@ -12,9 +12,9 @@
             [frontend.db.conn :as conn]
             [frontend.db.react :as react]
             [frontend.db.utils :as db-utils]
-            [frontend.format :as format]
             [frontend.state :as state]
             [frontend.util :as util :refer [react]]
+            [logseq.graph-parser.util :as gp-util]
             [frontend.db.rules :refer [rules]]
             [frontend.db.default :as default-db]
             [frontend.util.drawer :as drawer]))
@@ -61,7 +61,7 @@
    (db-utils/transact! (state/get-current-repo) tx-data))
   ([repo-url tx-data]
    (when-not config/publishing?
-     (let [tx-data (->> (util/remove-nils tx-data)
+     (let [tx-data (->> (gp-util/remove-nils tx-data)
                         (remove nil?)
                         (map #(dissoc % :file/handle :file/type)))]
        (when (seq tx-data)
@@ -293,7 +293,7 @@
       (:block/format page)
       (when-let [file (:block/file page)]
         (when-let [path (:file/path (db-utils/entity (:db/id file)))]
-          (format/get-format path)))))
+          (gp-util/get-format path)))))
    (state/get-preferred-format)
    :markdown))
 
@@ -409,6 +409,29 @@
                      f))
                  form))
 
+(defn get-sorted-page-block-ids
+  [page-id]
+  (let [root (db-utils/entity page-id)]
+    (loop [result []
+           children (sort-by-left (:block/_parent root) root)]
+      (if (seq children)
+        (let [child (first children)]
+          (recur (conj result (:db/id child))
+                 (concat
+                  (sort-by-left (:block/_parent child) child)
+                  (rest children))))
+        result))))
+
+(defn sort-page-random-blocks
+  "Blocks could be non consecutive."
+  [blocks]
+  (assert (every? #(= (:block/page %) (:block/page (first blocks))) blocks) "Blocks must to be in a same page.")
+  (let [page-id (:db/id (:block/page (first blocks)))
+        ;; TODO: there's no need to sort all the blocks
+        sorted-ids (get-sorted-page-block-ids page-id)
+        blocks-map (zipmap (map :db/id blocks) blocks)]
+    (keep blocks-map sorted-ids)))
+
 (defn has-children?
   ([block-id]
    (has-children? (conn/get-db) block-id))
@@ -573,13 +596,67 @@
 
 (defn recursive-child?
   [repo child-id parent-id]
-  (loop [node (db-utils/entity repo child-id)]
-    (if node
-      (let [parent (:block/parent node)]
-        (if (= (:db/id parent) parent-id)
-          true
-          (recur parent)))
-      false)))
+  (let [*last-node (atom nil)]
+    (loop [node (db-utils/entity repo child-id)]
+      (when-not (= @*last-node node)
+        (reset! *last-node node)
+        (if node
+          (let [parent (:block/parent node)]
+            (if (= (:db/id parent) parent-id)
+              true
+              (recur parent)))
+          false)))))
+
+(defn get-prev-sibling
+  [db id]
+  (when-let [e (d/entity db id)]
+    (let [left (:block/left e)]
+      (when (not= (:db/id left) (:db/id (:block/parent e)))
+        left))))
+
+(defn get-right-sibling
+  [db db-id]
+  (when-let [block (d/entity db db-id)]
+    (get-by-parent-&-left db
+                          (:db/id (:block/parent block))
+                          db-id)))
+
+(defn last-child-block?
+  "The child block could be collapsed."
+  [db parent-id child-id]
+  (when-let [child (d/entity db child-id)]
+    (cond
+      (= parent-id child-id)
+      true
+
+      (get-right-sibling db child-id)
+      false
+
+      :else
+      (last-child-block? db parent-id (:db/id (:block/parent child))))))
+
+(defn- consecutive-block?
+  [block-1 block-2]
+  (let [db (conn/get-db)
+        aux-fn (fn [block-1 block-2]
+                 (and (= (:block/page block-1) (:block/page block-2))
+                      (or
+                       ;; sibling or child
+                       (= (:db/id (:block/left block-2)) (:db/id block-1))
+                       (when-let [prev-sibling (get-prev-sibling db (:db/id block-2))]
+                         (last-child-block? db (:db/id prev-sibling) (:db/id block-1))))))]
+    (or (aux-fn block-1 block-2) (aux-fn block-2 block-1))))
+
+(defn get-non-consecutive-blocks
+  [blocks]
+  (vec
+   (keep-indexed
+    (fn [i _block]
+      (when (< (inc i) (count blocks))
+        (when-not (consecutive-block? (nth blocks i)
+                                      (nth blocks (inc i)))
+          (nth blocks i))))
+    blocks)))
 
 (defn- get-start-id-for-pagination-query
   [repo-url current-db {:keys [db-before tx-meta] :as tx-report}
@@ -587,7 +664,11 @@
   (let [db-before (or db-before current-db)
         cached-ids (map :db/id @result)
         cached-ids-set (set (conj cached-ids page-id))
-        first-changed-id (if (= outliner-op :move-blocks)
+        first-changed-id (cond
+                           (= (:real-outliner-op tx-meta) :indent-outdent)
+                           (last (:move-blocks tx-meta))
+
+                           (= outliner-op :move-blocks)
                            (let [{:keys [move-blocks target from-page to-page]} tx-meta]
                              (cond
                                (= page-id target) ; move to the first block
@@ -605,8 +686,10 @@
                                    (if id
                                      (if (contains? match-ids id)
                                        id
-                                       (recur others))
+                                       (when (seq others)
+                                         (recur others)))
                                      nil)))))
+                           :else
                            (let [insert? (= :insert-blocks outliner-op)]
                              (some #(when (and (or (and insert? (not (contains? cached-ids-set %)))
                                                    true)
@@ -631,7 +714,6 @@
         (let [start-page? (:block/name (db-utils/entity start-id))]
           (when-not start-page?
             (let [previous-blocks (take-while (fn [b] (not= start-id (:db/id b))) @result)
-                  previous-count (count previous-blocks)
                   limit 25
                   more (get-paginated-blocks-no-cache current-db start-id {:limit limit
                                                                            :include-start? true
@@ -810,6 +892,20 @@
           block-uuid)
         (sort-by-left (db-utils/entity [:block/uuid block-uuid])))))
 
+(defn sub-block-direct-children
+  "Doesn't include nested children."
+  [repo block-uuid]
+  (when-let [db (conn/get-db repo)]
+    (-> (react/q repo [:frontend.db.react/block-direct-children block-uuid] {}
+          '[:find [(pull ?b [*]) ...]
+            :in $ ?parent-id
+            :where
+            [?parent :block/uuid ?parent-id]
+            [?b :block/parent ?parent]]
+          block-uuid)
+        react
+        (sort-by-left (db-utils/entity [:block/uuid block-uuid])))))
+
 (defn get-block-children
   "Including nested children."
   [repo block-uuid]
@@ -894,8 +990,8 @@
 
 (defn get-page
   [page-name]
-  (if (util/uuid-string? page-name)
-    (db-utils/entity [:block/uuid (uuid page-name)])
+  (if-let [id (parse-uuid page-name)]
+    (db-utils/entity [:block/uuid id])
     (db-utils/entity [:block/name (util/page-name-sanity-lc page-name)])))
 
 (defn get-redirect-page-name
@@ -965,16 +1061,6 @@
         (reverse)
         (take n))))))
 
-(defn journal-day-exists?
-  [graph day]
-  (d/q
-    '[:find ?p .
-      :in $ ?day
-      :where
-      [?p :block/journal-day ?day]]
-    (conn/get-db graph)
-    day))
-
 ;; get pages that this page referenced
 (defn get-page-referenced-pages
   [repo page]
@@ -1077,38 +1163,20 @@
        (let [page-id (:db/id (db-utils/entity [:block/name (util/safe-page-name-sanity-lc page)]))
              pages (page-alias-set repo page)
              aliases (set/difference pages #{page-id})
-             query-result (if (seq aliases)
-                            (let [rules '[[(find-blocks ?block ?ref-page ?pages ?alias ?aliases)
-                                           [?block :block/page ?alias]
-                                           [(contains? ?aliases ?alias)]]
-                                          [(find-blocks ?block ?ref-page ?pages ?alias ?aliases)
-                                           [?block :block/refs ?ref-page]
-                                           [(contains? ?pages ?ref-page)]]]]
-                              (react/q repo
-                                       [:frontend.db.react/page<-blocks-or-block<-blocks page-id]
-                                       {}
-                                       '[:find [(pull ?block ?block-attrs) ...]
-                                         :in $ % ?pages ?aliases ?block-attrs
-                                         :where
-                                         (find-blocks ?block ?ref-page ?pages ?alias ?aliases)]
-                                       rules
-                                       pages
-                                       aliases
-                                       block-attrs))
-                            (react/q repo
-                                     [:frontend.db.react/page<-blocks-or-block<-blocks page-id]
-                                     {:use-cache? false}
-                                     '[:find [(pull ?ref-block ?block-attrs) ...]
-                                       :in $ ?page ?block-attrs
-                                       :where
-                                       [?ref-block :block/refs ?page]]
-                                     page-id
-                                     block-attrs))
+             query-result (react/q repo
+                            [:frontend.db.react/page<-blocks-or-block<-blocks page-id]
+                            {}
+                            '[:find [(pull ?block ?block-attrs) ...]
+                              :in $ [?ref-page ...] ?block-attrs
+                              :where
+                              [?block :block/refs ?ref-page]]
+                            pages
+                            (butlast block-attrs))
              result (->> query-result
                          react
-                         (sort-by-left-recursive)
                          (remove (fn [block]
                                    (= page-id (:db/id (:block/page block)))))
+                         (sort-by-left-recursive)
                          db-utils/group-by-page
                          (map (fn [[k blocks]]
                                 (let [k (if (contains? aliases (:db/id k))
@@ -1124,35 +1192,14 @@
   ([repo page]
    (when repo
      (when-let [db (conn/get-db repo)]
-       (let [page-id (:db/id (db-utils/entity [:block/name (util/safe-page-name-sanity-lc page)]))
-             pages (page-alias-set repo page)
-             aliases (set/difference pages #{page-id})
-             query-result (if (seq aliases)
-                            (let [rules '[[(find-blocks ?block ?ref-page ?pages ?alias ?aliases)
-                                           [?block :block/page ?alias]
-                                           [(contains? ?aliases ?alias)]]
-                                          [(find-blocks ?block ?ref-page ?pages ?alias ?aliases)
-                                           [?block :block/refs ?ref-page]
-                                           [(contains? ?pages ?ref-page)]]]]
-                              (d/q
-                                '[:find ?block
-                                  :in $ % ?pages ?aliases ?block-attrs
-                                  :where
-                                  (find-blocks ?block ?ref-page ?pages ?alias ?aliases)]
-                                db
-                                rules
-                                pages
-                                aliases
-                                block-attrs))
-                            (d/q
-                              '[:find ?ref-block
-                                :in $ ?page ?block-attrs
-                                :where
-                                [?ref-block :block/refs ?page]]
-                              db
-                              page-id
-                              block-attrs))]
-         query-result)))))
+       (let [pages (page-alias-set repo page)]
+         (d/q
+           '[:find ?block
+             :in $ [?ref-page ...]
+             :where
+             [?block :block/refs ?ref-page]]
+           db
+           pages))))))
 
 (defn get-date-scheduled-or-deadlines
   [journal-title]
@@ -1252,9 +1299,8 @@
 
 (defn get-referenced-blocks-ids
   [page-name-or-block-uuid]
-  (if (util/uuid-string? (str page-name-or-block-uuid))
-    (let [id (uuid page-name-or-block-uuid)]
-      (get-block-referenced-blocks-ids id))
+  (if-let [id (parse-uuid (str page-name-or-block-uuid))]
+    (get-block-referenced-blocks-ids id)
     (get-page-referenced-blocks-ids page-name-or-block-uuid)))
 
 (defn get-matched-blocks
@@ -1362,7 +1408,7 @@
   [name]
   (when (string? name)
     (->> (d/q
-           '[:find (pull ?b [*])
+           '[:find [(pull ?b [*]) ...]
              :in $ ?name
              :where
              [?b :block/properties ?p]
@@ -1370,7 +1416,8 @@
              [(= ?t ?name)]]
            (conn/get-db)
            name)
-         ffirst)))
+         (sort-by :block/name)
+         (first))))
 
 (defonce blocks-count-cache (atom nil))
 

+ 8 - 7
src/main/frontend/db/query_dsl.cljs

@@ -6,13 +6,14 @@
             [clojure.set :as set]
             [clojure.string :as string]
             [clojure.walk :as walk]
+            [frontend.state :as state]
             [frontend.date :as date]
             [frontend.db.model :as model]
             [frontend.db.query-react :as query-react]
             [frontend.db.utils :as db-utils]
             [frontend.db.rules :as rules]
             [frontend.template :as template]
-            [frontend.text :as text]
+            [logseq.graph-parser.text :as text]
             [frontend.util :as util]))
 
 
@@ -66,7 +67,7 @@
           (date/journal-title->int input)))
 
       :else
-      (let [duration (util/parse-int (subs input 0 (dec (count input))))
+      (let [duration (parse-long (subs input 0 (dec (count input))))
             kind (last input)
             tf (case kind
                  "y" t/years
@@ -98,7 +99,7 @@
           (date/journal-title->long input)))
 
       :else
-      (let [duration (util/parse-int (subs input 0 (dec (count input))))
+      (let [duration (parse-long (subs input 0 (dec (count input))))
             kind (last input)
             tf (case kind
                  "y" t/years
@@ -237,7 +238,7 @@
   (let [k (string/replace (name (nth e 1)) "_" "-")
         v (nth e 2)
         v (if-not (nil? v)
-            (text/parse-property k v)
+            (text/parse-property k v (state/get-config))
             v)
         v (if (coll? v) (first v) v)]
     {:query (list 'property '?b (keyword k) v)
@@ -282,7 +283,7 @@
   (let [[k v] (rest e)
         k (string/replace (name k) "_" "-")]
     (if (some? v)
-      (let [v' (text/parse-property k v)
+      (let [v' (text/parse-property k v (state/get-config))
             val (if (coll? v') (first v') v')]
         {:query (list 'page-property '?p (keyword k) val)
          :rules [:page-property]})
@@ -309,8 +310,8 @@
   [e sample]
   (when-let [num (second e)]
     (when (integer? num)
-      (reset! sample num))
-    nil))
+      (reset! sample num)
+      {:query [['?p :block/uuid]]})))
 
 (defn- build-sort-by
   [e sort-by_]

+ 8 - 5
src/main/frontend/db/query_react.cljs

@@ -10,8 +10,9 @@
             [frontend.debug :as debug]
             [frontend.extensions.sci :as sci]
             [frontend.state :as state]
-            [frontend.text :as text]
+            [logseq.graph-parser.text :as text]
             [frontend.util :as util]
+            [frontend.date :as date]
             [lambdaisland.glogi :as log]))
 
 (defn resolve-input
@@ -28,17 +29,19 @@
     (= :tomorrow input)
     (date->int (t/plus (t/today) (t/days 1)))
     (= :current-page input)
-    ;; This sometimes runs when there isn't a current page e.g. :home route
-    (some-> (state/get-current-page) string/lower-case)
+    (some-> (or (state/get-current-page)
+                (:page (state/get-default-home))
+                (date/today)) string/lower-case)
+
     (and (keyword? input)
          (util/safe-re-find #"^\d+d(-before)?$" (name input)))
     (let [input (name input)
-          days (util/parse-int (subs input 0 (dec (count input))))]
+          days (parse-long (re-find #"^\d+" input))]
       (date->int (t/minus (t/today) (t/days days))))
     (and (keyword? input)
          (util/safe-re-find #"^\d+d(-after)?$" (name input)))
     (let [input (name input)
-          days (util/parse-int (subs input 0 (dec (count input))))]
+          days (parse-long (re-find #"^\d+" input))]
       (date->int (t/plus (t/today) (t/days days))))
 
     (and (string? input) (text/page-ref? input))

+ 14 - 5
src/main/frontend/db/react.cljs

@@ -23,6 +23,8 @@
 ;; ::block-and-children
 ;; get block&children react-query
 (s/def ::block-and-children (s/tuple #(= ::block-and-children %) uuid?))
+
+(s/def ::block-direct-children (s/tuple #(= ::block-direct-children %) uuid?))
 ;; ::journals
 ;; get journal-list react-query
 (s/def ::journals (s/tuple #(= ::journals %)))
@@ -47,6 +49,7 @@
 (s/def ::react-query-keys (s/or :block ::block
                                 :page-blocks ::page-blocks
                                 :block-and-children ::block-and-children
+                                :block-direct-children ::block-direct-children
                                 :journals ::journals
                                 :page->pages ::page->pages
                                 :page<-pages ::page<-pages
@@ -212,9 +215,8 @@
 
 (defn get-affected-queries-keys
   "Get affected queries through transaction datoms."
-  [{:keys [tx-data]}]
+  [{:keys [tx-data db-before]}]
   {:post [(s/valid? ::affected-keys %)]}
-
   (let [blocks (->> (filter (fn [datom] (contains? #{:block/left :block/parent :block/page} (:a datom))) tx-data)
                     (map :v)
                     (distinct))
@@ -236,8 +238,15 @@
                                              (:db/id (:block/page block)))
                                     blocks [[::block (:block/uuid block)]]
                                     others (when page-id
-                                             [[::page-blocks page-id]
-                                              [::page->pages page-id]])]
+                                             (let [db-after-parent-uuid (:block/uuid (:block/parent block))
+                                                   db-before-parent-uuid (:block/uuid (:block/parent (d/entity db-before
+                                                                                                               [:block/uuid (:block/uuid block)])))]
+                                               [[::page-blocks page-id]
+                                                [::page->pages page-id]
+                                                [::block-direct-children db-after-parent-uuid]
+                                                (when (and db-before-parent-uuid
+                                                           (not= db-before-parent-uuid db-after-parent-uuid))
+                                                  [::block-direct-children db-before-parent-uuid])]))]
                                 (concat blocks others)))))
                         blocks)
 
@@ -250,7 +259,7 @@
                                 (if (:block/name entity) ; page
                                   [::page-blocks ref]
                                   [::page-blocks (:db/id (:block/page entity))])))
-                            refs))
+                         refs))
         others (->>
                 (keys @query-state)
                 (filter (fn [ks]

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно