Browse Source

Merge branch 'master' into feat/outliner-core

Tienson Qin 4 years ago
parent
commit
ac4de8a8c3
100 changed files with 4814 additions and 2171 deletions
  1. 329 0
      .github/workflows/build-desktop-release.yml
  2. 3 0
      .gitignore
  3. 2 1
      Dockerfile
  4. 35 12
      README.md
  5. 9 12
      deps.edn
  6. 35 0
      docs/Build LogSeq Desktop for windows on Ubuntu.md
  7. 3 0
      externs.js
  8. 46 1
      gulpfile.js
  9. 21 5
      package.json
  10. 1 1
      public/index.html
  11. 146 55
      resources/css/common.css
  12. 0 4
      resources/css/excalidraw.min.css
  13. 13 0
      resources/css/fonts.css
  14. 39 39
      resources/css/inter.css
  15. 4 2
      resources/css/style.css
  16. 15 0
      resources/css/style.dev.css
  17. 16 0
      resources/css/tooltip.css
  18. 25 0
      resources/dev.html
  19. 111 0
      resources/electron-dev.html
  20. 112 0
      resources/electron.html
  21. 12 0
      resources/entitlements.plist
  22. 0 0
      resources/fonts/Cascadia.woff2
  23. 0 0
      resources/fonts/FG_Virgil.woff2
  24. BIN
      resources/fonts/Virgil.woff2
  25. 56 0
      resources/forge.config.js
  26. BIN
      resources/icons/logseq.icns
  27. BIN
      resources/icons/logseq.ico
  28. BIN
      resources/icons/logseq.png
  29. BIN
      resources/icons/logseq_big_sur.icns
  30. BIN
      resources/icons/logseq_big_sur.ico
  31. BIN
      resources/icons/logseq_big_sur.png
  32. BIN
      resources/img/dmg-bg.png
  33. 0 1
      resources/js/excalidraw.min.js
  34. 1 0
      resources/js/interact.min.js
  35. 176 0
      resources/js/isomorphic-git/1.7.4/http-web-index.umd.js
  36. 0 0
      resources/js/isomorphic-git/1.7.4/index.umd.min.js
  37. 124 0
      resources/js/preload.js
  38. 4 4
      resources/js/worker.js
  39. 34 0
      resources/package.json
  40. 17 0
      scripts/publishing.sh
  41. 19 0
      shadow-cljs.edn
  42. 5 0
      src/dev-cljs/shadow/user.clj
  43. 149 0
      src/electron/electron/core.cljs
  44. 160 0
      src/electron/electron/handler.cljs
  45. 4 0
      src/electron/electron/state.cljs
  46. 125 0
      src/electron/electron/updater.cljs
  47. 11 0
      src/electron/electron/utils.cljs
  48. 1 1
      src/main/api.cljs
  49. 9 0
      src/main/electron/ipc.cljs
  50. 58 0
      src/main/electron/listener.cljs
  51. 47 6
      src/main/frontend/commands.cljs
  52. 242 152
      src/main/frontend/components/block.cljs
  53. 13 3
      src/main/frontend/components/block.css
  54. 1 1
      src/main/frontend/components/commit.cljs
  55. 26 3
      src/main/frontend/components/content.cljs
  56. 6 4
      src/main/frontend/components/diff.cljs
  57. 0 497
      src/main/frontend/components/draw.cljs
  58. 0 23
      src/main/frontend/components/draw.css
  59. 105 498
      src/main/frontend/components/editor.cljs
  60. 174 0
      src/main/frontend/components/encryption.cljs
  61. 3 3
      src/main/frontend/components/file.cljs
  62. 126 112
      src/main/frontend/components/header.cljs
  63. 23 6
      src/main/frontend/components/header.css
  64. 19 12
      src/main/frontend/components/journal.cljs
  65. 7 1
      src/main/frontend/components/onboarding.cljs
  66. 88 50
      src/main/frontend/components/page.cljs
  67. 1 1
      src/main/frontend/components/page.css
  68. 1 1
      src/main/frontend/components/project.cljs
  69. 68 10
      src/main/frontend/components/reference.cljs
  70. 41 27
      src/main/frontend/components/repo.cljs
  71. 83 45
      src/main/frontend/components/right_sidebar.cljs
  72. 72 20
      src/main/frontend/components/search.cljs
  73. 38 7
      src/main/frontend/components/search.css
  74. 281 190
      src/main/frontend/components/settings.cljs
  75. 105 0
      src/main/frontend/components/settings.css
  76. 34 22
      src/main/frontend/components/sidebar.cljs
  77. 25 4
      src/main/frontend/components/sidebar.css
  78. 80 1
      src/main/frontend/components/svg.cljs
  79. 22 10
      src/main/frontend/components/theme.cljs
  80. 137 13
      src/main/frontend/components/theme.css
  81. 28 23
      src/main/frontend/components/widgets.cljs
  82. 4 3
      src/main/frontend/config.cljs
  83. 2 2
      src/main/frontend/core.cljs
  84. 13 19
      src/main/frontend/date.cljs
  85. 27 14
      src/main/frontend/db.cljs
  86. 5 1
      src/main/frontend/db/conn.cljs
  87. 9 0
      src/main/frontend/db/default.cljs
  88. 170 48
      src/main/frontend/db/model.cljs
  89. 10 98
      src/main/frontend/db/query_custom.cljs
  90. 35 16
      src/main/frontend/db/query_dsl.cljs
  91. 103 0
      src/main/frontend/db/query_react.cljs
  92. 36 55
      src/main/frontend/db/react.cljs
  93. 6 2
      src/main/frontend/db/utils.cljs
  94. 8 6
      src/main/frontend/db_schema.cljs
  95. 262 8
      src/main/frontend/dicts.cljs
  96. 104 0
      src/main/frontend/encrypt.cljs
  97. 23 0
      src/main/frontend/extensions/age_encryption.cljs
  98. 149 0
      src/main/frontend/extensions/excalidraw.cljs
  99. 2 1
      src/main/frontend/extensions/latex.cljs
  100. 20 15
      src/main/frontend/extensions/slide.cljs

+ 329 - 0
.github/workflows/build-desktop-release.yml

@@ -0,0 +1,329 @@
+# This is a basic workflow to help you get started with Actions
+
+name: Build-Desktop-Release
+
+on:
+  workflow_dispatch:
+    inputs:
+      tag-version:
+        description: "Release Tag Version"
+        required: true
+      git-ref:
+        description: "Release Git Ref"
+        required: true
+        default: "master"
+      is-draft:
+        description: 'Draft Release? '
+        required: true
+        default: "true"
+      is-pre-release:
+        description: 'Pre Release?'
+        required: true
+        default: "true"
+
+jobs:
+  compile-cljs:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Check out Git repository
+        uses: actions/checkout@v1
+
+      - name: Install Node.js, NPM and Yarn
+        uses: actions/setup-node@v1
+        with:
+          node-version: 14
+
+      - name: Setup Java JDK
+        uses: actions/[email protected]
+        with:
+          java-version: 1.8
+
+      - name: Cache local Maven repository
+        uses: actions/cache@v2
+        with:
+          path: ~/.m2/repository
+          key: ${{ runner.os }}-maven
+
+      - name: Install clojure
+        run: |
+          curl -O https://download.clojure.org/install/linux-install-1.10.1.763.sh
+          chmod +x linux-install-1.10.1.763.sh
+          sudo ./linux-install-1.10.1.763.sh
+
+      - name: Compile CLJS
+        run: yarn install --frozen-lockfile && gulp build  && yarn cljs:release
+
+      - name: Update APP Version
+        run: |
+          sed -i 's/"version": "0.0.1"/"version": "${{ github.event.inputs.tag-version }}"/g' ./package.json
+        working-directory: ./static
+
+      - name: Update OSX Packager Config
+        run: |
+          sed -i 's/appleId: "my-fake-apple-id"/appleId: "${{ secrets.APPLE_ID_EMAIL }}"/' ./forge.config.js
+          sed -i 's/appleIdPassword: "my-fake-apple-id-password"/appleIdPassword: "${{ secrets.APPLE_ID_PASSWORD }}"/' ./forge.config.js
+        working-directory: ./static
+
+      - name: Display Package.json
+        run: cat ./package.json
+        working-directory: ./static
+
+      - name: List Files
+        run: ls -al
+        working-directory: ./static
+
+      - name: Compress Static Files
+        run: zip -r static.zip ./static
+
+      - name: Cache Static File
+        uses: actions/upload-artifact@v1
+        with:
+          name: static.zip
+          path: static.zip
+
+  build-linux:
+    runs-on: ubuntu-latest
+    needs: [ compile-cljs ]
+    steps:
+      - name: Download The Static Asset
+        uses: actions/download-artifact@v1
+        with:
+          name: static.zip
+          path: ./
+
+      - name: Uncompress Static FIles
+        run: unzip static.zip
+
+      - name: Install Node.js, NPM and Yarn
+        uses: actions/setup-node@v1
+        with:
+          node-version: 14
+
+      - name: Cache Node Modules
+        uses: actions/cache@v2
+        with:
+          path: |
+            **/node_modules
+          key: ${{ runner.os }}-node-modules
+
+      - name: Build/Release Electron App
+        run: yarn install --frozen-lockfile && yarn electron:make
+        working-directory: ./static
+
+      - name: Change Artifact Name For ZIP File
+        run: mv static/out/make/zip/linux/x64/Logseq-linux-x64-*.zip  static/out/make/zip/linux/x64/Logseq-linux.zip
+
+      - name: Change Artifact Name For AppImage File
+        run: mv static/out/make/Logseq-*.AppImage  static/out/make/Logseq-linux.AppImage
+
+      - name: Cache Artifact With ZIP format
+        uses: actions/upload-artifact@v1
+        with:
+          name: Logseq-linux.zip
+          path: static/out/make/zip/linux/x64/Logseq-linux.zip
+
+      - name: Cache Artifact With AppImage format
+        uses: actions/upload-artifact@v1
+        with:
+          name: Logseq-linux.AppImage
+          path: static/out/make/Logseq-linux.AppImage
+
+  build-windows:
+    runs-on: windows-latest
+    needs: [ compile-cljs ]
+    steps:
+      - name: Download The Static Asset
+        uses: actions/download-artifact@v1
+        with:
+          name: static.zip
+          path: ./
+
+      - name: Uncompress Static FIles
+        run: unzip static.zip
+
+      - name: Install Node.js, NPM and Yarn
+        uses: actions/setup-node@v1
+        with:
+          node-version: 14
+
+      - name: Cache Node Modules
+        uses: actions/cache@v2
+        with:
+          path: |
+            **/node_modules
+          key: ${{ runner.os }}-node-modules
+
+      - name: Build/Release Electron app
+        run: yarn install --frozen-lockfile && yarn electron:make
+        working-directory: ./static
+
+      - name: Change Artifact Name
+        run: Get-ChildItem  static\out\make\squirrel.windows\x64\*.exe | Rename-Item -NewName Logseq-win64.exe
+
+      - name: List Directory
+        run: dir
+        working-directory: static/out/make/squirrel.windows/x64/
+
+      - name: Cache Artifact
+        uses: actions/upload-artifact@v1
+        with:
+          name: Logseq-win64.exe
+          path: static/out/make/squirrel.windows/x64/Logseq-win64.exe
+
+  build-macos:
+    needs: [ compile-cljs ]
+    runs-on: macos-latest
+
+    steps:
+      - name: Download The Static Asset
+        uses: actions/download-artifact@v1
+        with:
+          name: static.zip
+          path: ./
+
+      - name: Uncompress Static Files
+        run: unzip ./static.zip
+
+      - name: List Static Files
+        run: ls -al ./static
+
+      - name: Install Node.js, NPM and Yarn
+        uses: actions/setup-node@v1
+        with:
+          node-version: 14
+
+      - name: Signing By Apple Developer ID
+        uses: apple-actions/import-codesign-certs@v1
+        with:
+          p12-file-base64: ${{ secrets.APPLE_CERTIFICATES_P12 }}
+          p12-password: ${{ secrets.APPLE_CERTIFICATES_P12_PASSWORD }}
+
+      - name: Cache Node Modules
+        uses: actions/cache@v2
+        with:
+          path: |
+            **/node_modules
+          key: ${{ runner.os }}-node-modules
+
+      - name: Build/Release Electron App
+        run: yarn install --frozen-lockfile && yarn electron:make
+        working-directory: ./static
+
+      - name: Change DMG Name
+        run: mv static/out/make/Logseq.dmg static/out/make/logseq-darwin-x64-${{ github.event.inputs.tag-version }}.dmg
+
+      - name: Cache Artifact DMG
+        uses: actions/upload-artifact@v1
+        with:
+          name: Logseq-x64.dmg
+          path: static/out/make/logseq-darwin-x64-${{ github.event.inputs.tag-version }}.dmg
+
+      - name: Cache Artifact ZIP
+        uses: actions/upload-artifact@v1
+        with:
+          name: Logseq-x64.zip
+          path: static/out/make/zip/darwin/x64/logseq-darwin-x64-${{ github.event.inputs.tag-version }}.zip
+
+  release:
+    needs: [ build-macos, build-linux, build-windows ]
+    runs-on: ubuntu-latest
+
+    steps:
+      - name: Download The MacOS X64 DMG Artifact
+        uses: actions/download-artifact@v1
+        with:
+          name: Logseq-x64.dmg
+          path: ./
+
+      - name: Download The MacOS X64 ZIP Artifact
+        uses: actions/download-artifact@v1
+        with:
+          name: Logseq-x64.zip
+          path: ./
+
+      - name: Download The Linux Artifact In Zip format
+        uses: actions/download-artifact@v1
+        with:
+          name: Logseq-linux.zip
+          path: ./
+
+      - name: Download The Linux Artifact In AppImage format
+        uses: actions/download-artifact@v1
+        with:
+          name: Logseq-linux.AppImage
+          path: ./
+
+      - name: Download The Windows Artifact
+        uses: actions/download-artifact@v1
+        with:
+          name: Logseq-win64.exe
+          path: ./
+
+      - name: List files
+        run: ls -rl
+
+      - name: Create Release Draft
+        id: create_release
+        uses: actions/create-release@v1
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        with:
+          tag_name: ${{ github.event.inputs.tag-version }}
+          release_name: Desktop APP ${{ github.event.inputs.tag-version }} (Alpha Testing)
+          draft: ${{ github.event.inputs.is-draft }}
+          prerelease: ${{ github.event.inputs.is-pre-release }}
+
+      - name: Upload MacOS X64 ZIP Artifact
+        id: upload-macos-x64-zip-artifact
+        uses: actions/upload-release-asset@v1
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        with:
+          upload_url: ${{ steps.create_release.outputs.upload_url }}
+          asset_path: ./logseq-darwin-x64-${{ github.event.inputs.tag-version }}.zip
+          asset_name: logseq-darwin-x64-${{ github.event.inputs.tag-version }}.zip
+          asset_content_type: application/zip
+
+      - name: Upload MacOS X64 DMG Artifact
+        id: upload-macos-x64-dmg-artifact
+        uses: actions/upload-release-asset@v1
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        with:
+          upload_url: ${{ steps.create_release.outputs.upload_url }}
+          asset_path: ./logseq-darwin-x64-${{ github.event.inputs.tag-version }}.dmg
+          asset_name: logseq-darwin-x64-${{ github.event.inputs.tag-version }}.dmg
+          asset_content_type: application/x-apple-diskimage
+
+      - name: Upload Linux Artifact With Zip format
+        id: upload-linux-artifact-with-zip-format
+        uses: actions/upload-release-asset@v1
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        with:
+          upload_url: ${{ steps.create_release.outputs.upload_url }}
+          asset_path: ./Logseq-linux.zip
+          asset_name: logseq-linux-x64-${{ github.event.inputs.tag-version }}.zip
+          asset_content_type: application/zip
+
+      - name: Upload Linux Artifact With AppImage format
+        id: upload-linux-artifact-with-appimage-format
+        uses: actions/upload-release-asset@v1
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        with:
+          upload_url: ${{ steps.create_release.outputs.upload_url }}
+          asset_path: ./Logseq-linux.AppImage
+          asset_name: logseq-linux-x64-${{ github.event.inputs.tag-version }}.AppImage
+          asset_content_type: application/octet-stream
+
+      - name: Upload Windows Artifact
+        id: upload-win-artifact
+        uses: actions/upload-release-asset@v1
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        with:
+          upload_url: ${{ steps.create_release.outputs.upload_url }}
+          asset_path: ./Logseq-win64.exe
+          asset_name: logseq-win-x64-${{ github.event.inputs.tag-version }}.exe
+          asset_content_type: application/octet-stream

+ 3 - 0
.gitignore

@@ -28,3 +28,6 @@ report.html
 strings.csv
 
 .calva
+resources/electron.js
+.clj-kondo/
+.lsp/

+ 2 - 1
Dockerfile

@@ -1,4 +1,5 @@
-FROM clojure:openjdk-11-tools-deps
+# NOTE: please keep it in sync with .github pipelines
+FROM clojure:openjdk-11-tools-deps-1.10.1.727
 
 RUN curl -sL https://deb.nodesource.com/setup_15.x | bash - && \
     apt-get install -y nodejs

+ 35 - 12
README.md

@@ -16,7 +16,7 @@ Use it to organize your todo list, to write your journals, or to record your uni
 ## Why Logseq?
 
 [Logseq](https://logseq.com) is a platform for knowledge sharing and management. It focuses on privacy, longevity, and user control.
-Notice: the backend code will not be open-sourced for security reasons and other potential risks. 
+Notice: the backend code will be open-sourced as soon as we’re sure that the backend service meets the security standards.
 
 The server will never store or analyze your private notes. Your data are plain text files and we currently support both Markdown and Emacs Org mode (more to be added soon).
 
@@ -25,11 +25,12 @@ In the unlikely event that the website is down or cannot be maintained, your dat
 ![Image of logseq](https://cdn.logseq.com/%2F8b9a461d-437e-4ca5-a2da-18b51077b5142020_07_25_Screenshot%202020-07-25%2013-29-49%20%2B0800.png?Expires=4749255017&Signature=Qbx6jkgAytqm6nLxVXQQW1igfcf~umV1OcG6jXUt09TOVhgXyA2Z5jHJ3AGJASNcphs31pZf4CjFQ5mRCyVKw6N8wb8Nn-MxuTJl0iI8o-jLIAIs9q1v-2cusCvuFfXH7bq6ir8Lpf0KYAprzuZ00FENin3dn6RBW35ENQwUioEr5Ghl7YOCr8bKew3jPV~OyL67MttT3wJig1j3IC8lxDDT8Ov5IMG2GWcHERSy00F3mp3tJtzGE17-OUILdeuTFz6d-NDFAmzB8BebiurYz0Bxa4tkcdLUpD5ToFHU08jKzZExoEUY8tvaZ1-t7djmo3d~BAXDtlEhC2L1YC2aVQ__&Key-Pair-Id=APKAJE5CCD6X7MP6PTEA)
 
 ## Feature requests
+
 Please go to https://discuss.logseq.com/c/feature-requests/7.
 
 ## How can I use it?
 
-1. Make sure you have registered a [GitHub account](https://github.com/join) and already created a repository (could be an old one). _Currently we only support GitHub, but more sync  options (e.g. Gitlab, Dropbox, Google Drive, WebDAV, etc.) will be added soon._
+1. Make sure you have registered a [GitHub account](https://github.com/join) and already created a repository (could be an old one). _Currently we only support GitHub, but more sync options (e.g. Gitlab, Dropbox, Google Drive, WebDAV, etc.) will be added soon._
 
 2. Visit our website <https://logseq.com/>.
 
@@ -60,7 +61,7 @@ Logseq is also made possible by the following projects:
 - Discord: https://discord.gg/KpN4eHY - Where we answer questions, disucss workflows and share tips
 - Github: https://github.com/logseq/logseq - everyone is encouraged to report issues!
 
-- - - -
+---
 
 The following is for developers and designers who want to build and run Logseq locally and contribute to this project.
 
@@ -73,7 +74,7 @@ The following is for developers and designers who want to build and run Logseq l
 
 ### 2. Compile to JavaScript
 
-``` bash
+```bash
 git clone https://github.com/logseq/logseq
 yarn
 yarn watch
@@ -85,48 +86,70 @@ Open <http://localhost:3001>.
 
 ### 4. Build a release
 
-``` bash
+```bash
 yarn release
 ```
 
 ### 5. Run tests
 
 Run ClojureScript tests
+
 ```bash
-yarn
-yarn cljs:test
-node static/tests.js
+yarn test
 ```
 
 Run Clojure tests. (Note: `.cljc` files may be tested both by ClojureScript, and Clojure.)
+
 ```bash
 clj -Mtest-clj
 ```
 
+## Desktop app development
+
+### 1. Compile to JavaScript
+
+```bash
+yarn watch
+```
+
+### 2. Open the dev app
+
+```bash
+yarn dev-electron-app
+```
+
+### 3. Build a release
+
+```bash
+yarn release-electron
+```
+
 ## Alternative: Docker based development environment
 
+Basically it just pre-installs Java, Clojure and NodeJS for your convenience.
+
 ### 1. Fetch sources
 
-``` bash
+```bash
 git clone https://github.com/logseq/logseq
 ```
 
 ### 2. Build Docker image
 
-``` bash
+```bash
 cd logseq
 docker build -t logseq-docker .
 ```
 
 ### 3. Run Docker container
 
-``` bash
+```bash
 docker run -v $(pwd):/home/logseq/logseq -p 3001:3001 -p 9630:9630 -p 8701:8701 --rm -it logseq-docker /bin/bash
 ```
 
 ### 4. Inside the container compile as described above
 
-``` bash
+```bash
 cd logseq
 yarn
 yarn watch

+ 9 - 12
deps.edn

@@ -8,7 +8,7 @@
   ;; FIXME: doesn't work on my archlinux laptop (tienson)
   ;; The required namespace "datascript.core" is not available, it was required by "frontend/db.cljs".
   datascript/datascript       {:git/url "https://github.com/tiensonqin/datascript",
-                               :sha "7c2822565d9a114c7d8604c335af89de4640e2e5"}
+                               :sha "efde8d389e6703b6f60ca3538f484a579b0d6de0"}
   ;; datascript                  {:mvn/version "1.0.1"}
   datascript-transit/datascript-transit
   {:mvn/version "0.3.0"
@@ -21,7 +21,9 @@
   cljs-bean/cljs-bean         {:mvn/version "1.5.0"}
   prismatic/dommy             {:mvn/version "1.1.0"}
   org.clojure/core.match      {:mvn/version "1.0.0"}
-  com.andrewmcveigh/cljs-time {:mvn/version "0.5.2"}
+  ;; fork
+  com.andrewmcveigh/cljs-time {:git/url "https://github.com/logseq/cljs-time",
+                               :sha "5704fbf48d3478eedcf24d458c8964b3c2fd59a9"}
   cljs-drag-n-drop/cljs-drag-n-drop
   {:mvn/version "0.1.0"}
   borkdude/sci                {:mvn/version "0.1.1-alpha.6"}
@@ -33,28 +35,23 @@
   expound/expound             {:mvn/version "0.8.6"}
   lambdaisland/glogi          {:mvn/version "1.0.74"}
   nano-id/nano-id             {:mvn/version "1.0.0"}}
+  binaryage/devtools          {:mvn/version "1.0.2"}
 
- :aliases {:cljs {:extra-paths ["src/dev-cljs/" "src/test/"]
+ :aliases {:cljs {:extra-paths ["src/dev-cljs/" "src/test/" "src/electron/"]
                   :extra-deps  {org.clojure/clojurescript   {:mvn/version "1.10.764"}
-                                binaryage/devtools          {:mvn/version "1.0.2"}
                                 org.clojure/tools.namespace {:mvn/version "0.2.11"}
-                                cider/cider-nrepl           {:mvn/version "0.25.5"}
-                                org.clojars.knubie/cljs-run-test {:mvn/version "1.0.1"}}
+                                cider/cider-nrepl           {:mvn/version "0.25.5"}}
                   :main-opts ["-m" "shadow.cljs.devtools.cli"]}
            :test
            {:extra-paths ["src/test/"]
-            :extra-deps  {
-                          org.clojure/clojurescript {:mvn/version "1.10.764"}
+            :extra-deps  {org.clojure/clojurescript {:mvn/version "1.10.764"}
                           org.clojure/test.check {:mvn/version "RELEASE"}}
             :main-opts   ["-m" "shadow.cljs.devtools.cli"]}
 
            :test-clj
            {:extra-paths ["src/test/"]
             :extra-deps
-            {
-             com.cognitect/test-runner
+            {com.cognitect/test-runner
              {:git/url "https://github.com/cognitect-labs/test-runner",
               :sha "76568540e7f40268ad2b646110f237a60295fa3c"}},
             :main-opts ["-m" "cognitect.test-runner" "-d" "src/test"]}}}
-
-

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

@@ -0,0 +1,35 @@
+# Building LogSeq Desktop app for Windows on Ubuntu
+## Intro
+My LogSeq dev machine is on Ubuntu 18.x and my production machine is running Windows 10, I needed a way to compile the LogSeq desktop APP for Windows.
+I tired & failed to make the "build" run on my windows machine but I did, however, succeed in letting my Ubuntu machine make Windows x64 files
+## Pre-requisites 
+These are the steps I took to make it work on my Ubuntu machine, sharing them hoping it helps someone else. I assume you have all the basic pre-requisites for LogSeq, if not you can find them at https://github.com/logseq/logseq#1-requirements
+1. clone LogSeq repo if you haven't already 
+`git clone https://github.com/logseq/logseq/`
+1. Install wine 
+```shell
+sudo dpkg --add-architecture i386
+sudo apt update
+sudo apt install wine64 wine32
+```
+1. Install winetricks & install dotnet using winetricks
+```shell
+sudo apt install winetricks
+
+winetricks dotnet46
+```
+1. Install nuget and mono (N.B. I had to install mono-complete for it to work)
+```shell
+sudo apt install nuget
+
+sudo apt-get install mono-complete
+```
+1. in `~/logseq/resources/package.json` line 10 `"electron:make": "electron-forge make --platform=win32 --arch=x64 --asar",`
+1. Compile using
+```shell
+cd logseq
+yarn
+yarn release
+yarn release-electron
+```
+the executable should be in the `static/out/make/squirrel.windows/x64/` folder

+ 3 - 0
externs.js

@@ -17,6 +17,9 @@ dummy.getRangeAt = function() {};
 dummy.getElementsByClassName = function() {};
 dummy.containsNode = function() {};
 dummy.select = function() {};
+dummy.search = function() {};
+dummy.add = function() {};
+dummy.remove = function() {};
 dummy.closest = function () {};
 dummy.setAttribute = function() {};
 dummy.getAttribute = function() {};

+ 46 - 1
gulpfile.js

@@ -1,4 +1,5 @@
 const fs = require('fs')
+const cp = require('child_process')
 const path = require('path')
 const gulp = require('gulp')
 const postcss = require('gulp-postcss')
@@ -62,7 +63,7 @@ const css = {
 
 const common = {
   clean () {
-    return del(outputPath)
+    return del(['./static/**/*', '!./static/yarn.lock', '!./static/node_modules'])
   },
 
   syncResourceFile () {
@@ -74,6 +75,50 @@ const common = {
   }
 }
 
+exports.electron = () => {
+  if (!fs.existsSync(path.join(outputPath, 'node_modules'))) {
+    cp.execSync('yarn', {
+      cwd: outputPath,
+      stdio: 'inherit'
+    })
+  }
+
+  cp.execSync('yarn electron:dev', {
+    cwd: outputPath,
+    stdio: 'inherit'
+  })
+}
+
+exports.electronMaker = async () => {
+  cp.execSync('yarn cljs:electron-release', {
+    stdio: 'inherit'
+  })
+
+  const pkgPath = path.join(outputPath, 'package.json')
+  const pkg = require(pkgPath)
+  const version = fs.readFileSync(path.join(__dirname, 'src/main/frontend/version.cljs'))
+    .toString().match(/[0-9.]{3,}/)[0]
+
+  if (!version) {
+    throw new Error('release version error in src/**/*/version.cljs')
+  }
+
+  pkg.version = version
+  fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2))
+
+  if (!fs.existsSync(path.join(outputPath, 'node_modules'))) {
+    cp.execSync('yarn', {
+      cwd: outputPath,
+      stdio: 'inherit'
+    })
+  }
+
+  cp.execSync('yarn electron:make', {
+    cwd: outputPath,
+    stdio: 'inherit'
+  })
+}
+
 exports.clean = common.clean
 exports.watch = gulp.parallel(common.keepSyncResourceFile, css.watchCSS)
 exports.build = gulp.series(common.clean, common.syncResourceFile, css.buildCSS)

+ 21 - 5
package.json

@@ -2,6 +2,7 @@
     "name": "logseq",
     "version": "0.0.1",
     "private": true,
+    "main": "static/electron.js",
     "devDependencies": {
         "@tailwindcss/ui": "0.7.2",
         "@types/gulp": "^4.0.7",
@@ -25,19 +26,25 @@
     },
     "scripts": {
         "watch": "run-p gulp:build gulp:watch cljs:watch",
+        "electron-watch": "run-p gulp:build gulp:watch cljs:electron-watch",
         "release": "run-s gulp:build cljs:release",
         "watch-app": "run-p gulp:watch cljs:watch-app",
         "release-app": "run-s gulp:build cljs:release-app",
         "release-publishing": "run-s gulp:build cljs:release-publishing",
         "dev-release-app": "run-s gulp:build cljs:dev-release-app",
+        "dev-electron-app": "gulp electron",
+        "release-electron": "gulp build && gulp electronMaker",
+        "debug-electron": "cd static/ && yarn electron:debug",
         "clean": "gulp clean",
         "test": "run-s cljs:test cljs:run-test",
         "report": "run-s cljs:report",
         "style:lint": "stylelint \"src/**/*.css\" ",
         "gulp:watch": "gulp watch",
         "gulp:build": "cross-env NODE_ENV=production gulp build",
-        "cljs:watch": "clojure -M:cljs watch app publishing",
-        "cljs:release": "clojure -M:cljs release app publishing",
+        "cljs:watch": "clojure -M:cljs watch app publishing electron",
+        "cljs:electron-watch": "clojure -M:cljs watch app electron",
+        "cljs:release": "clojure -M:cljs release app publishing electron",
+        "cljs:electron-release": "clojure -M:cljs release app publishing electron --config-merge '{:asset-path \"./js\"}'",
         "cljs:test": "clojure -A:test compile test",
         "cljs:run-test": "node static/tests.js",
         "cljs:watch-app": "clojure -M:cljs watch app",
@@ -45,23 +52,32 @@
         "cljs:release-publishing": "clojure -M:cljs release publishing",
         "cljs:dev-release-app": "clojure -M:cljs release app --config-merge '{:closure-defines {frontend.config/DEV-RELEASE true}}'",
         "cljs:debug": "clojure -M:cljs release app --debug",
-        "cljs:report": "clojure -M:cljs run shadow.cljs.build-report app report.html"
+        "cljs:report": "clojure -M:cljs run shadow.cljs.build-report app report.html",
+        "cljs:build-electron": "clojure -A:cljs compile app electron"
     },
     "dependencies": {
+        "@excalidraw/excalidraw": "^0.4.2",
+        "@kanru/rage-wasm": "^0.2.1",
+        "chokidar": "^3.5.1",
+        "chrono-node": "^2.2.1",
         "codemirror": "^5.58.1",
         "diff": "5.0.0",
         "diff-match-patch": "^1.0.5",
-        "fuzzysort": "^1.1.4",
+        "electron": "^11.2.0",
+        "fs": "^0.0.1-security",
+        "fuse.js": "^6.4.6",
         "gulp-cached": "^1.1.1",
         "ignore": "^5.1.8",
         "jszip": "^3.5.0",
-        "mldoc": "^0.3.7",
+        "mldoc": "^0.5.0",
         "mousetrap": "^1.6.5",
+        "path": "^0.12.7",
         "react": "^17.0.1",
         "react-dom": "^17.0.1",
         "react-resize-context": "^3.0.0",
         "react-textarea-autosize": "^8.0.1",
         "react-transition-group": "^4.3.0",
+        "url": "^0.11.0",
         "yargs-parser": "^20.2.4"
     }
 }

+ 1 - 1
public/index.html

@@ -13,4 +13,4 @@ const portal = new MagicPortal(worker);
   const workerThread = await portal.get('workerThread');
   window.workerThread = workerThread;
 })();
-</script><script src="/static/js/main.js"></script><script src="/static/js/highlight.min.js"></script></body></html>
+</script><script src="/static/js/main.js"></script><script src="/static/js/highlight.min.js"></script><script src="/static/js/interact.min.js"></script></body></html>

+ 146 - 55
resources/css/common.css

@@ -1,6 +1,6 @@
 :root {
-  --ls-tag-text-opacity: 0.6;
-  --ls-tag-text-hover-opacity: 0.8;
+  --ls-tag-text-opacity: 0.8;
+  --ls-tag-text-hover-opacity: 1;
   --ls-page-text-size: 1em;
   --ls-page-title-size: 36px;
   --ls-font-family: 'Inter';
@@ -36,8 +36,9 @@ html[data-theme=dark] {
   --ls-active-secondary-color: #d0e8e8;
   --ls-block-properties-background-color: #02222a;
   --ls-block-ref-link-text-color: #1a6376;
-  --ls-search-background-color: var(--ls-primary-background-color);
+  --ls-search-background-color: linear-gradient(to right,#021c23 0,#021b21 200px,#002b36 100%);
   --ls-border-color: #0e5263;
+  --ls-secondary-border-color: #126277;
   --ls-guideline-color: #0b4a5a;
   --ls-menu-hover-color: var(--ls-secondary-background-color);
   --ls-primary-text-color: #a4b5b6;
@@ -61,8 +62,8 @@ html[data-theme=dark] {
   --ls-page-blockquote-border-color: var(--ls-border-color);
   --ls-page-inline-code-color: var(--ls-primary-text-color);
   --ls-page-inline-code-bg-color: #01222a;
-  --ls-scrollbar-foreground-color: rgba(255, 255, 255, 0.1);
-  --ls-scrollbar-background-color: rgba(255, 255, 255, 0.05);
+  --ls-scrollbar-foreground-color: rgba(255, 255, 255, 0.05);
+  --ls-scrollbar-background-color: rgba(30, 60, 67, 0.9);
   --ls-scrollbar-thumb-hover-color: rgba(255, 255, 255, 0.2);
   --ls-head-text-color: var(--ls-link-text-color);
   --ls-icon-color: var(--ls-link-text-color);
@@ -80,44 +81,45 @@ html[data-theme=dark] {
 .white-theme,
 html[data-theme=light] {
   --ls-primary-background-color: white;
-  --ls-secondary-background-color: #dee9f2;
-  --ls-tertiary-background-color: #f0f8ff;
-  --ls-quaternary-background-color: #e1f0fe;
+  --ls-secondary-background-color: #f7f6f4;
+  --ls-tertiary-background-color: #f1eee8;
+  --ls-quaternary-background-color: #e8e5de;
   --ls-table-tr-even-background-color: #f4f5f7;
   --ls-active-primary-color: rgb(4, 85, 145);
   --ls-active-secondary-color: #003761;
-  --ls-block-properties-background-color: var(--ls-tertiary-background-color);
+  --ls-block-properties-background-color: #f7f6f4;
   --ls-block-ref-link-text-color: #D8E1E8;
   --ls-search-background-color: var(--ls-primary-background-color);
   --ls-border-color: #ccc;
-  --ls-guideline-color: var(--ls-border-color);
+  --ls-secondary-border-color: #e2e2e2;
+  --ls-guideline-color: rgba(46, 27, 5, 0.08);
   --ls-menu-hover-color: var(--ls-a-chosen-bg);
-  --ls-primary-text-color: #24292e;
+  --ls-primary-text-color: #433F38;
   --ls-secondary-text-color: #161e2e;
-  --ls-title-text-color: #222;
-  --ls-link-text-color: var(--ls-active-primary-color);
-  --ls-link-text-hover-color: var(--ls-active-secondary-color);
-  --ls-link-ref-text-color: var(--ls-link-text-color);
-  --ls-link-ref-text-hover-color: var(--ls-link-text-hover-color);
-  --ls-tag-text-color: var(--ls-link-text-color);
-  --ls-tag-text-hover-color: var(--ls-link-text-hover-color);
+  --ls-title-text-color: var(--ls-primary-text-color);
+  --ls-link-text-color: #106BA3;
+  --ls-link-text-hover-color: #1a537c;
+  --ls-link-ref-text-color: #106BA3;
+  --ls-link-ref-text-hover-color: #1a537c;
+  --ls-tag-text-color: var(--ls-link-ref-text-color);
+  --ls-tag-text-hover-color: var(--ls-link-ref-text-hover-color);
   --ls-slide-background-color: #fff;
-  --ls-block-bullet-border-color: var(--ls-border-color);
-  --ls-block-bullet-color: #394b59;
+  --ls-block-bullet-border-color: #dedede;
+  --ls-block-bullet-color: rgba(67, 63, 56, 0.25);
   --ls-block-highlight-color: #c0e6fd;
   --ls-selection-background-color: #e4f2ff;
-  --ls-page-checkbox-color: var(--ls-active-primary-color);
-  --ls-page-checkbox-border-color: #8c8c8c;
+  --ls-page-checkbox-color: #9dbbd8;
+  --ls-page-checkbox-border-color: var(--ls-page-checkbox-color);
   --ls-page-blockquote-color: var(--ls-primary-text-color);
-  --ls-page-blockquote-bg-color: var(--ls-secondary-background-color);
-  --ls-page-blockquote-border-color: var(--ls-active-primary-color);
-  --ls-page-inline-code-bg-color: var(--ls-secondary-background-color);
+  --ls-page-blockquote-bg-color: #fbfaf8;
+  --ls-page-blockquote-border-color: #799bbc;
+  --ls-page-inline-code-bg-color: #f7f6f4;
   --ls-page-inline-code-color: var(--ls-primary-text-color);
   --ls-scrollbar-foreground-color: rgba(0, 0, 0, 0.1);
   --ls-scrollbar-background-color: rgba(0, 0, 0, 0.05);
   --ls-scrollbar-thumb-hover-color: rgba(0, 0, 0, 0.2);
   --ls-head-text-color: var(--ls-link-text-color);
-  --ls-icon-color: #6b7280;
+  --ls-icon-color: #c1bdb7;
   --ls-search-icon-color: var(--ls-icon-color);
   --ls-a-chosen-bg: #f4f5f7;
   --ls-right-sidebar-code-bg-color: var(--ls-secondary-background-color);
@@ -223,12 +225,6 @@ pre.code {
 
 }
 
-mark {
-  background: #fef3ac;
-  color: #262626;
-  padding: 0 1px;
-}
-
 dl {
   margin: 1rem 0;
 }
@@ -443,6 +439,10 @@ li p:last-child,
   opacity: 0.6;
 }
 
+.opacity-30 {
+    opacity: 0.3;
+}
+
 .opacity-70 {
   opacity: 0.7;
 }
@@ -540,22 +540,6 @@ li p:last-child,
   overflow-y: auto;
 }
 
-.marker-switch {
-  font-size: 85%;
-  margin-right: 6px;
-  margin-left: 2px;
-  border-radius: 3px;
-  font-weight: 500;
-  display: inline-block;
-  text-align: center;
-  width: 16px;
-  height: 18px;
-  opacity: 0.5;
-  padding: 0 2px 0 2px;
-  border: 1px solid;
-  line-height: 1.3;
-}
-
 .heading-bg {
   border-radius: 50%;
   width: 12px;
@@ -569,6 +553,7 @@ h1.title {
   margin-bottom: 1.5rem;
   color: var(--ls-title-text-color, #222);
   font-size: var(--ls-page-title-size, 36px);
+  font-weight: 700;
 }
 
 .block-highlight,
@@ -634,10 +619,6 @@ a.login:hover {
   color: var(--ls-link-text-hover-color, #000);
 }
 
-a.marker-switch:hover {
-  opacity: 1;
-}
-
 a.tooltip-priority {
   display: contents;
   position: absolute;
@@ -670,12 +651,17 @@ img.small {
 }
 
 a.tag {
-  opacity: var(--ls-tag-text-opacity, 0.6);
-  color: var(--ls-tag-text-color, #045591);
+    font-size: 13px;
+    text-align: center;
+    text-decoration: none;
+    display: inline-block;
+    cursor: pointer;
+    color: var(--ls-tag-text-color, #045591);
+    opacity: var(--ls-tag-text-opacity, 0.8);
 }
 
 a.tag:hover {
-  opacity: var(--ls-tag-text-hover-opacity, 0.8);
+  opacity: var(--ls-tag-text-hover-opacity, 1);
   color: var(--ls-tag-text-hover-color, #045591);
 }
 
@@ -711,3 +697,108 @@ hr {
     resize: both;
     overflow: hidden;
 }
+
+/* ideas from https://github.com/PiotrSss/logseq-bujo-theme/blob/main/main.css */
+
+/***************************************************************
+***************************** TOP ******************************
+***************************************************************/
+
+.cp__header-logo, .fade-link {
+    opacity: .3;
+    transition: .3s;
+}
+
+a.fade-link:hover {
+    opacity: 1;
+}
+
+/* import (arrows) icon */
+
+#head .refresh svg {
+    height: 20px;
+}
+
+/* < > buttons */
+
+a.navigation {
+    border-radius: 3px;
+    transition: .3s;
+}
+
+/* text mark/highlight */
+
+mark {
+    background: #fef3ac;
+    color: #262626;
+    padding: 2px 4px;
+    border-radius: 3px;
+    font-size: 14px;
+}
+
+/* search results */
+.search-results mark {
+    padding: 0;
+    border-radius: 0;
+}
+
+/* page reference */
+
+.page-reference {
+    border-radius: 3px;
+    padding: 2px 0px;
+    transition: .3s;
+}
+
+.page-reference .bracket {
+    opacity: .3;
+}
+
+/* block references */
+
+.block-ref {
+    padding: 2px 5px;
+}
+
+.block-ref .block-ref {
+    padding: 6px 5px;
+    border: none;
+}
+
+/* inline code */
+:not(pre)>code {
+    border-radius: 3px;
+    font-size: .9em;
+    font-family: MonoLisa, "Fira Code", Monaco, Menlo, Consolas, "COURIER NEW", monospace;
+    padding: 3px 5px !important;
+}
+
+a {
+    transition: .3s;
+}
+
+a.tooltip-priority {
+    transition: none;
+}
+
+.page-reference:hover {
+    background: var(--ls-secondary-background-color);
+}
+
+.references-blocks .page-reference:hover {
+    background: var(--ls-tertiary-background-color);
+}
+
+#head .fade-link {
+    font-weight: 600;
+    font-size: 13px;
+}
+
+/* excalidraw */
+.Island > div > div > div {
+    width: 44px;
+}
+
+.excalidraw hr {
+    margin: 0;
+}

File diff suppressed because it is too large
+ 0 - 4
resources/css/excalidraw.min.css


+ 13 - 0
resources/css/fonts.css

@@ -0,0 +1,13 @@
+/* http://www.eaglefonts.com/fg-virgil-ttf-131249.htm */
+@font-face {
+    font-family: "Virgil";
+    src: url("../fonts/Virgil.woff2");
+    font-display: swap;
+}
+
+/* https://github.com/microsoft/cascadia-code */
+@font-face {
+    font-family: "Cascadia";
+    src: url("../fonts/Cascadia.woff2");
+    font-display: swap;
+}

+ 39 - 39
resources/css/inter.css

@@ -3,16 +3,16 @@
   font-style:  normal;
   font-weight: 100;
   font-display: swap;
-  src: url("/static/fonts/inter/Inter-Thin.woff2?v=3.15") format("woff2"),
-       url("/static/fonts/inter/Inter-Thin.woff?v=3.15") format("woff");
+  src: url("../fonts/inter/Inter-Thin.woff2?v=3.15") format("woff2"),
+       url("../fonts/inter/Inter-Thin.woff?v=3.15") format("woff");
 }
 @font-face {
   font-family: 'Inter';
   font-style:  italic;
   font-weight: 100;
   font-display: swap;
-  src: url("/static/fonts/inter/Inter-ThinItalic.woff2?v=3.15") format("woff2"),
-       url("/static/fonts/inter/Inter-ThinItalic.woff?v=3.15") format("woff");
+  src: url("../fonts/inter/Inter-ThinItalic.woff2?v=3.15") format("woff2"),
+       url("../fonts/inter/Inter-ThinItalic.woff?v=3.15") format("woff");
 }
 
 @font-face {
@@ -20,16 +20,16 @@
   font-style:  normal;
   font-weight: 200;
   font-display: swap;
-  src: url("/static/fonts/inter/Inter-ExtraLight.woff2?v=3.15") format("woff2"),
-       url("/static/fonts/inter/Inter-ExtraLight.woff?v=3.15") format("woff");
+  src: url("../fonts/inter/Inter-ExtraLight.woff2?v=3.15") format("woff2"),
+       url("../fonts/inter/Inter-ExtraLight.woff?v=3.15") format("woff");
 }
 @font-face {
   font-family: 'Inter';
   font-style:  italic;
   font-weight: 200;
   font-display: swap;
-  src: url("/static/fonts/inter/Inter-ExtraLightItalic.woff2?v=3.15") format("woff2"),
-       url("/static/fonts/inter/Inter-ExtraLightItalic.woff?v=3.15") format("woff");
+  src: url("../fonts/inter/Inter-ExtraLightItalic.woff2?v=3.15") format("woff2"),
+       url("../fonts/inter/Inter-ExtraLightItalic.woff?v=3.15") format("woff");
 }
 
 @font-face {
@@ -37,16 +37,16 @@
   font-style:  normal;
   font-weight: 300;
   font-display: swap;
-  src: url("/static/fonts/inter/Inter-Light.woff2?v=3.15") format("woff2"),
-       url("/static/fonts/inter/Inter-Light.woff?v=3.15") format("woff");
+  src: url("../fonts/inter/Inter-Light.woff2?v=3.15") format("woff2"),
+       url("../fonts/inter/Inter-Light.woff?v=3.15") format("woff");
 }
 @font-face {
   font-family: 'Inter';
   font-style:  italic;
   font-weight: 300;
   font-display: swap;
-  src: url("/static/fonts/inter/Inter-LightItalic.woff2?v=3.15") format("woff2"),
-       url("/static/fonts/inter/Inter-LightItalic.woff?v=3.15") format("woff");
+  src: url("../fonts/inter/Inter-LightItalic.woff2?v=3.15") format("woff2"),
+       url("../fonts/inter/Inter-LightItalic.woff?v=3.15") format("woff");
 }
 
 @font-face {
@@ -54,16 +54,16 @@
   font-style:  normal;
   font-weight: 400;
   font-display: swap;
-  src: url("/static/fonts/inter/Inter-Regular.woff2?v=3.15") format("woff2"),
-       url("/static/fonts/inter/Inter-Regular.woff?v=3.15") format("woff");
+  src: url("../fonts/inter/Inter-Regular.woff2?v=3.15") format("woff2"),
+       url("../fonts/inter/Inter-Regular.woff?v=3.15") format("woff");
 }
 @font-face {
   font-family: 'Inter';
   font-style:  italic;
   font-weight: 400;
   font-display: swap;
-  src: url("/static/fonts/inter/Inter-Italic.woff2?v=3.15") format("woff2"),
-       url("/static/fonts/inter/Inter-Italic.woff?v=3.15") format("woff");
+  src: url("../fonts/inter/Inter-Italic.woff2?v=3.15") format("woff2"),
+       url("../fonts/inter/Inter-Italic.woff?v=3.15") format("woff");
 }
 
 @font-face {
@@ -71,16 +71,16 @@
   font-style:  normal;
   font-weight: 500;
   font-display: swap;
-  src: url("/static/fonts/inter/Inter-Medium.woff2?v=3.15") format("woff2"),
-       url("/static/fonts/inter/Inter-Medium.woff?v=3.15") format("woff");
+  src: url("../fonts/inter/Inter-Medium.woff2?v=3.15") format("woff2"),
+       url("../fonts/inter/Inter-Medium.woff?v=3.15") format("woff");
 }
 @font-face {
   font-family: 'Inter';
   font-style:  italic;
   font-weight: 500;
   font-display: swap;
-  src: url("/static/fonts/inter/Inter-MediumItalic.woff2?v=3.15") format("woff2"),
-       url("/static/fonts/inter/Inter-MediumItalic.woff?v=3.15") format("woff");
+  src: url("../fonts/inter/Inter-MediumItalic.woff2?v=3.15") format("woff2"),
+       url("../fonts/inter/Inter-MediumItalic.woff?v=3.15") format("woff");
 }
 
 @font-face {
@@ -88,16 +88,16 @@
   font-style:  normal;
   font-weight: 600;
   font-display: swap;
-  src: url("/static/fonts/inter/Inter-SemiBold.woff2?v=3.15") format("woff2"),
-       url("/static/fonts/inter/Inter-SemiBold.woff?v=3.15") format("woff");
+  src: url("../fonts/inter/Inter-SemiBold.woff2?v=3.15") format("woff2"),
+       url("../fonts/inter/Inter-SemiBold.woff?v=3.15") format("woff");
 }
 @font-face {
   font-family: 'Inter';
   font-style:  italic;
   font-weight: 600;
   font-display: swap;
-  src: url("/static/fonts/inter/Inter-SemiBoldItalic.woff2?v=3.15") format("woff2"),
-       url("/static/fonts/inter/Inter-SemiBoldItalic.woff?v=3.15") format("woff");
+  src: url("../fonts/inter/Inter-SemiBoldItalic.woff2?v=3.15") format("woff2"),
+       url("../fonts/inter/Inter-SemiBoldItalic.woff?v=3.15") format("woff");
 }
 
 @font-face {
@@ -105,16 +105,16 @@
   font-style:  normal;
   font-weight: 700;
   font-display: swap;
-  src: url("/static/fonts/inter/Inter-Bold.woff2?v=3.15") format("woff2"),
-       url("/static/fonts/inter/Inter-Bold.woff?v=3.15") format("woff");
+  src: url("../fonts/inter/Inter-Bold.woff2?v=3.15") format("woff2"),
+       url("../fonts/inter/Inter-Bold.woff?v=3.15") format("woff");
 }
 @font-face {
   font-family: 'Inter';
   font-style:  italic;
   font-weight: 700;
   font-display: swap;
-  src: url("/static/fonts/inter/Inter-BoldItalic.woff2?v=3.15") format("woff2"),
-       url("/static/fonts/inter/Inter-BoldItalic.woff?v=3.15") format("woff");
+  src: url("../fonts/inter/Inter-BoldItalic.woff2?v=3.15") format("woff2"),
+       url("../fonts/inter/Inter-BoldItalic.woff?v=3.15") format("woff");
 }
 
 @font-face {
@@ -122,16 +122,16 @@
   font-style:  normal;
   font-weight: 800;
   font-display: swap;
-  src: url("/static/fonts/inter/Inter-ExtraBold.woff2?v=3.15") format("woff2"),
-       url("/static/fonts/inter/Inter-ExtraBold.woff?v=3.15") format("woff");
+  src: url("../fonts/inter/Inter-ExtraBold.woff2?v=3.15") format("woff2"),
+       url("../fonts/inter/Inter-ExtraBold.woff?v=3.15") format("woff");
 }
 @font-face {
   font-family: 'Inter';
   font-style:  italic;
   font-weight: 800;
   font-display: swap;
-  src: url("/static/fonts/inter/Inter-ExtraBoldItalic.woff2?v=3.15") format("woff2"),
-       url("/static/fonts/inter/Inter-ExtraBoldItalic.woff?v=3.15") format("woff");
+  src: url("../fonts/inter/Inter-ExtraBoldItalic.woff2?v=3.15") format("woff2"),
+       url("../fonts/inter/Inter-ExtraBoldItalic.woff?v=3.15") format("woff");
 }
 
 @font-face {
@@ -139,16 +139,16 @@
   font-style:  normal;
   font-weight: 900;
   font-display: swap;
-  src: url("/static/fonts/inter/Inter-Black.woff2?v=3.15") format("woff2"),
-       url("/static/fonts/inter/Inter-Black.woff?v=3.15") format("woff");
+  src: url("../fonts/inter/Inter-Black.woff2?v=3.15") format("woff2"),
+       url("../fonts/inter/Inter-Black.woff?v=3.15") format("woff");
 }
 @font-face {
   font-family: 'Inter';
   font-style:  italic;
   font-weight: 900;
   font-display: swap;
-  src: url("/static/fonts/inter/Inter-BlackItalic.woff2?v=3.15") format("woff2"),
-       url("/static/fonts/inter/Inter-BlackItalic.woff?v=3.15") format("woff");
+  src: url("../fonts/inter/Inter-BlackItalic.woff2?v=3.15") format("woff2"),
+       url("../fonts/inter/Inter-BlackItalic.woff?v=3.15") format("woff");
 }
 
 /* -------------------------------------------------------
@@ -166,7 +166,7 @@ Usage:
   font-display: swap;
   font-style: normal;
   font-named-instance: 'Regular';
-  src: url("/static/fonts/inter/Inter-roman.var.woff2?v=3.15") format("woff2");
+  src: url("../fonts/inter/Inter-roman.var.woff2?v=3.15") format("woff2");
 }
 @font-face {
   font-family: 'Inter var';
@@ -174,7 +174,7 @@ Usage:
   font-display: swap;
   font-style: italic;
   font-named-instance: 'Italic';
-  src: url("/static/fonts/inter/Inter-italic.var.woff2?v=3.15") format("woff2");
+  src: url("../fonts/inter/Inter-italic.var.woff2?v=3.15") format("woff2");
 }
 
 
@@ -196,5 +196,5 @@ explicitly, e.g.
   font-weight: 100 900;
   font-display: swap;
   font-style: oblique 0deg 10deg;
-  src: url("/static/fonts/inter/Inter.var.woff2?v=3.15") format("woff2");
+  src: url("../fonts/inter/Inter.var.woff2?v=3.15") format("woff2");
 }

+ 4 - 2
resources/css/style.css

@@ -2,6 +2,7 @@
 @import "./inter.css";
 @import "./reveal.min.css";
 @import "./reveal_black.min.css";
+@import "./fonts.css";
 @import "./excalidraw.min.css";
 @import "./katex.min.css";
 @import "./codemirror.min.css";
@@ -9,6 +10,7 @@
 @import "./table.css";
 @import "./datepicker.css";
 @import "./highlight.css";
-@import "../../static/css/tailwind.core.css"; /* Build by gulp. Check `_buildTailwind` for more detail */
+@import "./tailwind.core.css"; /* Build by gulp. Check `_buildTailwind` for more detail */
+@import "./tooltip.css";
 @import "./common.css";
-@import "../../static/css/tailwind.build.css"; /* Build by gulp. Check `_buildTailwind` for more detail */
+@import "./tailwind.build.css"; /* Build by gulp. Check `_buildTailwind` for more detail */

+ 15 - 0
resources/css/style.dev.css

@@ -0,0 +1,15 @@
+@charset "utf-8";
+@import "./inter.css";
+@import "./reveal.min.css";
+@import "./reveal_black.min.css";
+@import "./fonts.css";
+@import "./excalidraw.min.css";
+@import "./katex.min.css";
+@import "./codemirror.min.css";
+@import "./animation.css";
+@import "./table.css";
+@import "./datepicker.css";
+@import "./highlight.css";
+@import "./tooltip.css";
+@import "./tailwind.core.css"; /* Build by gulp. Check `_buildTailwind` for more detail */
+@import "./common.css";

+ 16 - 0
resources/css/tooltip.css

@@ -0,0 +1,16 @@
+.Tooltip {
+    position:relative;
+}
+
+.Tooltip__label {--arrow-size: 4px;visibility:hidden;width:10ch;background:#000;color:#fff;text-align:center;border-radius:4px;padding:4px;position:absolute;z-index:10;font-size:0.7rem;line-height:1.5;top:calc(100% + var(--arrow-size) + 3px);left:calc(-50% + var(--arrow-size) / 2 - 1px);word-wrap:break-word}
+.Tooltip__label::after {content:"";border:var(--arrow-size) solid transparent;border-bottom-color:#000;position:absolute;bottom:100%;left:calc(50% - var(--arrow-size))}
+
+/* .Tooltip:not(:hover) {pointer-events:none} */
+
+.Tooltip:hover .Tooltip__label {
+    visibility:visible
+}
+
+.Tooltip__label:hover {
+    visibility:visible
+}

+ 25 - 0
resources/dev.html

@@ -0,0 +1,25 @@
+<!doctype html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport"
+        content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
+  <meta http-equiv="X-UA-Compatible" content="ie=edge">
+  <title>Electron Development Entries</title>
+</head>
+<body>
+<div style="padding: 50px; text-align: center;">
+  <h1>
+    Development Mode :)
+  </h1>
+  <h3>
+    <a href="http://localhost:3000">
+      http://localhost:3000
+    </a> <br> <br>
+    <a href="http://localhost:3001">
+      http://localhost:3001
+    </a>
+  </h3>
+</div>
+</body>
+</html>

+ 111 - 0
resources/electron-dev.html

@@ -0,0 +1,111 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <meta content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no" name="viewport">
+  <link href="./css/style.dev.css" rel="stylesheet" type="text/css">
+  <link href="./css/tailwind.build.css" rel="stylesheet" type="text/css">
+  <link href="./img/logo.png" rel="shortcut icon" type="image/png">
+  <link href="./img/logo.png" rel="shortcut icon" sizes="192x192">
+  <link href="./img/logo.png" rel="apple-touch-icon">
+  <meta content="Logseq" name="apple-mobile-web-app-title">
+  <meta content="yes" name="apple-mobile-web-app-capable">
+  <meta content="yes" name="apple-touch-fullscreen">
+  <meta content="black-translucent" name="apple-mobile-web-app-status-bar-style">
+  <meta content="yes" name="mobile-web-app-capable">
+  <meta content="summary" name="twitter:card">
+  <meta content="A local-first knowledge base which can be synced using Git." name="twitter:description">
+  <meta content="@logseq" name="twitter:site">
+  <meta content="A local-first knowledge base." name="twitter:title">
+  <meta content="https://asset.logseq.com/static/img/logo.png" name="twitter:image:src">
+  <meta content="A local-first knowledge base which can be synced using Git." name="twitter:image:alt">
+  <meta content="A local-first knowledge base." property="og:title">
+  <meta content="site" property="og:type">
+  <meta content="https://logseq.com" property="og:url">
+  <meta content="https://asset.logseq.com/static/img/logo.png" property="og:image">
+  <meta content="A local-first knowledge base which can be synced using Git." property="og:description">
+  <title>Logseq: A local-first knowledge base</title>
+  <meta content="logseq" property="og:site_name">
+  <meta content="A local-first knowledge base which can be synced using Git." name="description">
+</head>
+<body>
+<div id="root">
+  <svg class="ls-center" fill="none" height="300" viewbox="0 0 300 300" width="300">
+    <g filter="url(#filter0_d)">
+      <path class="fade-in one"
+            d="M85.2474 196.999C78.9469 195.427 75.5941 186.78 77.7586 177.685C79.9232 168.589 86.7856 162.49 93.0861 164.061C99.3866 165.632 102.739 174.279 100.575 183.375C98.4102 192.47 91.5479 198.57 85.2474 196.999Z"
+            fill="white"></path>
+      <path class="fade-in one"
+            d="M159.307 218.517C159.091 225.031 149.797 229.996 138.548 229.605C127.298 229.214 118.354 223.616 118.57 217.102C118.786 210.587 128.081 205.623 139.33 206.014C150.579 206.404 159.523 212.002 159.307 218.517Z"
+            fill="white"></path>
+      <path class="fade-in one"
+            d="M96.8481 135.55C101.197 138.758 100.722 147.042 95.7864 154.053C90.8513 161.065 83.3252 164.149 78.9764 160.941C74.6276 157.734 75.103 149.45 80.0381 142.438C84.9732 135.426 92.4993 132.343 96.8481 135.55Z"
+            fill="white"></path>
+      <path class="fade-in one"
+            d="M114.449 221.038C111.977 230.437 99.6731 236.491 86.9668 234.559C74.2605 232.626 65.9638 223.44 68.4357 214.04C70.9075 204.641 83.2119 198.587 95.9182 200.52C108.625 202.452 116.921 211.638 114.449 221.038Z"
+            fill="white"></path>
+      <path class="fade-in one"
+            d="M85.8103 132.35C75.571 131.027 67.8608 120.196 68.589 108.16C69.3173 96.123 78.2083 87.438 88.4476 88.7613C98.6869 90.0845 106.397 100.915 105.669 112.951C104.941 124.988 96.0496 133.673 85.8103 132.35Z"
+            fill="white"></path>
+      <path class="fade-in two"
+            d="M209.177 221.12C206.705 230.519 194.401 236.573 181.694 234.641C168.988 232.708 160.691 223.522 163.163 214.123C165.635 204.723 177.939 198.669 190.646 200.602C203.352 202.534 211.649 211.72 209.177 221.12Z"
+            fill="white"></path>
+      <path class="fade-in two"
+            d="M135.635 151.371C129.334 149.799 125.982 141.152 128.146 132.057C130.311 122.961 137.173 116.862 143.474 118.433C149.774 120.004 153.127 128.651 150.962 137.747C148.798 146.842 141.935 152.942 135.635 151.371Z"
+            fill="white"></path>
+      <path class="fade-in two"
+            d="M209.694 172.889C209.478 179.403 200.184 184.368 188.935 183.977C177.686 183.586 168.742 177.988 168.958 171.473C169.174 164.959 178.468 159.995 189.717 160.386C200.966 160.776 209.91 166.374 209.694 172.889Z"
+            fill="white"></path>
+      <path class="fade-in two"
+            d="M147.236 89.9221C151.584 93.1296 151.109 101.414 146.174 108.425C141.239 115.437 133.713 118.521 129.364 115.313C125.015 112.106 125.49 103.822 130.426 96.81C135.361 89.7984 142.887 86.7146 147.236 89.9221Z"
+            fill="white"></path>
+      <path class="fade-in two"
+            d="M164.837 175.41C162.365 184.809 150.061 190.863 137.354 188.931C124.648 186.998 116.351 177.812 118.823 168.412C121.295 159.013 133.599 152.959 146.306 154.892C159.012 156.824 167.309 166.01 164.837 175.41Z"
+            fill="white"></path>
+      <path class="fade-in three"
+            d="M136.198 86.7217C125.958 85.3985 118.248 74.5682 118.977 62.5316C119.705 50.4949 128.596 41.81 138.835 43.1332C149.074 44.4564 156.785 55.2867 156.056 67.3234C155.328 79.36 146.437 88.045 136.198 86.7217Z"
+            fill="white"></path>
+      <path class="fade-in three"
+            d="M259.564 175.492C257.092 184.891 244.788 190.945 232.082 189.013C219.375 187.08 211.079 177.894 213.551 168.495C216.023 159.095 228.327 153.041 241.033 154.974C253.739 156.906 262.036 166.092 259.564 175.492Z"
+            fill="white"></path>
+    </g>
+    <defs>
+      <filter color-interpolation-filters="sRGB" filterunits="userSpaceOnUse" height="200" id="filter0_d" width="200"
+              x="64" y="43">
+        <feFlood flood-opacity="0" result="BackgroundImageFix"></feFlood>
+        <feColorMatrix in="SourceAlpha" type="matrix"
+                       values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"></feColorMatrix>
+        <feOffset dy="4"></feOffset>
+        <feGaussianBlur stddeviation="2"></feGaussianBlur>
+        <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"></feColorMatrix>
+        <feBlend in2="BackgroundImageFix" mode="normal" result="effect1_dropShadow"></feBlend>
+        <feBlend in2="effect1_dropShadow" in="SourceGraphic" mode="normal" result="shape"></feBlend>
+      </filter>
+    </defs>
+  </svg>
+</div>
+<script>window.user = null</script>
+<script src="./js/magic_portal.js"></script>
+<script>let worker = new Worker('./js/worker.js')
+const portal = new MagicPortal(worker);
+(async () => {
+  const git = await portal.get('git')
+  window.git = git
+  const fs = await portal.get('fs')
+  window.fs = fs
+  const pfs = await portal.get('pfs')
+  window.pfs = pfs
+  const gitHttp = await portal.get('gitHttp')
+  window.gitHttp = gitHttp
+  const workerThread = await portal.get('workerThread')
+  window.workerThread = workerThread
+})()
+</script>
+<script defer src="./js/highlight.min.js"></script>
+<script defer src="./js/interact.min.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>

+ 112 - 0
resources/electron.html

@@ -0,0 +1,112 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <meta content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no" name="viewport">
+  <link href="./css/style.css" rel="stylesheet" type="text/css">
+  <link href="./img/logo.png" rel="shortcut icon" type="image/png">
+  <link href="./img/logo.png" rel="shortcut icon" sizes="192x192">
+  <link href="./img/logo.png" rel="apple-touch-icon">
+  <meta content="Logseq" name="apple-mobile-web-app-title">
+  <meta content="yes" name="apple-mobile-web-app-capable">
+  <meta content="yes" name="apple-touch-fullscreen">
+  <meta content="black-translucent" name="apple-mobile-web-app-status-bar-style">
+  <meta content="yes" name="mobile-web-app-capable">
+  <meta content="summary" name="twitter:card">
+  <meta content="A local-first knowledge base which can be synced using Git." name="twitter:description">
+  <meta content="@logseq" name="twitter:site">
+  <meta content="A local-first knowledge base." name="twitter:title">
+  <meta content="https://asset.logseq.com/static/img/logo.png" name="twitter:image:src">
+  <meta content="A local-first knowledge base which can be synced using Git." name="twitter:image:alt">
+  <meta content="A local-first knowledge base." property="og:title">
+  <meta content="site" property="og:type">
+  <meta content="https://logseq.com" property="og:url">
+  <meta content="https://asset.logseq.com/static/img/logo.png" property="og:image">
+  <meta content="A local-first knowledge base which can be synced using Git." property="og:description">
+  <title>Logseq: A local-first knowledge base</title>
+  <meta content="logseq" property="og:site_name">
+  <meta content="A local-first knowledge base which can be synced using Git." name="description">
+  <script crossorigin="anonymous" defer onload="Sentry.init({dsn: 'https://[email protected]/5311485'});" src="https://asset.logseq.com/static/js/sentry.min.js">
+  </script>
+</head>
+<body>
+<div id="root">
+  <svg class="ls-center" fill="none" height="300" viewbox="0 0 300 300" width="300">
+    <g filter="url(#filter0_d)">
+      <path class="fade-in one"
+            d="M85.2474 196.999C78.9469 195.427 75.5941 186.78 77.7586 177.685C79.9232 168.589 86.7856 162.49 93.0861 164.061C99.3866 165.632 102.739 174.279 100.575 183.375C98.4102 192.47 91.5479 198.57 85.2474 196.999Z"
+            fill="white"></path>
+      <path class="fade-in one"
+            d="M159.307 218.517C159.091 225.031 149.797 229.996 138.548 229.605C127.298 229.214 118.354 223.616 118.57 217.102C118.786 210.587 128.081 205.623 139.33 206.014C150.579 206.404 159.523 212.002 159.307 218.517Z"
+            fill="white"></path>
+      <path class="fade-in one"
+            d="M96.8481 135.55C101.197 138.758 100.722 147.042 95.7864 154.053C90.8513 161.065 83.3252 164.149 78.9764 160.941C74.6276 157.734 75.103 149.45 80.0381 142.438C84.9732 135.426 92.4993 132.343 96.8481 135.55Z"
+            fill="white"></path>
+      <path class="fade-in one"
+            d="M114.449 221.038C111.977 230.437 99.6731 236.491 86.9668 234.559C74.2605 232.626 65.9638 223.44 68.4357 214.04C70.9075 204.641 83.2119 198.587 95.9182 200.52C108.625 202.452 116.921 211.638 114.449 221.038Z"
+            fill="white"></path>
+      <path class="fade-in one"
+            d="M85.8103 132.35C75.571 131.027 67.8608 120.196 68.589 108.16C69.3173 96.123 78.2083 87.438 88.4476 88.7613C98.6869 90.0845 106.397 100.915 105.669 112.951C104.941 124.988 96.0496 133.673 85.8103 132.35Z"
+            fill="white"></path>
+      <path class="fade-in two"
+            d="M209.177 221.12C206.705 230.519 194.401 236.573 181.694 234.641C168.988 232.708 160.691 223.522 163.163 214.123C165.635 204.723 177.939 198.669 190.646 200.602C203.352 202.534 211.649 211.72 209.177 221.12Z"
+            fill="white"></path>
+      <path class="fade-in two"
+            d="M135.635 151.371C129.334 149.799 125.982 141.152 128.146 132.057C130.311 122.961 137.173 116.862 143.474 118.433C149.774 120.004 153.127 128.651 150.962 137.747C148.798 146.842 141.935 152.942 135.635 151.371Z"
+            fill="white"></path>
+      <path class="fade-in two"
+            d="M209.694 172.889C209.478 179.403 200.184 184.368 188.935 183.977C177.686 183.586 168.742 177.988 168.958 171.473C169.174 164.959 178.468 159.995 189.717 160.386C200.966 160.776 209.91 166.374 209.694 172.889Z"
+            fill="white"></path>
+      <path class="fade-in two"
+            d="M147.236 89.9221C151.584 93.1296 151.109 101.414 146.174 108.425C141.239 115.437 133.713 118.521 129.364 115.313C125.015 112.106 125.49 103.822 130.426 96.81C135.361 89.7984 142.887 86.7146 147.236 89.9221Z"
+            fill="white"></path>
+      <path class="fade-in two"
+            d="M164.837 175.41C162.365 184.809 150.061 190.863 137.354 188.931C124.648 186.998 116.351 177.812 118.823 168.412C121.295 159.013 133.599 152.959 146.306 154.892C159.012 156.824 167.309 166.01 164.837 175.41Z"
+            fill="white"></path>
+      <path class="fade-in three"
+            d="M136.198 86.7217C125.958 85.3985 118.248 74.5682 118.977 62.5316C119.705 50.4949 128.596 41.81 138.835 43.1332C149.074 44.4564 156.785 55.2867 156.056 67.3234C155.328 79.36 146.437 88.045 136.198 86.7217Z"
+            fill="white"></path>
+      <path class="fade-in three"
+            d="M259.564 175.492C257.092 184.891 244.788 190.945 232.082 189.013C219.375 187.08 211.079 177.894 213.551 168.495C216.023 159.095 228.327 153.041 241.033 154.974C253.739 156.906 262.036 166.092 259.564 175.492Z"
+            fill="white"></path>
+    </g>
+    <defs>
+      <filter color-interpolation-filters="sRGB" filterunits="userSpaceOnUse" height="200" id="filter0_d" width="200"
+              x="64" y="43">
+        <feFlood flood-opacity="0" result="BackgroundImageFix"></feFlood>
+        <feColorMatrix in="SourceAlpha" type="matrix"
+                       values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"></feColorMatrix>
+        <feOffset dy="4"></feOffset>
+        <feGaussianBlur stddeviation="2"></feGaussianBlur>
+        <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"></feColorMatrix>
+        <feBlend in2="BackgroundImageFix" mode="normal" result="effect1_dropShadow"></feBlend>
+        <feBlend in2="effect1_dropShadow" in="SourceGraphic" mode="normal" result="shape"></feBlend>
+      </filter>
+    </defs>
+  </svg>
+</div>
+<script>window.user = null</script>
+<script src="./js/magic_portal.js"></script>
+<script>let worker = new Worker('./js/worker.js')
+const portal = new MagicPortal(worker);
+(async () => {
+  const git = await portal.get('git')
+  window.git = git
+  const fs = await portal.get('fs')
+  window.fs = fs
+  const pfs = await portal.get('pfs')
+  window.pfs = pfs
+  const gitHttp = await portal.get('gitHttp')
+  window.gitHttp = gitHttp
+  const workerThread = await portal.get('workerThread')
+  window.workerThread = workerThread
+})()
+</script>
+<script defer src="./js/highlight.min.js"></script>
+<script defer src="./js/interact.min.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>

+ 12 - 0
resources/entitlements.plist

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+  <dict>
+    <key>com.apple.security.cs.allow-jit</key>
+    <true/>
+    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
+    <true/>
+    <key>com.apple.security.cs.debugger</key>
+    <true/>
+  </dict>
+</plist>

+ 0 - 0
resources/css/Cascadia.woff2 → resources/fonts/Cascadia.woff2


+ 0 - 0
resources/css/FG_Virgil.woff2 → resources/fonts/FG_Virgil.woff2


BIN
resources/fonts/Virgil.woff2


+ 56 - 0
resources/forge.config.js

@@ -0,0 +1,56 @@
+const path = require('path')
+
+module.exports = {
+  packagerConfig: {
+    icon: './icons/logseq_big_sur.icns',
+    osxSign: {
+      identity: 'Developer ID Application: Tiansheng Qin',
+      'hardened-runtime': true,
+      entitlements: 'entitlements.plist',
+      'entitlements-inherit': 'entitlements.plist',
+      'signature-flags': 'library'
+    },
+    osxNotarize: {
+      appleId: "my-fake-apple-id",
+      appleIdPassword: "my-fake-apple-id-password",
+    },
+  },
+  makers: [
+    {
+      'name': '@electron-forge/maker-squirrel',
+      'config': {
+        'name': 'Logseq',
+        'setupIcon': './icons/logseq.ico'
+      }
+    },
+    {
+      name: '@electron-forge/maker-dmg',
+      config: {
+        format: 'ULFO',
+        icon: './icons/logseq_big_sur.icns',
+        name: 'Logseq'
+      }
+    },
+    {
+      name: '@electron-forge/maker-zip',
+      platforms: ['darwin', 'linux']
+    },
+    {
+      name: 'electron-forge-maker-appimage',
+      platforms: ['linux']
+    }
+  ],
+
+  publishers: [
+    {
+      name: '@electron-forge/publisher-github',
+      config: {
+        repository: {
+          owner: 'logseq',
+          name: 'logseq'
+        },
+        prerelease: true
+      }
+    }
+  ]
+}

BIN
resources/icons/logseq.icns


BIN
resources/icons/logseq.ico


BIN
resources/icons/logseq.png


BIN
resources/icons/logseq_big_sur.icns


BIN
resources/icons/logseq_big_sur.ico


BIN
resources/icons/logseq_big_sur.png


BIN
resources/img/dmg-bg.png


File diff suppressed because it is too large
+ 0 - 1
resources/js/excalidraw.min.js


File diff suppressed because it is too large
+ 1 - 0
resources/js/interact.min.js


+ 176 - 0
resources/js/isomorphic-git/1.7.4/http-web-index.umd.js

@@ -0,0 +1,176 @@
+(function (global, factory) {
+  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
+  typeof define === 'function' && define.amd ? define(['exports'], factory) :
+  (global = global || self, factory(global.GitHttp = {}));
+}(this, (function (exports) { 'use strict';
+
+  /**
+   * @typedef {Object} GitProgressEvent
+   * @property {string} phase
+   * @property {number} loaded
+   * @property {number} total
+   */
+
+  /**
+   * @callback ProgressCallback
+   * @param {GitProgressEvent} progress
+   * @returns {void | Promise<void>}
+   */
+
+  /**
+   * @typedef {Object} GitHttpRequest
+   * @property {string} url - The URL to request
+   * @property {string} [method='GET'] - The HTTP method to use
+   * @property {Object<string, string>} [headers={}] - Headers to include in the HTTP request
+   * @property {AsyncIterableIterator<Uint8Array>} [body] - An async iterator of Uint8Arrays that make up the body of POST requests
+   * @property {ProgressCallback} [onProgress] - Reserved for future use (emitting `GitProgressEvent`s)
+   * @property {object} [signal] - Reserved for future use (canceling a request)
+   */
+
+  /**
+   * @typedef {Object} GitHttpResponse
+   * @property {string} url - The final URL that was fetched after any redirects
+   * @property {string} [method] - The HTTP method that was used
+   * @property {Object<string, string>} [headers] - HTTP response headers
+   * @property {AsyncIterableIterator<Uint8Array>} [body] - An async iterator of Uint8Arrays that make up the body of the response
+   * @property {number} statusCode - The HTTP status code
+   * @property {string} statusMessage - The HTTP status message
+   */
+
+  /**
+   * @callback HttpFetch
+   * @param {GitHttpRequest} request
+   * @returns {Promise<GitHttpResponse>}
+   */
+
+  /**
+   * @typedef {Object} HttpClient
+   * @property {HttpFetch} request
+   */
+
+  // Convert a value to an Async Iterator
+  // This will be easier with async generator functions.
+  function fromValue(value) {
+    let queue = [value];
+    return {
+      next() {
+        return Promise.resolve({ done: queue.length === 0, value: queue.pop() })
+      },
+      return() {
+        queue = [];
+        return {}
+      },
+      [Symbol.asyncIterator]() {
+        return this
+      },
+    }
+  }
+
+  function getIterator(iterable) {
+    if (iterable[Symbol.asyncIterator]) {
+      return iterable[Symbol.asyncIterator]()
+    }
+    if (iterable[Symbol.iterator]) {
+      return iterable[Symbol.iterator]()
+    }
+    if (iterable.next) {
+      return iterable
+    }
+    return fromValue(iterable)
+  }
+
+  // Currently 'for await' upsets my linters.
+  async function forAwait(iterable, cb) {
+    const iter = getIterator(iterable);
+    while (true) {
+      const { value, done } = await iter.next();
+      if (value) await cb(value);
+      if (done) break
+    }
+    if (iter.return) iter.return();
+  }
+
+  async function collect(iterable) {
+    let size = 0;
+    const buffers = [];
+    // This will be easier once `for await ... of` loops are available.
+    await forAwait(iterable, value => {
+      buffers.push(value);
+      size += value.byteLength;
+    });
+    const result = new Uint8Array(size);
+    let nextIndex = 0;
+    for (const buffer of buffers) {
+      result.set(buffer, nextIndex);
+      nextIndex += buffer.byteLength;
+    }
+    return result
+  }
+
+  // Convert a web ReadableStream (not Node stream!) to an Async Iterator
+  // adapted from https://jakearchibald.com/2017/async-iterators-and-generators/
+  function fromStream(stream) {
+    // Use native async iteration if it's available.
+    if (stream[Symbol.asyncIterator]) return stream
+    const reader = stream.getReader();
+    return {
+      next() {
+        return reader.read()
+      },
+      return() {
+        reader.releaseLock();
+        return {}
+      },
+      [Symbol.asyncIterator]() {
+        return this
+      },
+    }
+  }
+
+  /* eslint-env browser */
+
+  /**
+   * HttpClient
+   *
+   * @param {GitHttpRequest} request
+   * @returns {Promise<GitHttpResponse>}
+   */
+  async function request({
+    onProgress,
+    url,
+    method = 'GET',
+    headers = {},
+    body,
+  }) {
+    // streaming uploads aren't possible yet in the browser
+    if (body) {
+      body = await collect(body);
+    }
+    const res = await fetch(url, { method, headers, body });
+    const iter =
+      res.body && res.body.getReader
+        ? fromStream(res.body)
+        : [new Uint8Array(await res.arrayBuffer())];
+    // convert Header object to ordinary JSON
+    headers = {};
+    for (const [key, value] of res.headers.entries()) {
+      headers[key] = value;
+    }
+    return {
+      url: res.url,
+      method: res.method,
+      statusCode: res.status,
+      statusMessage: res.statusText,
+      body: iter,
+      headers: headers,
+    }
+  }
+
+  var index = { request };
+
+  exports.default = index;
+  exports.request = request;
+
+  Object.defineProperty(exports, '__esModule', { value: true });
+
+})));

File diff suppressed because it is too large
+ 0 - 0
resources/js/isomorphic-git/1.7.4/index.umd.min.js


+ 124 - 0
resources/js/preload.js

@@ -0,0 +1,124 @@
+const fs = require('fs')
+const path = require('path')
+const { ipcRenderer, contextBridge, shell, clipboard } = require('electron')
+
+const IS_MAC = process.platform === 'darwin'
+const IS_WIN32 = process.platform === 'win32'
+
+function getFilePathFromClipboard () {
+  if (IS_WIN32) {
+    const rawFilePath = clipboard.read('FileNameW')
+    return rawFilePath.replace(new RegExp(String.fromCharCode(0), 'g'), '')
+  } else if (IS_MAC) {
+    return clipboard.read('public.file-url').replace('file://', '')
+  } else {
+    return clipboard.readText()
+  }
+}
+
+function isClipboardHasImage () {
+  return !clipboard.readImage().isEmpty()
+}
+
+contextBridge.exposeInMainWorld('apis', {
+  doAction: async (arg) => {
+    return await ipcRenderer.invoke('main', arg)
+  },
+
+  on: (channel, callback) => {
+    const newCallback = (_, data) => callback(data)
+    ipcRenderer.on(channel, newCallback)
+  },
+
+  checkForUpdates: async (...args) => {
+    await ipcRenderer.invoke('check-for-updates', ...args)
+  },
+
+  setUpdatesCallback (cb) {
+    if (typeof cb !== 'function') return
+
+    const channel = 'updates-callback'
+    ipcRenderer.removeAllListeners(channel)
+    ipcRenderer.on(channel, cb)
+  },
+
+  installUpdatesAndQuitApp () {
+    ipcRenderer.invoke('install-updates', true)
+  },
+
+  async openExternal (url, options) {
+    await shell.openExternal(url, options)
+  },
+
+  async openPath (path) {
+    await shell.openPath(path)
+  },
+
+  showItemInFolder (fullpath) {
+    if (IS_WIN32) {
+      shell.openPath(path.dirname(fullpath))
+    } else {
+      shell.showItemInFolder(fullpath)
+    }
+  },
+
+  /**
+   * When from is empty. The resource maybe from
+   * client paste or screenshoot.
+   * @param repoPathRoot
+   * @param to
+   * @param from?
+   * @returns {Promise<void>}
+   */
+  async copyFileToAssets (repoPathRoot, to, from) {
+    if (from && fs.statSync(from).isDirectory()) {
+      throw new Error('not support copy directory')
+    }
+
+    const dest = path.join(repoPathRoot, to)
+    const assetsRoot = path.dirname(dest)
+
+    if (!/assets$/.test(assetsRoot)) {
+      throw new Error('illegal assets dirname')
+    }
+
+    await fs.promises.mkdir(assetsRoot, { recursive: true })
+
+    from = decodeURIComponent(from || getFilePathFromClipboard())
+
+    if (from) {
+      // console.debug('copy file: ', from, dest)
+      await fs.promises.copyFile(from, dest)
+      return path.basename(from)
+    }
+
+    // support image
+    // console.debug('read image: ', from, dest)
+    const nImg = clipboard.readImage()
+
+    if (nImg && !nImg.isEmpty()) {
+      const rawExt = path.extname(dest)
+      return await fs.promises.writeFile(
+        dest.replace(rawExt, '.png'),
+        nImg.toPNG()
+      )
+    }
+  },
+
+  toggleMaxOrMinActiveWindow (isToggleMin = false) {
+    ipcRenderer.invoke('toggle-max-or-min-active-win', isToggleMin)
+  },
+
+  /**
+   * internal
+   * @param type
+   * @param args
+   * @private
+   */
+  async _callApplication (type, ...args) {
+    return await ipcRenderer.invoke('call-application', type, ...args)
+  },
+
+  getFilePathFromClipboard,
+  isClipboardHasImage
+})

+ 4 - 4
resources/js/worker.js

@@ -1,10 +1,10 @@
 importScripts(
   // Batched optimization
-  "/static/js/lightning-fs.min.js?v=0.0.2.3",
-  "https://cdn.jsdelivr.net/npm/isomorphic-git@1.7.4/index.umd.min.js",
-  "https://cdn.jsdelivr.net/npm/[email protected]/http/web/index.umd.js",
+  "./lightning-fs.min.js?v=0.0.2.3",
+  "./isomorphic-git/1.7.4/index.umd.min.js",
+  "./isomorphic-git/1.7.4/http-web-index.umd.js",
   // Fixed a bug
-  "/static/js/magic_portal.js"
+  "./magic_portal.js"
 );
 
 const detect = () => {

+ 34 - 0
resources/package.json

@@ -0,0 +1,34 @@
+{
+  "name": "Logseq",
+  "version": "0.0.1",
+  "main": "electron.js",
+  "author": "Logseq",
+  "description": "A privacy-first, open-source platform for knowledge sharing and management.",
+  "scripts": {
+    "electron:dev": "electron-forge start",
+    "electron:debug": "electron-forge start --inspect-electron",
+    "electron:make": "electron-forge make",
+    "electron:publish:github": "electron-forge publish"
+  },
+  "config": {
+    "forge": "./forge.config.js"
+  },
+  "dependencies": {
+    "electron-log": "^4.3.1",
+    "electron-squirrel-startup": "^1.0.0",
+    "update-electron-app": "^2.0.1",
+    "node-fetch": "^2.6.1",
+    "open": "^7.3.1",
+    "chokidar": "^3.5.1"
+  },
+  "devDependencies": {
+    "@electron-forge/cli": "^6.0.0-beta.54",
+    "@electron-forge/maker-deb": "^6.0.0-beta.54",
+    "@electron-forge/maker-dmg": "^6.0.0-beta.54",
+    "@electron-forge/maker-rpm": "^6.0.0-beta.54",
+    "@electron-forge/maker-squirrel": "^6.0.0-beta.54",
+    "@electron-forge/maker-zip": "^6.0.0-beta.54",
+    "electron-forge-maker-appimage": "trusktr/electron-forge-maker-appimage#patch-1",
+    "electron": "11.2.0"
+  }
+}

+ 17 - 0
scripts/publishing.sh

@@ -0,0 +1,17 @@
+#!/bin/sh
+yarn clean && yarn release-publishing
+/usr/bin/rm -rf /tmp/publishing
+mkdir /tmp/publishing
+cp -R ./static /tmp/publishing/
+cp ./static/404.html /tmp/publishing/
+/usr/bin/rm -rf /tmp/publishing/static/node_modules/
+/usr/bin/rm /tmp/publishing/static/electron*
+/usr/bin/rm /tmp/publishing/static/forge.config.js
+/usr/bin/rm /tmp/publishing/static/package.json
+/usr/bin/rm /tmp/publishing/static/yarn.lock
+/usr/bin/rm /tmp/publishing/static/index.html
+/usr/bin/rm /tmp/publishing/static/404.html
+/usr/bin/rm /tmp/publishing/static/public.css
+cd /tmp/publishing/
+mv ./static/js/publishing/code-editor.js ./static/js/
+tar -zcvf /tmp/logseq_publishing.tar.gz ./

+ 19 - 0
shadow-cljs.edn

@@ -9,6 +9,12 @@
    :modules {:main {:init-fn frontend.core/init}
              :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}}}
 
    :output-dir "./static/js"
@@ -33,6 +39,13 @@
     :watch-path   "static"
     :preloads     [devtools.preload]}}
 
+  :electron {:target :node-script
+             :output-to "static/electron.js"
+             :main electron.core/main
+             :devtools
+             {:before-load electron.core/stop
+              :after-load electron.core/start}}
+
   :test
   {:target :node-test
    :output-to "static/tests.js"
@@ -45,6 +58,12 @@
    :modules {:main {:init-fn frontend.publishing/init}
              :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}}}
 
    :output-dir "./static/js/publishing"

+ 5 - 0
src/dev-cljs/shadow/user.clj

@@ -5,3 +5,8 @@
   []
   (api/watch :app)
   (api/repl :app))
+
+(defn electron-repl
+  []
+  (api/watch :electron)
+  (api/repl :electron))

+ 149 - 0
src/electron/electron/core.cljs

@@ -0,0 +1,149 @@
+(ns electron.core
+  (:require [electron.handler :as handler]
+            [electron.updater :refer [init-updater]]
+            [electron.utils :refer [mac? win32? prod? dev? logger open]]
+            [clojure.string :as string]
+            ["fs" :as fs]
+            ["path" :as path]
+            ["electron" :refer [BrowserWindow app protocol ipcMain] :as electron]
+            [clojure.core.async :as async]
+            [electron.state :as state]))
+
+(def ROOT_PATH (path/join js/__dirname ".."))
+(def MAIN_WINDOW_ENTRY (str "file://" (path/join js/__dirname (if dev? "electron-dev.html" "electron.html"))))
+
+(defonce *setup-fn (volatile! nil))
+(defonce *teardown-fn (volatile! nil))
+
+;; Handle creating/removing shortcuts on Windows when installing/uninstalling.
+(when (js/require "electron-squirrel-startup") (.quit app))
+
+(defn create-main-window
+  "Creates main app window"
+  []
+  (let [win-opts {:width         980
+                  :height        700
+                  :frame         (not mac?)
+                  :autoHideMenuBar (not mac?)
+                  :titleBarStyle (if mac? "hidden" nil)
+                  :webPreferences
+                  {:nodeIntegration         false
+                   :nodeIntegrationInWorker false
+                   :contextIsolation        true
+                   :spellcheck              true
+                   :preload                 (path/join js/__dirname "js/preload.js")}}
+        url MAIN_WINDOW_ENTRY
+        win (BrowserWindow. (clj->js win-opts))]
+    (.loadURL win url)
+    (when dev? (.. win -webContents (openDevTools)))
+    win))
+
+(defn setup-updater! [^js win]
+  ;; manual updater
+  (init-updater {:repo   "logseq/logseq"
+                 :logger logger
+                 :win    win}))
+
+(defn setup-interceptor! []
+  (.registerFileProtocol
+   protocol "assets"
+   (fn [^js request callback]
+     (let [url (.-url request)
+           path (string/replace url "assets://" "")
+           path (js/decodeURIComponent path)]
+       (callback #js {:path path}))))
+  #(.unregisterProtocol protocol "assets"))
+
+(defn setup-app-manager!
+  [^js win]
+  (let [toggle-win-channel "toggle-max-or-min-active-win"
+        call-app-channel "call-application"
+        web-contents (. win -webContents)]
+    (doto ipcMain
+      (.handle toggle-win-channel
+               (fn [_ toggle-min?]
+                 (when-let [active-win (.getFocusedWindow BrowserWindow)]
+                   (if toggle-min?
+                     (if (.isMinimized active-win)
+                       (.restore active-win)
+                       (.minimize active-win))
+                     (if (.isMaximized active-win)
+                       (.unmaximize active-win)
+                       (.maximize active-win))))))
+      (.handle call-app-channel
+               (fn [_ type & args]
+                 (try
+                   (js-invoke app type args)
+                   (catch js/Error e
+                     (js/console.error e))))))
+
+    (.on web-contents  "new-window"
+         (fn [e url]
+           (let [url (if (string/starts-with? url "file:")
+                       (js/decodeURIComponent url) url)
+                 url (if-not win32? (string/replace url "file://" "") url)]
+             (.. logger (info "new-window" url))
+             (open url))
+           (.preventDefault e)))
+
+    (doto win
+      (.on "enter-full-screen" #(.send web-contents "full-screen" "enter"))
+      (.on "leave-full-screen" #(.send web-contents "full-screen" "leave")))
+
+    #(do (.removeHandler ipcMain toggle-win-channel)
+         (.removeHandler ipcMain call-app-channel))))
+
+(defonce *win (atom nil))
+
+(defn- destroy-window!
+  [^js win]
+  (.destroy win))
+
+(defn main
+  []
+  (.on app "window-all-closed" (fn [] (.quit app)))
+  (.on app "ready"
+       (fn []
+         (let [^js win (create-main-window)
+               _ (reset! *win win)
+               *quitting? (atom false)]
+           (.. logger (info (str "Logseq App(" (.getVersion app) ") Starting... ")))
+
+           (vreset! *setup-fn
+                    (fn []
+                      (let [t0 (setup-updater! win)
+                            t1 (setup-interceptor!)
+                            t2 (setup-app-manager! win)
+                            tt (handler/set-ipc-handler! win)]
+
+                        (vreset! *teardown-fn
+                                 #(doseq [f [t0 t1 t2 tt]]
+                                    (and f (f)))))))
+
+           ;; setup effects
+           (@*setup-fn)
+
+           ;; main window events
+           (.on win "close" (fn [e]
+                              (.preventDefault e)
+                              (let [web-contents (. win -webContents)]
+                                (.send web-contents "persistent-dbs"))
+                              (async/go
+                                ;; FIXME: What if persistence failed?
+                                (let [_ (async/<! state/persistent-dbs-chan)]
+                                  (if (or @*quitting? (not mac?))
+                                    (when-let [win @*win]
+                                      (destroy-window! win)
+                                      (reset! *win nil))
+                                    (do (.preventDefault ^js/Event e)
+                                        (.hide win)))))))
+           (.on app "before-quit" (fn [_e] (reset! *quitting? true)))
+           (.on app "activate" #(if @*win (.show win)))))))
+
+(defn start []
+  (js/console.log "Main - start")
+  (when @*setup-fn (@*setup-fn)))
+
+(defn stop []
+  (js/console.log "Main - stop")
+  (when @*teardown-fn (@*teardown-fn)))

+ 160 - 0
src/electron/electron/handler.cljs

@@ -0,0 +1,160 @@
+(ns electron.handler
+  (:require ["electron" :refer [ipcMain dialog app]]
+            [cljs-bean.core :as bean]
+            ["fs" :as fs]
+            ["path" :as path]
+            ["chokidar" :as watcher]
+            [promesa.core :as p]
+            [goog.object :as gobj]
+            [clojure.string :as string]
+            [electron.utils :as utils]
+            [electron.state :as state]
+            [clojure.core.async :as async]))
+
+(defmulti handle (fn [_window args] (keyword (first args))))
+
+(defmethod handle :mkdir [_window [_ dir]]
+  (fs/mkdirSync dir))
+
+(defn- readdir
+  [dir]
+  (->> (tree-seq
+        (fn [f] (.isDirectory (fs/statSync f) ()))
+        (fn [d] (map #(.join path d %) (fs/readdirSync d)))
+        dir)
+       (doall)
+       (vec)))
+
+(defmethod handle :readdir [_window [_ dir]]
+  (readdir dir))
+
+(defmethod handle :unlink [_window [_ path]]
+  (fs/unlinkSync path))
+
+(defn- read-file
+  [path]
+  (.toString (fs/readFileSync path)))
+(defmethod handle :readFile [_window [_ path]]
+  (read-file path))
+
+(defmethod handle :writeFile [_window [_ path content]]
+  ;; TODO: handle error
+  (fs/writeFileSync path content)
+  (fs/statSync path))
+
+(defmethod handle :rename [_window [_ old-path new-path]]
+  (fs/renameSync old-path new-path))
+
+(defmethod handle :stat [_window [_ path]]
+  (fs/statSync path))
+
+(defn- fix-win-path!
+  [path]
+  (when path
+    (if utils/win32?
+      (string/replace path "\\" "/")
+      path)))
+
+(defn- get-files
+  [path]
+  (let [result (->> (map
+                      (fn [path]
+                        (let [stat (fs/statSync path)]
+                          (when-not (.isDirectory stat)
+                            {:path (fix-win-path! path)
+                             :content (read-file path)
+                             :stat stat})))
+                      (readdir path))
+                    (remove nil?))]
+    (vec (cons {:path (fix-win-path! path)} result))))
+
+;; TODO: Is it going to be slow if it's a huge directory
+(defmethod handle :openDir [^js window _messages]
+  (let [result (.showOpenDialogSync dialog (bean/->js
+                                            {:properties ["openDirectory"]}))
+        path (first result)]
+    (.. ^js window -webContents
+        (send "open-dir-confirmed"
+              (bean/->js {:opened? true})))
+    (get-files path)))
+
+(defmethod handle :getFiles [window [_ path]]
+  (get-files path))
+
+(defmethod handle :persistent-dbs-saved [window _]
+  (async/put! state/persistent-dbs-chan true )
+  true)
+
+(defn- get-file-ext
+  [file]
+  (last (string/split file #"\.")))
+
+(defonce file-watcher-chan "file-watcher")
+(defn send-file-watcher! [^js win type payload]
+  (.. win -webContents
+      (send file-watcher-chan
+            (bean/->js {:type type :payload payload}))))
+
+(defn watch-dir!
+  [win dir]
+  (when (fs/existsSync dir)
+    (let [watcher (.watch watcher dir
+                          (clj->js
+                           {:ignored (fn [path]
+                                       (or
+                                        (some #(string/starts-with? path (str dir "/" %))
+                                              ["." "assets" "node_modules"])
+                                        (some #(string/ends-with? path (str dir "/" %))
+                                              [".swap" ".crswap" ".tmp"])))
+                            :ignoreInitial false
+                            :persistent true
+                            :awaitWriteFinish true}))]
+      (.on watcher "add"
+           (fn [path]
+             (send-file-watcher! win "add"
+                                 {:dir (fix-win-path! dir)
+                                  :path (fix-win-path! path)
+                                  :content (read-file path)
+                                  :stat (fs/statSync path)})))
+      (.on watcher "change"
+           (fn [path]
+             (send-file-watcher! win "change"
+                                 {:dir (fix-win-path! dir)
+                                  :path (fix-win-path! path)
+                                  :content (read-file path)
+                                  :stat (fs/statSync path)})))
+      ;; (.on watcher "unlink"
+      ;;      (fn [path]
+      ;;        (send-file-watcher! win "unlink"
+      ;;                            {:dir (fix-win-path! dir)
+      ;;                             :path (fix-win-path! path)})))
+      (.on watcher "error"
+           (fn [path]
+             (println "Watch error happened: "
+                      {:path path})))
+
+      (.on app "quit" #(.close watcher))
+
+      true)))
+
+(defmethod handle :addDirWatcher [window [_ dir]]
+  (when dir
+    (watch-dir! window dir)))
+
+(defmethod handle :default [args]
+  (println "Error: no ipc handler for: " (bean/->js args)))
+
+(defn set-ipc-handler! [window]
+  (let [main-channel "main"]
+    (.handle ipcMain main-channel
+             (fn [event args-js]
+               (try
+                 (let [message (bean/->clj args-js)]
+                   (bean/->js (handle window message)))
+                 (catch js/Error e
+                   (when-not (contains? #{"mkdir" "stat"} (nth args-js 0))
+                     (println "IPC error: " {:event event
+                                            :args args-js}
+                             e))
+                   e))))
+    #(.removeHandler ipcMain main-channel)))

+ 4 - 0
src/electron/electron/state.cljs

@@ -0,0 +1,4 @@
+(ns electron.state
+  (:require [clojure.core.async :as async]))
+
+(defonce persistent-dbs-chan (async/chan 1))

+ 125 - 0
src/electron/electron/updater.cljs

@@ -0,0 +1,125 @@
+(ns electron.updater
+  (:require [electron.utils :refer [mac? win32? prod? open fetch logger]]
+            [frontend.version :refer [version]]
+            [clojure.string :as string]
+            [promesa.core :as p]
+            [cljs-bean.core :as bean]
+            ["os" :as os]
+            ["fs" :as fs]
+            ["path" :as path]
+            ["electron" :refer [ipcMain app]]))
+
+(def *update-ready-to-install (atom nil))
+(def *update-pending (atom nil))
+(def debug (partial (.-warn logger) "[updater]"))
+
+;Event: 'error'
+;Event: 'checking-for-update'
+;Event: 'update-available'
+;Event: 'update-not-available'
+;Event: 'download-progress'
+;Event: 'update-downloaded'
+;Event: 'completed'
+
+(def electron-version
+  (let [parts (string/split version #"\.")
+        parts (take 3 parts)]
+    (string/join "." parts)))
+
+(defn get-latest-artifact-info
+  [repo]
+  (let [;endpoint "https://update.electronjs.org/xyhp915/cljs-todo/darwin-x64/0.0.4"
+        endpoint (str "https://update.electronjs.org/" repo "/" js/process.platform "-" js/process.arch "/" electron-version)]
+    (debug "[updater]" endpoint)
+    (p/catch
+     (p/let [res (fetch endpoint)
+             status (.-status res)
+             text (.text res)]
+       (if (.-ok res)
+         (let [info (if-not (string/blank? text) (js/JSON.parse text))]
+           (bean/->clj info))
+         (throw (js/Error. (str "[" status "] " text)))))
+     (fn [e]
+       (js/console.warn "[update server error] " e)
+       (throw e)))))
+
+(defn check-for-updates
+  [{:keys           [repo ^js ^js win]
+    [auto-download] :args}]
+  (let [emit (fn [type payload]
+               (.. win -webContents
+                   (send "updates-callback" (bean/->js {:type type :payload payload}))))]
+    (debug "check for updates #" repo version)
+    (p/create
+     (fn [resolve reject]
+       (emit "checking-for-update" nil)
+       (-> (p/let
+            [artifact (get-latest-artifact-info repo)
+             url (if-not artifact (do (emit "update-not-available" nil) (throw nil)) (:url artifact))
+             _ (if url (emit "update-available" (bean/->js artifact)) (throw (js/Error. "download url not exists")))
+               ;; start download FIXME: user's preference about auto download
+             _ (when-not auto-download (throw nil))
+             ^js dl-res (fetch url)
+             _ (if-not (.-ok dl-res) (throw (js/Error. "download resource not available")))
+             dest-info (p/create
+                        (fn [resolve1 reject1]
+                          (let [headers (. dl-res -headers)
+                                total-size (js/parseInt (.get headers "content-length"))
+                                body (.-body dl-res)
+                                start-at (.now js/Date)
+                                *downloaded (atom 0)
+                                dest-basename (path/basename url)
+                                tmp-dest-file (path/join (os/tmpdir) (str dest-basename ".pending"))
+                                dest-file (.createWriteStream fs tmp-dest-file)]
+                            (doto body
+                              (.on "data" (fn [chunk]
+                                            (let [downloaded (+ @*downloaded (.-length chunk))
+                                                  percent (.toFixed (/ (* 100 downloaded) total-size) 2)
+                                                  elapsed (/ (- (js/Date.now) start-at) 1000)]
+                                              (.write dest-file chunk)
+                                              (emit "download-progress" {:total      total-size
+                                                                         :downloaded downloaded
+                                                                         :percent    percent
+                                                                         :elapsed    elapsed})
+                                              (reset! *downloaded downloaded))))
+                              (.on "error" (fn [e]
+                                             (reject1 e)))
+                              (.on "end" (fn [e]
+                                           (.close dest-file)
+                                           (let [dest-file (string/replace tmp-dest-file ".pending" "")]
+                                             (fs/renameSync tmp-dest-file dest-file)
+                                             (resolve1 (merge artifact {:dest-file dest-file})))))))))]
+             (reset! *update-ready-to-install dest-info)
+             (emit "update-downloaded" dest-info)
+             (resolve nil))
+           (p/catch
+            (fn [e]
+              (if e
+                (do
+                  (emit "error" e)
+                  (reject e))
+                (resolve nil))))
+           (p/finally
+             (fn []
+               (emit "completed" nil))))))))
+
+(defn init-updater
+  [{:keys [repo logger ^js win] :as opts}]
+  (let [check-channel "check-for-updates"
+        install-channel "install-updates"
+        check-listener (fn [e & args]
+                         (when-not @*update-pending
+                           (reset! *update-pending true)
+                           (p/finally
+                             (check-for-updates (merge opts {:args args}))
+                             #(reset! *update-pending nil))))
+        install-listener (fn [e quit-app?]
+                           (when-let [dest-file (:dest-file @*update-ready-to-install)]
+                             (open dest-file)
+                             (and quit-app? (js/setTimeout #(.quit app) 1000))))]
+    (.handle ipcMain check-channel check-listener)
+    (.handle ipcMain install-channel install-listener)
+    #(do
+       (.removeHandler ipcMain install-channel)
+       (.removeHandler ipcMain check-channel)
+       (reset! *update-pending nil))))

+ 11 - 0
src/electron/electron/utils.cljs

@@ -0,0 +1,11 @@
+(ns electron.utils)
+
+(defonce mac? (= (.-platform js/process) "darwin"))
+(defonce win32? (= (.-platform js/process) "win32"))
+
+(defonce prod? (= js/process.env.NODE_ENV "production"))
+(defonce dev? (not prod?))
+(defonce logger (js/require "electron-log"))
+
+(defonce open (js/require "open"))
+(defonce fetch (js/require "node-fetch"))

+ 1 - 1
src/main/api.cljs

@@ -10,7 +10,7 @@
   (when-let [repo (state/get-current-repo)]
     (when-let [conn (db/get-conn repo)]
       (when-let [result (query-dsl/query repo query-string)]
-        @result))))
+        (clj->js @result)))))
 
 (defn ^:export datascript_query
   [query & inputs]

+ 9 - 0
src/main/electron/ipc.cljs

@@ -0,0 +1,9 @@
+(ns electron.ipc
+  (:require [cljs-bean.core :as bean]
+            [promesa.core :as p]))
+
+;; TODO: handle errors
+(defn ipc
+  [& args]
+  (p/let [result (js/window.apis.doAction (bean/->js args))]
+    result))

+ 58 - 0
src/main/electron/listener.cljs

@@ -0,0 +1,58 @@
+(ns electron.listener
+  (:require [frontend.state :as state]
+            [frontend.handler.route :as route-handler]
+            [cljs-bean.core :as bean]
+            [frontend.fs.watcher-handler :as watcher-handler]
+            [frontend.db :as db]
+            [frontend.idb :as idb]
+            [promesa.core :as p]
+            [electron.ipc :as ipc]
+            [frontend.handler.notification :as notification]
+            [frontend.ui :as ui]))
+
+(defn listen-to-open-dir!
+  []
+  (js/window.apis.on "open-dir-confirmed"
+                     (fn []
+                       (state/set-loading-files! true)
+                       (when-not (state/home?)
+                         (route-handler/redirect-to-home!)))))
+
+(defn run-dirs-watcher!
+  []
+  ;; TODO: move "file-watcher" to electron.ipc.channels
+  (js/window.apis.on "file-watcher"
+                     (fn [data]
+                       (let [{:keys [type payload]} (bean/->clj data)]
+                         (watcher-handler/handle-changed! type payload)))))
+
+(defn listen-persistent-dbs!
+  []
+  ;; TODO: move "file-watcher" to electron.ipc.channels
+  (js/window.apis.on
+   "persistent-dbs"
+   (fn [req]
+     (p/let [repos (idb/get-nfs-dbs)
+             repos (-> repos
+                       (conj (state/get-current-repo))
+                       (distinct))]
+       (if (seq repos)
+         (do
+           (notification/show!
+            (ui/loading "Logseq is saving the graphs to your local file system, please wait for several seconds.")
+            :warning)
+           (js/setTimeout
+            (fn []
+              (-> (p/all (map db/persist! repos))
+                 (p/then (fn []
+                           (ipc/ipc "persistent-dbs-saved")))
+                 (p/catch (fn [error]
+                            (js/console.dir error)))))
+            100))
+         (ipc/ipc "persistent-dbs-saved"))))))
+
+(defn listen!
+  []
+  (listen-to-open-dir!)
+  (run-dirs-watcher!)
+  (listen-persistent-dbs!))

+ 47 - 6
src/main/frontend/commands.cljs

@@ -3,11 +3,14 @@
             [frontend.date :as date]
             [frontend.state :as state]
             [frontend.search :as search]
+            [frontend.config :as config]
             [clojure.string :as string]
             [goog.dom :as gdom]
             [goog.object :as gobj]
             [frontend.format :as format]
-            [frontend.handler.common :as common-handler]))
+            [frontend.handler.common :as common-handler]
+            [frontend.handler.draw :as draw]
+            [promesa.core :as p]))
 
 ;; TODO: move to frontend.handler.editor.commands
 
@@ -101,10 +104,13 @@
      ["Scheduled" [[:editor/clear-current-slash]
                    [:editor/show-date-picker]]]
      ["Query" [[:editor/input "{{query }}" {:backward-pos 2}]]]
-     ["Draw" [[:editor/input "/draw "]
-              [:editor/show-input [{:command :draw
-                                    :id :title
-                                    :placeholder "Draw title"}]]]]
+     ["Draw" (fn []
+               (let [file (draw/file-name)
+                     path (str 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))]
      ["WAITING" (->marker "WAITING")]
      ["CANCELED" (->marker "CANCELED")]
      ["Tomorrow" #(get-page-ref-text (date/tomorrow))]
@@ -123,10 +129,24 @@
                   [:editor/search-template]]]
      ;; same as link
      ["Image Link" link-steps]
-     (when (state/logged?)
+     (cond
+       (and (util/electron?) (config/local-db? (state/get-current-repo)))
+
+       ["Upload an asset (image, pdf, docx, etc.)" [[:editor/click-hidden-file-input :id]]]
+
+       (state/logged?)
        ["Upload an image" [[:editor/click-hidden-file-input :id]]])
+
+     (when (util/zh-CN-supported?)
+       ["Embed Bilibili Video" [[:editor/input "{{bilibili }}" {:last-pattern slash
+                                                                :backward-pos 2}]]])
+
      ["Embed Youtube Video" [[:editor/input "{{youtube }}" {:last-pattern slash
                                                             :backward-pos 2}]]]
+
+     ["Embed Vimeo Video" [[:editor/input "{{vimeo }}" {:last-pattern slash
+                                                        :backward-pos 2}]]]
+
      ["Html Inline " (->inline "html")]
 
      ;; TODO:
@@ -186,6 +206,7 @@
      ["Tip" (->block "tip")]
      ["Important" (->block "important")]
      ["Caution" (->block "caution")]
+     ["Pinned" (->block "pinned")]
      ["Warning" (->block "warning")]
      ["Example" (->block "example")]
      ["Export" (->block "export")]
@@ -255,6 +276,26 @@
     (when check-fn
       (check-fn new-value (dec (count prefix)) new-pos))))
 
+(defn insert-before!
+  [id value
+   {:keys [backward-pos forward-pos check-fn]
+    :as option}]
+  (let [input (gdom/getElement id)
+        edit-content (gobj/get input "value")
+        current-pos (:pos (util/get-caret-pos input))
+        suffix (subs edit-content 0 current-pos)
+        new-value (str value
+                       suffix
+                       (subs edit-content current-pos))
+        new-pos (- (+ (count suffix)
+                      (count value)
+                      (or forward-pos 0))
+                   (or backward-pos 0))]
+    (state/set-block-content-and-last-pos! id new-value new-pos)
+    (util/move-cursor-to input new-pos)
+    (when check-fn
+      (check-fn new-value (dec (count suffix)) new-pos))))
+
 (defn simple-replace!
   [id value selected
    {:keys [backward-pos forward-pos check-fn]

+ 242 - 152
src/main/frontend/components/block.cljs

@@ -16,7 +16,6 @@
             [goog.dom :as gdom]
             [frontend.handler.expand :as expand]
             [frontend.components.svg :as svg]
-            [frontend.components.draw :as draw]
             [frontend.components.datetime :as datetime-comp]
             [frontend.ui :as ui]
             [frontend.handler.editor :as editor-handler]
@@ -47,7 +46,11 @@
             [reitit.frontend.easy :as rfe]
             [frontend.commands :as commands]
             [lambdaisland.glogi :as log]
-            [frontend.context.i18n :as i18n]))
+            [frontend.context.i18n :as i18n]
+            [frontend.template :as template]
+            [shadow.loader :as loader]))
+
+;; TODO: remove rum/with-context because it'll make reactive queries not working
 
 (defn safe-read-string
   ([s]
@@ -179,7 +182,7 @@
                              (reset! *resizing-image? true))
                            (reset! size value))
           :onMouseUp (fn []
-                       (when @size
+                       (when (and @size @*resizing-image?)
                          (when-let [block-id (:block/uuid config)]
                            (let [size (bean/->clj @size)]
                              (editor-handler/resize-image! block-id metadata full_text size))))
@@ -188,7 +191,7 @@
                          (js/setTimeout #(reset! *resizing-image? false) 200)))
           :onClick (fn [e]
                      (when @*resizing-image? (util/stop e)))}
-          (:width metadata)
+          (and (:width metadata) (not (util/mobile?)))
           (assoc :style {:width (:width metadata)}))
         [:div.asset-container
          [:img.rounded-sm.shadow-xl.relative
@@ -224,9 +227,9 @@
   (rum/local nil ::src)
   [state config title href label metadata full_text]
   (let [src (::src state)
-        granted? (state/sub [:nfs/user-granted? (state/get-current-repo)])]
-
-    (when granted?
+        granted? (state/sub [:nfs/user-granted? (state/get-current-repo)])
+        href (config/get-local-asset-absolute-path href)]
+    (when (or granted? (util/electron?))
       (p/then (editor-handler/make-asset-url href) #(reset! src %)))
 
     (when @src
@@ -234,14 +237,13 @@
 
 ;; TODO: safe encoding asciis
 ;; TODO: image link to another link
-
-
 (defn image-link [config url href label metadata full_text]
   (let [metadata (if (string/blank? metadata)
                    nil
                    (safe-read-string metadata false))
         title (second (first label))]
-    (if (config/local-asset? href)
+    (if (and (config/local-asset? href)
+             (config/local-db? (state/get-current-repo)))
       (asset-link config title href label metadata full_text)
       (let [href (if (util/starts-with? href "http")
                    href
@@ -347,7 +349,8 @@
                  (util/encode-str page)
                  (rfe/href :page {:name redirect-page-name}))]
       [:a.page-ref
-       {:href href
+       {:data-ref page-name
+        :href href
         :on-click (fn [e]
                     (util/stop e)
                     (if (gobj/get e "shiftKey")
@@ -376,32 +379,47 @@
            label
            original-page-name))])))
 
+(rum/defc asset-reference
+  [title path]
+  (let [repo-path (config/get-repo-dir (state/get-current-repo))
+        full-path (.. util/node-path (join repo-path (config/get-local-asset-absolute-path path)))]
+    [:a.asset-ref {:target "_blank" :href full-path} (or title path)]))
+
+(defonce excalidraw-loaded? (atom false))
+(rum/defc excalidraw < rum/reactive
+  {:init (fn [state]
+           (p/let [_ (loader/load :excalidraw)]
+             (reset! excalidraw-loaded? true))
+           state)}
+  [file]
+  (let [loaded? (rum/react excalidraw-loaded?)
+        draw-component (if loaded?
+                         (resolve 'frontend.extensions.excalidraw/draw))]
+    (when draw-component
+      (draw-component {:file file}))))
+
 (rum/defc page-reference < rum/reactive
   [html-export? s config label]
   (let [show-brackets? (state/show-brackets?)
         nested-link? (:nested-link? config)
-        contents-page? (= "contents" (string/lower-case (str (:id config))))]
-    [:span.page-reference
-     (when (and (or show-brackets? nested-link?)
-                (not html-export?)
-                (not contents-page?))
-       [:span.text-gray-500 "[["])
-     (if (string/ends-with? s ".excalidraw")
-       [:a.page-ref
-        {:on-click (fn [e]
-                     (util/stop e)
-                     (set! (.-href js/window.location)
-                           (rfe/href :draw nil {:file (string/replace s (str config/default-draw-directory "/") "")})))}
-        [:span
-         (svg/excalidraw-logo)
-         (string/capitalize (draw/get-file-title s))]]
+        contents-page? (= "contents" (string/lower-case (str (:id config))))
+        draw? (string/ends-with? s ".excalidraw")]
+    (if (string/ends-with? s ".excalidraw")
+      [:div.draw {:on-click (fn [e]
+                              (.stopPropagation e))}
+       (excalidraw s)]
+      [:span.page-reference
+       (when (and (or show-brackets? nested-link?)
+                  (not html-export?)
+                  (not contents-page?))
+         [:span.text-gray-500.bracket "[["])
        (page-cp (assoc config
                        :label (mldoc/plain->text label)
-                       :contents-page? contents-page?) {:page/name s}))
-     (when (and (or show-brackets? nested-link?)
-                (not html-export?)
-                (not contents-page?))
-       [:span.text-gray-500 "]]"])]))
+                       :contents-page? contents-page?) {:page/name s})
+       (when (and (or show-brackets? nested-link?)
+                  (not html-export?)
+                  (not contents-page?))
+         [:span.text-gray-500.bracket "]]"])])))
 
 (defn- latex-environment-content
   [name option content]
@@ -462,36 +480,35 @@
     (util/format "{{{%s}}}" name)))
 
 (declare block-content)
-(defn block-reference
+(rum/defc block-reference < rum/reactive
   [config id]
-  (rum/with-context [[t] i18n/*tongue-context*]
-    (when-not (string/blank? id)
-      (let [block (and (util/uuid-string? id)
-                       (db/pull-block (uuid id)))]
-        (if block
-          [:span
-           [:div.block-ref-wrap
-            {:on-click (fn [e]
-                         (util/stop e)
-                         (if (gobj/get e "shiftKey")
-                           (state/sidebar-add-block!
-                            (state/get-current-repo)
-                            (:db/id block)
-                            :block-ref
-                            {:block block})
-                           (route-handler/redirect! {:to          :page
-                                                     :path-params {:name id}})))}
-
-            (let [title (:block/title block)]
-              (if (empty? title)
-                ;; display the content
-                [:div.block-ref
-                 (block-content config block nil (:block/uuid block) (:slide? config))]
-                (->elem
-                 :span.block-ref
-                 (map-inline config title))))]]
-          [:span.warning.mr-1 {:title "Block ref invalid"}
-           (util/format "((%s))" id)])))))
+  (when-not (string/blank? id)
+    (let [block (and (util/uuid-string? id)
+                     (db/pull-block (uuid id)))]
+      (if block
+        [:span
+         [:div.block-ref-wrap
+          {:on-click (fn [e]
+                       (util/stop e)
+                       (if (gobj/get e "shiftKey")
+                         (state/sidebar-add-block!
+                          (state/get-current-repo)
+                          (:db/id block)
+                          :block-ref
+                          {:block block})
+                         (route-handler/redirect! {:to          :page
+                                                   :path-params {:name id}})))}
+
+          (let [title (:block/title block)]
+            (if (empty? title)
+              ;; display the content
+              [:div.block-ref
+               (block-content config block nil (:block/uuid block) (:slide? config))]
+              (->elem
+               :span.block-ref
+               (map-inline config title))))]]
+        [:span.warning.mr-1 {:title "Block ref invalid"}
+         (util/format "((%s))" id)]))))
 
 (defn inline-text
   [format v]
@@ -555,9 +572,9 @@
     (->elem :sub (map-inline config l))
     ["Tag" s]
     (if (and s (util/tag-valid? s))
-      [:a.tag {:href (rfe/href :page {:name s})
+      [:a.tag {:data-ref s
+               :href (rfe/href :page {:name s})
                :on-click (fn [e]
-                           (.preventDefault e)
                            (let [repo (state/get-current-repo)
                                  page (db/pull repo '[*] [:page/name (string/lower-case (util/url-decode s))])]
                              (when (gobj/get e "shiftKey")
@@ -565,7 +582,8 @@
                                 repo
                                 (:db/id page)
                                 :page
-                                {:page page}))))}
+                                {:page page})
+                               (.preventDefault e))))}
        (str "#" s)]
       [:span.warning.mr-1 {:title "Invalid tag, tags only accept alphanumeric characters, \"-\", \"_\", \"@\" and \"%\"."}
        (str "#" s)])
@@ -616,21 +634,30 @@
       (match url
         ["Search" s]
         (cond
+          (string/blank? s)
+          [:span.warning {:title "Invalid link"} full_text]
+
           ;; image
           (some (fn [fmt] (re-find (re-pattern (str "(?i)\\." fmt)) s)) img-formats)
           (image-link config url s label metadata full_text)
 
           (= \# (first s))
-          (->elem :a {:href (str "#" (mldoc/anchorLink (subs s 1)))} (map-inline config label))
+          (->elem :a {:on-click #(route-handler/jump-to-anchor! (mldoc/anchorLink (subs s 1)))} (subs s 1))
+
           ;; FIXME: same headline, see more https://orgmode.org/manual/Internal-Links.html
           (and (= \* (first s))
                (not= \* (last s)))
-          (->elem :a {:href (str "#" (mldoc/anchorLink (subs s 1)))} (map-inline config label))
+          (->elem :a {:on-click #(route-handler/jump-to-anchor! (mldoc/anchorLink (subs s 1)))} (subs s 1))
 
           (re-find #"(?i)^http[s]?://" s)
-          (->elem :a {:href s}
+          (->elem :a {:href s
+                      :data-href s
+                      :target "_blank"}
                   (map-inline config label))
 
+          (and (util/electron?) (config/local-asset? s))
+          (asset-reference (second (first label)) s)
+
           :else
           (page-reference html-export? s config label))
 
@@ -666,7 +693,9 @@
                   (->elem
                    :a
                    (cond->
-                    {:href href}
+                    {:href      (str "file://" href)
+                     :data-href href
+                     :target    "_blank"}
                      title
                      (assoc :title title))
                    (map-inline config label)))))
@@ -740,7 +769,7 @@
       [:sup.fn
        [:a {:id (str "fnr." encode-name)
             :class "footref"
-            :href (str "#fn." encode-name)}
+            :on-click #(route-handler/jump-to-anchor! (str "fn." encode-name))}
         name]])
 
     ["Macro" options]
@@ -762,27 +791,62 @@
 
         (= name "youtube")
         (let [url (first arguments)]
-          (when-let [youtube-id (cond
-                                  (string/starts-with? url "https://youtu.be/")
-                                  (string/replace url "https://youtu.be/" "")
-
-                                  (string? url)
-                                  url
-
-                                  :else
-                                  nil)]
-            (when-not (string/blank? youtube-id)
+          (let [YouTube-regex #"^((?:https?:)?//)?((?:www|m).)?((?:youtube.com|youtu.be))(/(?:[\w-]+\?v=|embed/|v/)?)([\w-]+)(\S+)?$"]
+            (when-let [youtube-id (cond
+                                    (== 11 (count url)) url
+                                    :else
+                                    (nth (re-find YouTube-regex url) 5))]
+              (when-not (string/blank? youtube-id)
+                (let [width (min (- (util/get-width) 96)
+                                 560)
+                      height (int (* width (/ 315 560)))]
+                  [:iframe
+                   {:allow-full-screen "allowfullscreen"
+                    :allow
+                    "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
+                    :frame-border "0"
+                    :src (str "https://www.youtube.com/embed/" youtube-id)
+                    :height height
+                    :width width}])))))
+
+        (= name "vimeo")
+        (let [url (first arguments)]
+          (let [Vimeo-regex #"^((?:https?:)?//)?((?:www).)?((?:player.vimeo.com|vimeo.com)?)((?:/video/)?)([\w-]+)(\S+)?$"]
+            (when-let [vimeo-id (nth (re-find Vimeo-regex url) 5)]
+              (when-not (string/blank? vimeo-id)
+                (let [width (min (- (util/get-width) 96)
+                                 560)
+                      height (int (* width (/ 315 560)))]
+                  [:iframe
+                   {:allow-full-screen "allowfullscreen"
+                    :allow
+                    "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
+                    :frame-border "0"
+                    :src (str "https://player.vimeo.com/video/" vimeo-id)
+                    :height height
+                    :width width}])))))
+
+        ;; TODO: support fullscreen mode, maybe we need a fullscreen dialog?
+        (= name "bilibili")
+        (let [url (first arguments)
+              id-regex #"https?://www\.bilibili\.com/video/([\w\W]+)"]
+          (when-let [id (cond
+                          (<= (count url) 15) url
+                          :else
+                          (last (re-find id-regex url)))]
+            (when-not (string/blank? id)
               (let [width (min (- (util/get-width) 96)
                                560)
                     height (int (* width (/ 315 560)))]
                 [:iframe
-                 {:allow-full-screen "allowfullscreen"
-                  :allow
-                  "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
-                  :frame-border "0"
-                  :src (str "https://www.youtube.com/embed/" youtube-id)
-                  :height height
-                  :width width}]))))
+                 {:allowfullscreen true
+                  :framespacing "0"
+                  :frameborder "no"
+                  :border "0"
+                  :scrolling "no"
+                  :src (str "https://player.bilibili.com/player.html?bvid=" id "&high_quality=1")
+                  :width width
+                  :height (max 500 height)}]))))
 
         (= name "embed")
         (let [a (first arguments)]
@@ -827,11 +891,15 @@
                                (get (state/get-macros) (keyword name)))
                 macro-content (if (and (seq arguments) macro-content)
                                 (block/macro-subs macro-content arguments)
-                                macro-content)]
+                                macro-content)
+                macro-content (when macro-content
+                                (template/resolve-dynamic-template! macro-content))]
             (render-macro config name arguments macro-content format))
 
           (when-let [macro-txt (macro->text name arguments)]
-            (let [format (get-in config [:block :block/format] :markdown)]
+            (let [macro-txt (when macro-txt
+                              (template/resolve-dynamic-template! macro-txt))
+                  format (get-in config [:block :block/format] :markdown)]
               (render-macro config name arguments macro-txt format))))))
 
     :else
@@ -898,13 +966,17 @@
      [:a (if (not dummy?)
            {:href (rfe/href :page {:name uuid})
             :on-click (fn [e]
-                        (.preventDefault e)
-                        (when (gobj/get e "shiftKey")
-                          (state/sidebar-add-block!
-                           (state/get-current-repo)
-                           (:db/id block)
-                           :block
-                           block)))})
+                        (if (gobj/get e "shiftKey")
+                          (do
+                            (state/sidebar-add-block!
+                             (state/get-current-repo)
+                             (:db/id block)
+                             :block
+                             block)
+                            (util/stop e))
+                          (when (:embed? config)
+                            (route-handler/redirect! {:to :page
+                                                      :path-params {:name (str uuid)}}))))})
       [:span.bullet-container.cursor
        {:id (str "dot-" uuid)
         :draggable true
@@ -1049,12 +1121,15 @@
      (mapv (fn [tag]
              (when-let [page (db/entity (:db/id tag))]
                (let [tag (:page/name page)]
-                 [:a.tag.mx-1 {:key (str "tag-" (:db/id tag))
+                 [:a.tag.mx-1 {:data-ref tag
+                               :key (str "tag-" (:db/id tag))
                                :href (rfe/href :page {:name tag})}
                   (str "#" tag)])))
            tags))))
 
-(defn build-block-part
+(declare block-content)
+
+(defn build-block-title
   [{:keys [slide?] :as config} {:block/keys [uuid title tags marker level priority anchor meta format content pre-block? dummy? block-refs-count page properties]
                                 :as t}]
   (let [config (assoc config :block t)
@@ -1088,25 +1163,17 @@
             {:style {:background-color bg-color
                      :padding-left 6
                      :padding-right 6
-                     :color "#FFFFFF"}}))
+                     :color "#FFFFFF"}
+             :class "with-bg-color"}))
          (remove-nils
           (concat
            [(when-not slide? checkbox)
             (when-not slide? marker-switch)
             marker-cp
             priority]
-           (cond
-             dummy?
-             [[:span.opacity-50 "Click here to start writing"]]
-
-             ;; empty item
-             (and contents? (or
-                             (empty? title)
-                             (= title [["Plain" "[[]]"]])))
-             [[:span.opacity-50 "Click here to add a page, e.g. [[favorite-page]]"]]
-
-             :else
-             (map-inline config title))
+           (if title
+             (map-inline config title)
+             [[:span.opacity-50 "Click here to start writing"]])
            [tags])))))))
 
 (defn dnd-same-block?
@@ -1140,7 +1207,7 @@
           only-title? (and (= 1 (count ast))
                            (= "Properties" (ffirst ast))
                            (let [m (second (first ast))]
-                             (= (keys m) [:title])))
+                             (every? #(contains? #{:title :filters} %) (keys m))))
           block-cp [:div {:class (if only-title?
                                    (util/hiccup->class "pre-block.opacity-50")
                                    (util/hiccup->class "pre-block.bg-base-2.p-2.rounded"))}
@@ -1148,8 +1215,7 @@
                       [:span (t :page/edit-properties-placeholder)]
                       (markup-elements-cp (assoc config :block/format format) ast))]]
       (if slide?
-        [:div [:h1 (:page-name config)]
-         block-cp]
+        [:div [:h1 (:page-name config)]]
         block-cp))))
 
 (rum/defc properties-cp
@@ -1260,11 +1326,17 @@
         (not slide?)
         (merge attrs))
 
-      (if pre-block?
+      (cond
+        pre-block?
         (pre-block-cp config (string/trim content) format)
-        (build-block-part config block))
 
-      (when (and dragging? (not slide?))
+        dummy?
+        [:span.opacity-50 "Click here to start writing"]
+
+        (seq title)
+        (build-block-title config block))
+
+      (when (and dragging? (not slide?) (not dummy?))
         (dnd-separator block 0 -4 false true))
 
       (when (and deadline deadline-ast)
@@ -1280,13 +1352,14 @@
         (properties-cp config block))
 
       (when (and (not pre-block?) (seq body))
-        [:div.block-body {:style {:display (if collapsed? "none" "")}}
-         ;; TODO: consistent id instead of the idx (since it could be changed later)
-         (let [body (block/trim-break-lines! (:block/body block))]
-           (for [[idx child] (medley/indexed body)]
-             (when-let [block (markup-element-cp config child)]
-               (rum/with-key (block-child block)
-                 (str uuid "-" idx)))))])]
+        (do
+          [:div.block-body {:style {:display (if (and collapsed? (seq title)) "none" "")}}
+          ;; TODO: consistent id instead of the idx (since it could be changed later)
+          (let [body (block/trim-break-lines! (:block/body block))]
+            (for [[idx child] (medley/indexed body)]
+              (when-let [block (markup-element-cp config child)]
+                (rum/with-key (block-child block)
+                  (str uuid "-" idx)))))]))]
      (when (and block-refs-count (> block-refs-count 0))
        [:div
         [:a.open-block-ref-link.bg-base-2
@@ -1339,7 +1412,7 @@
   [block slide? top?]
   (let [dragging? (rum/react *dragging?)]
     (cond
-      (and dragging? (not slide?))
+      (and dragging? (not slide?) (not (:block/dummy? block)))
       (dnd-separator block 20 0 top? false)
 
       :else
@@ -1353,9 +1426,9 @@
        (not @*dragging?)))
 
 (defn block-parents
-  ([repo block-id format]
-   (block-parents repo block-id format true))
-  ([repo block-id format show-page?]
+  ([config repo block-id format]
+   (block-parents config repo block-id format true))
+  ([config repo block-id format show-page?]
    (let [parents (db/get-block-parents repo block-id 3)
          page (db/get-block-page repo block-id)
          page-name (:page/name page)]
@@ -1373,12 +1446,10 @@
                           [:span.mx-2.opacity-50 "➤"])
 
                         (when (seq parents)
-                          (let [parents (for [{:block/keys [uuid content]} parents]
-                                          (let [title (string/trim (text/remove-level-spaces content format))]
-                                            (when (and (not (string/blank? title))
-                                                       (not= (string/lower-case page-name) (string/lower-case title)))
-                                              [:a {:href (rfe/href :page {:name uuid})}
-                                               title])))
+                          (let [parents (doall
+                                         (for [{:block/keys [uuid title]} parents]
+                                           [:a {:href (rfe/href :page {:name uuid})}
+                                            (map-inline config title)]))
                                 parents (remove nil? parents)]
                             (reset! parents-atom parents)
                             (when (seq parents)
@@ -1398,7 +1469,7 @@
    :should-update (fn [old-state new-state]
                     (not= (:block/content (second (:rum/args old-state)))
                           (:block/content (second (:rum/args new-state)))))}
-  [config {:block/keys [uuid title level body meta content dummy? page format repo children collapsed? pre-block? idx properties] :as block}]
+  [config {:block/keys [uuid title level body meta content dummy? page format repo children collapsed? pre-block? idx properties refs-with-children] :as block}]
   (let [ref? (boolean (:ref? config))
         breadcrumb-show? (:breadcrumb-show? config)
         sidebar? (boolean (:sidebar? config))
@@ -1480,10 +1551,20 @@
                                (when doc-mode?
                                  (when-let [parent (gdom/getElement block-id)]
                                    (when-let [node (.querySelector parent ".bullet-container")]
-                                     (d/add-class! node "hide-inner-bullet")))))}]
+                                     (d/add-class! node "hide-inner-bullet")))))}
+        data-refs (let [refs (model/get-page-names-by-ids
+                              (->> (map :db/id refs-with-children)
+                                   (remove nil?)))]
+                    (text/build-data-value refs))
+        data-refs-self (let [refs  (model/get-page-names-by-ids
+                                    (->> (map :db/id (:block/ref-pages block))
+                                         (remove nil?)))]
+                         (text/build-data-value refs))]
     [:div.ls-block.flex.flex-col.rounded-sm
      (cond->
       {:id block-id
+       :data-refs data-refs
+       :data-refs-self data-refs-self
        :style {:position "relative"}
        :class (str uuid
                    (when dummy? " dummy")
@@ -1497,7 +1578,7 @@
        (merge attrs))
 
      (when (and ref? breadcrumb-show?)
-       (when-let [comp (block-parents repo uuid format false)]
+       (when-let [comp (block-parents config repo uuid format false)]
          [:div.my-2.opacity-50.ml-4 comp]))
 
      (dnd-separator-wrapper block slide? (zero? idx))
@@ -1509,7 +1590,7 @@
       (block-content-or-editor config block edit-input-id block-id slide?)]
 
      (when (seq children)
-       [:div.block-children {:style {:margin-left (if doc-mode? 12 22)
+       [:div.block-children {:style {:margin-left (if doc-mode? 12 21)
                                      :display (if collapsed? "none" "")}}
         (for [child children]
           (when (map? child)
@@ -1519,7 +1600,8 @@
 
      (when ref?
        (let [children (-> (db/get-block-immediate-children repo uuid)
-                          db/sort-by-pos)]
+                          db/sort-by-pos)
+             children (block-handler/filter-blocks repo children (:filters config) false)]
          (when (seq children)
            [:div.ref-children.ml-12
             (blocks-container children (assoc config
@@ -1723,9 +1805,11 @@
             (and (seq result)
                  (or only-blocks? blocks-grouped-by-page?))
             (->hiccup result (cond-> (assoc config
-                                            ;; :editor-box editor/box
                                             :custom-query? true
-                                            :group-by-page? blocks-grouped-by-page?)
+                                            ;; :breadcrumb-show? true
+                                            :group-by-page? blocks-grouped-by-page?
+                                            ;; :ref? true
+)
                                children?
                                (assoc :ref? true))
                       {:style {:margin-top "0.25rem"
@@ -1771,6 +1855,7 @@
                     "important" svg/important
                     "caution" svg/caution
                     "warning" svg/warning
+                    "pinned" svg/pinned
                     nil)]
     [:div.flex.flex-row.admonitionblock.align-items {:class type}
      [:div.pr-4.admonition-icon.flex.flex-col.justify-center
@@ -1787,7 +1872,8 @@
        (let [format (:block/format config)]
          (for [[k v] (dissoc m :roam_alias :roam_tags)]
            (when (and (not (and (= k :macros) (empty? v))) ; empty macros
-)
+                      (not (= k :title))
+                      (not (= k :filters)))
              [:div.property
               [:span.font-medium.mr-1 (str (name k) ": ")]
               (if (coll? v)
@@ -1897,6 +1983,9 @@
 
       ["Custom" "warning" options result content]
       (admonition config "warning" options result)
+      
+      ["Custom" "pinned" options result content]
+      (admonition config "pinned" options result)
 
       ["Custom" name options l content]
       (->elem
@@ -1928,7 +2017,7 @@
            [:a.ml-1 {:id (str "fn." id)
                      :style {:font-size 14}
                      :class "footnum"
-                     :href (str "#fnr." id)}
+                     :on-click #(route-handler/jump-to-anchor! (str "fnr." id))}
             [:sup.fn (str name "↩︎")]])]])
 
       :else
@@ -2019,17 +2108,18 @@
      (assoc :class "doc-mode"))
    (if (:group-by-page? config)
      [:div.flex.flex-col
-      (for [[page blocks] blocks]
-        (let [alias? (:page/alias? page)
-              page (db/entity (:db/id page))]
-          [:div.my-2 (cond-> {:key (str "page-" (:db/id page))}
-                       (:ref? config)
-                       (assoc :class "color-level px-7 py-2 rounded"))
-           (ui/foldable
-            [:div
-             (page-cp config page)
-             (when alias? [:span.text-sm.font-medium.opacity-50 " Alias"])]
-            (blocks-container blocks config))]))]
+      (let [blocks (sort-by (comp :page/journal-day first) > blocks)]
+        (for [[page blocks] blocks]
+          (let [alias? (:page/alias? page)
+                page (db/entity (:db/id page))]
+            [:div.my-2 (cond-> {:key (str "page-" (:db/id page))}
+                         (:ref? config)
+                         (assoc :class "color-level px-7 py-2 rounded"))
+             (ui/foldable
+              [:div
+               (page-cp config page)
+               (when alias? [:span.text-sm.font-medium.opacity-50 " Alias"])]
+              (blocks-container blocks config))])))]
      (blocks-container blocks config))])
 
 (comment

+ 13 - 3
src/main/frontend/components/block.css

@@ -30,7 +30,6 @@
     display: inline-block;
     position: relative;
     margin-top: .5rem;
-    margin-bottom: .5rem;
 
     .ctl {
       position: absolute;
@@ -72,6 +71,16 @@
       }
     }
   }
+
+  .resize {
+    display: flex;
+  }
+
+  .draw [aria-labelledby="shapes-title"] {
+    position: absolute;
+    left: 50%;
+    transform: translate(-50%);
+  }
 }
 
 .open-block-ref-link {
@@ -91,7 +100,7 @@
 }
 
 .block-children {
-  border-left: 2px solid;
+  border-left: 1px solid;
   border-left-color: var(--ls-guideline-color, #ddd);
 
   padding-top: 2px;
@@ -115,7 +124,6 @@
 }
 
 .block-ref {
-  color: var(--ls-link-text-color);
   padding-bottom: 2px;
   border-bottom: 0.5px solid;
   border-bottom-color: var(--ls-block-ref-link-text-color);
@@ -173,6 +181,7 @@
 
   &:hover {
     color: var(--ls-link-text-hover-color);
+    opacity: 1;
   }
 }
 
@@ -355,3 +364,4 @@
 .embed-header {
   font-weight: 600;
 }
+

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

@@ -35,7 +35,7 @@
     [:div
      [:div.sm:flex.sm:items-start
       [:div.mt-3.text-center.sm:mt-0.sm:text-left.mb-2
-       [:h3#modal-headline.text-lg.leading-6.font-medium.text-gray-900
+       [:h3#modal-headline.text-lg.leading-6.font-medium
         "Your commit message:"]]]
 
      [:input#commit-message.form-input.block.w-full.sm:text-sm.sm:leading-5.my-2

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

@@ -72,21 +72,42 @@
    "#264c9b"
    "#793e3e"])
 
-(rum/defcs block-template <
+(defonce *including-parent? (atom nil))
+
+(rum/defc template-checkbox
+  [including-parent?]
+  [:div.flex.flex-row
+   [:span.text-medium.mr-2 "Including the parent block in the template?"]
+   (ui/toggle including-parent?
+              #(swap! *including-parent? not))])
+
+(rum/defcs block-template < rum/reactive
   (rum/local false ::edit?)
   (rum/local "" ::input)
+  {:will-unmount (fn [state]
+                   (reset! *including-parent? nil)
+                   state)}
   [state block-id]
   (let [edit? (get state ::edit?)
-        input (get state ::input)]
+        input (get state ::input)
+        including-parent? (rum/react *including-parent?)
+        block-id (if (string? block-id) (uuid block-id) block-id)
+        block (db/entity [:block/uuid block-id])
+        has-children? (seq (:block/children block))]
+    (when (and (nil? including-parent?) has-children?)
+      (reset! *including-parent? true))
+
     (if @edit?
       (do
         (state/clear-edit!)
         [:div.px-4.py-2 {:on-click (fn [e] (util/stop e))}
          [:p "What's the template's name?"]
-         [:input#new-template.form-input.block.w-full.sm:text-sm.sm:leading-5.my-2.text-gray-700
+         [:input#new-template.form-input.block.w-full.sm:text-sm.sm:leading-5.my-2
           {:auto-focus true
            :on-change (fn [e]
                         (reset! input (util/evalue e)))}]
+         (when has-children?
+           (template-checkbox including-parent?))
          (ui/button "Submit"
                     :on-click (fn []
                                 (let [title (string/trim @input)]
@@ -97,6 +118,8 @@
                                        :error)
                                       (do
                                         (editor-handler/set-block-property! block-id "template" title)
+                                        (when (false? including-parent?)
+                                          (editor-handler/set-block-property! block-id "including-parent" false))
                                         (state/hide-custom-context-menu!)))))))])
       (ui/menu-link
        {:key "Make template"

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

@@ -16,7 +16,8 @@
             [promesa.core :as p]
             [frontend.github :as github]
             [frontend.diff :as diff]
-            [medley.core :as medley]))
+            [medley.core :as medley]
+            [frontend.encrypt :as encrypt]))
 
 (defonce remote-hash-id (atom nil))
 (defonce diff-state (atom {}))
@@ -57,7 +58,7 @@
      [:div.cp__diff-file-header
       [:a.mr-2 {:on-click (fn [] (toggle-collapse? path))}
        (if collapse?
-         (svg/arrow-right)
+         (svg/arrow-right-2)
          (svg/arrow-down))]
       [:span.cp__diff-file-header-content {:style {:word-break "break-word"}}
        path]
@@ -162,8 +163,9 @@
               path
               remote-latest-commit
               (fn [{:keys [repo-url path ref content]}]
-                (swap! state/state
-                       assoc-in [:github/contents repo-url remote-latest-commit path] content))
+                (p/let [content (encrypt/decrypt content)]
+                  (swap! state/state
+                        assoc-in [:github/contents repo-url remote-latest-commit path] content)))
               (fn [response]
                 (when (= (gobj/get response "status") 401)
                   (notification/show!

+ 0 - 497
src/main/frontend/components/draw.cljs

@@ -1,497 +0,0 @@
-(ns frontend.components.draw
-  (:require [rum.core :as rum]
-            [goog.object :as gobj]
-            [frontend.rum :as r]
-            [frontend.util :as util :refer-macros [profile]]
-            [frontend.mixins :as mixins]
-            [frontend.storage :as storage]
-            [frontend.components.svg :as svg]
-            [cljs-bean.core :as bean]
-            [dommy.core :as d]
-            [clojure.string :as string]
-            [frontend.handler.notification :as notification]
-            [frontend.handler.draw :as draw :refer
-             [*files
-              *current-file
-              *current-title
-              *file-loading?
-              *elements
-              *unsaved?
-              *search-files
-              *saving-title
-              *excalidraw]]
-            [frontend.handler.file :as file]
-            [frontend.ui :as ui]
-            [frontend.loader :as loader]
-            [frontend.config :as config]
-            [frontend.state :as state]
-            [frontend.search :as search]
-            [frontend.components.repo :as repo]
-            [promesa.core :as p]
-            [reitit.frontend.easy :as rfe]))
-
-(defn loaded? []
-  js/window.Excalidraw)
-
-(defonce *loaded? (atom false))
-
-(defonce draw-state :draw-state)
-
-(defn get-draw-state []
-  (storage/get draw-state))
-(defn set-draw-state! [value]
-  (storage/set draw-state value))
-(defn get-k
-  ([k]
-   (get-k k (state/get-current-repo)))
-  ([repo k]
-   (when repo
-     (get-in (get-draw-state) [repo k]))))
-
-(defn set-k
-  [k v]
-  (when-let [repo (state/get-current-repo)]
-    (let [state (get-draw-state)]
-      (let [new-state (assoc-in state [repo k] v)]
-        (set-draw-state! new-state)))))
-
-(defn get-last-file
-  ([]
-   (get-k :last-file))
-  ([repo]
-   (get-k repo :last-file)))
-
-(defn get-last-title
-  ([]
-   (get-k :last-title))
-  ([repo]
-   (get-k repo :last-title)))
-
-(defn set-last-file!
-  [value]
-  (set-k :last-file value))
-(defn set-last-title!
-  [value]
-  (set-k :last-title value))
-
-(defn get-last-elements
-  []
-  (storage/get-json (str (state/get-current-repo) "-" "last-elements")))
-(defn get-last-app-state
-  []
-  (storage/get-json (str (state/get-current-repo) "-" "last-app-state")))
-
-(defn set-last-elements!
-  [value]
-  (storage/set-json (str (state/get-current-repo) "-" "last-elements") value))
-(defn set-last-app-state!
-  [value]
-  (storage/set-json (str (state/get-current-repo) "-" "last-app-state") value))
-
-(defn set-excalidraw-component!
-  []
-  (reset! *excalidraw (r/adapt-class
-                       (gobj/get js/window.Excalidraw "default"))))
-
-(defn serialize-as-json
-  [elements app-state]
-  (when (loaded?)
-    (when-let [f (gobj/get js/window.Excalidraw "serializeAsJSON")]
-      (f elements app-state))))
-
-;; api restore
-
-(defn from-json
-  [text]
-  (when-not (string/blank? text)
-    (try
-      (when-let [data (js/JSON.parse text)]
-        (if (not= "excalidraw" (gobj/get data "type"))
-          (notification/show!
-           (util/format "Could not load this invalid excalidraw file")
-           :error)
-          {:elements (gobj/get data "elements")
-           :app-state (gobj/get data "appState")}))
-      (catch js/Error e
-        (prn "from json error:")
-        (js/console.dir e)
-        (notification/show!
-         (util/format "Could not load this invalid excalidraw file")
-         :error)))))
-
-(defn get-file-title
-  [file]
-  (when file
-    (let [s (subs file 20)
-          title (string/replace s ".excalidraw" "")]
-      (string/replace title "-" " "))))
-
-(defn save-excalidraw!
-  [state _event file ok-handler]
-  (let [title @*current-title]
-    (cond
-      (string/blank? title)
-      (do
-        (reset! *saving-title nil)
-        (notification/show!
-         "Please specify a title first!"
-         :error)
-        ;; TODO: focus the title input
-)
-
-      (= title @*saving-title)
-      nil
-
-      :else
-      (when-let [elements (get-last-elements)]
-        (reset! *saving-title title)
-        (let [app-state (get-last-app-state)
-              [option] (:rum/args state)
-              file (util/trim-safe
-                    (or
-                     file
-                     @*current-file
-                     (:file option)
-                     (draw/title->file-name title)))
-              data (serialize-as-json elements app-state)]
-          (when file
-            (draw/save-excalidraw! file data
-                                   (fn [file]
-                                     (reset! *files
-                                             (distinct (conj @*files file)))
-                                     (reset! *current-file file)
-                                     (reset! *unsaved? false)
-                                     (set-last-file! file)
-                                     (when ok-handler (ok-handler file))
-                                     (reset! *saving-title nil)))))))))
-
-(defn- clear-canvas!
-  []
-  (when-let [canvas (d/by-id "canvas")]
-    (let [context (.getContext canvas "2d")]
-      (.clearRect context 0 0 (gobj/get canvas "width") (gobj/get canvas "height"))
-      (set! (.-fillStyle context) "#FFF")
-      (.fillRect context 0 0 (gobj/get canvas "width") (gobj/get canvas "height")))))
-
-(defn- new-file!
-  []
-  ;; TODO: save current firstly
-  (clear-canvas!)
-  (reset! *current-title "")
-  (reset! *current-file nil)
-  (reset! *elements nil)
-  (set-last-elements! nil)
-  (set-last-title! nil)
-  (set-last-file! nil)
-  (set-last-app-state! nil))
-
-(defn- rename-file!
-  [file new-title]
-  (when-not (string/blank? new-title)
-    (let [new-file (draw/title->file-name new-title)]
-      (when-not (= (string/trim file) (string/trim new-file))
-        (save-excalidraw!
-         {} {} new-file
-         (fn []
-           (set-last-file! new-file)
-           (util/p-handle
-            (file/remove-file!
-             (state/get-current-repo)
-             (str config/default-draw-directory "/" file))
-            (fn [_]
-              (reset! *files (->> (conj @*files new-file)
-                                  (remove #(= file %))
-                                  distinct
-                                  (vec)))
-              (reset! *current-file new-file)
-              (notification/show!
-               "File was renamed successfully!"
-               :success))
-            (fn [error]
-              (println "Rename file failed, reason: ")
-              (js/console.dir error)))))))))
-
-(rum/defc draw-title < rum/reactive
-  (mixins/event-mixin
-   (fn [state]
-     (let [old-title @*current-title]
-       (mixins/hide-when-esc-or-outside
-        state
-        :on-hide (fn [state e event]
-                   (let [title (and @*current-title (string/trim @*current-title))
-                         file @*current-file]
-                     (when (or
-                            (string/blank? old-title)
-                            (not= (string/trim old-title) title))
-                       (cond
-                         (and file (not (string/blank? title)))
-                         (rename-file! file title)
-
-                         (and (not file)
-                              (not (string/blank? title))
-                              (seq @*elements)) ; new file
-                         (save-excalidraw! {} {} nil nil)
-
-                         :else
-                         nil))))))
-     state))
-  []
-  (let [current-title (rum/react *current-title)]
-    [:input#draw-title.font-medium.w-48.px-2.py-1.ml-2
-     {:on-click (fn [e]
-                  (util/stop e))
-      :placeholder "Untitled"
-      :auto-complete "off"
-      :default-value (or (and current-title (string/capitalize current-title)) "")
-      :on-change (fn [e]
-                   (when-let [value (util/evalue e)]
-                     (set-last-title! value)
-                     (reset! *current-title value)))}]))
-
-(rum/defc files-search < rum/reactive
-  [state]
-  [:div#search-wrapper.relative.w-full.text-gray-400.focus-within:text-gray-600
-   [:div.absolute.inset-y-0.flex.items-center.pointer-events-none.left-3
-    [:svg.h-4.w-4
-     {:view-box "0 0 20 20", :fill "currentColor"}
-     [:path
-      {:d
-       "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z",
-       :clip-rule "evenodd",
-       :fill-rule "evenodd"}]]]
-   [:input.block.w-full.pl-2.sm:text-sm.sm:leading-3.mb-2.mt-2.border-none.outline-none.focus:outline-none
-    {:style {:padding-left "2rem"
-             :border-radius 0}
-     :placeholder "Search"
-     :auto-complete "off"
-     :on-change (fn [e]
-                  (let [value (util/evalue e)
-                        files @*files]
-                    (reset! *search-files
-                            (if (string/blank? value)
-                              files
-                              (search/fuzzy-search files value :limit 10)))))}]])
-
-(rum/defcs save-button < rum/reactive
-  [state]
-  (let [unsaved? (rum/react *unsaved?)]
-    [:a.ml-2 {:title (if unsaved? "Save changes" "Save")
-              :on-click (fn [e]
-                          (save-excalidraw! state e nil nil))}
-     [:div.ToolIcon__icon {:class (if unsaved? "bg-orange-400" "bg-gray-200")
-                           :style {:width "2rem"
-                                   :height "2rem"}}
-      svg/save]]))
-
-(rum/defcs files < rum/reactive
-  [state]
-  (let [all-files (rum/react *files)
-        search-files (rum/react *search-files)
-        files (if (seq search-files) search-files all-files)
-        current-file (rum/react *current-file)
-        unsaved? (rum/react *unsaved?)]
-    [:div.flex-row.flex.items-center
-     [:a.ml-2 {:title "New file"
-               :on-click new-file!}
-      [:div.ToolIcon__icon.bg-gray-200 {:style {:width "2rem"
-                                                :height "2rem"}}
-       svg/plus]]
-
-     (ui/dropdown-with-links
-      (fn [{:keys [toggle-fn]}]
-        [:div.ToolIcon__icon.ml-2.cursor.bg-gray-200 {:title "List files"
-                                                      :on-click toggle-fn
-                                                      :style {:width "2rem"
-                                                              :height "2rem"}}
-         svg/folder])
-      (mapv
-       (fn [file]
-         {:title (get-file-title file)
-          :options {:title file
-                    :on-click
-                    (fn [e]
-                      (util/stop e)
-                      (set-last-file! file)
-                      (reset! *current-file file)
-                      (reset! *current-title (get-file-title file))
-                      (reset! *search-files []))}})
-       files)
-      {:modal-class (util/hiccup->class
-                     "origin-top-right.absolute.left-0.mt-2.rounded-md.shadow-lg.whitespace-no-wrap.bg-white.w-48.dropdown-overflow-auto")
-       :links-header (when (>= (count all-files) 5)
-                       (files-search))})
-
-     (save-button)
-
-     (let [links (->> [(when @*current-file
-                         {:title "Delete"
-                          :options {:style {:color "#db1111"}
-                                    :on-click (fn [e]
-                                                (util/stop e)
-                                                (when-let [current-file @*current-file]
-                                                  (p/let [_ (file/remove-file! (state/get-current-repo)
-                                                                               (str config/default-draw-directory "/" current-file))]
-                                                    (reset! *files (remove #(= current-file %) @*files))
-                                                    (new-file!))))}})]
-                      (remove nil?))]
-       (when (seq links)
-         (ui/dropdown-with-links
-          (fn [{:keys [toggle-fn]}]
-            [:div.ToolIcon__icon.ml-2.cursor.bg-gray-200
-             {:title "More options"
-              :on-click toggle-fn
-              :style {:width "2rem"
-                      :height "2rem"}}
-             (svg/vertical-dots nil)])
-          links
-          {:modal-class (util/hiccup->class
-                         "origin-top-right.absolute.left-0.mt-2.rounded-md.shadow-lg.whitespace-no-wrap.bg-white.w-48.dropdown-overflow-auto")})))
-
-     (draw-title)]))
-
-(defn- set-canvas-actions-style!
-  [state]
-  (when-let [section (first (d/by-tag "section"))]
-    (when (= "canvasActions-title" (d/attr section "aria-labelledby"))
-      (d/set-style! section "margin-top" "48px")))
-  state)
-
-(rum/defcs draw-inner < rum/reactive
-  (mixins/keyboard-mixin (util/->system-modifier "ctrl+s")
-                         (fn [state e]
-                           (save-excalidraw! state e nil nil)))
-  (mixins/keyboard-mixin "alt+z" set-canvas-actions-style!)
-  {:init (fn [state]
-           (reset! *elements nil)
-           (let [[option] (:rum/args state)
-                 file (or @*current-file
-                          (:file option))]
-             (do
-               (reset! *current-title (get-file-title file))
-               (set-last-file! file))
-             (cond
-               file
-               (do
-                 (reset! *file-loading? true)
-                 (draw/load-excalidraw-file
-                  file
-                  (fn [data]
-                    (let [{:keys [elements app-state]} (from-json data)]
-                      (reset! *elements elements)
-                      (reset! *file-loading? false)))))
-
-               :else
-               (when-let [elements (get-last-elements)]
-                 ;; TODO: keep this for history undo
-                 (reset! *elements (remove #(gobj/get % "isDeleted") elements))))
-             (assoc state
-                    ::layout (atom [js/window.innerWidth js/window.innerHeight]))))
-   :did-mount set-canvas-actions-style!
-   :did-update set-canvas-actions-style!}
-  [state option]
-  (let [current-repo (state/sub :git/current-repo)
-        elements (rum/react *elements)
-        loading? (rum/react *file-loading?)
-        file (rum/react *current-file)
-        layout (get state ::layout)
-        [width height] (rum/react layout)
-        options (bean/->js {:zenModeEnabled true
-                            :viewBackgroundColor "#FFF"})
-        excalidraw-component @*excalidraw]
-    [:div.draw.white-theme {:style {:background "#FFF"}}
-     (when (and (or (and file elements)
-                    (nil? file))
-                excalidraw-component)
-       (excalidraw-component
-        {:width (get option :width width)
-         :height (get option :height height)
-         :on-resize (fn []
-                      (reset! layout [js/window.innerWidth js/window.innerHeight]))
-
-         :on-change (or (:on-change option)
-                        (fn [elements state]
-                          (when (not= (bean/->clj elements)
-                                      (bean/->clj @*elements))
-                            (reset! *unsaved? true))
-                          (set-last-elements! elements)
-                          (set-last-app-state! state)
-                          (reset! *elements elements)))
-         :options options
-         :user (bean/->js {:name (or (:user-name option)
-                                     (:name (state/get-me))
-                                     (util/unique-id))})
-         :on-username-change (fn [])
-         :initial-data (or elements #js [])}))
-     [:div.absolute.top-4.left-4.hidden.md:block
-      [:div.flex.flex-row.items-center
-       [:a.mr-3.opacity-70.hover:opacity-100 {:href (rfe/href :home)
-                                              :title "Back to logseq"}
-        (svg/logo false)]
-       (files)
-       (when loading?
-         svg/loading)]]
-     (ui/notification)
-
-     (when current-repo
-       [:div.absolute.top-4.right-4.hidden.md:block
-        [:div.flex.flex-row.items-center
-         (repo/sync-status current-repo)
-         (repo/repos-dropdown true
-                              (fn [repo]
-                                (reset! *current-file (get-last-file repo))))]])]))
-
-(rum/defcs draw-2 < rum/reactive
-  {:init (fn [state]
-           (let [repo (storage/get :git/current-repo)]
-
-             (let [current-title (get-last-title repo)]
-               (reset! *current-title current-title))
-             (let [current-file (or
-                                 (get-in (first (:rum/args state))
-                                         [:query-params :file])
-                                 (get-last-file repo))]
-               (reset! *current-file current-file)
-               (reset! *current-title (get-file-title current-file))))
-
-           (if (loaded?)
-             (set-excalidraw-component!)
-             (loader/load
-              (config/asset-uri "/static/js/excalidraw.min.js")
-              (fn []
-                (reset! *loaded? true)
-                (set-excalidraw-component!))))
-
-           (draw/get-all-excalidraw-files
-            (fn [files]
-              (reset! *files (distinct files))))
-
-           (state/set-draw! true)
-           state)
-   :will-unmount (fn [state]
-                   (state/set-draw! false)
-                   state)}
-  [state option]
-  (let [loaded? (or (loaded?)
-                    (rum/react *loaded?))
-        current-repo (state/sub :git/current-repo)
-        component (rum/react *excalidraw)]
-    (if component
-      (let [current-file (rum/react *current-file)
-            current-file (or current-file
-                             (and current-repo
-                                  (get-last-file current-repo)))]
-        (let [key (if current-repo
-                    (str current-repo "-"
-                         (or (and current-file (str "draw-" current-file))
-                             "draw-with-no-file"))
-                    "draw-with-no-file")]
-          (rum/with-key (draw-inner option) key)))
-      [:div.center svg/loading])))
-
-(rum/defc draw < rum/reactive
-  [option]
-  (let [db-restoring? (state/sub :db/restoring?)]
-    (if db-restoring?
-      [:div.ls-center
-       (ui/loading "Loading")]
-      (draw-2 option))))

+ 0 - 23
src/main/frontend/components/draw.css

@@ -1,23 +0,0 @@
-#draw {
-  -webkit-app-region: no-drag;
-  overflow: hidden;
-}
-
-#draw iframe {
-  width: 100%;
-  height: 100%;
-  border: none;
-}
-
-.draw {
-  display: flex;
-  position: fixed;
-  top: 0;
-  bottom: 0;
-  left: 0;
-  right: 0;
-}
-
-.excalidraw-embed .draw {
-  position: relative;
-}

+ 105 - 498
src/main/frontend/components/editor.cljs

@@ -3,25 +3,19 @@
             [frontend.components.svg :as svg]
             [frontend.config :as config]
             [frontend.handler.editor :as editor-handler :refer [get-state]]
+            [frontend.handler.editor.lifecycle :as lifecycle]
             [frontend.util :as util :refer-macros [profile]]
-            [frontend.handler.file :as file]
             [frontend.handler.block :as block-handler]
             [frontend.handler.page :as page-handler]
-            [frontend.handler.editor.keyboards :as keyboards-handler]
             [frontend.components.datetime :as datetime-comp]
-            [promesa.core :as p]
             [frontend.state :as state]
             [frontend.mixins :as mixins]
             [frontend.ui :as ui]
             [frontend.db :as db]
-            [frontend.config :as config]
-            [frontend.handler.web.nfs :as nfs]
             [dommy.core :as d]
             [goog.object :as gobj]
             [goog.dom :as gdom]
             [clojure.string :as string]
-            [clojure.set :as set]
-            [cljs.core.match :refer-macros [match]]
             [frontend.commands :as commands
              :refer [*show-commands
                      *matched-commands
@@ -29,9 +23,6 @@
                      *angle-bracket-caret-pos
                      *matched-block-commands
                      *show-block-commands]]
-            [medley.core :as medley]
-            [cljs-drag-n-drop.core :as dnd]
-            [frontend.text :as text]
             ["/frontend/utils" :as utils]))
 
 (rum/defc commands < rum/reactive
@@ -58,7 +49,7 @@
                        (editor-handler/insert-command! id command-steps
                                                        format
                                                        {:restore? restore-slash?})))
-        :class "black"}))))
+        :class     "black"}))))
 
 (rum/defc block-commands < rum/reactive
   [id format]
@@ -71,7 +62,7 @@
                      (editor-handler/insert-command! id (get (into {} matched) chosen)
                                                      format
                                                      {:last-pattern commands/angle-bracket}))
-        :class "black"}))))
+        :class     "black"}))))
 
 (rum/defc page-search < rum/reactive
   {:will-unmount (fn [state] (reset! editor-handler/*selected-text nil) state)}
@@ -90,92 +81,44 @@
                  (when (> (count edit-content) current-pos)
                    (util/safe-subs edit-content pos current-pos)))
               matched-pages (when-not (string/blank? q)
-                              (editor-handler/get-matched-pages q))
-              chosen-handler (if (state/sub :editor/show-page-search-hashtag?)
-                               (fn [chosen _click?]
-                                 (state/set-editor-show-page-search! false)
-                                 (let [chosen (if (re-find #"\s+" chosen)
-                                                (util/format "[[%s]]" chosen)
-                                                chosen)]
-                                   (editor-handler/insert-command! id
-                                                                   (str "#" chosen)
-                                                                   format
-                                                                   {:last-pattern (str "#" (if @editor-handler/*selected-text "" q))})))
-                               (fn [chosen _click?]
-                                 (state/set-editor-show-page-search! false)
-                                 (let [page-ref-text (page-handler/get-page-ref-text chosen)]
-                                   (editor-handler/insert-command! id
-                                                                   page-ref-text
-                                                                   format
-                                                                   {:last-pattern (str "[[" (if @editor-handler/*selected-text "" q))
-                                                                    :postfix-fn (fn [s] (util/replace-first "]]" s ""))}))))
-              non-exist-page-handler (fn [_state]
-                                       (state/set-editor-show-page-search! false)
-                                       (if (state/org-mode-file-link? (state/get-current-repo))
-                                         (let [page-ref-text (page-handler/get-page-ref-text q)
-                                               value (gobj/get input "value")
-                                               old-page-ref (util/format "[[%s]]" q)
-                                               new-value (string/replace value
-                                                                         old-page-ref
-                                                                         page-ref-text)]
-                                           (state/set-edit-content! id new-value)
-                                           (let [new-pos (+ current-pos
-                                                            (- (count page-ref-text)
-                                                               (count old-page-ref))
-                                                            2)]
-                                             (util/move-cursor-to input new-pos)))
-                                         (util/cursor-move-forward input 2)))]
+                              (editor-handler/get-matched-pages q))]
           (ui/auto-complete
            matched-pages
-           {:on-chosen chosen-handler
-            :on-enter non-exist-page-handler
+           {:on-chosen (page-handler/on-chosen-handler input id q pos format)
+            :on-enter #(page-handler/page-not-exists-handler input id q current-pos)
             :empty-div [:div.text-gray-500.pl-4.pr-4 "Search for a page"]
-            :class "black"}))))))
+            :class     "black"}))))))
 
-(rum/defc block-search < rum/reactive
-  {:will-unmount (fn [state] (reset! editor-handler/*selected-text nil) state)}
-  [id format]
+(rum/defcs block-search < rum/reactive
+  {:will-unmount (fn [state]
+                   (reset! editor-handler/*selected-text nil)
+                   (state/clear-search-result!)
+                   state)}
+  [state id format]
   (when (state/sub :editor/show-block-search?)
     (let [pos (:editor/last-saved-cursor @state/state)
-          input (gdom/getElement id)]
+          input (gdom/getElement id)
+          [id format] (:rum/args state)
+          current-pos (:pos (util/get-caret-pos input))
+          edit-content (state/sub [:editor/content id])
+          edit-block (state/get-edit-block)
+          q (or
+             @editor-handler/*selected-text
+             (when (> (count edit-content) current-pos)
+               (subs edit-content pos current-pos)))
+          matched-blocks (when-not (string/blank? q)
+                           (editor-handler/get-matched-blocks q (:block/uuid edit-block)))]
       (when input
-        (let [current-pos (:pos (util/get-caret-pos input))
-              edit-content (state/sub [:editor/content id])
-              q (or
-                 @editor-handler/*selected-text
-                 (when (> (count edit-content) current-pos)
-                   (subs edit-content pos current-pos)))
-              matched-blocks (when-not (string/blank? q)
-                               (editor-handler/get-matched-blocks q))
-              chosen-handler (fn [chosen _click?]
-                               (state/set-editor-show-block-search! false)
-                               (let [uuid-string (str (:block/uuid chosen))]
-
-                                 ;; block reference
-                                 (editor-handler/insert-command! id
-                                                                 (util/format "((%s))" uuid-string)
-                                                                 format
-                                                                 {:last-pattern (str "((" (if @editor-handler/*selected-text "" q))
-                                                                  :postfix-fn (fn [s] (util/replace-first "))" s ""))})
-
-                                 ;; Save it so it'll be parsed correctly in the future
-                                 (editor-handler/set-block-property! (:block/uuid chosen)
-                                                                     "ID"
-                                                                     uuid-string)
-
-                                 (when-let [input (gdom/getElement id)]
-                                   (.focus input))))
-              non-exist-block-handler (fn [_state]
-                                        (state/set-editor-show-block-search! false)
-                                        (util/cursor-move-forward input 2))]
+        (let [chosen-handler (editor-handler/block-on-chosen-handler input id q format)
+              non-exist-block-handler (editor-handler/block-non-exist-handler input)]
           (ui/auto-complete
            matched-blocks
-           {:on-chosen chosen-handler
-            :on-enter non-exist-block-handler
-            :empty-div [:div.text-gray-500.pl-4.pr-4 "Search for a block"]
+           {:on-chosen   chosen-handler
+            :on-enter    non-exist-block-handler
+            :empty-div   [:div.text-gray-500.pl-4.pr-4 "Search for a block"]
             :item-render (fn [{:block/keys [content]}]
                            (subs content 0 64))
-            :class "black"}))))))
+            :class       "black"}))))))
 
 (rum/defc template-search < rum/reactive
   {:will-unmount (fn [state] (reset! editor-handler/*selected-text nil) state)}
@@ -192,43 +135,16 @@
                    (subs edit-content pos current-pos))
                  "")
               matched-templates (editor-handler/get-matched-templates q)
-              chosen-handler (fn [[template db-id] _click?]
-                               (if-let [block (db/entity db-id)]
-                                 (let [new-level (:block/level edit-block)
-                                       template-parent-level (:block/level block)
-                                       pattern (config/get-block-pattern format)
-                                       content
-                                       (block-handler/get-block-full-content
-                                        (state/get-current-repo)
-                                        (:block/uuid block)
-                                        (fn [{:block/keys [level content properties] :as block}]
-                                          (let [new-level (+ new-level (- level template-parent-level))
-                                                properties' (dissoc (into {} properties) "id" "custom_id" "template")]
-                                            (-> content
-                                                (string/replace-first (apply str (repeat level pattern))
-                                                                      (apply str (repeat new-level pattern)))
-                                                text/remove-properties!
-                                                (text/rejoin-properties properties')))))
-                                       content (if (string/includes? (string/trim edit-content) "\n")
-                                                 content
-                                                 (text/remove-level-spaces content format))]
-                                   (state/set-editor-show-template-search! false)
-                                   (editor-handler/insert-command! id
-                                                                   content
-                                                                   format
-                                                                   {})))
-                               (when-let [input (gdom/getElement id)]
-                                 (.focus input)))
               non-exist-handler (fn [_state]
                                   (state/set-editor-show-template-search! false))]
           (ui/auto-complete
            matched-templates
-           {:on-chosen chosen-handler
-            :on-enter non-exist-handler
-            :empty-div [:div.text-gray-500.pl-4.pr-4 "Search for a template"]
+           {:on-chosen   (editor-handler/template-on-chosen-handler input id q format edit-block edit-content)
+            :on-enter    non-exist-handler
+            :empty-div   [:div.text-gray-500.pl-4.pr-4 "Search for a template"]
             :item-render (fn [[template _block-db-id]]
                            template)
-            :class "black"}))))))
+            :class       "black"}))))))
 
 (rum/defc mobile-bar < rum/reactive
   [parent-state parent-id]
@@ -248,22 +164,28 @@
    [:button.bottom-action
     {:on-click #(commands/simple-insert! parent-id "\n" {})}
     svg/multi-line-input]
+   [:button.bottom-action
+    {:on-click #(commands/insert-before! parent-id "TODO " {})}
+    svg/checkbox]
    [:button.font-extrabold.bottom-action.-mt-1
     {:on-click #(commands/simple-insert!
                  parent-id "[[]]"
                  {:backward-pos 2
-                  :check-fn (fn [_ _ new-pos]
-                              (reset! commands/*slash-caret-pos new-pos)
-                              (commands/handle-step [:editor/search-page]))})}
+                  :check-fn     (fn [_ _ new-pos]
+                                  (reset! commands/*slash-caret-pos new-pos)
+                                  (commands/handle-step [:editor/search-page]))})}
     "[[]]"]
    [:button.font-extrabold.bottom-action.-mt-1
     {:on-click #(commands/simple-insert!
                  parent-id "(())"
                  {:backward-pos 2
-                  :check-fn (fn [_ _ new-pos]
-                              (reset! commands/*slash-caret-pos new-pos)
-                              (commands/handle-step [:editor/search-block]))})}
-    "(())"]])
+                  :check-fn     (fn [_ _ new-pos]
+                                  (reset! commands/*slash-caret-pos new-pos)
+                                  (commands/handle-step [:editor/search-block]))})}
+    "(())"]
+   [:button.font-extrabold.bottom-action.-mt-1
+    {:on-click #(commands/simple-insert! parent-id "/" {})}
+    "/"]])
 
 (rum/defcs input < rum/reactive
   (rum/local {} ::input-value)
@@ -306,11 +228,11 @@
               [:input.form-input.block.w-full.pl-2.sm:text-sm.sm:leading-5
                (merge
                 (cond->
-                 {:key (str "modal-input-" (name id))
-                  :id (str "modal-input-" (name id))
-                  :type (or type "text")
-                  :on-change (fn [e]
-                               (swap! input-value assoc id (util/evalue e)))
+                 {:key           (str "modal-input-" (name id))
+                  :id            (str "modal-input-" (name id))
+                  :type          (or type "text")
+                  :on-change     (fn [e]
+                                   (swap! input-value assoc id (util/evalue e)))
                   :auto-complete (if (util/chrome?) "chrome-off" "off")}
                   placeholder
                   (assoc :placeholder placeholder))
@@ -357,31 +279,31 @@
     (when-let [pos (rum/react pos)]
       (ui/css-transition
        {:class-names "fade"
-        :timeout {:enter 500
-                  :exit 300}}
+        :timeout     {:enter 500
+                      :exit  300}}
        (absolute-modal cp set-default-width? pos)))))
 
 (rum/defc image-uploader < rum/reactive
   {:did-mount    (fn [state]
                    (let [[id format] (:rum/args state)]
-                     (add-watch editor-handler/*image-pending-file ::pending-image
+                     (add-watch editor-handler/*asset-pending-file ::pending-asset
                                 (fn [_ _ _ f]
                                   (reset! *slash-caret-pos (util/get-caret-pos (gdom/getElement id)))
-                                  (editor-handler/upload-image id #js[f] format editor-handler/*image-uploading? true))))
+                                  (editor-handler/upload-asset id #js[f] format editor-handler/*asset-uploading? true))))
                    state)
    :will-unmount (fn [state]
-                   (remove-watch editor-handler/*image-pending-file ::pending-image))}
+                   (remove-watch editor-handler/*asset-pending-file ::pending-asset))}
   [id format]
   [:div.image-uploader
    [:input
-    {:id "upload-file"
-     :type "file"
+    {:id        "upload-file"
+     :type      "file"
      :on-change (fn [e]
                   (let [files (.-files (.-target e))]
-                    (editor-handler/upload-image id files format editor-handler/*image-uploading? false)))
-     :hidden true}]
-   (when-let [uploading? (util/react editor-handler/*image-uploading?)]
-     (let [processing (util/react editor-handler/*image-uploading-process)]
+                    (editor-handler/upload-asset id files format editor-handler/*asset-uploading? false)))
+     :hidden    true}]
+   (when-let [uploading? (util/react editor-handler/*asset-uploading?)]
+     (let [processing (util/react editor-handler/*asset-uploading-process)]
        (transition-cp
         [:div.flex.flex-row.align-center.rounded-md.shadow-sm.bg-base-2.px-1.py-1
          (ui/loading
@@ -389,373 +311,58 @@
         false
         *slash-caret-pos)))])
 
-(rum/defcs box < rum/reactive
-  (mixins/event-mixin
-   (fn [state]
-     (let [{:keys [id format block]} (get-state state)
-           input-id id
-           input (gdom/getElement input-id)
-           repo (:block/repo block)]
-       (mixins/on-key-down
-        state
-        {;; enter
-         13 (fn [state e]
-              (when (and (not (gobj/get e "ctrlKey"))
-                         (not (gobj/get e "metaKey"))
-                         (not (editor-handler/in-auto-complete? input)))
-                (let [{:keys [block config]} (get-state state)]
-                  (when (and block
-                             (not (:ref? config))
-                             (not (:custom-query? config))) ; in reference section
-                    (let [content (state/get-edit-content)]
-                      (if (and
-                           (> (:block/level block) 2)
-                           (string/blank? content))
-                        (do
-                          (util/stop e)
-                          (editor-handler/adjust-block-level! state :left))
-                        (let [shortcut (state/get-new-block-shortcut)
-                              insert? (cond
-                                        config/mobile?
-                                        true
-
-                                        (and (= shortcut "alt+enter") (not (gobj/get e "altKey")))
-                                        false
-
-                                        (gobj/get e "shiftKey")
-                                        false
-
-                                        :else
-                                        true)]
-                          (when (and
-                                 insert?
-                                 (not (editor-handler/in-auto-complete? input)))
-                            (util/stop e)
-                            (profile
-                             "Insert block"
-                             (editor-handler/insert-new-block! state))))))))))
-         ;; up
-         38 (fn [state e]
-              (when (and
-                     (not (gobj/get e "ctrlKey"))
-                     (not (gobj/get e "metaKey"))
-                     (not (editor-handler/in-auto-complete? input)))
-                (editor-handler/on-up-down state e true)))
-         ;; down
-         40 (fn [state e]
-              (when (and
-                     (not (gobj/get e "ctrlKey"))
-                     (not (gobj/get e "metaKey"))
-                     (not (editor-handler/in-auto-complete? input)))
-                (editor-handler/on-up-down state e false)))
-         ;; backspace
-         8  (fn [state e]
-              (let [node (gdom/getElement input-id)
-                    current-pos (:pos (util/get-caret-pos node))
-                    value (gobj/get node "value")
-                    deleted (and (> current-pos 0)
-                                 (util/nth-safe value (dec current-pos)))
-                    selected-start (gobj/get node "selectionStart")
-                    selected-end (gobj/get node "selectionEnd")
-                    block-id (:block-id (first (:rum/args state)))
-                    page (state/get-current-page)]
-                (cond
-                  (not= selected-start selected-end)
-                  nil
-
-                  (and (zero? current-pos)
-                       ;; not the top block in a block page
-                       (not (and page
-                                 (util/uuid-string? page)
-                                 (= (medley/uuid page) block-id))))
-                  (editor-handler/delete-block! state repo e)
-
-                  (and (> current-pos 1)
-                       (= (util/nth-safe value (dec current-pos)) commands/slash))
-                  (do
-                    (reset! *slash-caret-pos nil)
-                    (reset! *show-commands false))
+(defn- set-up-key-down!
+  [repo state input input-id format]
+  (mixins/on-key-down
+   state
+   {;; enter
+    13 (editor-handler/keydown-enter-handler state input)
+    ;; up
+    38 (editor-handler/keydown-up-down-handler input true)
+    ;; down
+    40 (editor-handler/keydown-up-down-handler input false)
+    ;; backspace
+    8 (editor-handler/keydown-backspace-handler repo input input-id)
+    ;; tab
+    9 (editor-handler/keydown-tab-handler input input-id)}
+   {:not-matched-handler (editor-handler/keydown-not-matched-handler input input-id format)}))
+
+(defn- set-up-key-up!
+  [state input input-id search-timeout]
+  (mixins/on-key-up
+   state
+   {}
+   (editor-handler/keyup-handler state input input-id search-timeout)))
+
+(def search-timeout (atom nil))
+
+(defn- setup-key-listener!
+  [state]
+  (let [{:keys [id format block]} (get-state state)
+        input-id id
+        input (gdom/getElement input-id)
+        repo (:block/repo block)]
+    (set-up-key-down! repo state input input-id format)
+    (set-up-key-up! state input input-id search-timeout)))
 
-                  (and (> current-pos 1)
-                       (= (util/nth-safe value (dec current-pos)) commands/angle-bracket))
-                  (do
-                    (reset! *angle-bracket-caret-pos nil)
-                    (reset! *show-block-commands false))
-
-                  ;; pair
-                  (and
-                   deleted
-                   (contains?
-                    (set (keys editor-handler/delete-map))
-                    deleted)
-                   (>= (count value) (inc current-pos))
-                   (= (util/nth-safe value current-pos)
-                      (get editor-handler/delete-map deleted)))
-
-                  (do
-                    (util/stop e)
-                    (commands/delete-pair! id)
-                    (cond
-                      (and (= deleted "[") (state/get-editor-show-page-search?))
-                      (state/set-editor-show-page-search! false)
-
-                      (and (= deleted "(") (state/get-editor-show-block-search?))
-                      (state/set-editor-show-block-search! false)
-
-                      :else
-                      nil))
-
-                  ;; deleting hashtag
-                  (and (= deleted "#") (state/get-editor-show-page-search-hashtag?))
-                  (state/set-editor-show-page-search-hashtag! false)
-
-                  :else
-                  nil)))
-         ;; tab
-         9  (fn [state e]
-              (let [input-id (state/get-edit-input-id)
-                    input (and input-id (gdom/getElement id))
-                    pos (and input (:pos (util/get-caret-pos input)))]
-                (when (and (not (state/get-editor-show-input))
-                           (not (state/get-editor-show-date-picker?))
-                           (not (state/get-editor-show-template-search?)))
-                  (util/stop e)
-                  (let [direction (if (gobj/get e "shiftKey") ; shift+tab move to left
-                                    :left
-                                    :right)]
-                    (p/let [_ (editor-handler/adjust-block-level! state direction)]
-                      (and input pos (js/setTimeout #(when-let [input (gdom/getElement input-id)]
-                                                       (util/move-cursor-to input pos))
-                                                    0)))))))}
-        {:not-matched-handler
-         (fn [e key-code]
-           (let [key (gobj/get e "key")
-                 value (gobj/get input "value")
-                 ctrlKey (gobj/get e "ctrlKey")
-                 metaKey (gobj/get e "metaKey")
-                 pos (util/get-input-pos input)]
-             (cond
-               (or ctrlKey metaKey)
-               nil
-
-               (or
-                (and (= key "#")
-                     (and
-                      (> pos 0)
-                      (= "#" (util/nth-safe value (dec pos)))))
-                (and (= key " ")
-                     (state/get-editor-show-page-search-hashtag?)))
-               (state/set-editor-show-page-search-hashtag! false)
-
-               (or
-                (editor-handler/surround-by? input "#" " ")
-                (editor-handler/surround-by? input "#" :end)
-                (= key "#"))
-               (do
-                 (commands/handle-step [:editor/search-page-hashtag])
-                 (state/set-last-pos! (:pos (util/get-caret-pos input)))
-                 (reset! commands/*slash-caret-pos (util/get-caret-pos input)))
-
-               (and
-                (= key " ")
-                (state/get-editor-show-page-search-hashtag?))
-               (state/set-editor-show-page-search-hashtag! false)
-
-               (and
-                (contains? (set/difference (set (keys editor-handler/reversed-autopair-map))
-                                           #{"`"})
-                           key)
-                (= (editor-handler/get-current-input-char input) key))
-               (do
-                 (util/stop e)
-                 (util/cursor-move-forward input 1))
-
-               (contains? (set (keys editor-handler/autopair-map)) key)
-               (do
-                 (util/stop e)
-                 (editor-handler/autopair input-id key format nil)
-                 (cond
-                   (editor-handler/surround-by? input "[[" "]]")
-                   (do
-                     (commands/handle-step [:editor/search-page])
-                     (reset! commands/*slash-caret-pos (util/get-caret-pos input)))
-                   (editor-handler/surround-by? input "((" "))")
-                   (do
-                     (commands/handle-step [:editor/search-block :reference])
-                     (reset! commands/*slash-caret-pos (util/get-caret-pos input)))
-                   :else
-                   nil))
-
-               (let [sym "$"]
-                 (and (= key sym)
-                      (>= (count value) 1)
-                      (> pos 0)
-                      (= (nth value (dec pos)) sym)
-                      (if (> (count value) pos)
-                        (not= (nth value pos) sym)
-                        true)))
-               (commands/simple-insert! input-id "$$" {:backward-pos 2})
-
-               (let [sym "^"]
-                 (and (= key sym)
-                      (>= (count value) 1)
-                      (> pos 0)
-                      (= (nth value (dec pos)) sym)
-                      (if (> (count value) pos)
-                        (not= (nth value pos) sym)
-                        true)))
-               (commands/simple-insert! input-id "^^" {:backward-pos 2})
-
-               :else
-               nil)))})
-       (mixins/on-key-up
-        state
-        {}
-        (fn [e key-code]
-          (let [k (gobj/get e "key")
-                format (:format (get-state state))]
-            (when-not (state/get-editor-show-input)
-              (when (and @*show-commands (not= key-code 191))     ; not /
-                (let [matched-commands (editor-handler/get-matched-commands input)]
-                  (if (seq matched-commands)
-                    (do
-                      (reset! *show-commands true)
-                      (reset! *matched-commands matched-commands))
-                    (reset! *show-commands false))))
-              (when (and @*show-block-commands (not= key-code 188))     ; not <
-                (let [matched-block-commands (editor-handler/get-matched-block-commands input)]
-                  (if (seq matched-block-commands)
-                    (cond
-                      (= key-code 9)      ;tab
-                      (when @*show-block-commands
-                        (util/stop e)
-                        (editor-handler/insert-command! input-id
-                                                        (last (first matched-block-commands))
-                                                        format
-                                                        {:last-pattern commands/angle-bracket}))
-
-                      :else
-                      (reset! *matched-block-commands matched-block-commands))
-                    (reset! *show-block-commands false))))
-              (editor-handler/close-autocomplete-if-outside input))))))))
-  {:did-mount (fn [state]
-                (let [[{:keys [dummy? format block-parent-id]} id] (:rum/args state)
-                      content (get-in @state/state [:editor/content id])
-                      input (gdom/getElement id)]
-                  (when block-parent-id
-                    (state/set-editing-block-dom-id! block-parent-id))
-                  (if (= :indent-outdent (state/get-editor-op))
-                    (when input
-                      (when-let [pos (state/get-edit-pos)]
-                        (util/set-caret-pos! input pos)))
-                    (editor-handler/restore-cursor-pos! id content dummy?))
-
-                  (when input
-                    (dnd/subscribe!
-                     input
-                     :upload-images
-                     {:drop (fn [e files]
-                              (editor-handler/upload-image id files format editor-handler/*image-uploading? true))}))
-
-                  ;; Here we delay this listener, otherwise the click to edit event will trigger a outside click event,
-                  ;; which will hide the editor so no way for editing.
-                  (js/setTimeout #(keyboards-handler/esc-save! state) 100)
-
-                  (when-let [element (gdom/getElement id)]
-                    (.focus element)))
-                state)
-   :did-remount (fn [_old-state state]
-                  (keyboards-handler/esc-save! state)
-                  state)
-   :will-unmount (fn [state]
-                   (let [{:keys [id value format block repo dummy? config]} (get-state state)
-                         file? (:file? config)]
-                     (when-let [input (gdom/getElement id)]
-                       ;; (.removeEventListener input "paste" (fn [event]
-                       ;;                                       (append-paste-doc! format event)))
-                       (let [s (str "cljs-drag-n-drop." :upload-images)
-                             a (gobj/get input s)
-                             timer (:timer a)]
-
-                         (and timer
-                              (dnd/unsubscribe!
-                               input
-                               :upload-images))))
-                     (editor-handler/clear-when-saved!)
-                     (if file?
-                       (let [path (:file-path config)
-                             content (db/get-file-no-sub path)
-                             value (some-> (gdom/getElement path)
-                                           (gobj/get "value"))]
-                         (when (and
-                                (not (string/blank? value))
-                                (not= (string/trim value) (string/trim content)))
-                           (let [old-page-name (db/get-file-page path false)]
-                             (page-handler/rename-when-alter-title-property! old-page-name path format content value)
-                             (file/alter-file (state/get-current-repo) path (string/trim value)
-                                              {:re-render-root? true}))))
-                       (when-not (contains? #{:insert :indent-outdent :auto-save} (state/get-editor-op))
-                         (editor-handler/save-block! (get-state state) value))))
-                   state)}
+(rum/defcs box < rum/reactive
+  (mixins/event-mixin setup-key-listener!)
+  lifecycle/lifecycle
   [state {:keys [on-hide dummy? node format block block-parent-id]
-          :or {dummy? false}
-          :as option} id config]
+          :or   {dummy? false}
+          :as   option} id config]
   (let [content (state/get-edit-content)]
     [:div.editor-inner {:class (if block "block-editor" "non-block-editor")}
      (when config/mobile? (mobile-bar state id))
      (ui/ls-textarea
       {:id                id
+       :class             "mousetrap"
        :cacheMeasurements true
        :default-value     (or content "")
        :minRows           (if (state/enable-grammarly?) 2 1)
-       :on-click          (fn [_e]
-                            (let [input (gdom/getElement id)
-                                  current-pos (:pos (util/get-caret-pos input))]
-                              (state/set-edit-pos! current-pos)
-                              (editor-handler/close-autocomplete-if-outside input)))
-       :on-change         (fn [e]
-                            (let [value (util/evalue e)
-                                  current-pos (:pos (util/get-caret-pos (gdom/getElement id)))]
-                              (state/set-edit-content! id value false)
-                              (state/set-edit-pos! current-pos)
-                              (when-let [repo (or (:block/repo block)
-                                                  (state/get-current-repo))]
-                                (state/set-editor-last-input-time! repo (util/time-ms))
-                                (db/clear-repo-persistent-job! repo))
-                              (let [input (gdom/getElement id)
-                                    native-e (gobj/get e "nativeEvent")
-                                    last-input-char (util/nth-safe value (dec current-pos))]
-                                (case last-input-char
-                                  "/"
-                                   ;; TODO: is it cross-browser compatible?
-                                  (when (not= (gobj/get native-e "inputType") "insertFromPaste")
-                                    (when-let [matched-commands (seq (editor-handler/get-matched-commands input))]
-                                      (reset! *slash-caret-pos (util/get-caret-pos input))
-                                      (reset! *show-commands true)))
-                                  "<"
-                                  (when-let [matched-commands (seq (editor-handler/get-matched-block-commands input))]
-                                    (reset! *angle-bracket-caret-pos (util/get-caret-pos input))
-                                    (reset! *show-block-commands true))
-                                  nil))))
-       :on-paste          (fn [e]
-                            (when-let [handled
-                                       (let [pick-one-allowed-item
-                                             (fn [items]
-                                               (when (and items (.-length items))
-                                                 (let [files (. (js/Array.from items) (filter #(= (.-kind %) "file")))
-                                                       it (gobj/get files 0) ;;; TODO: support multiple files
-                                                       mime (and it (.-type it))]
-                                                   (cond
-                                                     (contains? #{"image/jpeg" "image/png" "image/jpg" "image/gif"} mime) [:image (. it getAsFile)]))))
-                                             clipboard-data (gobj/get e "clipboardData")
-                                             items (or (.-items clipboard-data)
-                                                       (.-files clipboard-data))
-                                             picked (pick-one-allowed-item items)]
-                                         (when (and picked (get picked 1))
-                                           (match picked
-                                             [:image file] (editor-handler/set-image-pending-file file))
-                                           true))]
-                              (util/stop e)))
+       :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)
        :auto-focus        false})
 
      ;; TODO: how to render the transitions asynchronously?

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

@@ -0,0 +1,174 @@
+(ns frontend.components.encryption
+  (:require [rum.core :as rum]
+            [promesa.core :as p]
+            [frontend.encrypt :as e]
+            [frontend.util :as util :refer-macros [profile]]
+            [frontend.context.i18n :as i18n]
+            [frontend.db.utils :as db-utils]
+            [clojure.string :as string]
+            [frontend.state :as state]
+            [frontend.handler.metadata :as metadata-handler]
+            [frontend.ui :as ui]
+            [frontend.handler.notification :as notification]))
+
+(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?)
+        secret-phrase (e/get-key-pair repo-url)
+        public-key (e/get-public-key repo-url)
+        private-key (e/get-secret-key repo-url)]
+    (rum/with-context [[t] i18n/*tongue-context*]
+      [: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 public-key]
+          (if @reveal-secret-phrase?
+            [:div
+             [:div.mt-1.font-medium "Private Key:"]
+             [:div private-key]]
+            [:div "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]
+  (rum/with-context [[t] i18n/*tongue-context*]
+    (let [password (get state ::password)
+          password-confirm (get state ::password-confirm)]
+      [:div.sm:w-96
+       [: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]
+  (rum/with-context [[t] i18n/*tongue-context*]
+    [:div.sm:w-96
+     [: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)
+  [state repo-url db-encrypted-secret close-fn]
+  (rum/with-context [[t] i18n/*tongue-context*]
+    (let [secret (get state ::secret)]
+      [: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 []
+                       (let [value @secret]
+                         (when-not (string/blank? value) ; TODO: length or other checks
+                           (p/let [repo (state/get-current-repo)
+                                   keys (e/decrypt-with-passphrase value db-encrypted-secret)]
+                             (e/save-key-pair! repo keys)
+                             (close-fn true)))))}
+          "Submit"]]]])))
+
+(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))))

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

@@ -71,7 +71,7 @@
   (let [path (get-path state)
         format (format/get-format path)
         page (db/get-file-page path)
-        config? (= path (str config/app-name "/" config/config-file))]
+        config? (= path (config/get-config-path))]
     (rum/with-context [[tongue] i18n/*tongue-context*]
       [:div.file {:id (str "file-" path)}
        [:h1.title
@@ -81,14 +81,14 @@
           [:a.bg-base-2.p-1.ml-1 {:style {:border-radius 4}
                                   :href (rfe/href :page {:name page})
                                   :on-click (fn [e]
-                                              (.preventDefault e)
                                               (when (gobj/get e "shiftKey")
                                                 (when-let [page (db/entity [:page/name (string/lower-case page)])]
                                                   (state/sidebar-add-block!
                                                    (state/get-current-repo)
                                                    (:db/id page)
                                                    :page
-                                                   {:page page}))))}
+                                                   {:page page}))
+                                                (util/stop e)))}
            page]])
 
        (when (and page (not (string/starts-with? page "logseq/")))

+ 126 - 112
src/main/frontend/components/header.cljs

@@ -9,12 +9,14 @@
             [frontend.storage :as storage]
             [frontend.config :as config]
             [frontend.context.i18n :as i18n]
+            [frontend.handler.ui :as ui-handler]
             [frontend.handler.user :as user-handler]
             [frontend.handler.export :as export]
             [frontend.components.svg :as svg]
             [frontend.components.repo :as repo]
             [frontend.components.search :as search]
             [frontend.handler.project :as project-handler]
+            [frontend.handler.page :as page-handler]
             [frontend.handler.web.nfs :as nfs]
             [goog.dom :as gdom]
             [goog.object :as gobj]))
@@ -22,7 +24,7 @@
 (rum/defc logo < rum/reactive
   [{:keys [white?]}]
   [:a.cp__header-logo
-   {:href "/"
+   {:href     (rfe/href :home)
     :on-click (fn []
                 (util/scroll-to-top)
                 (state/set-journals-length! 1))}
@@ -31,6 +33,29 @@
      [:img.cp__header-logo-img {:src logo}]
      (svg/logo (not white?)))])
 
+(rum/defc login
+  [logged?]
+  (rum/with-context [[t] i18n/*tongue-context*]
+    (when (and (not logged?)
+               (not config/publishing?))
+
+      (ui/dropdown-with-links
+       (fn [{:keys [toggle-fn]}]
+         [:a.fade-link {:on-click toggle-fn}
+          [:span.ml-1 (t :login)]])
+       (let [list [{:title (t :login-google)
+                    :url (str config/website "/login/google")}
+                   {:title (t :login-github)
+                    :url (str config/website "/login/github")}]]
+         (mapv
+          (fn [{:keys [title url]}]
+            {:title title
+             :options
+             {:on-click
+              (fn [_] (set! (.-href js/window.location) url))}})
+          list))
+       nil))))
+
 (rum/defc left-menu-button < rum/reactive
   [{:keys [on-click]}]
   [:button#left-menu.cp__header-left-menu
@@ -45,93 +70,85 @@
 
 (rum/defc dropdown-menu < rum/reactive
   [{:keys [me current-repo t default-home]}]
-  (let [projects (state/sub [:me :projects])]
+  (let [projects (state/sub [:me :projects])
+        logged? (state/logged?)]
     (ui/dropdown-with-links
      (fn [{:keys [toggle-fn]}]
-       [:button.max-w-xs.flex.items-center.text-sm.rounded-full.focus:outline-none.focus:shadow-outline.h-7.w-7.ml-2
+       [:a.cp__right-menu-button
         {:on-click toggle-fn}
-        (if-let [avatar (:avatar me)]
-          [:img#avatar.h-7.w-7.rounded-full
-           {:src avatar
-            :on-error (fn [this]
-                        (let [elem (gdom/getElement "avatar")]
-                          (gobj/set elem "src" (config/asset-uri "/static/img/broken-avatar.png"))))}]
-          [:div.h-7.w-7.rounded-full.bg-base-2.opacity-70.hover:opacity-100 {:style {:padding 1.5}}
-           [:a svg/user]])])
-     (let [logged? (:name me)]
-       (->>
-        [(when current-repo
-           {:title (t :graph)
-            :options {:href (rfe/href :graph)}
-            :icon svg/graph-sm})
-
-         (when (or logged? (and (nfs/supported?) current-repo))
-           {:title (t :all-graphs)
-            :options {:href (rfe/href :repos)}
-            :icon svg/repos-sm})
-
-         (when current-repo
-           {:title (t :all-pages)
-            :options {:href (rfe/href :all-pages)}
-            :icon svg/pages-sm})
-
-         (when current-repo
-           {:title (t :all-files)
-            :options {:href (rfe/href :all-files)}
-            :icon svg/folder-sm})
-
-         (when (and default-home current-repo)
-           {:title (t :all-journals)
-            :options {:href (rfe/href :all-journals)}
-            :icon svg/calendar-sm})
-
-         (when (project-handler/get-current-project current-repo projects)
-           {:title (t :my-publishing)
-            :options {:href (rfe/href :my-publishing)}})
-
-         (when-let [project (and current-repo
-                                 (project-handler/get-current-project current-repo projects))]
-           (let [link (str config/website "/" project)]
-             {:title (str (t :go-to) "/" project)
-              :options {:href link
-                        :target "_blank"}
-              :icon svg/external-link}))
-
+        (svg/horizontal-dots nil)])
+     (->>
+      [(when-not (util/mobile?)
+         {:title (t :help/toggle-right-sidebar)
+          :options {:on-click state/toggle-sidebar-open?!}})
+
+       (when current-repo
+         {:title (t :graph-view)
+          :options {:href (rfe/href :graph)}
+          :icon svg/graph-sm})
+
+       (when (or logged?
+                 (util/electron?)
+                 (and (nfs/supported?) current-repo))
+         {:title (t :all-graphs)
+          :options {:href (rfe/href :repos)}
+          :icon svg/repos-sm})
+
+       (when current-repo
+         {:title (t :all-pages)
+          :options {:href (rfe/href :all-pages)}
+          :icon svg/pages-sm})
+
+       (when current-repo
+         {:title (t :all-files)
+          :options {:href (rfe/href :all-files)}
+          :icon svg/folder-sm})
+
+       (when (and default-home current-repo)
+         {:title (t :all-journals)
+          :options {:href (rfe/href :all-journals)}
+          :icon svg/calendar-sm})
+
+       (when (project-handler/get-current-project current-repo projects)
+         {:title (t :my-publishing)
+          :options {:href (rfe/href :my-publishing)}})
+
+       (when-let [project (and current-repo
+                               (project-handler/get-current-project current-repo projects))]
+         (let [link (str config/website "/" project)]
+           {:title (str (t :go-to) "/" project)
+            :options {:href link
+                      :target "_blank"}
+            :icon svg/external-link}))
+
+       (when current-repo
          {:title (t :settings)
-          :options {:href (rfe/href :settings)}
-          :icon svg/settings-sm}
-
-         (when current-repo
-           {:title (t :export)
-            :options {:on-click (fn []
-                                  (export/export-repo-as-html! current-repo))}
-            :icon nil})
-         (when current-repo
-           {:title (t :import)
-            :options {:href (rfe/href :import)}
-            :icon svg/import-sm})
-         {:title [:div.flex-row.flex.justify-between.items-center
-                  [:span (t :join-community)]]
-          :options {:href "https://discord.gg/KpN4eHY"
-                    :title (t :discord-title)
-                    :target "_blank"}
-          :icon svg/discord}
-         {:title [:div.flex-row.flex.justify-between.items-center
-                  [:span (t :sponsor-us)]]
-          :options {:href "https://opencollective.com/logseq"
-                    :target "_blank"}}
-         (when logged?
-           {:title (t :sign-out)
-            :options {:on-click user-handler/sign-out!}
-            :icon svg/logout-sm})]
-        (remove nil?)))
-     {})))
-
-(rum/defc right-menu-button < rum/reactive
-  []
-  [:a.cp__right-menu-button
-   {:on-click state/toggle-sidebar-open?!}
-   (svg/menu nil)])
+          :options {:on-click #(ui-handler/toggle-settings-modal!)}
+          :icon svg/settings-sm})
+
+       (when current-repo
+         {:title (t :export)
+          :options {:on-click (fn []
+                                (export/export-repo-as-html! current-repo))}
+          :icon nil})
+       (when current-repo
+         {:title (t :import)
+          :options {:href (rfe/href :import)}
+          :icon svg/import-sm})
+       {:title [:div.flex-row.flex.justify-between.items-center
+                [:span (t :join-community)]]
+        :options {:href "https://discord.gg/KpN4eHY"
+                  :title (t :discord-title)
+                  :target "_blank"}
+        :icon svg/discord}
+       (when logged?
+         {:title (t :sign-out)
+          :options {:on-click user-handler/sign-out!}
+          :icon svg/logout-sm})]
+      (remove nil?))
+     ;; {:links-footer (when (and (util/electron?) (not logged?))
+     ;;                  [:div.px-2.py-2 (login logged?)])}
+)))
 
 (rum/defc header
   < rum/reactive
@@ -141,37 +158,36 @@
                    (remove #(= (:url %) config/local-repo)))]
     (rum/with-context [[t] i18n/*tongue-context*]
       [:div.cp__header#head
+       {:on-double-click (fn [^js e]
+                           (when-let [target (.-target e)]
+                             (when (and (util/electron?)
+                                        (or (.. target -classList (contains "cp__header"))
+                                            (. target (closest "#search"))))
+                               (js/window.apis.toggleMaxOrMinActiveWindow))))}
        (left-menu-button {:on-click (fn []
                                       (open-fn)
                                       (state/set-left-sidebar-open! true))})
 
        (logo {:white? white?})
 
+       (when (util/electron?)
+         [:a.mr-1.opacity-30.hover:opacity-100.it.navigation
+          {:style {:margin-left -10}
+           :title "Go Back" :on-click #(js/window.history.back)} (svg/arrow-left)])
+
+       (when (util/electron?)
+         [:a.opacity-30.hover:opacity-100.it.navigation
+          {:style {:margin-right 15}
+           :title "Go Forward" :on-click #(js/window.history.forward)} (svg/arrow-right)])
+
        (if current-repo
          (search/search)
          [:div.flex-1])
 
        (new-block-mode)
 
-       (when (and (not logged?)
-                  (not config/publishing?))
-
-         (ui/dropdown-with-links
-          (fn [{:keys [toggle-fn]}]
-            [:a {:on-click toggle-fn}
-             [:span.ml-1.text-sm (t :login)]])
-          (let [list [{:title (t :login-google)
-                       :url "/login/google"}
-                      {:title (t :login-github)
-                       :url "/login/github"}]]
-            (mapv
-             (fn [{:keys [title url]}]
-               {:title title
-                :options
-                {:on-click
-                 (fn [_] (set! (.-href js/window.location) url))}})
-             list))
-          nil))
+       (when-not (util/electron?)
+         (login logged?))
 
        (repo/sync-status current-repo)
 
@@ -182,7 +198,7 @@
                   (not config/publishing?))
          [:a.text-sm.font-medium.opacity-70.hover:opacity-100.ml-3.block
           {:on-click (fn []
-                       (nfs/ls-dir-files))}
+                       (page-handler/ls-dir-files!))}
           [:div.flex.flex-row.text-center
            [:span.inline-block svg/folder-add]
            (when-not config/mobile?
@@ -191,14 +207,12 @@
 
        (if config/publishing?
          [:a.text-sm.font-medium.ml-3 {:href (rfe/href :graph)}
-          (t :graph)]
+          (t :graph)])
 
-         (dropdown-menu {:me me
-                         :t t
-                         :current-repo current-repo
-                         :default-home default-home}))
+       (dropdown-menu {:me me
+                       :t t
+                       :current-repo current-repo
+                       :default-home default-home})
 
        [:a#download-as-html.hidden]
-       [:a#download-as-zip.hidden]
-
-       (right-menu-button)])))
+       [:a#download-as-zip.hidden]])))

+ 23 - 6
src/main/frontend/components/header.css

@@ -10,6 +10,20 @@
   width: 100%;
   top: 0;
   left: 0;
+
+  user-select: none;
+
+  .it svg {
+      transform: scale(0.8);
+  }
+
+  .repos {
+    .dropdown-wrapper {
+      left: unset;
+      right: -46px;
+      min-width: 14rem;
+    }
+  }
 }
 
 .cp__header-left-menu {
@@ -32,8 +46,11 @@
 
 .cp__header-logo,
 .cp__right-menu-button {
-  opacity: 0.7;
-  display: none;
+  opacity: 0.3;
+}
+
+.cp__header-logo {
+    display: none;
 }
 
 .cp__header-logo:hover,
@@ -50,6 +67,10 @@
   @apply ml-3;
 }
 
+.cp__right-menu-button {
+    display: block;
+}
+
 @screen sm {
   .cp__header {
     @apply shadow-none;
@@ -63,8 +84,4 @@
     display: flex;
     align-items: center;
   }
-
-  .cp__right-menu-button {
-    display: block;
-  }
 }

+ 19 - 12
src/main/frontend/components/journal.cljs

@@ -1,5 +1,6 @@
 (ns frontend.components.journal
   (:require [rum.core :as rum]
+            [reitit.frontend.easy :as rfe]
             [frontend.util :as util :refer-macros [profile]]
             [frontend.config :as config]
             [frontend.date :as date]
@@ -8,6 +9,7 @@
             [frontend.handler.page :as page-handler]
             [frontend.handler.editor :as editor-handler]
             [frontend.db :as db]
+            [frontend.db.model :as model]
             [frontend.state :as state]
             [frontend.ui :as ui]
             [frontend.config :as config]
@@ -20,7 +22,8 @@
             [frontend.components.onboarding :as onboarding]
             [goog.object :as gobj]
             [clojure.string :as string]
-            [frontend.handler.block :as block-handler]))
+            [frontend.handler.block :as block-handler]
+            [frontend.text :as text]))
 
 (rum/defc blocks-inner < rum/static
   {:did-mount (fn [state]
@@ -73,21 +76,28 @@
         intro? (and (not (state/logged?))
                     (not (config/local-db? repo))
                     (not config/publishing?)
-                    today?)]
-    [:div.flex-1.journal.page {:class (if intro? "intro" "")}
+                    today?)
+        page-entity (db/pull [:page/name (string/lower-case title)])
+        data-page-tags (when (seq (:page/tags page-entity))
+                         (let [page-names (model/get-page-names-by-ids (map :db/id (:page/tags page)))]
+                           (text/build-data-value page-names)))]
+    [:div.flex-1.journal.page (cond->
+                               {:class (if intro? "intro" "")}
+                                data-page-tags
+                                (assoc :data-page-tags data-page-tags))
      (ui/foldable
       [:a.initial-color.title
-       {:href (str "/page/" encoded-page-name)
+       {:href     (rfe/href :page {:name page})
         :on-click (fn [e]
-                    (.preventDefault e)
                     (when (gobj/get e "shiftKey")
-                      (when-let [page (db/pull [:page/name (string/lower-case title)])]
+                      (when-let [page page-entity]
                         (state/sidebar-add-block!
                          (state/get-current-repo)
                          (:db/id page)
                          :page
-                         {:page page
-                          :journal? true}))))}
+                         {:page     page
+                          :journal? true}))
+                      (.preventDefault e)))}
        [:h1.title
         (util/capitalize-all title)]]
 
@@ -101,10 +111,7 @@
 
      (when intro? (onboarding/intro))]))
 
-(rum/defc journals <
-  {:did-mount (fn [state]
-                (editor-handler/open-last-block! true)
-                state)}
+(rum/defc journals
   [latest-journals]
   [:div#journals
    (ui/infinite-list

+ 7 - 1
src/main/frontend/components/onboarding.cljs

@@ -176,6 +176,10 @@
        [:a {:href "https://logseq.com/blog/about"
             :target "_blank"}
         (t :help/about)]]
+      [:li
+       [:a {:href "https://trello.com/b/8txSM12G/roadmap"
+            :target "_blank"}
+        (t :help/roadmap)]]
       [:li
        [:a {:href "https://github.com/logseq/logseq/issues/new?assignees=&labels=&template=bug_report.md&title="
             :target "_blank"}
@@ -258,11 +262,13 @@
          [:tr [:td (t :help/open-link-in-sidebar)] [:td "Shift-Click"]]
          [:tr [:td (t :help/context-menu)] [:td "Right Click"]]
          [:tr [:td (t :help/fold-unfold)] [:td "Tab"]]
+         [:tr [:td (t :help/toggle-contents)] [:td "t c"]]
          [:tr [:td (t :help/toggle-doc-mode)] [:td "t d"]]
          [:tr [:td (t :help/toggle-theme)] [:td "t t"]]
          [:tr [:td (t :help/toggle-right-sidebar)] [:td "t r"]]
+         [:tr [:td (t :help/toggle-settings)] [:td "t s"]]
          [:tr [:td (t :help/toggle-insert-new-block)] [:td "t e"]]
-         [:tr [:td (t :help/jump-to-journals)] [:td (util/->platform-shortcut "Alt-j")]]]]
+         [:tr [:td (t :help/jump-to-journals)] [:td (util/->platform-shortcut "Ctrl-j")]]]]
        [:table
         [:thead
          [:tr

+ 88 - 50
src/main/frontend/components/page.cljs

@@ -1,6 +1,7 @@
 (ns frontend.components.page
   (:require [rum.core :as rum]
             [frontend.util :as util :refer-macros [profile]]
+            [frontend.tools.html-export :as html-export]
             [frontend.handler.file :as file]
             [frontend.handler.page :as page-handler]
             [frontend.handler.ui :as ui-handler]
@@ -21,6 +22,8 @@
             [frontend.components.project :as project]
             [frontend.config :as config]
             [frontend.db :as db]
+            [frontend.db.model :as model]
+            [frontend.db.utils :as db-utils]
             [frontend.mixins :as mixins]
             [frontend.db-mixins :as db-mixins]
             [goog.dom :as gdom]
@@ -34,6 +37,7 @@
             [cljs.pprint :as pprint]
             [frontend.context.i18n :as i18n]
             [reitit.frontend.easy :as rfe]
+            [frontend.text :as text]
             [frontend.handler.block :as block-handler]))
 
 (defn- get-page-name
@@ -54,6 +58,9 @@
   db-mixins/query
   [repo page file-path page-name page-original-name encoded-page-name sidebar? journal? block? block-id format]
   (let [raw-page-blocks (get-blocks repo page-name page-original-name block? block-id)
+        grouped-blocks-by-file (into {} (for [[k v] (db-utils/group-by-file raw-page-blocks)]
+                                          [(:file/path (db-utils/entity (:db/id k))) v]))
+        raw-page-blocks (get grouped-blocks-by-file file-path raw-page-blocks)
         page-blocks (block-handler/with-dummy-block raw-page-blocks format
                       (if (empty? raw-page-blocks)
                         (let [content (db/get-file repo file-path)]
@@ -73,11 +80,23 @@
                        :editor-box editor/box}
         hiccup-config (common-handler/config-with-document-mode hiccup-config)
         hiccup (block/->hiccup page-blocks hiccup-config {})]
-    (rum/with-key
-      (content/content page-name
-                       {:hiccup hiccup
-                        :sidebar? sidebar?})
-      (str encoded-page-name "-hiccup"))))
+    [:div.page-blocks-inner
+     (when (and (seq grouped-blocks-by-file)
+                (> (count grouped-blocks-by-file) 1))
+       (ui/admonition
+        :warning
+        [:div.text-sm
+         [:p.font-medium "Those pages have the same title, you might want to only keep one file."]
+         [:ol
+          (for [[file-path blocks] (into (sorted-map) grouped-blocks-by-file)]
+            [:li [:a {:key file-path
+                      :href (rfe/href :file {:path file-path})} file-path]])]]))
+
+     (rum/with-key
+       (content/content page-name
+                        {:hiccup   hiccup
+                         :sidebar? sidebar?})
+       (str encoded-page-name "-hiccup"))]))
 
 (defn contents-page
   [{:page/keys [name original-name file] :as contents}]
@@ -136,7 +155,7 @@
             :stroke-linejoin "round"
             :stroke-linecap "round"}]]]
         [:div.mt-3.text-center.sm:mt-0.sm:ml-4.sm:text-left
-         [:h3#modal-headline.text-lg.leading-6.font-medium.text-gray-900
+         [:h3#modal-headline.text-lg.leading-6.font-medium
           (t :page/delete-confirmation)]]]
 
        [:div.mt-5.sm:mt-4.sm:flex.sm:flex-row-reverse
@@ -157,10 +176,10 @@
   [state page-name close-fn]
   (let [input (get state ::input)]
     (rum/with-context [[t] i18n/*tongue-context*]
-      [:div
+      [:div.w-full.sm:max-w-lg.sm:w-96
        [: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.text-gray-900
+         [:h3#modal-headline.text-lg.leading-6.font-medium
           (t :page/rename-to page-name)]]]
 
        [:input.form-input.block.w-full.sm:text-sm.sm:leading-5.my-2
@@ -202,18 +221,13 @@
          [:ul.mt-2
           (for [[original-name name] pages]
             [:li {:key (str "tagged-page-" name)}
-             [:a {:href (str "/page/" (util/encode-str name))}
-              original-name]])])]])))
+             [:a {:href (rfe/href :page {:name name})}
+              original-name]])] false)]])))
 
-(defonce last-route (atom :home))
 ;; A page is just a logical block
 (rum/defcs page < rum/reactive
   {:did-mount (fn [state]
                 (ui-handler/scroll-and-highlight! state)
-                ;; only when route changed
-                (when (not= @last-route (state/get-current-route))
-                  (editor-handler/open-last-block! false))
-                (reset! last-route (state/get-current-route))
                 state)
    :did-update (fn [state]
                  (ui-handler/scroll-and-highlight! state)
@@ -223,14 +237,17 @@
         repo (or repo current-repo)
         encoded-page-name (or (get-page-name state)
                               (state/get-current-page))
-        page-name (string/lower-case (util/url-decode encoded-page-name))
-        path-page-name page-name
+        path-page-name (util/url-decode encoded-page-name)
+        page-name (string/lower-case path-page-name)
         marker-page? (util/marker? page-name)
         priority-page? (contains? #{"a" "b" "c"} page-name)
-        format (db/get-page-format page-name)
-        journal? (db/journal-page? page-name)
         block? (util/uuid-string? page-name)
         block-id (and block? (uuid page-name))
+        format (let [page (if block-id
+                            (:page/name (:block/page (db/entity [:block/uuid block-id])))
+                            page-name)]
+                 (db/get-page-format page))
+        journal? (db/journal-page? page-name)
         sidebar? (:sidebar? option)]
     (rum/with-context [[t] i18n/*tongue-context*]
       (cond
@@ -255,7 +272,8 @@
                           (db/entity repo))
                      (db/entity repo [:page/name page-name]))
               page (if page page (do
-                                   (db/transact! repo [{:page/name page-name}])
+                                   (db/transact! repo [{:page/name page-name
+                                                        :page/original-name path-page-name}])
                                    (db/entity repo [:page/name page-name])))
               properties (:page/properties page)
               page-name (:page/name page)
@@ -268,23 +286,35 @@
               developer-mode? (state/sub [:ui/developer-mode?])
               published? (= "true" (:published properties))
               public? (= "true" (:public properties))]
-          [:div.flex-1.page.relative
+          [:div.flex-1.page.relative (if (seq (:page/tags page))
+                                       (let [page-names (model/get-page-names-by-ids (map :db/id (:page/tags page)))]
+                                         {:data-page-tags (text/build-data-value page-names)})
+                                       {})
            [:div.relative
             (when (and (not block?)
                        (not sidebar?)
                        (not config/publishing?))
 
-              (let [links (->>
-                           [(when file
-                              {:title (t :page/re-index)
-                               :options {:on-click (fn []
-                                                     (file/re-index! file))}})
-                            {:title (t :page/add-to-contents)
-                             :options {:on-click (fn [] (page-handler/handle-add-page-to-contents! page-original-name))}}
-                            {:title (t :page/rename)
-                             :options {:on-click #(state/set-modal! (rename-page-dialog page-name))}}
-                            {:title (t :page/delete)
-                             :options {:on-click #(state/set-modal! (delete-page-dialog page-name))}}
+              (let [contents? (= (string/lower-case (str page-name)) "contents")
+                    links (->>
+                           [(when-not contents?
+                              {:title (t :page/add-to-contents)
+                               :options {:on-click (fn [] (page-handler/handle-add-page-to-contents! page-original-name))}})
+
+                            (when-not contents?
+                              {:title (t :page/rename)
+                               :options {:on-click #(state/set-modal! (rename-page-dialog page-name))}})
+
+                            (when (and file-path (util/electron?))
+                              [{:title   (t :page/open-in-finder)
+                                :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-not contents?
+                              {:title (t :page/delete)
+                               :options {:on-click #(state/set-modal! (delete-page-dialog page-name))}})
+
                             {:title   (t :page/action-publish)
                              :options {:on-click
                                        (fn []
@@ -301,11 +331,15 @@
                                                                              (page-handler/unpublish-page! page-name))}}
                                                       {:title   (t :page/publish)
                                                        :options {:on-click (fn []
-                                                                             (page-handler/publish-page! page-name project/add-project))}})
+                                                                             (page-handler/publish-page!
+                                                                              page-name project/add-project
+                                                                              html-export/export-page))}})
                                                     (when-not published?
                                                       {:title   (t :page/publish-as-slide)
-                                                      :options {:on-click (fn []
-                                                                            (page-handler/publish-page-as-slide! page-name project/add-project))}})
+                                                       :options {:on-click (fn []
+                                                                             (page-handler/publish-page-as-slide!
+                                                                              page-name project/add-project
+                                                                              html-export/export-page))}})
                                                     {:title   (t (if public? :page/make-private :page/make-public))
                                                      :options {:background (if public? "gray" "indigo")
                                                                :on-click (fn []
@@ -313,6 +347,12 @@
                                                                             page-name
                                                                             (if public? false true))
                                                                            (state/close-modal!))}}])])))}}
+
+                            (when file
+                              {:title (t :page/re-index)
+                               :options {:on-click (fn []
+                                                     (file/re-index! file))}})
+
                             (when developer-mode?
                               {:title "(Dev) Show page data"
                                :options {:on-click (fn []
@@ -326,6 +366,7 @@
                                                                     :on-click #(.writeText js/navigator.clipboard page-data))]
                                                         :success
                                                         false)))}})]
+                           (flatten)
                            (remove nil?))]
                 (when (seq links)
                   (ui/dropdown-with-links
@@ -373,12 +414,11 @@
                  {:key "page-file"}
                  [:span.opacity-50 {:style {:margin-top 2}} (t :file/file)]
                  [:a.bg-base-2.px-1.ml-1.mr-3 {:style {:border-radius 4
-                                                       :word-break "break-word"}
-                                               :href (str "/file/" (util/url-encode file-path))}
+                                                       :word-break    "break-word"}
+                                               :href  (rfe/href :file {:path file-path})}
                   file-path]
 
-                 (when (and (not config/mobile?)
-                            (not journal?))
+                 (when (not config/mobile?)
                    (presentation repo page))])]
 
              (when (and repo (not block?))
@@ -387,12 +427,14 @@
                    [:div.text-sm.ml-1.mb-4 {:key "page-file"}
                     [:span.opacity-50 "Alias: "]
                     (for [item alias]
-                      [:a.ml-1.mr-1 {:href (str "/page/" (util/encode-str item))}
+                      [:a.ml-1.mr-1 {:href (rfe/href :page {:name item})}
                        item])])))
 
              (when (and block? (not sidebar?))
-               [:div.mb-4
-                (block/block-parents repo block-id format)])
+               (let [config {:id "block-parent"
+                             :block? true}]
+                 [:div.mb-4
+                  (block/block-parents config repo block-id format)]))
 
              ;; blocks
              (page-blocks-cp repo page file-path page-name page-original-name encoded-page-name sidebar? journal? block? block-id format)]]
@@ -434,7 +476,7 @@
            {:width (if (and (> width 1280) sidebar-open?)
                      (- width 24 600)
                      (- width 24))
-            :height (- height 120)
+            :height height
             :ref (fn [v] (reset! graph-ref v))
             :ref-atom graph-ref}))
          [:div.ls-center.mt-20
@@ -473,11 +515,10 @@
               [:th (t :page/name)]
               [:th (t :file/last-modified-at)]]]
             [:tbody
-             (for [[page modified-at] pages]
+             (for [page pages]
                (let [encoded-page (util/encode-str page)]
                  [:tr {:key encoded-page}
                   [:td [:a {:on-click (fn [e]
-                                        (.preventDefault e)
                                         (let [repo (state/get-current-repo)
                                               page (db/pull repo '[*] [:page/name (string/lower-case page)])]
                                           (when (gobj/get e "shiftKey")
@@ -489,10 +530,7 @@
                             :href (rfe/href :page {:name encoded-page})}
                         page]]
                   [:td [:span.text-gray-500.text-sm
-                        (if (zero? modified-at)
-                          (t :file/no-data)
-                          (date/get-date-time-string
-                           (t/to-default-time-zone (tc/to-date-time modified-at))))]]]))]]))])))
+                        (t :file/no-data)]]]))]]))])))
 
 (rum/defcs new < rum/reactive
   (rum/local "" ::title)
@@ -510,7 +548,7 @@
       [:div#page-new.flex-1.flex-col {:style {:flex-wrap "wrap"}}
        [:div.mt-10.mb-2 {:style {:font-size "1.5rem"}}
         (t :page/new-title)]
-       [:input#page-title.focus:outline-none.ml-1.text-gray-900
+       [:input#page-title.focus:outline-none.ml-1
         {:style {:border "none"
                  :font-size "1.8rem"
                  :max-width 300}

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

@@ -27,4 +27,4 @@
       }
     }
   }
-}
+}

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

@@ -21,7 +21,7 @@
           :stroke-linejoin "round",
           :stroke-linecap "round"}]]]
       [:div.mt-3.text-center.sm:mt-0.sm:ml-4.sm:text-left
-       [:h3#modal-headline.text-lg.leading-6.font-medium.text-gray-900
+       [:h3#modal-headline.text-lg.leading-6.font-medium
         "Setup a public project on Logseq"]
        [:div.mt-2
         [:p.text-sm.leading-5.text-gray-500

+ 68 - 10
src/main/frontend/components/reference.cljs

@@ -10,21 +10,59 @@
             [frontend.date :as date]
             [frontend.components.editor :as editor]
             [frontend.db-mixins :as db-mixins]
-            [clojure.string :as string]))
+            [clojure.string :as string]
+            [frontend.config :as config]
+            [frontend.components.svg :as svg]
+            [frontend.handler.page :as page-handler]
+            [frontend.handler.block :as block-handler]
+            [medley.core :as medley]))
+
+(rum/defc filter-dialog-inner < rum/reactive
+  [close-fn references page-name]
+  (let [filter-state (page-handler/get-filter page-name)]
+    [:div.filters
+     [:div.sm:flex.sm:items-start
+      [:div.mx-auto.flex-shrink-0.flex.items-center.justify-center.h-12.w-12.rounded-full.bg-gray-200.text-gray-500.sm:mx-0.sm:h-10.sm:w-10
+       (svg/filter-icon)]
+      [:div.mt-3.text-center.sm:mt-0.sm:ml-4.sm:text-left
+       [:h3#modal-headline.text-lg.leading-6.font-medium "Filter"]
+       [:span.text-xs
+        "Click to include and shift-click to exclude. Click again to remove."]]]
+     (when (seq references)
+       [:div.mt-5.sm:mt-4.sm:flex.sm.gap-1.flex-wrap
+        (for [reference references]
+          (let [filtered (get (rum/react filter-state) reference)
+                color (condp = filtered
+                        true "text-green-400"
+                        false "text-red-400"
+                        nil)]
+            [:button.border.rounded.px-1.mb-1 {:key reference :class color :style {:border-color "currentColor"}
+                                               :on-click (fn [e]
+                                                           (swap! filter-state #(if (nil? (get @filter-state reference))
+                                                                                  (assoc % reference (not (.-shiftKey e)))
+                                                                                  (dissoc % reference)))
+                                                           (page-handler/save-filter! page-name @filter-state))}
+             reference]))])]))
 
-(rum/defc references < rum/reactive db-mixins/query
+(defn filter-dialog
+  [references page-name]
+  (fn [close-fn]
+    (filter-dialog-inner close-fn references page-name)))
+
+(rum/defc references < rum/reactive
   [page-name marker? priority?]
   (when page-name
     (let [block? (util/uuid-string? page-name)
           block-id (and block? (uuid page-name))
           page-name (string/lower-case page-name)
           journal? (date/valid-journal-title? (string/capitalize page-name))
+          repo (state/get-current-repo)
           ref-blocks (cond
                        priority?
-                       (db/get-blocks-by-priority (state/get-current-repo) page-name)
+                       (db/get-blocks-by-priority repo page-name)
 
                        marker?
-                       (db/get-marker-blocks (state/get-current-repo) page-name)
+                       (db/get-marker-blocks repo page-name)
                        block-id
                        (db/get-block-referenced-blocks block-id)
                        :else
@@ -32,7 +70,14 @@
           scheduled-or-deadlines (if journal?
                                    (db/get-date-scheduled-or-deadlines (string/capitalize page-name))
                                    nil)
-          n-ref (count ref-blocks)]
+          references (db/get-page-linked-refs-refed-pages repo page-name)
+          filter-state (rum/react (page-handler/get-filter page-name))
+          included (filter (fn [[x v]]) filter-state)
+          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)
+          n-ref (count filtered-ref-blocks)]
       (when (or (> n-ref 0)
                 (seq scheduled-or-deadlines))
         [:div.references.mt-6.flex-1.flex-row
@@ -53,16 +98,28 @@
                                  {:hiccup ref-hiccup}))]))
 
           (ui/foldable
-           [:h2.font-bold.opacity-50 (let []
-                                       (str n-ref " Linked References"))]
+           [:div.flex.flex-row.flex-1.justify-between
+            [:h2.font-bold.opacity-50 (let []
+                                        (str n-ref " Linked Reference"
+                                             (if (> n-ref 1) "s")))]
+            [:a.opacity-50.hover:opacity-100
+             {:title "Filter"
+              :on-click #(state/set-modal! (filter-dialog references page-name))}
+              (svg/filter-icon (cond
+                                 (empty? filter-state) nil
+                                 (every? true? (vals filter-state)) "text-green-400"
+                                 (every? false? (vals filter-state)) "text-red-400"
+                                 :else "text-yellow-400"))]]
+
            [:div.references-blocks
-            (let [ref-hiccup (block/->hiccup ref-blocks
+            (let [ref-hiccup (block/->hiccup filtered-ref-blocks
                                              {:id page-name
                                               :start-level 2
                                               :ref? true
                                               :breadcrumb-show? true
                                               :group-by-page? true
-                                              :editor-box editor/box}
+                                              :editor-box editor/box
+                                              :filters filters}
                                              {})]
               (content/content page-name
                                {:hiccup ref-hiccup}))])]]))))
@@ -98,7 +155,8 @@
           (ui/foldable
            [:h2.font-bold {:style {:opacity "0.3"}}
             (if @n-ref
-              (str @n-ref " Unlinked References")
+              (str @n-ref " Unlinked Reference" (if (> @n-ref 1)
+                                                  "s"))
               "Unlinked References")]
            (fn [] (unlinked-references-aux page-name n-ref))
            true)]]))))

+ 41 - 27
src/main/frontend/components/repo.cljs

@@ -4,17 +4,20 @@
             [frontend.ui :as ui]
             [frontend.state :as state]
             [frontend.db :as db]
+            [frontend.encrypt :as e]
             [frontend.handler.repo :as repo-handler]
             [frontend.handler.common :as common-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.export :as export-handler]
             [frontend.handler.web.nfs :as nfs-handler]
+            [frontend.handler.page :as page-handler]
             [frontend.util :as util]
             [frontend.config :as config]
             [reitit.frontend.easy :as rfe]
             [frontend.version :as version]
             [frontend.components.commit :as commit]
             [frontend.components.svg :as svg]
+            [frontend.components.encryption :as encryption]
             [frontend.context.i18n :as i18n]
             [clojure.string :as string]
             [clojure.string :as str]))
@@ -37,18 +40,23 @@
       (if (seq repos)
         [:div#graphs
          [:h1.title "All Graphs"]
+         [:p.ml-2.opacity-70
+          (if (state/github-authed?)
+            "A \"graph\" in Logseq could be either a local directory or a git repo."
+            "A \"graph\" in Logseq means a local directory.")]
 
-         [:div.pl-1.content
+         [:div.pl-1.content.mt-3
           [:div.flex.flex-row.my-4
            (when (nfs-handler/supported?)
              [:div.mr-8
               (ui/button
                (t :open-a-directory)
-               :on-click nfs-handler/ls-dir-files)])
-           (when (state/logged?)
+               :on-click page-handler/ls-dir-files!)])
+           (when (and (state/logged?) (not (util/electron?)))
              (ui/button
               "Add another git repo"
-              :href (rfe/href :repo-add nil {:graph-types "github"})))]
+              :href (rfe/href :repo-add nil {:graph-types "github"})
+              :intent "logseq"))]
           (for [{:keys [id url] :as repo} repos]
             (let [local? (config/local-db? url)]
               [:div.flex.justify-between.mb-1 {:key id}
@@ -59,24 +67,23 @@
                       :href url}
                   (db/get-repo-path url)])
                [:div.controls
-                [:a.control {:title (if local?
-                                      "Sync with the local directory"
-                                      "Clone again and re-index the db")
-                             :on-click (fn []
-                                         (if local?
-                                           (nfs-handler/rebuild-index! url
-                                                                 repo-handler/create-today-journal!)
-                                           (repo-handler/rebuild-index! url))
-                                         (js/setTimeout
-                                          (fn []
-                                            (route-handler/redirect! {:to :home}))
-                                          500))}
-                 "Re-index"]
-                [:a.control.ml-4 {:title "Clone again and re-index the db"
+                (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.control.ml-4 {:title (if local?
+                                           "Sync with the local directory"
+                                           "Clone again and re-index the db")
                                   :on-click (fn []
-                                              (export-handler/export-repo-as-json! (:url repo)))}
-                 "Export as JSON"]
-                [:a.text-gray-400.ml-4 {:on-click (fn []
+                                              (repo-handler/re-index! nfs-handler/rebuild-index!))}
+                 "Re-index"]
+                ;; [:a.control.ml-4 {:title "Export as JSON"
+                ;;                   :on-click (fn []
+                ;;                               (export-handler/export-repo-as-json! (:url repo)))}
+                ;;  "Export as JSON"]
+                [:a.text-gray-400.ml-4 {:title "No worries, unlink this graph will clear its cache only, it does not remove your files on the disk."
+                                        :on-click (fn []
                                                     (repo-handler/remove-repo! repo))}
                  "Unlink"]]]))]
 
@@ -93,11 +100,11 @@
       (when-not (= repo config/local-repo)
         (if (and nfs-repo? (nfs-handler/supported?))
           (let [syncing? (state/sub :graph/syncing?)]
-            [:div.ml-2.mr-1.opacity-70.hover:opacity-100 {:class (if syncing? "loader" "initial")}
+            [:div.ml-2.mr-2.opacity-30.refresh.hover:opacity-100 {:class (if syncing? "loader" "initial")}
              [:a
               {:on-click #(nfs-handler/refresh! repo
                                                 repo-handler/create-today-journal!)
-               :title (str "Sync files with the local directory: " (config/get-local-dir repo) ".\nVersion: "
+               :title (str "Import files from the local directory: " (config/get-local-dir repo) ".\nVersion: "
                            version/version)}
               svg/refresh]])
           (let [changed-files (state/sub [:repo/changed-files repo])
@@ -198,8 +205,12 @@
           (> (count repos) 1)
           (ui/dropdown-with-links
            (fn [{:keys [toggle-fn]}]
-             [:a#repo-switch {:on-click toggle-fn}
-              [:span (get-repo-name current-repo)]
+             [:a#repo-switch.fade-link {:on-click toggle-fn}
+              (let [repo-name (get-repo-name current-repo)
+                    repo-name (if (util/electron?)
+                                (last (string/split repo-name #"/"))
+                                repo-name)]
+                [:span repo-name])
               [:span.dropdown-caret.ml-1 {:style {:border-top-color "#6b7280"}}]])
            (mapv
             (fn [{:keys [id url]}]
@@ -221,8 +232,11 @@
           (and current-repo (not local-repo?))
           (let [repo-name (get-repo-name current-repo)]
             (if (config/local-db? current-repo)
-              repo-name
-              [:a
+              [:span.fade-link
+               (if (util/electron?)
+                 (last (string/split repo-name #"/"))
+                 repo-name)]
+              [:a.fade-link
                {:href current-repo
                 :target "_blank"}
                repo-name]))

+ 83 - 45
src/main/frontend/components/right_sidebar.cljs

@@ -28,16 +28,16 @@
 (rum/defc block-cp < rum/reactive
   [repo idx block]
   (let [id (:block/uuid block)]
-    (page/page {:parameters {:path {:name (str id)}}
-                :sidebar? true
+    (page/page {:parameters  {:path {:name (str id)}}
+                :sidebar?    true
                 :sidebar/idx idx
-                :repo repo})))
+                :repo        repo})))
 
 (rum/defc page-cp < rum/reactive
   [repo page-name]
   (page/page {:parameters {:path {:name page-name}}
-              :sidebar? true
-              :repo repo}))
+              :sidebar?   true
+              :repo       repo}))
 
 (rum/defc page-graph < db-mixins/query
   [page]
@@ -51,7 +51,7 @@
        (graph-2d/graph
         (graph/build-graph-opts
          graph dark? false
-         {:width 600
+         {:width  600
           :height 600}))])))
 
 (defn recent-pages
@@ -60,17 +60,17 @@
     [:div.recent-pages.text-sm.flex-col.flex.ml-3.mt-2
      (if (seq pages)
        (for [page pages]
-         [:a.mb-1 {:key (str "recent-page-" page)
-                   :href (rfe/href :page {:name page})
+         [:a.mb-1 {:key      (str "recent-page-" page)
+                   :href     (rfe/href :page {:name page})
                    :on-click (fn [e]
-                               (.preventDefault e)
                                (when (gobj/get e "shiftKey")
                                  (when-let [page (db/pull [:page/name (string/lower-case page)])]
                                    (state/sidebar-add-block!
                                     (state/get-current-repo)
                                     (:db/id page)
                                     :page
-                                    {:page page}))))}
+                                    {:page page}))
+                                 (.preventDefault e)))}
           page]))]))
 
 (rum/defc contents < rum/reactive db-mixins/query
@@ -87,7 +87,7 @@
                       (util/stop e)
                       (if-not (db/entity [:page/name "contents"])
                         (page-handler/create! "contents")
-                        (route-handler/redirect! {:to :page
+                        (route-handler/redirect! {:to          :page
                                                   :path-params {:name "contents"}})))}
       (t :right-side-bar/contents)]
      (contents)]
@@ -109,7 +109,8 @@
              block-id (:block/uuid block)
              format (:block/format block)]
          [[:div.ml-2.mt-1
-           (block/block-parents repo block-id format)]
+           (block/block-parents {:id     "block-parent"
+                                 :block? true} repo block-id format)]
           [:div.ml-2
            (block-cp repo idx block)]])])
 
@@ -117,13 +118,14 @@
     (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 repo block-id format)
+        [(block/block-parents {:id     "block-parent"
+                               :block? true} repo block-id format)
          [:div.ml-2
           (block-cp repo idx block-data)]]))
 
     :page
     (let [page-name (:page/name block-data)]
-      [[:a {:href (rfe/href :page {:name page-name})
+      [[:a {:href     (rfe/href :page {:name page-name})
             :on-click (fn [e]
                         (when (gobj/get e "shiftKey")
                           (.preventDefault e)))}
@@ -138,12 +140,12 @@
           blocks (if journal?
                    (rest blocks)
                    blocks)
-          sections (block/build-slide-sections blocks {:id "slide-reveal-js"
+          sections (block/build-slide-sections blocks {:id          "slide-reveal-js"
                                                        :start-level 2
-                                                       :slide? true
-                                                       :sidebar? true
-                                                       :page-name page-name})]
-      [[:a {:href (str "/page/" (util/url-encode page-name))}
+                                                       :slide?      true
+                                                       :sidebar?    true
+                                                       :page-name   page-name})]
+      [[:a {:href (rfe/href :page {:name page-name})}
         (db-model/get-page-original-name page-name)]
        [:div.ml-2.slide.mt-2
         (slide/slide sections)]])
@@ -206,9 +208,41 @@
         theme (:ui/theme @state/state)]
     (get-page match)))
 
+(rum/defc sidebar-resizer
+  []
+  (let [el-ref (rum/use-ref nil)]
+    (rum/use-effect!
+     (fn []
+       (when-let [el (and (fn? js/window.interact) (rum/deref el-ref))]
+         (-> (js/interact el)
+             (.draggable
+              (bean/->js
+               {:listeners
+                {:move
+                 (fn [^js/MouseEvent e]
+                   (let [width js/document.documentElement.clientWidth
+                         offset (.-left (.-rect e))
+                         to-val (- 1 (.toFixed (/ offset width) 6))
+                         to-val (cond
+                                  (< to-val 0.2) 0.2
+                                  (> to-val 0.7) 0.7
+                                  :else to-val)]
+                     (.setProperty (.-style js/document.documentElement)
+                                   "--ls-right-sidebar-width"
+                                   (str (* to-val 100) "%"))))}}))
+             (.styleCursor false)
+             (.on "dragstart" #(.. js/document.documentElement -classList (add "is-resizing-buf")))
+             (.on "dragend" #(.. js/document.documentElement -classList (remove "is-resizing-buf")))))
+       #())
+     [])
+    [:span.resizer {:ref el-ref}]))
+
 (rum/defcs sidebar < rum/reactive
   [state]
   (let [blocks (state/sub :sidebar/blocks)
+        blocks (if (empty? blocks)
+                 [[(state/get-current-repo) "contents" :contents nil]]
+                 blocks)
         sidebar-open? (state/sub :ui/sidebar-open?)
         repo (state/sub :git/current-repo)
         match (state/sub :route-match)
@@ -219,32 +253,36 @@
        {:class (if sidebar-open? "is-open")}
        (if sidebar-open?
          [:div.cp__right-sidebar-inner
-          [: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))}
-             (t :right-side-bar/contents)]]
-
-           [:div.ml-4.text-sm
-            [:a.cp__right-sidebar-settings-btn {:on-click (fn [_e]
-                                                            (state/sidebar-add-block! repo "recent" :recent nil))}
-
-             (t :right-side-bar/recent)]]
-
-           [:div.ml-4.text-sm
-            [:a.cp__right-sidebar-settings-btn {:on-click (fn []
-                                                            (when-let [page (get-current-page)]
-                                                              (state/sidebar-add-block!
-                                                               repo
-                                                               (str "page-graph-" page)
-                                                               :page-graph
-                                                               page)))}
-             (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))}
-             (t :right-side-bar/help)]]]
+          (sidebar-resizer)
+          [:div.flex.flex-row.justify-between.items-center
+           [: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))}
+              (t :right-side-bar/contents)]]
+
+            [:div.ml-4.text-sm
+             [:a.cp__right-sidebar-settings-btn {:on-click (fn [_e]
+                                                             (state/sidebar-add-block! repo "recent" :recent nil))}
+
+              (t :right-side-bar/recent)]]
+
+            [:div.ml-4.text-sm
+             [:a.cp__right-sidebar-settings-btn {:on-click (fn []
+                                                             (when-let [page (get-current-page)]
+                                                               (state/sidebar-add-block!
+                                                                repo
+                                                                (str "page-graph-" page)
+                                                                :page-graph
+                                                                page)))}
+              (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))}
+              (t :right-side-bar/help)]]]
+           [:a.close-arrow.opacity-50.hover:opacity-100 {:on-click state/toggle-sidebar-open?!}
+            (svg/big-arrow-right)]]
 
           (for [[idx [repo db-id block-type block-data]] (medley/indexed blocks)]
             (rum/with-key

+ 72 - 20
src/main/frontend/components/search.cljs

@@ -31,6 +31,41 @@
   (let [switch (reductions not= true (map pred? coll (rest coll)))]
     (map (partial map first) (partition-by second (map list coll switch)))))
 
+(defn highlight-exact-query
+  [content q]
+  (let [q-words (string/split q #" ")
+        lc-content (string/lower-case content)
+        lc-q (string/lower-case q)]
+    (if (or (string/includes? lc-content lc-q)
+            (not (re-find #" " q)))
+      (let [i (string/index-of lc-content lc-q)
+            [before after] [(subs content 0 i) (subs content (+ i (count q)))]]
+        [:p
+         (when-not (string/blank? before)
+           [:span before])
+         [:mark (subs content i (+ i (count q)))]
+         (when-not (string/blank? after)
+           [:span after])])
+      (let [elements (loop [words q-words
+                            content content
+                            result []]
+                       (if (and (seq words) content)
+                         (let [word (first words)
+                               lc-word (string/lower-case word)
+                               lc-content (string/lower-case content)]
+                           (if-let [i (string/index-of lc-content lc-word)]
+                             (recur (rest words)
+                                    (subs content (+ i (count word)))
+                                    (vec
+                                     (concat result
+                                             [[:span (subs content 0 i)]
+                                              [:mark (subs content i (+ i (count word)))]])))
+                             (recur nil
+                                    content
+                                    result)))
+                         (conj result [:span content])))]
+        [:p elements]))))
+
 (rum/defc highlight-fuzzy
   [content indexes]
   (let [n (count content)
@@ -86,19 +121,27 @@
 
 (defn- leave-focus
   []
-  (when-let [input (gdom/getElement "search_field")]
+  (when-let [input (gdom/getElement "search-field")]
     (.blur input)))
 
+(defonce search-timeout (atom nil))
+
 (rum/defc search-auto-complete
-  [{:keys [pages files blocks]} search-q]
+  [{:keys [pages files blocks] :as result} search-q]
   (rum/with-context [[t] i18n/*tongue-context*]
-    (let [new-page [{:type :new-page}]
-          new-file (when-let [ext (util/get-file-ext search-q)]
+    (let [new-file (when-let [ext (util/get-file-ext search-q)]
                      (when (contains? config/mldoc-support-formats (keyword (string/lower-case ext)))
                        [{:type :new-file}]))
           pages (map (fn [page] {:type :page :data page}) pages)
           files (map (fn [file] {:type :file :data file}) files)
           blocks (map (fn [block] {:type :block :data block}) blocks)
+          new-page (if (or
+                        (and (seq pages)
+                             (= (string/lower-case search-q)
+                                (string/lower-case (:data (first pages)))))
+                        (nil? result))
+                     []
+                     [{:type :new-page}])
           result (if config/publishing?
                    (concat pages files blocks)
                    (concat new-page pages new-file files blocks))]
@@ -109,7 +152,8 @@
                  :width 500})}
        (ui/auto-complete
         result
-        {:on-chosen (fn [{:keys [type data]}]
+        {:class "search-results"
+         :on-chosen (fn [{:keys [type data]}]
                       (search-handler/clear-search!)
                       (leave-focus)
                       (case type
@@ -129,9 +173,10 @@
 
                         :block
                         (let [block-uuid (uuid (:block/uuid data))
-                              page (:page/name (:block/page (db/entity [:block/uuid block-uuid])))
-                              path (str "/page/" (util/encode-str page) "#ls-block-" (:block/uuid data))]
-                          (route/redirect-with-fragment! path))
+                              page (:page/name (:block/page (db/entity [:block/uuid block-uuid])))]
+                          (route/redirect! {:to :page
+                                            :path-params {:name page}
+                                            :query-params {:anchor (str "ls-block-" (:block/uuid data))}}))
                         nil))
          :on-shift-chosen (fn [{:keys [type data]}]
                             (case type
@@ -181,11 +226,12 @@
                                          (:page/name page))]
                             [:div.flex-1
                              [:div.text-sm.font-medium (str "-> " page)]
-                             (highlight-fuzzy content indexes)])
+                             (highlight-exact-query content search-q)])
 
                           nil))})])))
 
-(rum/defc search < rum/reactive
+(rum/defcs search < rum/reactive
+  (rum/local false ::inside-box?)
   (mixins/event-mixin
    (fn [state]
      (mixins/hide-when-esc-or-outside
@@ -193,36 +239,42 @@
       :on-hide (fn []
                  (search-handler/clear-search!)
                  (leave-focus)))))
-  []
+  [state]
   (let [search-result (state/sub :search/result)
         search-q (state/sub :search/q)
-        show-result? (boolean (seq search-result))]
+        show-result? (boolean (seq search-result))
+        blocks-count (or (db/blocks-count) 0)
+        timeout (if (> blocks-count 2000) 500 100)]
     (rum/with-context [[t] i18n/*tongue-context*]
       [:div#search.flex-1.flex
-       [:div.flex.md:ml-0
-        [:label.sr-only {:for "search_field"} (t :search)]
+       [:div.inner
+        [:label.sr-only {:for "search-field"} (t :search)]
         [:div#search-wrapper.relative.w-full.text-gray-400.focus-within:text-gray-600
-         [:div.absolute.inset-y-0.flex.items-center.pointer-events-none.left-0
+         [:div.absolute.inset-y-0.flex.items-center.pointer-events-none {:style {:left 6}}
           [:svg.h-5.w-5
            {:view-box "0 0 20 20", :fill "currentColor"}
            [:path
             {:d
-             "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z",
-             :clip-rule "evenodd",
+             "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
+             :clip-rule "evenodd"
              :fill-rule "evenodd"}]]]
-         [:input#search_field.block.w-full.h-full.pr-3.py-2.rounded-md.focus:outline-none.placeholder-gray-500.focus:placeholder-gray-400.sm:text-sm.sm:bg-transparent
-
+         [:input#search-field.block.w-full.h-full.pr-3.py-2.rounded-md.focus:outline-none.placeholder-gray-500.focus:placeholder-gray-400.sm:text-sm.sm:bg-transparent
           {:style {:padding-left "2rem"}
            :placeholder (t :search)
            :auto-complete (if (util/chrome?) "chrome-off" "off") ; off not working here
            :default-value ""
            :on-change (fn [e]
+                        (when @search-timeout
+                          (js/clearTimeout @search-timeout))
                         (let [value (util/evalue e)]
                           (if (string/blank? value)
                             (search-handler/clear-search!)
                             (do
                               (state/set-q! value)
-                              (search-handler/search value)))))}]
+                              (reset! search-timeout
+                                      (js/setTimeout
+                                       #(search-handler/search value)
+                                       timeout))))))}]
          (when-not (string/blank? search-q)
            (ui/css-transition
             {:class-names "fade"

+ 38 - 7
src/main/frontend/components/search.css

@@ -1,16 +1,47 @@
-#search p {
-  margin: 0;
+#search {
+  > .inner {
+    width: 100%;
+  }
 }
 
 #search-wrapper svg {
-  color: var(--ls-search-icon-color, #9fa6b2);
+    color: var(--ls-search-icon-color, #9fa6b2);
+    opacity: 0.3;
+    transition: .3s;
 }
 
-#search-wrapper:focus-within svg {
-  color: var(--ls-link-text-hover-color, #4b5563);
+#search-wrapper:hover svg, #search-wrapper:focus-within svg {
+    color: var(--ls-link-text-hover-color, #4b5563);
+    opacity: 0.8;
 }
 
-#search_field {
+#search-field {
   background-color: var(--ls-search-background-color, #fff);
   color: var(--ls-secondary-text-color, #161e2e);
-}
+  transition: background .3s;
+  max-width: 545px;
+  opacity: 0;
+}
+
+#search-wrapper {
+    transition: .3s;
+    padding-right: 12px;
+}
+
+#search-field:hover,
+#search-field:focus-within {
+    opacity: 1;
+}
+
+#search>.inner {
+    max-width: 100%;
+    border-radius: 4px;
+}
+
+#search-field:focus {
+    background: var(--ls-search-background-color);
+}
+
+.dark-theme #search-field:focus {
+    box-shadow: 0px 0px 20px 0px rgba(18, 18, 18, .3);
+}

+ 281 - 190
src/main/frontend/components/settings.cljs

@@ -1,6 +1,7 @@
 (ns frontend.components.settings
   (:require [rum.core :as rum]
             [frontend.ui :as ui]
+            [frontend.components.svg :as svg]
             [frontend.handler.notification :as notification]
             [frontend.handler.user :as user-handler]
             [frontend.handler.ui :as ui-handler]
@@ -8,12 +9,14 @@
             [frontend.handler.config :as config-handler]
             [frontend.handler.page :as page-handler]
             [frontend.state :as state]
+            [frontend.version :refer [version]]
             [frontend.util :as util]
             [frontend.config :as config]
             [frontend.dicts :as dicts]
             [clojure.string :as string]
             [goog.object :as gobj]
-            [frontend.context.i18n :as i18n]))
+            [frontend.context.i18n :as i18n]
+            [reitit.frontend.easy :as rfe]))
 
 (rum/defcs set-email < (rum/local "" ::email)
   [state]
@@ -24,8 +27,8 @@
        [:div
         [:h1.title.mb-1
          "Your email address:"]
-        [:div.mt-2.mb-4.relative.rounded-md.shadow-sm.max-w-xs
-         [:input#.form-input.block.w-full.pl-2.sm:text-sm.sm:leading-5
+        [:div.mt-2.mb-4.relative.rounded-md.max-w-xs
+         [:input#.form-input.is-small
           {:autoFocus true
            :on-change (fn [e]
                         (reset! email (util/evalue e)))}]]]]
@@ -48,8 +51,8 @@
        [:div
         [:h1.title.mb-1
          "Your cors address:"]
-        [:div.mt-2.mb-4.relative.rounded-md.shadow-sm.max-w-xs
-         [:input#.form-input.block.w-full.pl-2.sm:text-sm.sm:leading-5
+        [:div.mt-2.mb-4.relative.rounded-md.max-w-xs
+         [:input#.form-input.is-small
           {:autoFocus true
            :on-change (fn [e]
                         (reset! cors (util/evalue e)))}]]]]
@@ -65,22 +68,80 @@
 
 (defn toggle
   [label-for name state on-toggle]
-  [:div.mt-6.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start.sm:pt-5
+  [:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start
    [:label.block.text-sm.font-medium.leading-5.opacity-70
     {:for label-for}
     name]
    [:div.mt-1.sm:mt-0.sm:col-span-2
-    [:div.max-w-lg.rounded-md.sm:max-w-xs
-     (ui/toggle state on-toggle)]]])
+    [:div.rounded-md.sm:max-w-xs
+     (ui/toggle state on-toggle true)]]])
+
+(rum/defcs app-updater < rum/reactive
+  [state]
+  (let [update-pending? (state/sub :electron/updater-pending?)
+        {:keys [type payload]} (state/sub :electron/updater)]
+    [:div.cp__settings-app-updater
+     (ui/button
+      (if update-pending? "Checking ..." "Check for updates")
+
+      :intent "logseq"
+      :class "check-update"
+      :disabled update-pending?
+      :on-click #(js/window.apis.checkForUpdates false))
+     (when-not (or update-pending?
+                   (string/blank? type))
+       [:div.update-state
+        (case type
+          "update-not-available"
+          [:p "😀 Your app is up-to-date!"]
+
+          "update-available"
+          (let [{:keys [name url]} payload]
+            [:p (str "Found new release ")
+             [:a.link
+              {:on-click
+               (fn [e]
+                 (js/window.apis.openExternal url)
+                 (util/stop e))}
+              svg/external-link name " 🎉"]])
+
+          "error"
+          [:p "⚠️ Oops, Something Went Wrong!" [:br] " Please check out the "
+           [:a.link
+            {:on-click
+             (fn [e]
+               (js/window.apis.openExternal "https://github.com/logseq/logseq/releases")
+               (util/stop e))}
+            svg/external-link " release channel"]])])]))
+
+(rum/defc delete-account-confirm
+  [close-fn]
+  (rum/with-context [[t] i18n/*tongue-context*]
+    [:div
+     (ui/admonition
+      :important
+      [:p.text-gray-700 (t :user/delete-account-notice)])
+     [:div.mt-5.sm:mt-4.sm:flex.sm:flex-row-reverse
+      [:span.flex.w-full.rounded-md.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 user-handler/delete-account!}
+        (t :user/delete-account)]]
+      [:span.mt-3.flex.w-full.rounded-md.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}
+        "Cancel"]]]]))
 
 (rum/defcs settings < rum/reactive
   []
   (let [preferred-format (state/get-preferred-format)
-        preferred-workflow (keyword (state/sub [:me :preferred_workflow]))
+        preferred-workflow (state/get-preferred-workflow)
         preferred-language (state/sub [:preferred-language])
         enable-timetracking? (state/enable-timetracking?)
         current-repo (state/get-current-repo)
         enable-journals? (state/enable-journals? current-repo)
+        enable-encryption? (state/enable-encryption? current-repo)
         enable-git-auto-push? (state/enable-git-auto-push? current-repo)
         enable-block-time? (state/enable-block-time?)
         show-brackets? (state/show-brackets?)
@@ -92,197 +153,227 @@
         dark? (= "dark" theme)
         switch-theme (if dark? "white" "dark")]
     (rum/with-context [[t] i18n/*tongue-context*]
-      [:div#settings
+      [:div#settings.cp__settings-main
        [:h1.title (t :settings)]
 
-       [:div.mb-1.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start.sm:pt-5.pl-1
-        [:label.block.text-sm.font-medium.leading-5.sm:mt-px.sm:pt-2.opacity-70
-         {:for "toggle_theme"}
-         (t :right-side-bar/switch-theme (string/capitalize switch-theme))]
-        [:div.flex.flex-row.mt-1.sm:mt-0.sm:col-span-2.pt-2
-         [:div.max-w-lg.rounded-md.sm:max-w-xs
-          (ui/toggle dark?
-                     (fn []
-                       (state/set-theme! switch-theme)))]
-         [:span.ml-4.opacity-50 "t t"]]]
-
-       [:div.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start.sm:pt-5.pl-1
-        [:label.block.text-sm.font-medium.leading-5.opacity-70
-         {:for "show_brackets"}
-         (t :settings-page/show-brackets)]
-        [:div.flex.flex-row.mt-1.sm:mt-0.sm:col-span-2
-         [:div.max-w-lg.rounded-md.sm:max-w-xs
-          (ui/toggle show-brackets?
-                     config-handler/toggle-ui-show-brackets!)]
-         [:span.ml-4.opacity-50 "Ctrl-c Ctrl-b"]]]
-
-       [:div.mb-6.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start.sm:pt-5.pl-1
-        [:label.block.text-sm.font-medium.leading-5.sm:mt-px.sm:pt-2.opacity-70
-         {:for "preferred_language"}
-         (t :language)]
-        [:div.mt-1.sm:mt-0.sm:col-span-2
-         [:div.max-w-lg.rounded-md.shadow-sm.sm:max-w-xs
-          [:select.mt-1.form-select.block.w-full.pl-3.pr-10.py-2.text-base.leading-6.border-gray-300.focus:outline-none.focus:shadow-outline-blue.focus:border-blue-300.sm:text-sm.sm:leading-5
-           {:on-change (fn [e]
-                         (let [lang (util/evalue e)
-                               lang-val (filter (fn [el] (if (= (:label el) lang) true nil)) dicts/languages)
-                               lang-val (name (:value (first lang-val)))]
-                           (state/set-preferred-language! lang-val)
-                           (ui-handler/re-render-root!)))}
-           (for [language dicts/languages]
-             [:option (cond->
-                       {:key (:value language)}
-                        (= (name (:value language)) preferred-language)
-                        (assoc :selected "selected"))
-              (:label language)])]]]]
-
-       [:div.pl-1
-        ;; config.edn
+       [:div.panel-wrap
+        [:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start
+         [:label.block.text-sm.font-medium.leading-5.opacity-70
+          {:for "toggle_theme"}
+          (t :right-side-bar/switch-theme (string/capitalize switch-theme))]
+         [:div.flex.flex-row.mt-1.sm:mt-0.sm:col-span-2
+          [:div.rounded-md.sm:max-w-xs
+           (ui/toggle dark?
+                      (fn []
+                        (state/set-theme! switch-theme))
+                      true)]
+          [:span.ml-4.opacity-50.text-sm "t t"]]]
+
+        [:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start
+         [:label.block.text-sm.font-medium.leading-5.opacity-70
+          {:for "show_brackets"}
+          (t :settings-page/show-brackets)]
+         [:div.flex.flex-row.mt-1.sm:mt-0.sm:col-span-2
+          [:div.rounded-md.sm:max-w-xs
+           (ui/toggle show-brackets?
+                      config-handler/toggle-ui-show-brackets!
+                      true)]
+          [:span.ml-4.opacity-50.text-sm "Ctrl-c Ctrl-b"]]]
+
+        [:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start
+         [:label.block.text-sm.font-medium.leading-5.opacity-70
+          {:for "preferred_language"}
+          (t :language)]
+         [:div.mt-1.sm:mt-0.sm:col-span-2
+          [:div.max-w-lg.rounded-md
+           [:select.form-select.is-small
+            {:on-change (fn [e]
+                          (let [lang (util/evalue e)
+                                lang-val (filter (fn [el] (if (= (:label el) lang) true nil)) dicts/languages)
+                                lang-val (name (:value (first lang-val)))]
+                            (state/set-preferred-language! lang-val)
+                            (ui-handler/re-render-root!)))}
+            (for [language dicts/languages]
+              [:option (cond->
+                        {:key (:value language)}
+                         (= (name (:value language)) preferred-language)
+                         (assoc :selected "selected"))
+               (:label language)])]]]]
+
+                        ;; config.edn
         (when current-repo
-          [:a {:href (str "/file/" (util/url-encode (str config/app-name "/" config/config-file)))}
-           (t :settings-page/edit-config-edn)])
-
-        [:hr]
-
-        [:div.mt-6.sm:mt-5
-         [:div.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start.sm:pt-5
-          [:label.block.text-sm.font-medium.leading-5.sm:mt-px.sm:pt-2.opacity-70
-           {:for "preferred_format"}
-           (t :settings-page/preferred-file-format)]
-          [:div.mt-1.sm:mt-0.sm:col-span-2
-           [:div.max-w-lg.rounded-md.shadow-sm.sm:max-w-xs
-            [:select.mt-1.form-select.block.w-full.pl-3.pr-10.py-2.text-base.leading-6.border-gray-300.focus:outline-none.focus:shadow-outline-blue.focus:border-blue-300.sm:text-sm.sm:leading-5
-             {:on-change (fn [e]
-                           (let [format (-> (util/evalue e)
-                                            (string/lower-case)
-                                            keyword)]
-                             (user-handler/set-preferred-format! format)))}
-             (for [format [:org :markdown]]
-               [:option (cond->
-                         {:key (name format)}
-                          (= format preferred-format)
-                          (assoc :selected "selected"))
-                (string/capitalize (name format))])]]]]
-         [:div.mt-6.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start.sm:pt-5
-          [:label.block.text-sm.font-medium.leading-5.sm:mt-px.sm:pt-2.opacity-70
-           {:for "preferred_workflow"}
-           (t :settings-page/preferred-workflow)]
-          [:div.mt-1.sm:mt-0.sm:col-span-2
-           [:div.max-w-lg.rounded-md.shadow-sm.sm:max-w-xs
-            [:select.mt-1.form-select.block.w-full.pl-3.pr-10.py-2.text-base.leading-6.border-gray-300.focus:outline-none.focus:shadow-outline-blue.focus:border-blue-300.sm:text-sm.sm:leading-5
-             {:on-change (fn [e]
-                           (let [workflow (-> (util/evalue e)
-                                              (string/lower-case)
-                                              keyword)
-                                 workflow (if (= workflow :now/later)
-                                            :now
-                                            :todo)]
-                             (user-handler/set-preferred-workflow! workflow)))}
-             (for [workflow [:now :todo]]
-               [:option (cond->
-                         {:key (name workflow)}
-                          (= workflow preferred-workflow)
-                          (assoc :selected "selected"))
-                (if (= workflow :now)
-                  "NOW/LATER"
-                  "TODO/DOING")])]]]]
-
-         (toggle "enable_timetracking"
-                 (t :settings-page/enable-timetracking)
-                 enable-timetracking?
-                 (fn []
-                   (let [value (not enable-timetracking?)]
-                     (config-handler/set-config! :feature/enable-timetracking? value))))
-
-         ;; (toggle "enable_block_time"
-         ;;         (t :settings-page/enable-block-time)
-         ;;         enable-block-time?
-         ;;         (fn []
-         ;;           (let [value (not enable-block-time?)]
-         ;;             (config-handler/set-config! :feature/enable-block-time? value))))
-
-         (toggle "enable_journals"
-                 (t :settings-page/enable-journals)
-                 enable-journals?
-                 (fn []
-                   (let [value (not enable-journals?)]
-                     (config-handler/set-config! :feature/enable-journals? value))))
-
-         (when (not enable-journals?)
-           [:div.mt-6.sm:mt-5.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start.sm:pt-5
-             [:label.block.text-sm.font-medium.leading-5.sm:mt-px.sm:pt-2.opacity-70
-              {:for "default page"}
-              (t :settings-page/home-default-page)]
-             [:div.mt-1.sm:mt-0.sm:col-span-2
-              [:div.max-w-lg.rounded-md.shadow-sm.sm:max-w-xs
-               [:input#home-default-page.form-input.block.w-full.transition.duration-150.ease-in-out.sm:text-sm.sm:leading-5
-                {:default-value (state/sub-default-home-page)
-                 :on-blur (fn [event]
-                            (let [value (util/evalue event)]
-                              (cond
-                                (string/blank? value)
-                                (let [home (get (state/get-config) :default-home {})
-                                      new-home (dissoc home :page)]
-                                  (config-handler/set-config! :default-home new-home)
-                                  (notification/show! "Home default page updated successfully!" :success))
-
-                                (page-handler/page-exists? (string/lower-case value))
-                                (let [home (get (state/get-config) :default-home {})
-                                      new-home (assoc home :page value)]
-                                  (config-handler/set-config! :default-home new-home)
-                                  (notification/show! "Home default page updated successfully!" :success))
-
-                                :else
-                                (notification/show! "Please make sure the page exists!" :warning))))}]]]])
-
-         (when (string/starts-with? current-repo "https://")
-           (toggle "enable_git_auto_push"
+          [:div.mt-5.text-sm
+           [:a {:href     (rfe/href :file {:path (config/get-config-path)})
+                :on-click #(js/setTimeout (fn [] (ui-handler/toggle-settings-modal!)))}
+            (t :settings-page/edit-config-edn)]])]
+
+       [:hr]
+
+       [:div.panel-wrap
+        [:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start
+         [:label.block.text-sm.font-medium.leading-5.opacity-70
+          {:for "preferred_format"}
+          (t :settings-page/preferred-file-format)]
+         [:div.mt-1.sm:mt-0.sm:col-span-2
+          [:div.max-w-lg.rounded-md
+           [:select.form-select.is-small
+            {:on-change (fn [e]
+                          (let [format (-> (util/evalue e)
+                                           (string/lower-case)
+                                           keyword)]
+                            (user-handler/set-preferred-format! format)))}
+            (for [format [:org :markdown]]
+              [:option (cond->
+                        {:key (name format)}
+                         (= format preferred-format)
+                         (assoc :selected "selected"))
+               (string/capitalize (name format))])]]]]
+
+        [:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start
+         [:label.block.text-sm.font-medium.leading-5.opacity-70
+          {:for "preferred_workflow"}
+          (t :settings-page/preferred-workflow)]
+         [:div.mt-1.sm:mt-0.sm:col-span-2
+          [:div.max-w-lg.rounded-md
+           [:select.form-select.is-small
+            {:on-change (fn [e]
+                          (-> (util/evalue e)
+                              string/lower-case
+                              keyword
+                              (#(if (= % :now/later) :now :todo))
+                              user-handler/set-preferred-workflow!))}
+            (for [workflow [:now :todo]]
+              [:option (cond->
+                        {:key (name workflow)}
+                         (= workflow preferred-workflow)
+                         (assoc :selected "selected"))
+               (if (= workflow :now)
+                 "NOW/LATER"
+                 "TODO/DOING")])]]]]
+
+        (toggle "enable_timetracking"
+                (t :settings-page/enable-timetracking)
+                enable-timetracking?
+                (fn []
+                  (let [value (not enable-timetracking?)]
+                    (config-handler/set-config! :feature/enable-timetracking? value))))
+
+                        ;; (toggle "enable_block_time"
+                        ;;         (t :settings-page/enable-block-time)
+                        ;;         enable-block-time?
+                        ;;         (fn []
+                        ;;           (let [value (not enable-block-time?)]
+                        ;;             (config-handler/set-config! :feature/enable-block-time? value))))
+
+        (toggle "enable_journals"
+                (t :settings-page/enable-journals)
+                enable-journals?
+                (fn []
+                  (let [value (not enable-journals?)]
+                    (config-handler/set-config! :feature/enable-journals? value))))
+
+        (when (not enable-journals?)
+          [:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start
+           [:label.block.text-sm.font-medium.leading-5.opacity-70
+            {:for "default page"}
+            (t :settings-page/home-default-page)]
+           [:div.mt-1.sm:mt-0.sm:col-span-2
+            [:div.max-w-lg.rounded-md.sm:max-w-xs
+             [:input#home-default-page.form-input.is-small.transition.duration-150.ease-in-out
+              {:default-value (state/sub-default-home-page)
+               :on-blur       (fn [event]
+                                (let [value (util/evalue event)]
+                                  (cond
+                                    (string/blank? value)
+                                    (let [home (get (state/get-config) :default-home {})
+                                          new-home (dissoc home :page)]
+                                      (config-handler/set-config! :default-home new-home)
+                                      (notification/show! "Home default page updated successfully!" :success))
+
+                                    (page-handler/page-exists? (string/lower-case value))
+                                    (let [home (get (state/get-config) :default-home {})
+                                          new-home (assoc home :page value)]
+                                      (config-handler/set-config! :default-home new-home)
+                                      (notification/show! "Home default page updated successfully!" :success))
+
+                                    :else
+                                    (notification/show! "Please make sure the page exists!" :warning))))}]]]])
+
+        (toggle "enable_encryption"
+                (t :settings-page/enable-encryption)
+                enable-encryption?
+                (fn []
+                  (let [value (not enable-encryption?)]
+                    (config-handler/set-config! :feature/enable-encryption? value))))
+
+        (when (string/starts-with? current-repo "https://")
+          (toggle "enable_git_auto_push"
                   "Enable Git auto push"
                   enable-git-auto-push?
                   (fn []
                     (let [value (not enable-git-auto-push?)]
-                      (config-handler/set-config! :git-auto-push value)))))
-
-
-         [:hr]
-
-         (when logged?
-           [:div
-            (ui/admonition
-             :important
-             [:p (t :settings-page/dont-use-other-peoples-proxy-servers)
-              [:a {:href "https://github.com/isomorphic-git/cors-proxy"
-                   :target "_blank"}
-               "https://github.com/isomorphic-git/cors-proxy"]])
-            [:div.mt-6.sm:mt-5.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start.sm:pt-5
-             [:label.block.text-sm.font-medium.leading-5.sm:mt-px.sm:pt-2.opacity-70
-              {:for "cors"}
-              (t :settings-page/custom-cors-proxy-server)]
-             [:div.mt-1.sm:mt-0.sm:col-span-2
-              [:div.max-w-lg.rounded-md.shadow-sm.sm:max-w-xs
-               [:input#pat.form-input.block.w-full.transition.duration-150.ease-in-out.sm:text-sm.sm:leading-5
-                {:default-value cors-proxy
-                 :on-blur (fn [event]
-                            (when-let [server (util/evalue event)]
-                              (user-handler/set-cors! server)
-                              (notification/show! "Custom CORS proxy updated successfully!" :success)))
-                 :on-key-press (fn [event]
+                      (config-handler/set-config! :git-auto-push value)))))]
+
+       [:hr]
+
+       [:div.panel-wrap
+        [:div.it.app-updater.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start
+         [:label.block.text-sm.font-medium.leading-5.opacity-70
+          (t :settings-page/current-version)]
+         [:div.wrap.sm:mt-0.sm:col-span-2
+          [:div.ver version]
+          (if (util/electron?) (app-updater))]]
+
+        [:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start
+         [:label.block.text-sm.font-medium.leading-5.opacity-70
+          {:for "developer_mode"}
+          (t :settings-page/developer-mode)]
+
+         [:div.mt-1.sm:mt-0.sm:col-span-2
+          [:div.rounded-md.sm:max-w-xs
+           (ui/toggle developer-mode?
+                      #(state/set-developer-mode! (not developer-mode?))
+                      true)]]]
+        [:div.text-sm.opacity-50
+         (t :settings-page/developer-mode-desc)]
+
+        (when logged?
+          [:div
+           [:div.mt-6.sm:mt-5.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-center.sm:pt-5
+            [:label.block.text-sm.font-medium.leading-5.sm:mt-px..opacity-70
+             {:for "cors"}
+             (t :settings-page/custom-cors-proxy-server)]
+            [:div.mt-1.sm:mt-0.sm:col-span-2
+             [:div.max-w-lg.rounded-md.sm:max-w-xs
+              [:input#pat.form-input.is-small.transition.duration-150.ease-in-out
+               {:default-value cors-proxy
+                :on-blur       (fn [event]
+                                 (when-let [server (util/evalue event)]
+                                   (user-handler/set-cors! server)
+                                   (notification/show! "Custom CORS proxy updated successfully!" :success)))
+                :on-key-press  (fn [event]
                                  (let [k (gobj/get event "key")]
                                    (if (= "Enter" k)
                                      (when-let [server (util/evalue event)]
                                        (user-handler/set-cors! server)
                                        (notification/show! "Custom CORS proxy updated successfully!" :success)))))}]]]]
+           (ui/admonition
+            :important
+            [:p (t :settings-page/dont-use-other-peoples-proxy-servers)
+             [:a {:href   "https://github.com/isomorphic-git/cors-proxy"
+                  :target "_blank"}
+              "https://github.com/isomorphic-git/cors-proxy"]])])
 
-            [:hr]])
-
-         [:div.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start.sm:pt-5
-          [:label.block.text-sm.font-medium.leading-5.sm:mt-px.sm:pt-2.opacity-70
-           {:for "developer_mode"}
-           (t :settings-page/developer-mode)]
-          [:div.mt-1.sm:mt-0.sm:col-span-2
-           [:div.max-w-lg.rounded-md.shadow-sm.sm:max-w-xs
-            (ui/button (if developer-mode? (t :settings-page/disable-developer-mode) (t :settings-page/enable-developer-mode))
-                       :on-click #(state/set-developer-mode! (not developer-mode?)))]]]
-
-         [:br]
-         (t :settings-page/developer-mode-desc)]]])))
+        (when logged?
+          [:div
+           [:hr]
+           [:div.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-center.sm:pt-5
+            [:label.block.text-sm.font-medium.leading-5.opacity-70.text-red-600.dark:text-red-400
+             {:for "delete account"}
+             (t :user/delete-account)]
+            [:div.mt-1.sm:mt-0.sm:col-span-2
+             [:div.max-w-lg.rounded-md.sm:max-w-xs
+              (ui/button (t :user/delete-your-account)
+                         :on-click (fn []
+                                     (ui-handler/toggle-settings-modal!)
+                                     (js/setTimeout #(state/set-modal! delete-account-confirm))))]]]])]])))

+ 105 - 0
src/main/frontend/components/settings.css

@@ -0,0 +1,105 @@
+.cp__settings {
+  &-main {
+    > h1.title {
+      margin-bottom: 2rem;
+    }
+
+    hr {
+      margin: 1rem 0;
+      opacity: .5;
+    }
+
+    .panel-wrap {
+      padding: 0 12px;
+
+      > .it {
+        margin-bottom: 0;
+        padding-bottom: 20px;
+        align-items: center;
+
+        label {
+          display: flex;
+          align-items: center;
+
+          & + div {
+            display: flex;
+            align-items: center;
+            min-height: 24px;
+            user-select: none;
+
+            .max-w-lg {
+              width: 100%;
+            }
+          }
+        }
+
+        &.app-updater {
+          padding-top: 15px;
+          align-items: start;
+
+          > .wrap {
+            display: block;
+
+            .ver {
+              position: relative;
+              top: -2px;
+            }
+          }
+        }
+
+        .form-select, .form-input {
+          width: 68%;
+        }
+      }
+    }
+
+    .admonitionblock {
+      p {
+        @apply text-sm;
+      }
+    }
+  }
+
+  &-app-updater {
+    min-height: 20px;
+    position: relative;
+    margin-bottom: -5px;
+
+    button.check-update {
+      position: absolute;
+      right: 0;
+      top: -42px;
+
+      &:disabled {
+        cursor: progress;
+      }
+    }
+
+    .update-state {
+      padding: 15px;
+      background-color: var(--ls-secondary-background-color);
+      border-radius: 4px;
+
+      > p {
+        margin: 0;
+      }
+
+      .link {
+        font-size: 16px;
+        line-height: 1em;
+        letter-spacing: 1px;
+
+        svg {
+          display: inline-block;
+          position: relative;
+          top: -1px;
+          margin-right: 2px;
+        }
+
+        &:hover {
+          text-decoration: underline;
+        }
+      }
+    }
+  }
+}

+ 34 - 22
src/main/frontend/components/sidebar.cljs

@@ -17,6 +17,7 @@
             [frontend.storage :as storage]
             [frontend.util :as util]
             [frontend.state :as state]
+            [frontend.handler.ui :as ui-handler]
             [frontend.handler.user :as user-handler]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.route :as route-handler]
@@ -55,16 +56,16 @@
         left-sidebar? (state/sub :ui/left-sidebar-open?)]
     (when left-sidebar?
       [:nav.flex-1.left-sidebar-inner
-       (nav-item "Journals" "/"
+       (nav-item "Journals" "#/"
                  "M3 12l9-9 9 9M5 10v10a1 1 0 001 1h3a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1h3a1 1 0 001-1V10M9 21h6"
                  (active? :home)
                  close-modal-fn)
-       (nav-item "All Pages" "/all-pages"
+       (nav-item "All Pages" "#/all-pages"
                  "M6 2h9a1 1 0 0 1 .7.3l4 4a1 1 0 0 1 .3.7v13a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4c0-1.1.9-2 2-2zm9 2.41V7h2.59L15 4.41zM18 9h-3a2 2 0 0 1-2-2V4H6v16h12V9zm-2 7a1 1 0 0 1-1 1H9a1 1 0 0 1 0-2h6a1 1 0 0 1 1 1zm0-4a1 1 0 0 1-1 1H9a1 1 0 0 1 0-2h6a1 1 0 0 1 1 1zm-5-4a1 1 0 0 1-1 1H9a1 1 0 1 1 0-2h1a1 1 0 0 1 1 1z"
                  (active? :all-pages)
                  close-modal-fn)
        (when-not config/publishing?
-         (nav-item "All Files" "/all-files"
+         (nav-item "All Files" "#/all-files"
                    "M3 7V17C3 18.1046 3.89543 19 5 19H19C20.1046 19 21 18.1046 21 17V9C21 7.89543 20.1046 7 19 7H13L11 5H5C3.89543 5 3 5.89543 3 7Z"
                    (active? :all-files)
                    close-modal-fn))
@@ -130,7 +131,7 @@
            (ui/loading (t :loading))]]
 
          :else
-         [:div {:style {:margin-bottom (if global-graph-pages? 0 120)}}
+         [:div.max-w-7xl.mx-auto {:style {:margin-bottom (if global-graph-pages? 0 120)}}
           main-content])]]
      (right-sidebar/sidebar)]))
 
@@ -175,7 +176,7 @@
         preferred-format (state/sub [:me :preferred_format])
         logged? (:name me)]
     (rum/with-context [[t] i18n/*tongue-context*]
-      [:div.max-w-7xl.mx-auto
+      [:div
        (cond
          (and default-home
               (= :home (state/get-current-route))
@@ -243,6 +244,21 @@
                     (state/sidebar-add-block! (state/get-current-repo) "help" :help nil))}
        "?"])))
 
+(rum/defc settings-modal
+  [settings-open?]
+  (rum/use-effect!
+   (fn []
+     (if settings-open?
+       (state/set-modal!
+        (fn [close-fn]
+          (gobj/set close-fn "user-close" #(ui-handler/toggle-settings-modal!))
+          [:div.settings-modal (settings/settings)]))
+       (state/set-modal! nil))
+
+     (util/lock-global-scroll settings-open?)
+     #())
+   [settings-open?]) nil)
+
 (rum/defcs sidebar <
   (mixins/modal :modal/show?)
   rum/reactive
@@ -258,6 +274,7 @@
                         (editor-handler/clear-selection! e)
                         (state/set-selection-start-block! nil))))
 
+     ;; TODO: move to keyboards
      (mixins/on-key-down
       state
       {;; esc
@@ -275,22 +292,10 @@
        ;; ?
        191 (fn [state e]
              (when-not (util/input? (gobj/get e "target"))
-               (state/sidebar-add-block! (state/get-current-repo) "help" :help nil)))
-       ;; c
-       67 (fn [state e]
-            (when (and (not (util/input? (gobj/get e "target")))
-                       (not (gobj/get e "shiftKey"))
-                       (not (gobj/get e "ctrlKey"))
-                       (not (gobj/get e "altKey"))
-                       (not (gobj/get e "metaKey")))
-              (when-let [repo-url (state/get-current-repo)]
-                (when-not (state/get-edit-input-id)
-                  (util/stop e)
-                  (state/set-modal! commit/add-commit-message)))))})))
+               (ui-handler/toggle-help!)))})))
   {:did-mount (fn [state]
                 (keyboards/bind-shortcuts!)
                 state)}
-  (mixins/keyboards-mixin keyboards/keyboards)
   [state route-match main-content]
   (let [{:keys [open? close-fn open-fn]} state
         close-fn (fn []
@@ -298,9 +303,11 @@
                    (state/set-left-sidebar-open! false))
         me (state/sub :me)
         current-repo (state/sub :git/current-repo)
+        granted? (state/sub [:nfs/user-granted? (state/get-current-repo)])
         theme (state/sub :ui/theme)
         white? (= "white" (state/sub :ui/theme))
-        sidebar-open? (state/sub :ui/sidebar-open?)
+        settings-open? (state/sub :ui/settings-open?)
+        sidebar-open?  (state/sub :ui/sidebar-open?)
         route-name (get-in route-match [:data :name])
         global-graph-pages? (= :graph route-name)
         logged? (:name me)
@@ -311,8 +318,11 @@
         default-home (get-default-home-if-valid)]
     (rum/with-context [[t] i18n/*tongue-context*]
       (theme/container
-       {:theme theme
-        :on-click editor-handler/unhighlight-block!}
+       {:theme         theme
+        :route         route-match
+        :nfs-granted?  granted?
+        :db-restoring? db-restoring?
+        :on-click      editor-handler/unhighlight-block!}
 
        [:div.theme-inner
         (sidebar-mobile-sidebar
@@ -320,7 +330,8 @@
           :close-fn    close-fn
           :route-match route-match})
         [:div.#app-container.cp__sidebar-layout
-         {:class (if sidebar-open? "is-right-sidebar-open")}
+         {:class (if sidebar-open? "is-right-sidebar-open")
+          :style {:padding-bottom (if global-graph-pages? 0 30)}}
          (header/header {:open-fn        open-fn
                          :white?         white?
                          :current-repo   current-repo
@@ -343,6 +354,7 @@
 
         (ui/notification)
         (ui/modal)
+        (settings-modal settings-open?)
         (custom-context-menu)
         [:a#download.hidden]
         (when

+ 25 - 4
src/main/frontend/components/sidebar.css

@@ -52,11 +52,14 @@
   }
 }
 
+.settings-modal {
+  margin: -15px;
+}
+
 .cp__sidebar-layout {
   display: flex;
   flex-direction: column;
   min-height: 100vh;
-  padding-bottom: 30px;
 }
 
 .cp__sidebar-main-layout {
@@ -67,7 +70,7 @@
 
 .cp__sidebar-layout.is-right-sidebar-open {
   .cp__sidebar-main-layout {
-    margin-right: 40%;
+    margin-right: var(--ls-right-sidebar-width);
   }
 }
 
@@ -81,7 +84,7 @@
 }
 
 .cp__sidebar-main-content {
-  padding: 6rem 1.5rem;
+  padding: 5rem 1.5rem;
   max-width: var(--ls-main-content-max-width);
   min-height: 100vh;
   flex: 1;
@@ -139,12 +142,26 @@
 
   &-inner {
     padding: 15px;
+    padding-top: 0;
+    position: relative;
+    min-height: 100%;
+
+    .resizer {
+      position: absolute;
+      top: 0;
+      bottom: 0;
+      left: 0;
+      width: 4px;
+      user-select: none;
+      cursor: col-resize !important;
+    }
   }
 
   &-settings {
     @apply flex flex-row mb-2;
     margin: -15px;
     margin-bottom: 0;
+    margin-top: 0;
     overflow: auto;
 
     &-btn {
@@ -154,9 +171,13 @@
     }
   }
 
+  .close-arrow svg {
+    transform: scale(0.8);
+  }
+
   &.is-open {
     display: block;
-    width: 40%;
+    width: var(--ls-right-sidebar-width);
     opacity: 1;
   }
 

+ 80 - 1
src/main/frontend/components/svg.cljs

@@ -15,7 +15,7 @@
     {:d "M5 11L0 6l1.5-1.5L5 8.25 8.5 4.5 10 6l-5 5z"
      :fill-rule "evenodd"}]])
 
-(rum/defc arrow-right
+(rum/defc arrow-right-2
   []
   [:svg
    {:aria-hidden "true"
@@ -29,6 +29,26 @@
     {:d "M7.5 8l-5 5L1 11.5 4.75 8 1 4.5 2.5 3l5 5z"
      :fill-rule "evenodd"}]])
 
+(rum/defc arrow-left
+  []
+  [:svg.w-6.h-6
+   {:viewBox "0 0 24 24", :stroke "currentColor", :fill "none"}
+   [:path
+    {:d "M15 19l-7-7 7-7",
+     :stroke-width "2",
+     :stroke-linejoin "round",
+     :stroke-linecap "round"}]])
+
+(rum/defc arrow-right
+  []
+  [:svg.w-6.h-6
+   {:viewBox "0 0 24 24", :stroke "currentColor", :fill "none"}
+   [:path
+    {:d "M9 5l7 7-7 7",
+     :stroke-width "2",
+     :stroke-linejoin "round",
+     :stroke-linecap "round"}]])
+
 (rum/defc big-arrow-right
   []
   [:svg
@@ -83,6 +103,22 @@
    [:path.opacity-75 {:fill "currentColor"
                       :d "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"}]])
 
+(defonce minus
+  [:svg.w-6.h-6
+   {:viewBox "0 0 24 24", :stroke "currentColor", :fill "none"}
+   [:path
+    {:d               "M20 12H4"
+     :stroke-width    "2"
+     :stroke-linejoin "round"
+     :stroke-linecap  "round"}]])
+
+(defonce rectangle
+  [:svg.w-6.h-6
+   {:viewBox "0 0 24 24", :stroke "currentColor", :fill "none"}
+   [:path
+    {:d               "M3.16580358,18.5038125 L20.5529464,18.5038125 C22.6525178,18.5038125 23.7072321,17.4593839 23.7072321,15.3902411 L23.7072321,3.12495537 C23.7072321,1.0558125 22.6525178,0.0113839219 20.5529464,0.0113839219 L3.16580358,0.0113839219 C1.07651787,0.0118125 0.0115178672,1.04638392 0.0115178672,3.12495537 L0.0115178672,15.3906696 C0.0115178672,17.4696696 1.07651787,18.5042411 3.16580358,18.5042411 L3.16580358,18.5038125 Z M3.19580358,16.8868125 C2.19123216,16.8868125 1.62894642,16.3545268 1.62894642,15.3096696 L1.62894642,3.20638392 C1.62894642,2.16152679 2.19123213,1.62924108 3.19580358,1.62924108 L20.5229464,1.62924108 C21.5172321,1.62924108 22.0898036,2.16152679 22.0898036,3.20638392 L22.0898036,15.3092411 C22.0898036,16.3540982 21.5172322,16.8863839 20.5229464,16.8863839 L3.19580358,16.8868125 Z"
+     :stroke-width    "2"}]])
+
 (defn- hero-icon
   ([d]
    (hero-icon d {}))
@@ -182,6 +218,9 @@
 (defn vertical-dots
   [options]
   (hero-icon "M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z" options))
+(defn horizontal-dots
+  [options]
+  (hero-icon "M5 12h.01M12 12h.01M19 12h.01M6 12a1 1 0 11-2 0 1 1 0 012 0zm7 0a1 1 0 11-2 0 1 1 0 012 0zm7 0a1 1 0 11-2 0 1 1 0 012 0z" options))
 (def external-link
   [:svg {:fill "none", :view-box "0 0 24 24", :height "21", :width "21"
          :stroke "currentColor"}
@@ -252,6 +291,20 @@
      {:d
       "M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"}]]))
 
+
+(rum/defc pinned
+  []
+  [:svg.h-8.w-8.pinned
+   {:view-box "0 0 352 512"
+    :fill "currentColor"}
+   [:path
+    {:d
+     "M322.397,252.352l75.068-75.067c19.346,5.06,40.078,3.441,58.536-4.873L339.589,56c-8.313,18.458-9.933,39.189-4.873,58.536
+	l-75.066,75.067c-35.168-16.745-76.173-17.14-111.618-1.176l65.009,65.01L55.999,456l202.563-157.041l65.01,65.01
+	C339.535,328.526,339.142,287.519,322.397,252.352z M201.513,216.553c0,0-16.568-16.568-21.323-21.035
+	c37.027-10.806,61.375,4.323,61.375,4.323C218.946,192.781,201.513,216.553,201.513,216.553z"}]])
+
+
 (rum/defc caret-down
   []
   [:svg.h-4.w-4
@@ -434,6 +487,17 @@
      :stroke-linejoin "round"
      :stroke-linecap "round"}]])
 
+(def checkbox
+  [:svg.h-6.w-6
+   {:stroke "currentColor", :view-box "0 0 24 24", :fill "none"}
+   [:path
+    {:d "M11.167 16.167l-4.167-4.416 1.166-1.192 2.978 3.113 5.477-5.839 1.213 1.169-6.667 7.164zm9.167-12.5v16.667h-16.667v-16.667h16.667zm1.667-1.667h-20v20h20v-20z"
+     :fill-rule "evenodd"
+     :clip-rule "evenodd"
+     :stroke-width "2"
+     :stroke-linejoin "round"
+     :stroke-linecap "round"}]])
+
 (def page
   [:svg.h-5.w-4 {:viewBox "0 0 24 24", :fill "none", :xmlns "http://www.w3.org/2000/svg"}
    [:path {:d "M2 0.5H6.78272L13.5 7.69708V18C13.5 18.8284 12.8284 19.5 12 19.5H2C1.17157 19.5 0.5 18.8284 0.5 18V2C0.5 1.17157 1.17157 0.5 2 0.5Z", :fill "var(--ls-active-primary-color)"}]
@@ -441,3 +505,18 @@
 
 (def online
   (hero-icon "M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0"))
+
+(rum/defc filter-icon
+  [class]
+  [:svg
+   {:stroke "currentColor"
+    :fill "currentColor"
+    :view-box "0 0 16.06 16.06"
+    :width "16"
+    :height "16"
+    :class class }
+   [:path
+    {:d "M.53.53h15l-5 7v8h-5v-8z" :stroke-width "1.06" :stroke-linejoin "round"}]])
+
+(def collapse-right
+  (hero-icon "M4 6h16M4 12h16m-7 6h7"))

+ 22 - 10
src/main/frontend/components/theme.cljs

@@ -1,17 +1,29 @@
 (ns frontend.components.theme
-  (:require [rum.core :as rum]))
+  (:require [rum.core :as rum]
+            [frontend.util :as util]
+            [frontend.handler.route :as route-handler]
+            [frontend.components.svg :as svg]))
 
 (rum/defc container
-  [{:keys [theme on-click] :as props} child]
-  rum/use-effect! (let [doc js/document.documentElement
-                        cls (.-classList doc)]
-                    (.setAttribute doc "data-theme" (if (= theme "white") "light" theme))
-                    (if (= theme "dark")                    ;; for tailwind dark mode
-                      (.add cls "dark")
-                      (.remove cls "dark")))
+  [{:keys [route theme on-click nfs-granted? db-restoring?] :as props} child]
+  (rum/use-effect!
+   #(let [doc js/document.documentElement
+          cls (.-classList doc)]
+      (.setAttribute doc "data-theme" (if (= theme "white") "light" theme))
+      (if (= theme "dark")                                 ;; for tailwind dark mode
+        (.add cls "dark")
+        (.remove cls "dark")))
+   [theme])
+
+  (rum/use-effect!
+   #(let [db-restored? (false? db-restoring?)]
+      (if db-restoring?
+        (util/set-title! "Loading")
+        (when (or nfs-granted? db-restored?)
+          (route-handler/update-page-title! route))))
+   [nfs-granted? db-restoring? route])
 
-  [theme]
   [:div
-   {:class (str theme "-theme")
+   {:class    (str theme "-theme")
     :on-click on-click}
    child])

+ 137 - 13
src/main/frontend/components/theme.css

@@ -8,9 +8,11 @@
   --ls-z-index-level-3: 999;
   --ls-z-index-level-4: 9999;
   --ls-z-index-level-5: 99999;
+
+  --ls-right-sidebar-width: 40%;
 }
 
-html:not(.is-mac) {
+html {
   ::-webkit-scrollbar-thumb {
     background-color: var(--ls-scrollbar-foreground-color);
   }
@@ -26,7 +28,6 @@ html:not(.is-mac) {
   ::-webkit-scrollbar {
     width: 8px;
     height: 8px;
-    -webkit-border-radius: 100px;
   }
 
   ::-webkit-scrollbar-thumb {
@@ -42,23 +43,23 @@ html:not(.is-mac) {
   }
 }
 
+.form-checkbox {
+  color: var(--ls-page-checkbox-color, #6093a0);
+  background-color: var(--ls-page-checkbox-color, #6093a0);
+  border-color: var(--ls-page-checkbox-border-color, #6093a0);
+  border: none;
+}
+
+.form-checkbox:hover {
+  transform: scale(1.1);
+}
+
 html[data-theme=dark] {
   background-color: var(--ls-primary-background-color);
 
-  input {
-    color: var(--ls-secondary-text-color);
-  }
-
   input.form-input {
     background: none;
   }
-
-  .form-checkbox {
-    color: var(--ls-page-checkbox-color, #6093a0);
-    background-color: var(--ls-page-checkbox-color, #6093a0);
-    border-color: var(--ls-page-checkbox-border-color, #6093a0);
-    border: none;
-  }
 }
 
 html[data-theme=light] {
@@ -91,3 +92,126 @@ html[data-theme=light] {
     display: none;
   }
 }
+
+html.is-electron {
+  --frame-top-height: 24px;
+
+  .theme-inner {
+  }
+
+  .cp__header {
+    height: 2.6rem;
+    background-color: var(--ls-primary-background-color);
+    top: 0;
+  }
+
+  &.is-mac {
+    .cp__header {
+      height: calc(2.2rem + var(--frame-top-height));
+      padding-top: var(--frame-top-height);
+
+      &-logo {
+        height: var(--frame-top-height);
+      }
+
+      &:before {
+        content: " ";
+        position: fixed;
+        top: 0;
+        left: 0;
+        z-index: 8;
+        -webkit-app-region: drag;
+        width: 100%;
+        height: var(--frame-top-height);
+      }
+    }
+
+    .cp__right-sidebar {
+      top: 4rem;
+    }
+
+    &.is-fullscreen {
+      .cp__header {
+        padding-top: 0;
+        height: 2.6rem;
+
+        &:before {
+          display: none;
+        }
+      }
+    }
+  }
+
+  #search {
+    -webkit-app-region: drag;
+
+    #search-wrapper {
+      -webkit-app-region: no-drag;
+    }
+  }
+
+  .ls-window-frame-title-bar {
+    background-color: var(--ls-primary-background-color);
+    position: fixed;
+    left: 0;
+    right: 0;
+    z-index: 9;
+    height: var(--frame-top-height);
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    user-select: none;
+    -webkit-app-region: drag;
+
+    & > .l {
+      display: flex;
+    }
+
+    & > .r {
+      & > .inner {
+        display: flex;
+      }
+    }
+
+    & > .c {
+      font-size: .7rem;
+    }
+
+    a.it {
+      padding: 0 2px;
+      cursor: pointer;
+      -webkit-app-region: no-drag;
+
+      &:hover {
+        background-color: var(--ls-secondary-background-color);
+      }
+
+      &:active {
+        background-color: var(--ls-primary-background-color);
+      }
+
+      svg {
+        transform: scale(.6);
+        color: var(--ls-primary-text-color);
+        cursor: pointer;
+      }
+
+      &.maximize {
+        svg {
+          transform: scale(.5) translateY(2px) translateX(1px);
+          opacity: .7;
+        }
+      }
+    }
+  }
+}
+
+html.locked-scroll {
+  overflow: hidden !important;
+}
+
+html.is-resizing-buf {
+  #right-sidebar {
+    transition: none;
+  }
+}

+ 28 - 23
src/main/frontend/components/widgets.cljs

@@ -5,6 +5,7 @@
             [frontend.handler.repo :as repo-handler]
             [frontend.handler.notification :as notification]
             [frontend.handler.web.nfs :as nfs]
+            [frontend.handler.page :as page-handler]
             [frontend.state :as state]
             [clojure.string :as string]
             [frontend.ui :as ui]
@@ -59,24 +60,24 @@
                           (reset! branch (util/evalue e)))}]]]]
 
         (ui/button
-          (t :git/add-repo-prompt-confirm)
-          :on-click
-          (fn []
-            (let [branch (string/trim @branch)]
-              (if (string/blank? branch)
-                (notification/show!
-                 [:p.text-gray-700.dark:text-gray-300 "Please input a branch, make sure it's matched with your setting on Github."]
-                 :error
-                 false)
-                (let [repo (util/lowercase-first @repo)]
-                  (if (util/starts-with? repo "https://github.com/")
-                    (let [repo (string/replace repo ".git" "")]
-                      (repo-handler/create-repo! repo branch))
+         (t :git/add-repo-prompt-confirm)
+         :on-click
+         (fn []
+           (let [branch (string/trim @branch)]
+             (if (string/blank? branch)
+               (notification/show!
+                [:p.text-gray-700.dark:text-gray-300 "Please input a branch, make sure it's matched with your setting on Github."]
+                :error
+                false)
+               (let [repo (util/lowercase-first @repo)]
+                 (if (util/starts-with? repo "https://github.com/")
+                   (let [repo (string/replace repo ".git" "")]
+                     (repo-handler/create-repo! repo branch))
 
-                    (notification/show!
-                     [:p.text-gray-700.dark:text-gray-300 "Please input a valid repo url, e.g. https://github.com/username/repo"]
-                     :error
-                     false)))))))]])))
+                   (notification/show!
+                    [:p.text-gray-700.dark:text-gray-300 "Please input a valid repo url, e.g. https://github.com/username/repo"]
+                    :error
+                    false)))))))]])))
 
 (rum/defcs add-local-directory
   []
@@ -87,11 +88,15 @@
        [:div.cp__widgets-open-local-directory
         [:div.select-file-wrap.cursor
          (when nfs-supported?
-           {:on-click nfs/ls-dir-files})
+           {:on-click page-handler/ls-dir-files!})
          [:div
           [:h1.title "Open a local directory"]
-          [:p.text-sm
-           "Your data will be stored only in your device."]
+          [:p "Logseq supports both Markdown and Org-mode, you can open an existing directory or creating a new one. Your data will be stored only on this device."]
+          [:p "After you opened your directory, it will create three sub-directories in that directory:"]
+          [:ul
+           [:li "/journals - store your journal pages"]
+           [:li "/pages - store the other pages"]
+           [:li "/logseq - store configuration, custom.css, and some metadata."]]
           (when-not nfs-supported?
             (ui/admonition :warning
                            [:p "It seems that your browser doesn't support the "
@@ -105,13 +110,13 @@
   [state & {:keys [graph-types]
             :or {graph-types [:local :github]}
             :as opts}]
-  (let [github-authed? (:github-authed? (state/get-me))
+  (let [github-authed? (state/github-authed?)
         generate-f (fn [x]
                      (case x
                        :github
-                       (when github-authed?
+                       (when (and github-authed? (not (util/electron?)))
                          (rum/with-key (add-github-repo)
-                                       "add-github-repo"))
+                           "add-github-repo"))
 
                        :local
                        (rum/with-key (add-local-directory)

File diff suppressed because it is too large
+ 4 - 3
src/main/frontend/config.cljs


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

@@ -13,10 +13,10 @@
 (defn set-router!
   []
   (rfe/start!
-   (rf/router routes/routes {})
+   (rf/router routes/routes nil)
    route/set-route-match!
    ;; set to false to enable HistoryAPI
-   {:use-fragment false}))
+   {:use-fragment true}))
 
 (defn display-welcome-message
   []

+ 13 - 19
src/main/frontend/date.cljs

@@ -7,7 +7,13 @@
             [cljs-bean.core :as bean]
             [frontend.util :as util]
             [clojure.string :as string]
-            [goog.object :as gobj]))
+            [goog.object :as gobj]
+            ["chrono-node" :as chrono]))
+
+(defn nld-parse
+  [s]
+  (when (string? s)
+    ((gobj/get chrono "parseDate") s)))
 
 (defn format
   [date]
@@ -127,21 +133,7 @@
      (gobj/get js/window.navigator "language")
      (bean/->js {:hour "2-digit"
                  :minute "2-digit"
-                 :hour12 false}))))
-
-(defn journals-path
-  [year month preferred-format]
-  (let [month (if (< month 10) (str "0" month) month)
-        format (string/lower-case (name preferred-format))
-        format (if (= format "markdown") "md" format)]
-    (str "journals/" year "_" month "." format)))
-
-(defn current-journal-path
-  [preferred-format]
-  (when preferred-format
-    (let [{:keys [year month]} (get-date)
-          preferred-format preferred-format]
-      (journals-path year month preferred-format))))
+                 :hourCycle "h23"}))))
 
 (defn valid?
   [s]
@@ -156,7 +148,7 @@
 (defn valid-journal-title?
   [title]
   (and title
-       (valid? (string/capitalize title))))
+       (valid? (util/capitalize-all title))))
 
 (defn journal-title->
   [journal-title then-fn]
@@ -164,7 +156,7 @@
     (when-let [time (->> (map
                           (fn [formatter]
                             (try
-                              (tf/parse (tf/formatter formatter) journal-title)
+                              (tf/parse (tf/formatter formatter) (util/capitalize-all journal-title))
                               (catch js/Error _e
                                 nil)))
                           (journal-title-formatters))
@@ -174,7 +166,9 @@
 
 (defn journal-title->int
   [journal-title]
-  (journal-title-> journal-title #(util/parse-int (tf/unparse (tf/formatter "yyyyMMdd") %))))
+  (when journal-title
+    (let [journal-title (util/capitalize-all journal-title)]
+      (journal-title-> journal-title #(util/parse-int (tf/unparse (tf/formatter "yyyyMMdd") %))))))
 
 (defn journal-title->long
   [journal-title]

+ 27 - 14
src/main/frontend/db.cljs

@@ -5,11 +5,13 @@
             [frontend.db.model]
             [frontend.db.react]
             [frontend.db.query-custom]
+            [frontend.db.query-react]
             [frontend.util :as util]
             [datascript.core :as d]
             [frontend.state :as state]
             [promesa.core :as p]
             [frontend.db-schema :as db-schema]
+            [frontend.db.default :as default-db]
             [clojure.core.async :as async]
             [frontend.idb :as idb]))
 
@@ -45,30 +47,37 @@
   get-date-scheduled-or-deadlines get-db-type get-empty-pages get-file get-file-after-blocks get-file-after-blocks-meta
   get-file-blocks get-file-contents get-file-last-modified-at get-file-no-sub get-file-page get-file-page-id
   get-file-pages get-files get-files-blocks get-files-full get-files-that-referenced-page get-journals-length
-  get-latest-journals get-marker-blocks get-matched-blocks get-page get-page-alias get-page-alias-names get-page-blocks
+  get-latest-journals get-marker-blocks get-matched-blocks get-page get-page-alias get-page-alias-names get-page-blocks get-page-linked-refs-refed-pages
   get-page-blocks-count get-page-blocks-no-cache get-page-file get-page-format get-page-name get-page-properties
   get-page-properties-content get-page-referenced-blocks get-page-referenced-pages get-page-unlinked-references
   get-pages get-pages-relation get-pages-that-mentioned-page get-public-pages get-tag-pages
   journal-page? local-native-fs? mark-repo-as-cloned! page-alias-set page-blocks-transform pull-block
-  set-file-last-modified-at! transact-files-db! with-block-refs-count get-modified-pages page-empty? get-alias-source-page]
+  set-file-last-modified-at! transact-files-db! with-block-refs-count get-modified-pages page-empty? get-alias-source-page
+  set-file-content!]
 
  [frontend.db.react
-  get-current-marker get-current-page get-current-priority get-handler-keys set-file-content! set-key-value
+  get-current-marker get-current-page get-current-priority get-handler-keys set-key-value
   transact-react! remove-key! remove-q! remove-query-component! add-q! add-query-component! clear-query-state!
   clear-query-state-without-refs-and-embeds! get-block-blocks-cache-atom get-page-blocks-cache-atom kv q
   query-state query-components query-entity-in-component remove-custom-query! set-new-result! sub-key-value]
 
  [frontend.db.query-custom
-  custom-query custom-query-result-transform])
+  custom-query]
+
+ [frontend.db.query-react
+  react-query custom-query-result-transform]
+ )
 
 ;; persisting DBs between page reloads
 (defn persist! [repo]
   (let [file-key (datascript-files-db repo)
         non-file-key (datascript-db repo)
-        file-db (d/db (get-files-conn repo))
-        non-file-db (d/db (get-conn repo false))
-        file-db-str (db->string file-db)
-        non-file-db-str (db->string non-file-db)]
+        files-conn (get-files-conn repo)
+        file-db (when files-conn (d/db files-conn))
+        non-file-conn (get-conn repo false)
+        non-file-db (d/db non-file-conn)
+        file-db-str (if file-db (db->string file-db) "")
+        non-file-db-str (if non-file-db (db->string non-file-db) "")]
     (p/let [_ (idb/set-batch! [{:key file-key :value file-db-str}
                                {:key non-file-key :value non-file-db-str}])]
       (state/set-last-persist-transact-id! repo true (get-max-tx-id file-db))
@@ -104,10 +113,11 @@
   [repo conn files-db?]
   (d/listen! conn :persistence
              (fn [tx-report]
-               (let [tx-id (get-tx-id tx-report)]
-                 (state/set-last-transact-time! repo (util/time-ms))
-                 ;; (state/persist-transaction! repo files-db? tx-id (:tx-data tx-report))
-                 (persist-if-idle! repo))
+               (when-not (util/electron?)
+                (let [tx-id (get-tx-id tx-report)]
+                  (state/set-last-transact-time! repo (util/time-ms))
+                  ;; (state/persist-transaction! repo files-db? tx-id (:tx-data tx-report))
+                  (persist-if-idle! repo)))
 
                ;; rebuild search indices
                (when-not files-db?
@@ -146,7 +156,8 @@
          (p/let [stored (idb/get-item db-name)
                  _ (when stored
                      (let [stored-db (string->db stored)
-                           attached-db (d/db-with stored-db [(me-tx stored-db me)])]
+                           attached-db (d/db-with stored-db
+                                                  [(me-tx stored-db me)])]
                        (conn/reset-conn! db-conn attached-db)))
                  db-name (datascript-db repo)
                  db-conn (d/create-conn db-schema/schema)
@@ -155,7 +166,9 @@
                  stored (idb/get-item db-name)
                  _ (if stored
                      (let [stored-db (string->db stored)
-                           attached-db (d/db-with stored-db [(me-tx stored-db me)])]
+                           attached-db (d/db-with stored-db (concat
+                                                             [(me-tx stored-db me)]
+                                                             default-db/built-in-pages))]
                        (conn/reset-conn! db-conn attached-db))
                      (when logged?
                        (d/transact! db-conn [(me-tx (d/db db-conn) me)])))]

+ 5 - 1
src/main/frontend/db/conn.cljs

@@ -2,6 +2,7 @@
   "Contains db connections."
   (:require [clojure.string :as string]
             [frontend.db-schema :as db-schema]
+            [frontend.db.default :as default-db]
             [frontend.util :as util]
             [frontend.state :as state]
             [frontend.config :as config]
@@ -89,7 +90,10 @@
      (when me
        (d/transact! db-conn [(me-tx (d/db db-conn) me)]))
 
-     (when listen-handler (listen-handler repo)))))
+     (d/transact! db-conn default-db/built-in-pages)
+
+     (when (and listen-handler (not (util/electron?)))
+       (listen-handler repo)))))
 
 (defn destroy-all!
   []

+ 9 - 0
src/main/frontend/db/default.cljs

@@ -0,0 +1,9 @@
+(ns frontend.db.default
+  (:require [clojure.string :as string]))
+
+(def built-in-pages
+  (mapv (fn [p]
+          {:page/name (string/lower-case p)
+           :page/original-name p
+           :page/journal? false})
+        #{"NOW" "LATER" "DOING" "DONE" "IN-PROGRESS" "TODO" "WAIT" "WAITING" "A" "B" "C"}))

+ 170 - 48
src/main/frontend/db/model.cljs

@@ -12,7 +12,6 @@
             [clojure.set :as set]
             [frontend.utf8 :as utf8]
             [frontend.config :as config]
-            [frontend.format.block :as block]
             [cljs.reader :as reader]
             [cljs-time.core :as t]
             [cljs-time.coerce :as tc]
@@ -22,6 +21,31 @@
 ;; TODO: extract to specific models and move data transform logic to the
 ;; correponding handlers.
 
+(def rules
+  '[[(parent ?p ?c)
+     [?p :block/children ?c]]
+    [(parent ?p ?c)
+     [?t :block/children ?c]
+     (parent ?p ?t)]
+
+    ;; from https://stackoverflow.com/questions/43784258/find-entities-whose-ref-to-many-attribute-contains-all-elements-of-input
+    ;; Quote:
+    ;; You're tackling the general problem of 'dynamic conjunction' in Datomic's Datalog.
+    ;; Write a dynamic Datalog query which uses 2 negations and 1 disjunction or a recursive rule
+    ;; Datalog has no direct way of expressing dynamic conjunction (logical AND / 'for all ...' / set intersection).
+    ;; However, you can achieve it in pure Datalog by combining one disjunction
+    ;; (logical OR / 'exists ...' / set union) and two negations, i.e
+    ;; (For all ?g in ?Gs p(?e,?g)) <=> NOT(Exists ?g in ?Gs, such that NOT(p(?e, ?g)))
+
+    ;; [(matches-all ?e ?a ?vs)
+    ;;  [(first ?vs) ?v0]
+    ;;  [?e ?a ?v0]
+    ;;  (not-join [?e ?vs]
+    ;;            [(identity ?vs) [?v ...]]
+    ;;            (not-join [?e ?v]
+    ;;                      [?e ?a ?v]))]
+])
+
 (defn transact-files-db!
   ([tx-data]
    (db-utils/transact! (state/get-current-repo) tx-data))
@@ -82,12 +106,12 @@
 
 (defn get-modified-pages
   [repo]
-  (d/q
-   '[:find ?page-name ?modified-at
-     :where
-     [?page :page/original-name ?page-name]
-     [(get-else $ ?page :page/last-modified-at 0) ?modified-at]]
-   (conn/get-conn repo)))
+  (-> (d/q
+       '[:find ?page-name
+         :where
+         [?page :page/original-name ?page-name]]
+       (conn/get-conn repo))
+      (db-utils/seq-flatten)))
 
 (defn get-page-alias
   [repo page-name]
@@ -127,13 +151,15 @@
   [repo]
   (when-let [conn (conn/get-conn repo)]
     (->> (d/q
-          '[:find ?path ?modified-at
+          '[:find ?path
+             ;; ?modified-at
             :where
             [?file :file/path ?path]
-            [(get-else $ ?file :file/last-modified-at 0) ?modified-at]]
+            ;; [?file :file/last-modified-at ?modified-at]
+]
           conn)
          (seq)
-         (sort-by last)
+         ;; (sort-by last)
          (reverse))))
 
 (defn get-files-blocks
@@ -203,7 +229,7 @@
 (defn set-file-last-modified-at!
   [repo path last-modified-at]
   (when (and repo path last-modified-at)
-    (when-let [conn (conn/get-files-conn repo)]
+    (when-let [conn (conn/get-conn repo false)]
       (d/transact! conn
                    [{:file/path path
                      :file/last-modified-at last-modified-at}]))))
@@ -211,7 +237,7 @@
 (defn get-file-last-modified-at
   [repo path]
   (when (and repo path)
-    (when-let [conn (conn/get-files-conn repo)]
+    (when-let [conn (conn/get-conn repo false)]
       (-> (d/entity (d/db conn) [:file/path path])
           :file/last-modified-at))))
 
@@ -258,7 +284,8 @@
 
 (defn get-custom-css
   []
-  (get-file "logseq/custom.css"))
+  (when-let [repo (state/get-current-repo)]
+    (get-file (config/get-file-path repo "logseq/custom.css"))))
 
 (defn get-file-no-sub
   ([path]
@@ -304,12 +331,29 @@
      (set)
      (set/union #{page-id}))))
 
+(defn get-page-names-by-ids
+  ([ids]
+   (get-page-names-by-ids (state/get-current-repo) ids))
+  ([repo ids]
+   (when repo
+     (->> (db-utils/pull-many repo '[:page/name] ids)
+          (map :page/name)))))
+
+(defn get-page-ids-by-names
+  ([names]
+   (get-page-ids-by-names (state/get-current-repo) names))
+  ([repo names]
+   (when repo
+     (let [lookup-refs (map (fn [name]
+                              [:page/name (string/lower-case name)]) names)]
+       (->> (db-utils/pull-many repo '[:db/id] lookup-refs)
+            (mapv :db/id))))))
+
 (defn get-page-alias-names
   [repo page-name]
   (let [alias-ids (page-alias-set repo page-name)]
     (when (seq alias-ids)
-      (->> (db-utils/pull-many repo '[:page/name] alias-ids)
-           (map :page/name)
+      (->> (get-page-names-by-ids repo alias-ids)
            distinct
            (remove #(= (string/lower-case %) (string/lower-case page-name)))))))
 
@@ -342,7 +386,7 @@
 (defn sort-blocks
   [blocks]
   (let [pages-ids (map (comp :db/id :block/page) blocks)
-        pages (db-utils/pull-many '[:db/id :page/last-modified-at :page/name :page/original-name] pages-ids)
+        pages (db-utils/pull-many '[:db/id :page/name :page/original-name :page/journal-day] pages-ids)
         pages-map (reduce (fn [acc p] (assoc acc (:db/id p) p)) {} pages)
         blocks (map
                 (fn [block]
@@ -465,7 +509,8 @@
 (defn get-block-parent
   [repo block-id]
   (when-let [conn (conn/get-conn repo)]
-    (d/entity conn [:block/children [:block/uuid block-id]])))
+    (when-let [block (d/entity conn [:block/uuid block-id])]
+      (d/entity conn [:block/children [:block/uuid block-id]]))))
 
 ;; non recursive query
 (defn get-block-parents
@@ -516,7 +561,7 @@
 
 (defn get-page-properties-content
   [page]
-  (when-let [content (let [blocks (get-page-blocks page)]
+  (when-let [content (let [blocks (get-page-blocks-no-cache page)]
                        (and (:block/pre-block? (first blocks))
                             (:block/content (first blocks))))]
     (let [format (get-page-format page)]
@@ -559,17 +604,14 @@
   (when-let [conn (conn/get-conn repo)]
     (let [eid (:db/id (db-utils/entity repo [:block/uuid block-uuid]))]
       (->> (d/q
-            '[:find ?e1
-              :in $ ?e2 %
-              :where (parent ?e2 ?e1)]
+            '[:find ?id
+              :in $ ?p %
+              :where
+              (parent ?p ?c)
+              [?c :block/uuid ?id]]
             conn
             eid
-             ;; recursive rules
-            '[[(parent ?e2 ?e1)
-               [?e2 :block/children ?e1]]
-              [(parent ?e2 ?e1)
-               [?t :block/children ?e1]
-               (parent ?e2 ?t)]])
+            rules)
            (apply concat)))))
 
 (defn get-block-immediate-children
@@ -583,7 +625,8 @@
 (defn get-block-children
   [repo block-uuid]
   (when-let [conn (conn/get-conn repo)]
-    (let [ids (get-block-children-ids repo block-uuid)]
+    (let [ids (get-block-children-ids repo block-uuid)
+          ids (map (fn [id] [:block/uuid id]) ids)]
       (when (seq ids)
         (db-utils/pull-many repo '[*] ids)))))
 
@@ -692,25 +735,28 @@
     (db-utils/entity [:block/uuid (uuid page-name)])
     (db-utils/entity [:page/name page-name])))
 
+(defn- heading-block?
+  [block]
+  (and
+   (vector? block)
+   (= "Heading" (first block))))
+
 (defn get-page-name
   [file ast]
   ;; headline
   (let [ast (map first ast)]
-    (if (util/starts-with? file "pages/contents.")
+    (if (string/includes? file "pages/contents.")
       "Contents"
-      (let [first-block (last (first (filter block/heading-block? ast)))
+      (let [first-block (last (first (filter heading-block? ast)))
             property-name (when (and (= "Properties" (ffirst ast))
                                      (not (string/blank? (:title (last (first ast))))))
                             (:title (last (first ast))))
-            first-block-name (and first-block
-                                  ;; FIXME:
-                                  (str (last (first (:title first-block)))))
+            first-block-name (let [title (last (first (:title first-block)))]
+                               (and first-block
+                                    (string? title)
+                                    title))
             file-name (when-let [file-name (last (string/split file #"/"))]
-                        (when-let [file-name (first (util/split-last "." file-name))]
-                          (-> file-name
-                              (string/replace "-" " ")
-                              (string/replace "_" " ")
-                              (util/capitalize-all))))]
+                        (first (util/split-last "." file-name)))]
         (or property-name
             (if (= (state/page-name-order) "file")
               (or file-name first-block-name)
@@ -793,6 +839,24 @@
                          db-utils/seq-flatten)]
       (mapv (fn [page] [page (get-page-alias repo page)]) ref-pages))))
 
+(defn get-page-linked-refs-refed-pages
+  [repo page]
+  (when-let [conn (conn/get-conn repo)]
+    (->
+     (d/q
+       '[:find [?ref-page ...]
+         :in $ % ?page
+         :where
+         [?p :page/name ?page]
+         [?b :block/path-ref-pages ?p]
+         [?b :block/ref-pages ?other-p]
+         [(not= ?p ?other-p)]
+         [?other-p :page/name ?ref-page]]
+       conn
+       rules
+       page)
+     (distinct))))
+
 ;; Ignore files with empty blocks for now
 (defn get-empty-pages
   [repo]
@@ -862,6 +926,27 @@
       (remove (fn [block] (contains? childrens (:db/id block))) blocks)
       blocks)))
 
+;; TODO: improve perf
+(defn with-children-refs
+  [repo blocks]
+  (when-let [conn (conn/get-conn repo)]
+    (when (seq blocks)
+      (let [block-ids (set (map :db/id blocks))
+            refs (d/q
+                   '[:find ?p ?ref
+                     :in $ % ?block-ids
+                     :where
+                     (parent ?p ?b)
+                     [(contains? ?block-ids ?p)]
+                     [?b :block/ref-pages ?ref]]
+                   conn
+                   rules
+                   block-ids)
+            refs (->> (group-by first refs)
+                      (medley/map-vals #(set (map (fn [[_ id]] {:db/id id}) %))))]
+        (map (fn [block] (assoc block :block/children-refs
+                                (get refs (:db/id block)))) blocks)))))
+
 (defn get-page-referenced-blocks
   ([page]
    (get-page-referenced-blocks (state/get-current-repo) page))
@@ -898,13 +983,15 @@
                          db-utils/seq-flatten
                          (remove (fn [block]
                                    (= page-id (:db/id (:block/page block)))))
+                         (remove-children!)
+                         (with-children-refs repo)
                          sort-blocks
                          db-utils/group-by-page
                          (map (fn [[k blocks]]
                                 (let [k (if (contains? aliases (:db/id k))
                                           (assoc k :page/alias? true)
                                           k)]
-                                  [k (remove-children! blocks)]))))]
+                                  [k blocks]))))]
          result)))))
 
 (defn get-date-scheduled-or-deadlines
@@ -923,9 +1010,7 @@
              react
              db-utils/seq-flatten
              sort-blocks
-             db-utils/group-by-page
-             (remove (fn [[page _blocks]]
-                       (= journal-title (:page/original-name page)))))))))
+             db-utils/group-by-page)))))
 
 (defn get-files-that-referenced-page
   [page-id]
@@ -1090,9 +1175,10 @@
   ([cache?]
    (if (and cache? @blocks-count-cache)
      @blocks-count-cache
-     (let [n (count (d/datoms (conn/get-conn) :avet :block/uuid))]
-       (reset! blocks-count-cache n)
-       n))))
+     (when-let [conn (conn/get-conn)]
+       (let [n (count (d/datoms conn :avet :block/uuid))]
+        (reset! blocks-count-cache n)
+        n)))))
 
 ;; block/uuid and block/content
 (defn get-all-block-contents
@@ -1119,10 +1205,9 @@
 
 (defn filter-only-public-pages-and-blocks
   [db]
-  (let [public-pages (get-public-pages db)
-        contents-id (:db/id (db-utils/entity [:page/name "contents"]))]
+  (let [public-pages (get-public-pages db)]
     (when (seq public-pages)
-      (let [public-pages (set (conj public-pages contents-id))
+      (let [public-pages (set public-pages)
             page-or-block? #(contains? #{"page" "block" "me" "recent" "file"} %)
             filtered-db (d/filter db
                                   (fn [db datom]
@@ -1188,3 +1273,40 @@
         tx-data (map (fn [page-id] [:db/retract page-id :page/alias]) page-ids)]
     (when (seq tx-data)
       (db-utils/transact! repo tx-data))))
+
+(defn set-file-content!
+  [repo path content]
+  (when (and repo path)
+    (let [tx-data {:file/path path
+                   :file/content content}]
+      (react/transact-react!
+       repo
+       [tx-data]
+       {:key [:file/content path]
+        :files-db? true}))))
+
+(comment
+  (def page-names ["foo" "bar"])
+
+  (def page-ids (set (get-page-ids-by-names page-names)))
+
+  (d/q '[:find [(pull ?b [*]) ...]
+         :in $ % ?refs
+         :where
+         [?b :block/ref-pages ?p]
+         ;; Filter other blocks
+         [(contains? ?refs ?p)]
+         (or-join [?b ?refs]
+                  (matches-all ?b :block/ref-pages ?refs)
+                  (and
+                   (parent ?p ?b)
+                   ;; FIXME: not working
+                   ;; (matches-all (union ?p ?b) :block/ref-pages ?refs)
+                   [?p :block/ref-pages ?p-ref]
+                   [?b :block/ref-pages ?b-ref]
+                   [(not= ?p-ref ?b-ref)]
+                   [(contains? ?refs ?p-ref)]
+                   [(contains? ?refs ?b-ref)]))]
+       (conn/get-conn)
+       rules
+    page-ids))

+ 10 - 98
src/main/frontend/db/query_custom.cljs

@@ -1,103 +1,11 @@
 (ns frontend.db.query-custom
   "Custom queries."
-  (:require [datascript.core :as d]
-            [frontend.db.utils :as db-utils :refer [date->int]]
-            [frontend.db.model :as model]
-            [cljs-time.core :as t]
-            [frontend.state :as state]
+  (:require [frontend.state :as state]
             [clojure.string :as string]
             [cljs.reader :as reader]
-            [frontend.extensions.sci :as sci]
-            [lambdaisland.glogi :as log]
-            [frontend.util :as util]
-            [frontend.db.react :as react]
-            [frontend.text :as text]
-            [clojure.walk :as walk]))
-
-(defn- resolve-input
-  [input]
-  (cond
-    (= :today input)
-    (date->int (t/today))
-    (= :yesterday input)
-    (date->int (t/yesterday))
-    (= :tomorrow input)
-    (date->int (t/plus (t/today) (t/days 1)))
-    (= :current-page input)
-    (string/lower-case (state/get-current-page))
-    (and (keyword? input)
-         (re-find #"^\d+d(-before)?$" (name input)))
-    (let [input (name input)
-          days (util/parse-int (subs input 0 (dec (count input))))]
-      (date->int (t/minus (t/today) (t/days days))))
-    (and (keyword? input)
-         (re-find #"^\d+d(-after)?$" (name input)))
-    (let [input (name input)
-          days (util/parse-int (subs input 0 (dec (count input))))]
-      (date->int (t/plus (t/today) (t/days days))))
-
-    (and (string? input) (text/page-ref? input))
-    (-> (text/page-ref-un-brackets! input)
-        (string/lower-case))
-
-    :else
-    input))
-
-(defn custom-query-result-transform
-  [query-result remove-blocks q]
-  (let [repo (state/get-current-repo)
-        result (db-utils/seq-flatten query-result)
-        block? (:block/uuid (first result))]
-    (let [result (if block?
-                   (let [result (if (seq remove-blocks)
-                                  (let [remove-blocks (set remove-blocks)]
-                                    (remove (fn [h]
-                                              (contains? remove-blocks (:block/uuid h)))
-                                            result))
-                                  result)]
-                     (some->> result
-                              (db-utils/with-repo repo)
-                              (model/with-block-refs-count repo)
-                              (model/sort-blocks)))
-                   result)]
-      (if-let [result-transform (:result-transform q)]
-        (if-let [f (sci/eval-string (pr-str result-transform))]
-          (try
-            (sci/call-fn f result)
-            (catch js/Error e
-              (log/error :sci/call-error e)
-              result))
-          result)
-        (if block?
-          (db-utils/group-by-page result)
-          result)))))
-
-(defn- resolve-query
-  [query]
-  (let [page-ref? #(and (string? %) (text/page-ref? %))]
-    (walk/postwalk
-     (fn [f]
-       (if (and (list? f)
-                (= (first f) '=)
-                (= 3 (count f))
-                (some page-ref? (rest f)))
-         (let [[x y] (rest f)
-               [page-ref sym] (if (page-ref? x) [x y] [y x])
-               page-ref (string/lower-case page-ref)]
-           (list 'contains? sym (text/page-ref-un-brackets! page-ref)))
-         f)) query)))
-
-(defn react-query
-  [repo {:keys [query inputs] :as query'} query-opts]
-  (try
-    (let [query (resolve-query query)
-          inputs (map resolve-input inputs)
-          repo (or repo (state/get-current-repo))
-          k [:custom query']]
-      (apply react/q repo k query-opts query inputs))
-    (catch js/Error e
-      (println "Custom query failed: ")
-      (js/console.dir e))))
+            [frontend.db.query-react :as react]
+            [frontend.template :as template]
+            [frontend.db.query-dsl :as dsl]))
 
 (defn custom-query
   ([query]
@@ -106,11 +14,15 @@
    (when-let [query' (cond
                        (and (string? query)
                             (not (string/blank? query)))
-                       (reader/read-string query)
+                       (let [query-string (template/resolve-dynamic-template! query)]
+                         (reader/read-string query))
 
                        (map? query)
                        query
 
                        :else
                        nil)]
-     (react-query (state/get-current-repo) query' query-opts))))
+     (let [repo (state/get-current-repo)]
+       (if (list? (:query query')) ; dsl query
+         (dsl/custom-query repo query' query-opts )
+         (react/react-query repo query' query-opts))))))

+ 35 - 16
src/main/frontend/db/query_dsl.cljs

@@ -4,9 +4,8 @@
             [datascript.core :as d]
             [lambdaisland.glogi :as log]
             [clojure.string :as string]
-            [frontend.db :as db]
             [frontend.text :as text]
-            [frontend.db.query-custom :as query-custom]
+            [frontend.db.query-react :as react]
             [frontend.date :as date]
             [cljs-time.core :as t]
             [cljs-time.coerce :as tc]
@@ -14,7 +13,8 @@
             [medley.core :as medley]
             [clojure.walk :as walk]
             [clojure.core]
-            [clojure.set :as set]))
+            [clojure.set :as set]
+            [frontend.template :as template]))
 
 ;; Query fields:
 
@@ -146,9 +146,7 @@
        page-ref?
        (let [page-name (-> (text/page-ref-un-brackets! e)
                            (string/lower-case))]
-         (when (and (not (string/blank? page-name))
-                    (some? (db-utils/entity repo [:page/name page-name])))
-           [['?b :block/ref-pages [:page/name page-name]]]))
+         [['?b :block/path-ref-pages [:page/name page-name]]])
 
        (contains? #{'and 'or 'not} fe)
        (let [clauses (->> (map (fn [form]
@@ -273,7 +271,8 @@
              nil)))
 
        (= 'page fe)
-       (let [page-name (string/lower-case (first (rest e)))]
+       (let [page-name (string/lower-case (first (rest e)))
+             page-name (text/page-ref-un-brackets! page-name)]
          [['?b :block/page [:page/name page-name]]])
 
        (= 'page-property fe)
@@ -299,9 +298,11 @@
                tags (map (comp string/lower-case name) tags)]
            (when (seq tags)
              (let [tags (set (map (comp text/page-ref-un-brackets! string/lower-case name) tags))]
-               [['?p :page/tags '?t]
-                ['?t :page/name '?tag]
-                [(list 'contains? tags '?tag)]]))))
+               (let [sym-1 (uniq-symbol counter "?t")
+                     sym-2 (uniq-symbol counter "?tag")]
+                 [['?p :page/tags sym-1]
+                  [sym-1 :page/name sym-2]
+                  [(list 'contains? tags sym-2)]])))))
 
        (= 'all-page-tags fe)
        [['?e :page/tags '?p]]
@@ -377,14 +378,32 @@
 
 (defn query
   [repo query-string]
-  (when query-string
-    (let [{:keys [query sort-by blocks?]} (parse repo query-string)]
+  (when (string? query-string)
+    (let [query-string (template/resolve-dynamic-template! query-string)
+          {:keys [query sort-by blocks?]} (parse repo query-string)]
       (when query
         (let [query (query-wrapper query blocks?)]
-          (query-custom/react-query repo
-                                    {:query query}
-                                    (if sort-by
-                                      {:transform-fn sort-by})))))))
+          (react/react-query repo
+                             {:query query}
+                             (if sort-by
+                               {:transform-fn sort-by})))))))
+
+(defn custom-query
+  [repo query-m query-opts]
+  (when (seq (:query query-m))
+    (let [query-string (pr-str (:query query-m))
+          query-string (template/resolve-dynamic-template! query-string)
+          {:keys [query sort-by blocks?]} (parse repo query-string)]
+      (when query
+        (let [query (query-wrapper query blocks?)]
+          (react/react-query repo
+                             (merge
+                              query-m
+                              {:query query})
+                             (merge
+                              query-opts
+                              (if sort-by
+                                {:transform-fn sort-by}))))))))
 
 (comment
   ;; {{query (and (page-property foo bar) [[hello]])}}

+ 103 - 0
src/main/frontend/db/query_react.cljs

@@ -0,0 +1,103 @@
+(ns frontend.db.query-react
+  "Custom queries."
+  (:require [datascript.core :as d]
+            [frontend.db.utils :as db-utils :refer [date->int]]
+            [frontend.db.model :as model]
+            [cljs-time.core :as t]
+            [frontend.state :as state]
+            [clojure.string :as string]
+            [cljs.reader :as reader]
+            [frontend.extensions.sci :as sci]
+            [lambdaisland.glogi :as log]
+            [frontend.util :as util]
+            [frontend.db.react :as react]
+            [frontend.text :as text]
+            [clojure.walk :as walk]))
+
+(defn- resolve-input
+  [input]
+  (cond
+    (= :today input)
+    (date->int (t/today))
+    (= :yesterday input)
+    (date->int (t/yesterday))
+    (= :tomorrow input)
+    (date->int (t/plus (t/today) (t/days 1)))
+    (= :current-page input)
+    (string/lower-case (state/get-current-page))
+    (and (keyword? input)
+         (re-find #"^\d+d(-before)?$" (name input)))
+    (let [input (name input)
+          days (util/parse-int (subs input 0 (dec (count input))))]
+      (date->int (t/minus (t/today) (t/days days))))
+    (and (keyword? input)
+         (re-find #"^\d+d(-after)?$" (name input)))
+    (let [input (name input)
+          days (util/parse-int (subs input 0 (dec (count input))))]
+      (date->int (t/plus (t/today) (t/days days))))
+
+    (and (string? input) (text/page-ref? input))
+    (-> (text/page-ref-un-brackets! input)
+        (string/lower-case))
+
+    :else
+    input))
+
+(defn custom-query-result-transform
+  [query-result remove-blocks q]
+  (try
+    (let [repo (state/get-current-repo)
+         result (db-utils/seq-flatten query-result)
+         block? (:block/uuid (first result))]
+     (let [result (if block?
+                    (let [result (if (seq remove-blocks)
+                                   (let [remove-blocks (set remove-blocks)]
+                                     (remove (fn [h]
+                                               (contains? remove-blocks (:block/uuid h)))
+                                             result))
+                                   result)]
+                      (some->> result
+                               (db-utils/with-repo repo)
+                               (model/with-block-refs-count repo)
+                               (model/sort-blocks)))
+                    result)]
+       (if-let [result-transform (:result-transform q)]
+         (if-let [f (sci/eval-string (pr-str result-transform))]
+           (try
+             (sci/call-fn f result)
+             (catch js/Error e
+               (log/error :sci/call-error e)
+               result))
+           result)
+         (if block?
+           (db-utils/group-by-page result)
+           result))))
+    (catch js/Error e
+      (log/error :query/failed e))))
+
+(defn- resolve-query
+  [query]
+  (let [page-ref? #(and (string? %) (text/page-ref? %))]
+    (walk/postwalk
+     (fn [f]
+       (if (and (list? f)
+                (= (first f) '=)
+                (= 3 (count f))
+                (some page-ref? (rest f)))
+         (let [[x y] (rest f)
+               [page-ref sym] (if (page-ref? x) [x y] [y x])
+               page-ref (string/lower-case page-ref)]
+           (list 'contains? sym (text/page-ref-un-brackets! page-ref)))
+         f)) query)))
+
+(defn react-query
+  [repo {:keys [query inputs] :as query'} query-opts]
+  (try
+    (let [query (resolve-query query)
+          inputs (map resolve-input inputs)
+          repo (or repo (state/get-current-repo))
+          k [:custom query']]
+      (apply react/q repo k query-opts query inputs))
+    (catch js/Error e
+      (println "Custom query failed: ")
+      (js/console.dir e))))

+ 36 - 55
src/main/frontend/db/react.cljs

@@ -283,46 +283,42 @@
   [repo-url tx-data {:keys [key data files-db?] :as handler-opts
                      :or {files-db? false}}]
   (when-not config/publishing?
-    (try
-      (let [repo-url (or repo-url (state/get-current-repo))
-            tx-data (->> (util/remove-nils tx-data)
-                         (remove nil?))
-            get-conn (fn [] (if files-db?
-                              (conn/get-files-conn repo-url)
-                              (conn/get-conn repo-url false)))]
-        (when (and (seq tx-data) (get-conn))
-          (let [tx-result (d/transact! (get-conn) (vec tx-data))
-                db (:db-after tx-result)
-                handler-keys (get-handler-keys handler-opts)]
-            (doseq [handler-key handler-keys]
-              (let [handler-key (vec (cons repo-url handler-key))]
-                (when-let [cache (get @query-state handler-key)]
-                  (let [{:keys [query inputs transform-fn query-fn inputs-fn]} cache]
-                    (when (or query query-fn)
-                      (let [new-result (->
-                                        (cond
-                                          query-fn
-                                          (profile
-                                           "Query:"
-                                           (doall (query-fn db)))
-
-                                          inputs-fn
-                                          (let [inputs (inputs-fn)]
-                                            (apply d/q query db inputs))
-
-                                          (keyword? query)
-                                          (db-utils/get-key-value repo-url query)
-
-                                          (seq inputs)
-                                          (apply d/q query db inputs)
-
-                                          :else
-                                          (d/q query db))
-                                        transform-fn)]
-                        (set-new-result! handler-key new-result))))))))))
-      (catch js/Error e
-        ;; FIXME: check error type and notice user
-        (log/error :db/transact! e)))))
+    (let [repo-url (or repo-url (state/get-current-repo))
+          tx-data (->> (util/remove-nils tx-data)
+                       (remove nil?))
+          get-conn (fn [] (if files-db?
+                            (conn/get-files-conn repo-url)
+                            (conn/get-conn repo-url false)))]
+      (when (and (seq tx-data) (get-conn))
+        (let [tx-result (d/transact! (get-conn) (vec tx-data))
+              db (:db-after tx-result)
+              handler-keys (get-handler-keys handler-opts)]
+          (doseq [handler-key handler-keys]
+            (let [handler-key (vec (cons repo-url handler-key))]
+              (when-let [cache (get @query-state handler-key)]
+                (let [{:keys [query inputs transform-fn query-fn inputs-fn]} cache]
+                  (when (or query query-fn)
+                    (let [new-result (->
+                                      (cond
+                                        query-fn
+                                        (profile
+                                         "Query:"
+                                         (doall (query-fn db)))
+
+                                        inputs-fn
+                                        (let [inputs (inputs-fn)]
+                                          (apply d/q query db inputs))
+
+                                        (keyword? query)
+                                        (db-utils/get-key-value repo-url query)
+
+                                        (seq inputs)
+                                        (apply d/q query db inputs)
+
+                                        :else
+                                        (d/q query db))
+                                      transform-fn)]
+                      (set-new-result! handler-key new-result))))))))))))
 
 (defn set-key-value
   [repo-url key value]
@@ -339,18 +335,3 @@
      (-> (q repo-url [:kv key] {} key key)
          react
          key))))
-
-(defn set-file-content!
-  [repo path content]
-  (when (and repo path)
-    (let [tx-data {:file/path path
-                   :file/content content
-                   :file/last-modified-at (util/time-ms)}
-          tx-data (if (config/local-db? repo)
-                    (dissoc tx-data :file/last-modified-at)
-                    tx-data)]
-      (transact-react!
-       repo
-       [tx-data]
-       {:key [:file/content path]
-        :files-db? true}))))

+ 6 - 2
src/main/frontend/db/utils.cljs

@@ -35,8 +35,12 @@
 (defn group-by-page
   [blocks]
   (some->> blocks
-           (group-by :block/page)
-           (sort-by (fn [[p _blocks]] (:page/last-modified-at p)) >)))
+           (group-by :block/page)))
+
+(defn group-by-file
+  [blocks]
+  (some->> blocks
+           (group-by :block/file)))
 
 (defn get-tx-id [tx-report]
   (get-in tx-report [:tempids :db/current-tx]))

+ 8 - 6
src/main/frontend/db_schema.cljs

@@ -5,7 +5,6 @@
 (def files-db-schema
   {:file/path {:db/unique :db.unique/identity}
    :file/content {}
-   :file/last-modified-at {}
    :file/size {}
    :file/handle {}})
 
@@ -13,9 +12,11 @@
 ;; a month journal file can have multiple pages,
 ;; also, each block can be treated as a page too.
 (def schema
-  {:schema/version {}
-   :db/type {}
-   :db/ident {:db/unique :db.unique/identity}
+  {:schema/version  {}
+   :db/type         {}
+   :db/ident        {:db/unique :db.unique/identity}
+   :db/encrypted?    {}
+   :db/encryption-keys {}
 
    ;; user
    :me/name {}
@@ -50,8 +51,6 @@
                :db/cardinality :db.cardinality/many}
    :page/journal? {}
    :page/journal-day {}
-   :page/created-at {}
-   :page/last-modified-at {}
 
    ;; block
    :block/uuid {:db/unique :db.unique/identity}
@@ -64,6 +63,9 @@
    ;; referenced pages
    :block/ref-pages {:db/valueType :db.type/ref
                      :db/cardinality :db.cardinality/many}
+   ;; referenced pages inherited from the parents
+   :block/path-ref-pages {:db/valueType   :db.type/ref
+                          :db/cardinality :db.cardinality/many}
 
    ;; Referenced pages
    ;; Notice: it's only for org mode, :tag1:tag2:

+ 262 - 8
src/main/frontend/dicts.cljs

@@ -17,7 +17,8 @@ title: $today
 :heading: true
 :END:
 ## Logseq is a _privacy-first_, _open-source_ platform for _knowledge_ sharing and management.
-## This is a 3 minutes tutorial on how to use Logseq. Let's get started!
+## This is a 3 minute tutorial on how to use Logseq. Let's get started!
+## (Feel free to edit anything, no change will be saved at this moment. If you do want to persist your work, click the **top-right** corner of the screen to connect LogSeq to either Github or local directory.)
 ## Here are some tips might be useful.
 #+BEGIN_TIP
 Click to edit any block.
@@ -115,6 +116,7 @@ title: How to take dummy notes?
         :on-boarding/sci-desc " - Small Clojure Interpreter"
         :on-boarding/isomorphic-git-desc " - A pure JavaScript implementation of git for node and browsers!"
         :help/about "About Logseq"
+        :help/roadmap "Roadmap"
         :help/bug "Bug report"
         :help/feature "Feature request"
         :help/changelog "Changelog"
@@ -141,8 +143,8 @@ title: How to take dummy notes?
         :help/select-nfs-browser "Please use another browser (like latest chrome) which support NFS features to open local directory."
         :undo "Undo"
         :redo "Redo"
-        :help/zoom-in "Zoom In"
-        :help/zoom-out "Zoom out"
+        :help/zoom-in "Zoom In/Forward"
+        :help/zoom-out "Zoom out/Back"
         :help/follow-link-under-cursor "Follow link under cursor"
         :help/open-link-in-sidebar "Open link in Sidebar"
         :expand "Expand"
@@ -157,8 +159,10 @@ title: How to take dummy notes?
         :help/context-menu "Context Menu"
         :help/fold-unfold "Fold/Unfold blocks (when not in edit mode)"
         :help/toggle-doc-mode "Toggle document mode"
+        :help/toggle-contents "Toggle Contents"
         :help/toggle-theme "Toggle between dark/light theme"
         :help/toggle-right-sidebar "Toggle right sidebar"
+        :help/toggle-settings "Toggle settings"
         :help/toggle-insert-new-block "Toggle Enter/Alt+Enter for inserting new block"
         :help/jump-to-journals "Jump to Journals"
         :formatting "Formatting"
@@ -222,6 +226,8 @@ title: How to take dummy notes?
         :page/re-index "Re-index this page"
         :page/copy-to-json "Copy the whole page as JSON"
         :page/rename "Rename page"
+        :page/open-in-finder "Open in directory"
+        :page/open-with-default-app "Open with default app"
         :page/action-publish "Publish"
         :page/make-public "Publish it when exporting to an html file"
         :page/make-private "Make it private"
@@ -283,6 +289,7 @@ title: How to take dummy notes?
         :settings-page/preferred-workflow "Preferred workflow"
         :settings-page/enable-timetracking "Enable timetracking"
         :settings-page/enable-journals "Enable journals"
+        :settings-page/enable-encryption "Enable encryption feature"
         :settings-page/home-default-page "Set the default home page"
         :settings-page/enable-block-time "Enable block timestamps"
         :settings-page/dont-use-other-peoples-proxy-servers "Don't use other people's proxy servers. It's very dangerous, which could make your token and notes stolen. Logseq will not be responsible for this loss if you use other people's proxy servers. You can deploy it yourself, check "
@@ -291,14 +298,17 @@ title: How to take dummy notes?
         :settings-page/enable-developer-mode "Enable developer mode"
         :settings-page/disable-developer-mode "Disable developer mode"
         :settings-page/developer-mode-desc "Developer mode helps contributors and extension developers test their integration with Logseq more efficient."
+        :settings-page/current-version "Current version"
         :logseq "Logseq"
         :dot-mode "Dot mode"
         :on "ON"
         :more-options "More options"
         :to "to"
         :yes "Yes"
+        :no "No"
         :submit "Submit"
         :cancel "Cancel"
+        :close "Close"
         :re-index "Re-index"
         :export-json "Export as JSON"
         :unlink "unlink"
@@ -308,6 +318,7 @@ title: How to take dummy notes?
         :new-page "New page"
         :new-file "New file"
         :graph "Graph"
+        :graph-view "View Graph"
         :publishing "Publishing"
         :export "Export public pages"
         :all-graphs "All graphs"
@@ -338,7 +349,239 @@ title: How to take dummy notes?
         :dark "Dark"
         :remove-background "Remove background"
         :open "Open"
-        :open-a-directory "Open a local directory"}
+        :open-a-directory "Open a local directory"
+        :user/delete-account "Delete account"
+        :user/delete-your-account "Delete your account"
+        :user/delete-account-notice "All your published pages on logseq.com will be deleted."}
+
+   :de {:help/about "Über Logseq"
+        :help/bug "Fehlerbericht"
+        :help/feature "Feature-Anfrage"
+        :help/changelog "Änderungsprotokoll"
+        :help/blog "Logseq Blog"
+        :help/docs "Dokumentation"
+        :help/privacy "Datenschutzrichtlinie"
+        :help/terms "Bedingungen"
+        :help/community "Discord-Community"
+        :help/shortcuts "Tastaturkürzel"
+        :help/shortcuts-triggers "Auslöser"
+        :help/shortcut "Tastaturkürzel"
+        :help/slash-autocomplete "/-Autovervollständigung"
+        :help/block-content-autocomplete "Blockinhalt (Quelltext, Zitate, Abfragen, etc.) Autovervollständigung"
+        :help/reference-autocomplete "Seitenverweis Autovervollständigung"
+        :help/block-reference "Blockverweis"
+        :help/key-commands "Tastenbefehle"
+        :help/working-with-lists " (mit Listen arbeiten)"
+        :help/indent-block-tab "Block einrücken"
+        :help/unindent-block "Block ausrücken"
+        :help/move-block-up "Block nach oben verschieben"
+        :help/move-block-down "Block nach unten verschieben"
+        :help/create-new-block "Neuen Block erstellen"
+        :help/new-line-in-block "Neue Zeile innerhalb des Blocks erstellen"
+        :help/select-nfs-browser "Bitte einen anderen Browser verwenden (z. B. den neuesten Chrome), der NFS-Funktionen unterstützt, um lokale Verzeichnisse zu öffnen."
+        :undo "Rückgängig machen"
+        :redo "Wiederholen"
+        :help/zoom-in "Heranzoomen"
+        :help/zoom-out "Herauszoomen"
+        :help/follow-link-under-cursor "Link unter dem Cursor folgen"
+        :help/open-link-in-sidebar "Link in Seitenleiste öffnen"
+        :expand "Erweitern"
+        :collapse "Zusammenklappen"
+        :select-block-above "Block oberhalb auswählen"
+        :select-block-below "Block unterhalb auswählen"
+        :select-all-blocks "Alle Blöcke auswählen"
+        :general "Allgemein"
+        :help/toggle "Hilfe aktivieren"
+        :help/git-commit-message "Git Commit-Nachricht"
+        :help/full-text-search "Volltextsuche"
+        :help/context-menu "Kontextmenü"
+        :help/fold-unfold "Blöcke ein-/ausklappen (wenn nicht im Bearbeitungsmodus)"
+        :help/toggle-doc-mode "Dokumentenmodus umschalten"
+        :help/toggle-theme "Umschalten zwischen dunklem/hellem Thema"
+        :help/toggle-right-sidebar "Rechte Seitenleiste umschalten"
+        :help/toggle-insert-new-block "Umschalten von Enter/Alt+Enter zum Einfügen eines neuen Blocks"
+        :help/jump-to-journals "Zu Journalen springen"
+        :formatting "Formatierung"
+        :help/markdown-syntax "Markdown-Syntax"
+        :help/org-mode-syntax "Org-Mode-Syntax"
+        :bold "Fett"
+        :italics "Kursiv"
+        :html-link "Html-Link"
+        :highlight "Hervorhebung"
+        :strikethrough "Durchstreichung"
+        :code "Quelltext"
+        :right-side-bar/help "Hilfe"
+        :right-side-bar/switch-theme "Zu {1} Thema wechseln"
+        :right-side-bar/theme "{1} Thema"
+        :right-side-bar/page "Seiten-Graph"
+        :right-side-bar/recent "Neueste"
+        :right-side-bar/contents "Inhalt"
+        :right-side-bar/graph-ref "Graph von "
+        :right-side-bar/block-ref "Blockreferenz"
+        :git/set-access-token "Persönliches Github-Zugangs-Token festlegen"
+        :git/token-is-encrypted "Das Token wird verschlüsselt und im lokalen Speicher des Browsers gespeichert"
+        :git/token-server "Auf dem Server wird es niemals gespeichert"
+        :git/create-personal-access-token "Wie erstellt man ein persönliches Github-Zugangs-Token?"
+        :git/push "Jetzt übertragen"
+        :git/push-failed "Übertragung fehlgeschlagen!"
+        :git/local-changes-synced "Alle lokalen Änderungen sind synchronisiert!"
+        :git/pull "Jetzt runterladen"
+        :git/last-pull "Zuletzt runtergeladen am"
+        :git/version "Version"
+        :git/import-notes "Notizen importieren"
+        :git/import-notes-helper "Notizen aus einem Repository auf Github importieren."
+        :git/add-another-repo "Ein weiteres Repository hinzufügen"
+        :git/re-index "Erneut runterladen und Datenbank neu indizieren"
+        :git/message "Ihre Commit-Nachricht"
+        :git/commit-and-push "Commit und Hochladen!"
+        :git/use-remote "Entfernte Version nutzen"
+        :git/keep-local "Lokale Version beibehalten"
+        :git/edit "Bearbeiten"
+        :git/title "Unterschiede"
+        :git/no-diffs "Keine Unterschiede"
+        :git/commit-message "Commit-Nachricht (optional)"
+        :git/pushing "Hochladen"
+        :git/force-push "Commit und Hochladen forcieren"
+        :git/a-force-push "Ein forciertes Hochladen"
+        :git/add-repo-prompt "Logseq im Repository installieren"
+        :git/add-repo-prompt-confirm "Hinzufügen und installieren"
+        :format/preferred-mode "Was ist Ihr bevorzugter Modus?"
+        :format/markdown "Markdown"
+        :format/org-mode "Org-Mode"
+        :reference/linked "Verknüpfte Referenz"
+        :reference/unlinked-ref "Unverknüpfte Referenz"
+        :project/setup "Einrichten eines öffentlichen Projekts auf Logseq"
+        :project/location "Alle veröffentlichten Seiten befinden sich unter"
+        :project/sync-settings "Projekteinstellungen synchronisieren"
+        :page/presentation-mode "Präsentationsmodus (angetrieben von Reveal.js)"
+        :page/edit-properties-placeholder "Klicken Sie hier, um die Eigenschaften dieser Seite zu bearbeiten"
+        :page/delete-success "Die Seite {1} wurde erfolgreich gelöscht!"
+        :page/delete-confirmation "Diese Seite und die zugehörige Datei löschen?"
+        :page/rename-to "\"{1}\" umbenennen nach:"
+        :page/priority "Priorität \"{1}\""
+        :page/re-index "Diese Seite neu indizieren"
+        :page/copy-to-json "Gesamte Seite als JSON kopieren"
+        :page/rename "Seite umbenennen"
+        :page/open-in-finder "Im Verzeichnis öffnen"
+        :page/open-with-default-app "Mit Standard-Anwendung öffnen"
+        :page/action-publish "Veröffentlichen"
+        :page/make-public "Beim Export in HTML veröffentlichen"
+        :page/make-private "Privat machen"
+        :page/delete "Seite löschen"
+        :page/publish "Diese Seite auf Logseq veröffentlichen"
+        :page/cancel-publishing "Veröffentlichung auf Logseq abbrechen"
+        :page/publish-as-slide "Diese Seite als Folie auf Logseq veröffentlichen"
+        :page/unpublish "Veröffentlichung dieser Seite auf Logseq rückgängig machen"
+        :page/add-to-contents "Zum Inhaltsverzeichnis hinzufügen"
+        :page/show-journals "Journal anzeigen"
+        :page/show-name "Seitennamen anzeigen"
+        :page/hide-name "Seitennamen verbergen"
+        :page/name "Seitenname"
+        :page/last-modified "Zuletzt geändert am"
+        :page/new-title "Wie lautet der neue Seitenname?"
+        :publishing/pages "Seiten"
+        :publishing/page-name "Seitenname"
+        :publishing/current-project "Aktuelles Projekt"
+        :publishing/delete-from-logseq "Vom Logseq Server löschen"
+        :publishing/edit "Bearbeiten"
+        :publishing/save "Speichern"
+        :publishing/cancel "Abbrechen"
+        :publishing/delete "Löschen"
+        :journal/multiple-files-with-different-formats "Es scheint, dass Sie mehrere Journaldateien (mit unterschiedlichen Formaten) für denselben Monat haben, bitte führen Sie nur eine Journaldatei für jeden Monat."
+        :journal/go-to "Zu Dateien gehen"
+        :file/name "Dateinamen"
+        :file/file "Datei: "
+        :file/last-modified-at "Zuletzt geändert am"
+        :file/no-data "Keine Daten"
+        :file/format-not-supported "Format .{1} wird nicht unterstützt."
+        :editor/block-search "Nach einem Block suchen"
+        :editor/image-uploading "Hochladen"
+        :draw/invalid-file "Diese ungültige Excalidraw-Datei konnte nicht geladen werden"
+        :draw/specify-title "Bitte zuerst einen Titel angeben!"
+        :draw/rename-success "Die Datei wurde erfolgreich umbenannt!"
+        :draw/rename-failure "Umbennenung der Datei fehlgeschlagen, Grund: "
+        :draw/title-placeholder "Unbenannt"
+        :draw/save "Speichern"
+        :draw/save-changes "Änderungen speichern"
+        :draw/new-file "Neue Datei"
+        :draw/list-files "Dateien auflisten"
+        :draw/delete "Löschen"
+        :draw/more-options "Weitere Optionen"
+        :draw/back-to-logseq "Zurück zu Logseq"
+        :text/image "Bild"
+        :asset/confirm-delete "{1} wirklich löschen?"
+        :asset/physical-delete "Datei ebenfalls entfernen (Achtung: die Datei kann nicht wiederhergestellt werden)"
+        :content/copy "Kopieren"
+        :content/cut "Ausschneiden"
+        :content/make-todos "{1}s erstellen"
+        :content/copy-block-ref "Blockreferenz kopieren"
+        :content/focus-on-block "Auf Block fokussieren"
+        :content/open-in-sidebar "In Seitenleiste öffnen"
+        :content/copy-as-json "Als JSON kopieren"
+        :content/click-to-edit "Klicken zum bearbeiten"
+        :settings-page/edit-config-edn "config.edn bearbeiten (im aktuellen Repository)"
+        :settings-page/show-brackets "Klammern anzeigen"
+        :settings-page/preferred-file-format "Bevorzugtes Datei-Format"
+        :settings-page/preferred-workflow "Bevorzugter Workflow"
+        :settings-page/enable-timetracking "Zeiterfassung einschalten"
+        :settings-page/enable-journals "Journale einschalten"
+        :settings-page/home-default-page "Standard-Homepage einrichten"
+        :settings-page/enable-block-time "Zeitstempel für Blöcke aktivieren"
+        :settings-page/dont-use-other-peoples-proxy-servers "Verwenden Sie keine fremden Proxyserver. Hierbei können das Token und die Notizen gelesen werden. Logseq ist für kann die Sicherheit nicht garantieren, wenn Sie die Proxy-Server anderer Leute verwenden. Sie können selber einen Proxy-Server einrichten, mehr dazu unter "
+        :settings-page/custom-cors-proxy-server "Benutzerdefinierter CORS-Proxy-Server"
+        :settings-page/developer-mode "Entwicklermodus"
+        :settings-page/enable-developer-mode "Entwicklermodus aktivieren"
+        :settings-page/disable-developer-mode "Entwicklermodus deaktivieren"
+        :settings-page/developer-mode-desc "Der Entwicklermodus hilft Mitwirkenden und Erweiterungsentwicklern, ihre Integration mit Logseq effizienter zu testen."
+        :settings-page/current-version "Aktuelle Version"
+        :logseq "Logseq"
+        :dot-mode "Punktmodus"
+        :on "AN"
+        :more-options "Weitere Optionen"
+        :to "zu"
+        :yes "Ja"
+        :submit "Senden"
+        :cancel "Abbrechen"
+        :re-index "Neu-Indizieren"
+        :export-json "Als JSON exportieren"
+        :unlink "Verknüpfung aufheben"
+        :search (if config/publishing?
+                  "Suchen"
+                  "Suchen oder Seite erstellen")
+        :new-page "Neue Seite"
+        :new-file "Neue Datei"
+        :graph "Graph"
+        :publishing "Veröffentlichung"
+        :export "Öffentliche Seiten exportieren"
+        :all-graphs "Alle Graphen"
+        :all-pages "Alle Seiten"
+        :all-files "Alle Dateien"
+        :all-journals "Alle Journale"
+        :my-publishing "Meine Veröffentlichungen"
+        :settings "Einstellungen"
+        :import "Importieren"
+        :join-community "Community"
+        :sponsor-us "Sponsern"
+        :discord-title "Unsere Discord-Gruppe!"
+        :sign-out "Abmelden"
+        :help-shortcut-title "Hier klicken, um Tastenkombinationen und weitere Tipps zu sehen"
+        :loading "Laden"
+        :cloning "Klonen"
+        :parsing-files "Dateien analysieren"
+        :loading-files "Dateien laden"
+        :login-github "Einloggen mit Github"
+        :login-google "Einloggen mit Google"
+        :login "Einloggen"
+        :go-to "Gehe zu "
+        :or "oder"
+        :download "Herunterladen"
+        :repo/download-zip "Alle Dateien als Zip herunterladen"
+        :language "Sprache"
+        :white "Hell"
+        :dark "Dunkel"
+        :remove-background "Hintergrund entfernen"
+        :open "Öffnen"
+        :open-a-directory "Öffne ein lokales Verzeichnis"}
 
    :fr {:help/about "A propos de Logseq"
         :help/bug "Signaler une anomalie"
@@ -628,7 +871,7 @@ title: How to take dummy notes?
            :undo "撤销"
            :redo "重做"
            :help/zoom-in "聚焦"
-           :help/zoom-out "出聚焦"
+           :help/zoom-out "退出聚焦"
            :help/follow-link-under-cursor "跟随光标下的链接"
            :help/open-link-in-sidebar "在侧边栏打开"
            :expand "展开"
@@ -643,8 +886,10 @@ title: How to take dummy notes?
            :help/context-menu "右键菜单"
            :help/fold-unfold "折叠/展开方块(不在编辑模式中)"
            :help/toggle-doc-mode "切换文档模式"
+           :help/toggle-contents "打开/关闭目录"
            :help/toggle-theme "“在暗色/亮色主题之间切换”"
            :help/toggle-right-sidebar "启用/关闭右侧栏"
+           :help/toggle-settings "显示/关闭设置"
            :help/toggle-insert-new-block "切换 Enter/Alt+Enter 以插入新块"
            :help/jump-to-journals "跳转到日记"
            :formatting "格式化"
@@ -708,6 +953,8 @@ title: How to take dummy notes?
            :page/re-index "对此页面重新建立索引"
            :page/copy-to-json "将整页以 JSON 格式复制"
            :page/rename "重命名本页"
+           :page/open-in-finder "打开文件对应目录"
+           :page/open-with-default-app "用默认应用打开文件"
            :page/action-publish "发布"
            :page/make-public "导出 HTML 时发布本页面"
            :page/make-private "导出 HTML 时取消发布本页面"
@@ -766,6 +1013,7 @@ title: How to take dummy notes?
            :settings-page/preferred-workflow "首选工作流"
            :settings-page/enable-timetracking "开启 timetracking"
            :settings-page/enable-journals "开启日记"
+           :settings-page/enable-encryption "激活加密功能"
            :settings-page/home-default-page "设置首页默认页面"
            :settings-page/enable-block-time "记录 block 创建/修改时间"
            :settings-page/dont-use-other-peoples-proxy-servers "不要使用其他人的代理服务器。这非常危险,可能会使您的令牌和笔记被盗。 如果您使用其他人的代理服务器,Logseq 将不会对此损失负责。您可以自己部署它,请查阅 "
@@ -774,6 +1022,7 @@ title: How to take dummy notes?
            :settings-page/enable-developer-mode "启用开发者模式"
            :settings-page/disable-developer-mode "禁用开发者模式"
            :settings-page/developer-mode-desc "开发者模式帮助贡献者和扩展开发者更有效地测试他们与 Logseq 的集成。"
+           :settings-page/current-version "当前版本"
            :logseq "Logseq"
            :dot-mode "点模式"
            :on "已打开"
@@ -791,6 +1040,7 @@ title: How to take dummy notes?
            :new-page "新页面"
            :new-file "新文件"
            :graph "图谱"
+           :graph-view "全局图谱"
            :publishing "发布"
            :export "导出公开页面"
            :all-graphs "所有库"
@@ -803,7 +1053,7 @@ title: How to take dummy notes?
            :sponsor-us "赞助我们!"
            :discord-title "我们的 Discord 社群!"
            :sign-out "登出"
-           :help-shortcut-title "点此查看快捷方式和更多游泳帮助"
+           :help-shortcut-title "点此查看快捷方式和更多有用帮助"
            :loading "加载中"
            :cloning "Clone 中"
            :parsing-files "正在解析文件"
@@ -820,7 +1070,10 @@ title: How to take dummy notes?
            :dark "暗黑"
            :remove-background "去除背景"
            :open "打开"
-           :open-a-directory "打开本地文件夹"}
+           :open-a-directory "打开本地文件夹"
+           :user/delete-account "删除帐号"
+           :user/delete-your-account "删除你的帐号"
+           :user/delete-account-notice "你在 logseq.com 发布的页面(假如有的话)也会被删除。"}
 
    :zh-Hant {:on-boarding/title "你好,歡迎使用 Logseq!"
              :on-boarding/sharing "分享"
@@ -1059,7 +1312,7 @@ title: How to take dummy notes?
              :join-community "加入社區"
              :discord-title "我們的 Discord 社群!"
              :sign-out "登出"
-             :help-shortcut-title "點此查看快捷方式和更多游泳幫助"
+             :help-shortcut-title "點此查看快捷方式和更多有用幫助"
              :loading "加載中"
              :cloning "Clone 中"
              :parsing-files "正在解析文件"
@@ -1323,6 +1576,7 @@ title: How to take dummy notes?
 
 (def languages [{:label "English" :value :en}
                 {:label "Français" :value :fr}
+                {:label "Deutsch" :value :de}
                 {:label "简体中文" :value :zh-CN}
                 {:label "繁體中文" :value :zh-Hant}
                 {:label "Afrikaans" :value :af}])

+ 104 - 0
src/main/frontend/encrypt.cljs

@@ -0,0 +1,104 @@
+(ns frontend.encrypt
+  (:require [frontend.utf8 :as utf8]
+            [frontend.db.utils :as db-utils]
+            [frontend.db :as db]
+            [promesa.core :as p]
+            [frontend.state :as state]
+            [clojure.string :as str]
+            [cljs.reader :as reader]
+            [shadow.loader :as loader]
+            [lambdaisland.glogi :as log]))
+
+(defonce age-pem-header-line "-----BEGIN AGE ENCRYPTED FILE-----")
+(defonce age-version-line "age-encryption.org/v1")
+
+(defn content-encrypted?
+  [content]
+  (when content
+    (or (str/starts-with? content age-pem-header-line)
+        (str/starts-with? content age-version-line))))
+
+(defn encrypted-db?
+  [repo-url]
+  (db-utils/get-key-value repo-url :db/encrypted?))
+
+(defn get-key-pair
+  [repo-url]
+  (db-utils/get-key-value repo-url :db/encryption-keys))
+
+(defn save-key-pair!
+  [repo-url keys]
+  (let [keys (if (string? keys) (reader/read-string keys) keys)]
+    (db/set-key-value repo-url :db/encryption-keys keys)
+    (db/set-key-value repo-url :db/encrypted? true)))
+
+(defn generate-key-pair
+  []
+  (p/let [_ (loader/load :age-encryption)
+          lazy-keygen (resolve 'frontend.extensions.age-encryption/keygen)
+          js-keys (lazy-keygen)]
+    (array-seq js-keys)))
+
+(defn generate-key-pair-and-save!
+  [repo-url]
+  (when-not (get-key-pair repo-url)
+    (p/let [keys (generate-key-pair)]
+      (save-key-pair! repo-url keys)
+      (pr-str keys))))
+
+(defn get-public-key
+  [repo-url]
+  (second (get-key-pair repo-url)))
+
+(defn get-secret-key
+  [repo-url]
+  (first (get-key-pair repo-url)))
+
+(defn encrypt
+  ([content]
+   (encrypt (state/get-current-repo) content))
+  ([repo-url content]
+   (cond
+     (encrypted-db? repo-url)
+     (p/let [_ (loader/load :age-encryption)
+             lazy-encrypt-with-x25519 (resolve 'frontend.extensions.age-encryption/encrypt-with-x25519)
+             content (utf8/encode content)
+             public-key (get-public-key repo-url)
+             encrypted (lazy-encrypt-with-x25519 public-key content true)]
+       (utf8/decode encrypted))
+     :else
+     (p/resolved content))))
+
+(defn decrypt
+  ([content]
+   (decrypt (state/get-current-repo) content))
+  ([repo-url content]
+   (cond
+     (and (encrypted-db? repo-url)
+          (content-encrypted? content))
+     (let [content (utf8/encode content)]
+       (if-let [secret-key (get-secret-key repo-url)]
+         (p/let [_ (loader/load :age-encryption)
+                 lazy-decrypt-with-x25519 (resolve 'frontend.extensions.age-encryption/decrypt-with-x25519)
+                 decrypted (lazy-decrypt-with-x25519 secret-key content)]
+           (utf8/decode decrypted))
+         (log/error :encrypt/empty-secret-key (str "Can't find the secret key for repo: " repo-url))))
+     :else
+     (p/resolved content))))
+
+(defn encrypt-with-passphrase
+  [passphrase content]
+  (p/let [_ (loader/load :age-encryption)
+          lazy-encrypt-with-user-passphrase (resolve 'frontend.extensions.age-encryption/encrypt-with-user-passphrase)
+          content (utf8/encode content)
+          encrypted (@lazy-encrypt-with-user-passphrase passphrase content true)]
+    (utf8/decode encrypted)))
+
+;; ;; TODO: What if decryption failed
+(defn decrypt-with-passphrase
+  [passphrase content]
+  (p/let [_ (loader/load :age-encryption)
+          lazy-decrypt-with-user-passphrase (resolve 'frontend.extensions.age-encryption/decrypt-with-user-passphrase)
+          content (utf8/encode content)
+          decrypted (lazy-decrypt-with-user-passphrase passphrase content)]
+    (utf8/decode decrypted)))

+ 23 - 0
src/main/frontend/extensions/age_encryption.cljs

@@ -0,0 +1,23 @@
+(ns frontend.extensions.age-encryption
+  (:require ["regenerator-runtime/runtime"] ;; required for async npm module
+            ["@kanru/rage-wasm" :as rage]))
+
+(defn keygen
+  []
+  (rage/keygen))
+
+(defn encrypt-with-x25519
+  [public-key content armor]
+  (rage/encrypt_with_x25519 public-key content armor))
+
+(defn decrypt-with-x25519
+  [secret-key content]
+  (rage/decrypt_with_x25519 secret-key content))
+
+(defn encrypt-with-user-passphrase
+  [passphrase content armor]
+  (rage/encrypt_with_user_passphrase passphrase content armor))
+
+(defn decrypt-with-user-passphrase
+  [passphrase content]
+  (rage/decrypt_with_user_passphrase passphrase content))

+ 149 - 0
src/main/frontend/extensions/excalidraw.cljs

@@ -0,0 +1,149 @@
+(ns frontend.extensions.excalidraw
+  (:require [rum.core :as rum]
+            [goog.object :as gobj]
+            [frontend.rum :as r]
+            [frontend.util :as util :refer-macros [profile]]
+            [frontend.mixins :as mixins]
+            [frontend.storage :as storage]
+            [frontend.components.svg :as svg]
+            [cljs-bean.core :as bean]
+            [dommy.core :as d]
+            [clojure.string :as string]
+            [frontend.handler.notification :as notification]
+            [frontend.handler.draw :as draw]
+            [frontend.handler.file :as file]
+            [frontend.handler.ui :as ui-handler]
+            [frontend.ui :as ui]
+            [frontend.loader :as loader]
+            [frontend.config :as config]
+            [frontend.state :as state]
+            [frontend.search :as search]
+            [frontend.components.repo :as repo]
+            [promesa.core :as p]
+            [reitit.frontend.easy :as rfe]
+            ["@excalidraw/excalidraw" :as Excalidraw]))
+
+(def excalidraw (r/adapt-class (gobj/get Excalidraw "default")))
+
+(defn from-json
+  [text]
+  (when-not (string/blank? text)
+    (try
+      (js/JSON.parse text)
+      (catch js/Error e
+        (println "from json error:")
+        (js/console.dir e)
+        (notification/show!
+         (util/format "Could not load this invalid excalidraw file")
+         :error)))))
+
+(defonce *bounding-width (atom nil))
+(defn- get-bounding-width
+  [ref]
+  (when ref
+    (when-let [current (gobj/get ref "current")]
+      (-> current
+         (.getBoundingClientRect)
+         (gobj/get "width")))))
+
+(defn- update-draw-content-width
+  [state]
+  (let [el ^js (rum/dom-node state)
+        el (and el (.querySelector el ".draw-wrap"))
+        width (and el (.-clientWidth el))]
+    (reset! (::draw-width state) width)
+    state))
+
+(rum/defcs draw-inner < rum/reactive
+  (rum/local 800 ::draw-width)
+  (rum/local true ::zen-mode?)
+  (rum/local false ::view-mode?)
+  (rum/local nil ::elements)
+  {:did-mount update-draw-content-width}
+  {:did-update update-draw-content-width}
+  [state data option]
+  (let [current-repo (state/sub :git/current-repo)
+        bounding-width (rum/react *bounding-width)
+        *draw-width (get state ::draw-width)
+        *zen-mode? (get state ::zen-mode?)
+        *view-mode? (get state ::view-mode?)
+        wide-mode? (state/sub :ui/wide-mode?)
+        *elements (get state ::elements)
+        file (:file option)]
+    (when data
+      [:div.overflow-hidden
+       [:div.my-1 {:style {:font-size 10}}
+        [:a.mr-2 {:on-click ui-handler/toggle-wide-mode!}
+         (util/format "Wide Mode (%s)" (if wide-mode? "ON" "OFF"))]
+        [:a.mr-2 {:on-click #(swap! *zen-mode? not)}
+         (util/format "Zen Mode (%s)" (if @*zen-mode? "ON" "OFF"))]
+        [:a.mr-2 {:on-click #(swap! *view-mode? not)}
+         (util/format "View Mode (%s)" (if @*view-mode? "ON" "OFF"))]]
+       [:div.draw-wrap
+        (excalidraw
+         (merge
+          {:on-change (fn [elements state]
+                        (let [elements->clj (bean/->clj elements)]
+                          (when (and (seq elements->clj)
+                                     (not= elements @*elements))
+                            (let [state (bean/->clj state)]
+                              (draw/save-excalidraw!
+                               file
+                               (-> {:type "excalidraw"
+                                    :version 2
+                                    :source config/website
+                                    :elements elements
+                                    :appState (select-keys state [:gridSize :viewBackgroundColor])}
+                                   bean/->js
+                                   (js/JSON.stringify)))
+                              (reset! *elements elements)))))
+           :zen-mode-enabled @*zen-mode?
+           :view-mode-enabled @*view-mode?
+           :grid-mode-enabled false
+           :initial-data data
+           :width  @*draw-width}
+          (if wide-mode?
+            {:height 650}
+            {:height 500})))]])))
+
+(rum/defcs draw-container < rum/reactive
+  {:init (fn [state]
+           (let [[option] (:rum/args state)
+                 file (:file option)
+                 *data (atom nil)
+                 *loading? (atom true)]
+             (when file
+               (draw/load-excalidraw-file
+                file
+                (fn [data]
+                  (let [data (from-json data)]
+                    (reset! *data data)
+                    (reset! *loading? false)))))
+             (assoc state
+                    ::data *data
+                    ::loading? *loading?)))}
+  [state option]
+  (let [*data (get state ::data)
+        *loading? (get state ::loading?)
+        loading? (rum/react *loading?)
+        data (rum/react *data)
+        db-restoring? (state/sub :db/restoring?)]
+    (when (:file option)
+      (cond
+        db-restoring?
+        [:div.ls-center
+         (ui/loading "Loading")]
+
+        (false? loading?)
+        (draw-inner data option)
+
+        :else                           ; loading
+        nil))))
+
+(rum/defc draw < rum/reactive
+  [option]
+  (let [repo (state/get-current-repo)
+        granted? (state/sub [:nfs/user-granted? repo])]
+    ;; Web granted
+    (when-not (and (config/local-db? repo) (not granted?) (not (util/electron?)))
+      (draw-container option))))

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

@@ -18,7 +18,8 @@
     (try
       (js/katex.render s (gdom/getElement id)
                       #js {:displayMode display?
-                           :throwOnError false})
+                           :throwOnError false
+                           :strict false})
       (catch js/Error e
         (js/console.error e)))))
 

+ 20 - 15
src/main/frontend/extensions/slide.cljs

@@ -29,28 +29,33 @@
               (bean/->js
                {:embedded true
                 :controls true
-                :history true
+                :history false
                 :center true
                 :transition "slide"}))]
     (.initialize deck)))
 
 (defn slide-content
   [loading? style sections]
-  [:div.reveal {:style style}
-   (when loading?
-     [:div.ls-center (ui/loading "")])
-   [:div.slides
-    (for [[idx sections] (medley/indexed sections)]
-      (if (> (count sections) 1)       ; nested
-        [:section {:key (str "slide-section-" idx)}
-         (for [[idx2 [block block-cp]] (medley/indexed sections)]
-           [:section (-> {:key (str "slide-section-" idx "-" idx2)}
+  [:div
+   [:p.text-sm
+    [:span.opacity-70 "Tip: press "]
+    [:code "F"]
+    [:span.opacity-70 " to go fullscreen"]]
+   [:div.reveal {:style style}
+    (when loading?
+      [:div.ls-center (ui/loading "")])
+    [:div.slides
+     (for [[idx sections] (medley/indexed sections)]
+       (if (> (count sections) 1)       ; nested
+         [:section {:key (str "slide-section-" idx)}
+          (for [[idx2 [block block-cp]] (medley/indexed sections)]
+            [:section (-> {:key (str "slide-section-" idx "-" idx2)}
+                          (with-properties block))
+             block-cp])]
+         (let [[block block-cp] (first sections)]
+           [:section (-> {:key (str "slide-section-" idx)}
                          (with-properties block))
-            block-cp])]
-        (let [[block block-cp] (first sections)]
-          [:section (-> {:key (str "slide-section-" idx)}
-                        (with-properties block))
-           block-cp])))]])
+            block-cp])))]]])
 
 (rum/defc slide < rum/reactive
   {:did-mount (fn [state]

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