浏览代码

chore: merge latest changes to filters

Tienson Qin 4 年之前
父节点
当前提交
74b42aa5a7
共有 100 个文件被更改,包括 5316 次插入2008 次删除
  1. 303 0
      .github/workflows/build-desktop-release.yml
  2. 53 0
      .github/workflows/build-stage.yml
  3. 3 0
      .gitignore
  4. 1 1
      Dockerfile
  5. 52 10
      README.md
  6. 17 13
      deps.edn
  7. 35 0
      docs/Build LogSeq Desktop for windows on Ubuntu.md
  8. 66 0
      docs/assets/jetbrains.svg
  9. 20 0
      externs.js
  10. 46 1
      gulpfile.js
  11. 24 7
      package.json
  12. 0 0
      public/index.html
  13. 135 64
      resources/css/common.css
  14. 39 39
      resources/css/inter.css
  15. 2 2
      resources/css/style.css
  16. 25 0
      resources/dev.html
  17. 109 0
      resources/electron-dev.html
  18. 110 0
      resources/electron.html
  19. 12 0
      resources/entitlements.plist
  20. 52 0
      resources/forge.config.js
  21. 二进制
      resources/icons/logseq.icns
  22. 二进制
      resources/icons/logseq.ico
  23. 二进制
      resources/icons/logseq.png
  24. 二进制
      resources/icons/logseq_big_sur.icns
  25. 二进制
      resources/icons/logseq_big_sur.ico
  26. 二进制
      resources/icons/logseq_big_sur.png
  27. 二进制
      resources/img/dmg-bg.png
  28. 1 0
      resources/js/interact.min.js
  29. 176 0
      resources/js/isomorphic-git/1.7.4/http-web-index.umd.js
  30. 0 0
      resources/js/isomorphic-git/1.7.4/index.umd.min.js
  31. 124 0
      resources/js/preload.js
  32. 4 4
      resources/js/worker.js
  33. 33 0
      resources/package.json
  34. 17 0
      scripts/publishing.sh
  35. 20 5
      shadow-cljs.edn
  36. 5 0
      src/dev-cljs/shadow/user.clj
  37. 133 0
      src/electron/electron/core.cljs
  38. 154 0
      src/electron/electron/handler.cljs
  39. 125 0
      src/electron/electron/updater.cljs
  40. 11 0
      src/electron/electron/utils.cljs
  41. 1 1
      src/main/api.cljs
  42. 9 0
      src/main/electron/ipc.cljs
  43. 26 0
      src/main/electron/listener.cljs
  44. 33 2
      src/main/frontend/commands.cljs
  45. 311 158
      src/main/frontend/components/block.cljs
  46. 108 6
      src/main/frontend/components/block.css
  47. 5 1
      src/main/frontend/components/commit.cljs
  48. 38 12
      src/main/frontend/components/content.cljs
  49. 6 4
      src/main/frontend/components/diff.cljs
  50. 121 474
      src/main/frontend/components/editor.cljs
  51. 8 0
      src/main/frontend/components/editor.css
  52. 177 0
      src/main/frontend/components/encryption.cljs
  53. 8 7
      src/main/frontend/components/file.cljs
  54. 2 0
      src/main/frontend/components/file.css
  55. 128 103
      src/main/frontend/components/header.cljs
  56. 23 6
      src/main/frontend/components/header.css
  57. 20 13
      src/main/frontend/components/journal.cljs
  58. 9 3
      src/main/frontend/components/onboarding.cljs
  59. 92 53
      src/main/frontend/components/page.cljs
  60. 1 1
      src/main/frontend/components/page.css
  61. 5 2
      src/main/frontend/components/project.cljs
  62. 5 2
      src/main/frontend/components/reference.cljs
  63. 63 36
      src/main/frontend/components/repo.cljs
  64. 94 66
      src/main/frontend/components/right_sidebar.cljs
  65. 77 25
      src/main/frontend/components/search.cljs
  66. 38 7
      src/main/frontend/components/search.css
  67. 299 158
      src/main/frontend/components/settings.cljs
  68. 107 0
      src/main/frontend/components/settings.css
  69. 41 28
      src/main/frontend/components/sidebar.cljs
  70. 45 5
      src/main/frontend/components/sidebar.css
  71. 64 1
      src/main/frontend/components/svg.cljs
  72. 22 10
      src/main/frontend/components/theme.cljs
  73. 137 13
      src/main/frontend/components/theme.css
  74. 93 52
      src/main/frontend/components/widgets.cljs
  75. 5 3
      src/main/frontend/config.cljs
  76. 2 2
      src/main/frontend/core.cljs
  77. 13 19
      src/main/frontend/date.cljs
  78. 16 9
      src/main/frontend/db.cljs
  79. 3 0
      src/main/frontend/db/conn.cljs
  80. 9 0
      src/main/frontend/db/default.cljs
  81. 203 96
      src/main/frontend/db/model.cljs
  82. 53 28
      src/main/frontend/db/query_custom.cljs
  83. 70 42
      src/main/frontend/db/query_dsl.cljs
  84. 2 23
      src/main/frontend/db/react.cljs
  85. 10 6
      src/main/frontend/db/utils.cljs
  86. 13 11
      src/main/frontend/db_schema.cljs
  87. 287 17
      src/main/frontend/dicts.cljs
  88. 22 3
      src/main/frontend/diff.cljs
  89. 103 0
      src/main/frontend/encrypt.cljs
  90. 23 0
      src/main/frontend/extensions/age_encryption.cljs
  91. 1 0
      src/main/frontend/extensions/code.cljs
  92. 2 1
      src/main/frontend/extensions/code.css
  93. 20 15
      src/main/frontend/extensions/slide.cljs
  94. 0 0
      src/main/frontend/external.cljc
  95. 2 2
      src/main/frontend/external/protocol.cljc
  96. 20 5
      src/main/frontend/external/roam.cljc
  97. 136 61
      src/main/frontend/format/block.cljs
  98. 33 10
      src/main/frontend/format/mldoc.cljs
  99. 44 0
      src/main/frontend/format/mldoc_test.cljs
  100. 101 260
      src/main/frontend/fs.cljs

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

@@ -0,0 +1,303 @@
+# 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
+        run: mv static/out/make/zip/linux/x64/Logseq-linux-x64-*.zip  static/out/make/zip/linux/x64/Logseq-linux.zip
+
+      - name: Cache Artifact
+        uses: actions/upload-artifact@v1
+        with:
+          name: Logseq-linux.zip
+          path: static/out/make/zip/linux/x64/Logseq-linux.zip
+
+  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
+        uses: actions/download-artifact@v1
+        with:
+          name: Logseq-linux.zip
+          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
+        id: upload-linux-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-linux.zip
+          asset_name: logseq-linux-x64-${{ github.event.inputs.tag-version }}.zip
+          asset_content_type: application/zip
+
+      - 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

+ 53 - 0
.github/workflows/build-stage.yml

@@ -0,0 +1,53 @@
+# This is a basic workflow to help you get started with Actions
+
+name: Build-Stage
+
+on:
+#  push:
+#    branches: [master, stage]
+
+  workflow_dispatch:
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    env:
+      asset-path: ${GITHUB_REF##*/}/static/js/
+
+    steps:
+      - uses: actions/checkout@v2
+
+      - name: Setup Java JDK
+        uses: actions/[email protected]
+        with:
+          java-version: 1.8
+
+      - name: Set up Node
+        uses: actions/setup-node@v1
+        with:
+            node-version: '12'
+
+      - 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: Fetch yarn deps
+        run: yarn cache clean && yarn install --frozen-lockfile
+
+      - name: Build Released-Web
+        run: yarn gulp:build && clojure -M:cljs release app  --config-merge '{:asset-path "${{env.asset-path}}"}'
+
+      - uses: jakejarvis/s3-sync-action@master
+        with:
+            #args: --acl public-read --follow-symlinks --delete
+            args: --acl public-read --follow-symlinks
+        env:
+          AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
+          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
+          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
+          AWS_REGION: 'us-west-1'   # optional: defaults to us-east-1
+          SOURCE_DIR: 'static'      # optional: defaults to entire repository
+          DEST_DIR: ${GITHUB_REF##*/}/static
+

+ 3 - 0
.gitignore

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

+ 1 - 1
Dockerfile

@@ -1,4 +1,4 @@
-FROM clojure:openjdk-11-tools-deps
+FROM clojure:openjdk-11-tools-deps-1.10.0.442
 
 
 RUN curl -sL https://deb.nodesource.com/setup_15.x | bash - && \
 RUN curl -sL https://deb.nodesource.com/setup_15.x | bash - && \
     apt-get install -y nodejs
     apt-get install -y nodejs

+ 52 - 10
README.md

@@ -15,7 +15,8 @@ Use it to organize your todo list, to write your journals, or to record your uni
 
 
 ## Why Logseq?
 ## Why Logseq?
 
 
-[Logseq](https://logseq.com) is an open-source platform for knowledge sharing and management. It focuses on privacy, longevity, and user control.
+[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 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).
 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).
 
 
@@ -24,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)
 ![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
 ## Feature requests
+
 Please go to https://discuss.logseq.com/c/feature-requests/7.
 Please go to https://discuss.logseq.com/c/feature-requests/7.
 
 
 ## How can I use it?
 ## 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/>.
 2. Visit our website <https://logseq.com/>.
 
 
@@ -59,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
 - 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!
 - 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.
 The following is for developers and designers who want to build and run Logseq locally and contribute to this project.
 
 
@@ -68,11 +70,11 @@ The following is for developers and designers who want to build and run Logseq l
 ### 1. Requirements
 ### 1. Requirements
 
 
 - [Node.js](https://nodejs.org/en/download/) & [Yarn](https://classic.yarnpkg.com/en/docs/install/)
 - [Node.js](https://nodejs.org/en/download/) & [Yarn](https://classic.yarnpkg.com/en/docs/install/)
-- [Java & Clojure](https://clojure.org/guides/getting_started)
+- [Java & Clojure](https://clojure.org/guides/getting_started). (If you run into `Execution error (FileNotFoundException) at java.io.FileInputStream/open0 (FileInputStream.java:-2). -M:cljs (No such file or directory)`, it means you have a wrong Clojure version installed. Please uninstall it and follow the instructions linked.)
 
 
 ### 2. Compile to JavaScript
 ### 2. Compile to JavaScript
 
 
-``` bash
+```bash
 git clone https://github.com/logseq/logseq
 git clone https://github.com/logseq/logseq
 yarn
 yarn
 yarn watch
 yarn watch
@@ -84,35 +86,75 @@ Open <http://localhost:3001>.
 
 
 ### 4. Build a release
 ### 4. Build a release
 
 
-``` bash
+```bash
 yarn release
 yarn release
 ```
 ```
 
 
+### 5. Run tests
+
+Run ClojureScript tests
+
+```bash
+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
 ## Alternative: Docker based development environment
 
 
+Basically it just pre-installs Java, Clojure and NodeJS for your convenience.
+
 ### 1. Fetch sources
 ### 1. Fetch sources
 
 
-``` bash
+```bash
 git clone https://github.com/logseq/logseq
 git clone https://github.com/logseq/logseq
 ```
 ```
 
 
 ### 2. Build Docker image
 ### 2. Build Docker image
 
 
-``` bash
+```bash
 cd logseq
 cd logseq
 docker build -t logseq-docker .
 docker build -t logseq-docker .
 ```
 ```
 
 
 ### 3. Run Docker container
 ### 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
 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
 ### 4. Inside the container compile as described above
 
 
-``` bash
+```bash
 cd logseq
 cd logseq
 yarn
 yarn
 yarn watch
 yarn watch
 ```
 ```
+
+## Thanks
+
+[![JetBrains](docs/assets/jetbrains.svg)](https://www.jetbrains.com/?from=logseq)

+ 17 - 13
deps.edn

@@ -1,13 +1,14 @@
 {:paths ["src/main"]
 {:paths ["src/main"]
  :deps
  :deps
  {org.clojure/clojure         {:mvn/version "1.10.0"}
  {org.clojure/clojure         {:mvn/version "1.10.0"}
+  cheshire/cheshire {:mvn/version "5.10.0"}
   rum/rum                     {:mvn/version "0.12.3"}
   rum/rum                     {:mvn/version "0.12.3"}
   ;; rum                         {:local/root "/home/tienson/codes/source/clj/rum"}
   ;; rum                         {:local/root "/home/tienson/codes/source/clj/rum"}
   ;; persistent-sorted-set       {:mvn/version "0.1.2"}
   ;; persistent-sorted-set       {:mvn/version "0.1.2"}
   ;; FIXME: doesn't work on my archlinux laptop (tienson)
   ;; FIXME: doesn't work on my archlinux laptop (tienson)
   ;; The required namespace "datascript.core" is not available, it was required by "frontend/db.cljs".
   ;; The required namespace "datascript.core" is not available, it was required by "frontend/db.cljs".
   datascript/datascript       {:git/url "https://github.com/tiensonqin/datascript",
   datascript/datascript       {:git/url "https://github.com/tiensonqin/datascript",
-                               :sha "7c2822565d9a114c7d8604c335af89de4640e2e5"}
+                               :sha "efde8d389e6703b6f60ca3538f484a579b0d6de0"}
   ;; datascript                  {:mvn/version "1.0.1"}
   ;; datascript                  {:mvn/version "1.0.1"}
   datascript-transit/datascript-transit
   datascript-transit/datascript-transit
   {:mvn/version "0.3.0"
   {:mvn/version "0.3.0"
@@ -20,7 +21,9 @@
   cljs-bean/cljs-bean         {:mvn/version "1.5.0"}
   cljs-bean/cljs-bean         {:mvn/version "1.5.0"}
   prismatic/dommy             {:mvn/version "1.1.0"}
   prismatic/dommy             {:mvn/version "1.1.0"}
   org.clojure/core.match      {:mvn/version "1.0.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
   cljs-drag-n-drop/cljs-drag-n-drop
   {:mvn/version "0.1.0"}
   {:mvn/version "0.1.0"}
   borkdude/sci                {:mvn/version "0.1.1-alpha.6"}
   borkdude/sci                {:mvn/version "0.1.1-alpha.6"}
@@ -28,25 +31,26 @@
   hiccups/hiccups             {:mvn/version "0.3.0"}
   hiccups/hiccups             {:mvn/version "0.3.0"}
   tongue/tongue               {:mvn/version "0.2.9"}
   tongue/tongue               {:mvn/version "0.2.9"}
   org.clojure/core.async      {:mvn/version "1.3.610"}
   org.clojure/core.async      {:mvn/version "1.3.610"}
-  thheller/shadow-cljs        {:mvn/version "2.8.81"}
+  thheller/shadow-cljs        {:mvn/version "2.11.14"}
   expound/expound             {:mvn/version "0.8.6"}
   expound/expound             {:mvn/version "0.8.6"}
-  lambdaisland/glogi          {:mvn/version "1.0.74"}}
+  lambdaisland/glogi          {:mvn/version "1.0.74"}
+  binaryage/devtools          {:mvn/version "1.0.2"}}
 
 
- :aliases {:cljs {:extra-paths ["src/dev-cljs/" "src/test/"]
-                  :extra-deps  {org.clojure/clojurescript   {:mvn/version "1.10.520"}
-                                thheller/shadow-cljs        {:mvn/version "2.8.81"}
-                                binaryage/devtools          {:mvn/version "0.9.10"}
+ :aliases {:cljs {:extra-paths ["src/dev-cljs/" "src/test/" "src/electron/"]
+                  :extra-deps  {org.clojure/clojurescript   {:mvn/version "1.10.764"}
                                 org.clojure/tools.namespace {:mvn/version "0.2.11"}
                                 org.clojure/tools.namespace {:mvn/version "0.2.11"}
-                                cider/cider-nrepl           {:mvn/version "0.23.0-SNAPSHOT"}}
+                                cider/cider-nrepl           {:mvn/version "0.25.5"}}
                   :main-opts ["-m" "shadow.cljs.devtools.cli"]}
                   :main-opts ["-m" "shadow.cljs.devtools.cli"]}
            :test
            :test
            {:extra-paths ["src/test/"]
            {:extra-paths ["src/test/"]
-            :extra-deps  {org.clojure/clojurescript {:mvn/version "1.10.520"}
+            :extra-deps  {org.clojure/clojurescript {:mvn/version "1.10.764"}
                           org.clojure/test.check {:mvn/version "RELEASE"}}
                           org.clojure/test.check {:mvn/version "RELEASE"}}
             :main-opts   ["-m" "shadow.cljs.devtools.cli"]}
             :main-opts   ["-m" "shadow.cljs.devtools.cli"]}
-           :runner
-           {:extra-deps
+
+           :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",
              {:git/url "https://github.com/cognitect-labs/test-runner",
               :sha "76568540e7f40268ad2b646110f237a60295fa3c"}},
               :sha "76568540e7f40268ad2b646110f237a60295fa3c"}},
-            :main-opts ["-m" "cognitect.test-runner" "-d" "test"]}}}
+            :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

+ 66 - 0
docs/assets/jetbrains.svg

@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="120.1px" height="130.2px" viewBox="0 0 120.1 130.2" style="enable-background:new 0 0 120.1 130.2;" xml:space="preserve"
+	>
+<g>
+	<linearGradient id="XMLID_2_" gradientUnits="userSpaceOnUse" x1="31.8412" y1="120.5578" x2="110.2402" y2="73.24">
+		<stop  offset="0" style="stop-color:#FCEE39"/>
+		<stop  offset="1" style="stop-color:#F37B3D"/>
+	</linearGradient>
+	<path id="XMLID_3041_" style="fill:url(#XMLID_2_);" d="M118.6,71.8c0.9-0.8,1.4-1.9,1.5-3.2c0.1-2.6-1.8-4.7-4.4-4.9
+		c-1.2-0.1-2.4,0.4-3.3,1.1l0,0l-83.8,45.9c-1.9,0.8-3.6,2.2-4.7,4.1c-2.9,4.8-1.3,11,3.6,13.9c3.4,2,7.5,1.8,10.7-0.2l0,0l0,0
+		c0.2-0.2,0.5-0.3,0.7-0.5l78-54.8C117.3,72.9,118.4,72.1,118.6,71.8L118.6,71.8L118.6,71.8z"/>
+	<linearGradient id="XMLID_3_" gradientUnits="userSpaceOnUse" x1="48.3607" y1="6.9083" x2="119.9179" y2="69.5546">
+		<stop  offset="0" style="stop-color:#EF5A6B"/>
+		<stop  offset="0.57" style="stop-color:#F26F4E"/>
+		<stop  offset="1" style="stop-color:#F37B3D"/>
+	</linearGradient>
+	<path id="XMLID_3049_" style="fill:url(#XMLID_3_);" d="M118.8,65.1L118.8,65.1L55,2.5C53.6,1,51.6,0,49.3,0
+		c-4.3,0-7.7,3.5-7.7,7.7v0c0,2.1,0.8,3.9,2.1,5.3l0,0l0,0c0.4,0.4,0.8,0.7,1.2,1l67.4,57.7l0,0c0.8,0.7,1.8,1.2,3,1.3
+		c2.6,0.1,4.7-1.8,4.9-4.4C120.2,67.3,119.7,66,118.8,65.1z"/>
+	<linearGradient id="XMLID_4_" gradientUnits="userSpaceOnUse" x1="52.9467" y1="63.6407" x2="10.5379" y2="37.1562">
+		<stop  offset="0" style="stop-color:#7C59A4"/>
+		<stop  offset="0.3852" style="stop-color:#AF4C92"/>
+		<stop  offset="0.7654" style="stop-color:#DC4183"/>
+		<stop  offset="0.957" style="stop-color:#ED3D7D"/>
+	</linearGradient>
+	<path id="XMLID_3042_" style="fill:url(#XMLID_4_);" d="M57.1,59.5C57,59.5,17.7,28.5,16.9,28l0,0l0,0c-0.6-0.3-1.2-0.6-1.8-0.9
+		c-5.8-2.2-12.2,0.8-14.4,6.6c-1.9,5.1,0.2,10.7,4.6,13.4l0,0l0,0C6,47.5,6.6,47.8,7.3,48c0.4,0.2,45.4,18.8,45.4,18.8l0,0
+		c1.8,0.8,3.9,0.3,5.1-1.2C59.3,63.7,59,61,57.1,59.5z"/>
+	<linearGradient id="XMLID_5_" gradientUnits="userSpaceOnUse" x1="52.1736" y1="3.7019" x2="10.7706" y2="37.8971">
+		<stop  offset="0" style="stop-color:#EF5A6B"/>
+		<stop  offset="0.364" style="stop-color:#EE4E72"/>
+		<stop  offset="1" style="stop-color:#ED3D7D"/>
+	</linearGradient>
+	<path id="XMLID_3057_" style="fill:url(#XMLID_5_);" d="M49.3,0c-1.7,0-3.3,0.6-4.6,1.5L4.9,28.3c-0.1,0.1-0.2,0.1-0.2,0.2l-0.1,0
+		l0,0c-1.7,1.2-3.1,3-3.9,5.1C-1.5,39.4,1.5,45.9,7.3,48c3.6,1.4,7.5,0.7,10.4-1.4l0,0l0,0c0.7-0.5,1.3-1,1.8-1.6l34.6-31.2l0,0
+		c1.8-1.4,3-3.6,3-6.1v0C57.1,3.5,53.6,0,49.3,0z"/>
+	<g id="XMLID_3008_">
+		<rect id="XMLID_3033_" x="34.6" y="37.4" style="fill:#000000;" width="51" height="51"/>
+		<rect id="XMLID_3032_" x="39" y="78.8" style="fill:#FFFFFF;" width="19.1" height="3.2"/>
+		<g id="XMLID_3009_">
+			<path id="XMLID_3030_" style="fill:#FFFFFF;" d="M38.8,50.8l1.5-1.4c0.4,0.5,0.8,0.8,1.3,0.8c0.6,0,0.9-0.4,0.9-1.2l0-5.3l2.3,0
+				l0,5.3c0,1-0.3,1.8-0.8,2.3c-0.5,0.5-1.3,0.8-2.3,0.8C40.2,52.2,39.4,51.6,38.8,50.8z"/>
+			<path id="XMLID_3028_" style="fill:#FFFFFF;" d="M45.3,43.8l6.7,0v1.9l-4.4,0V47l4,0l0,1.8l-4,0l0,1.3l4.5,0l0,2l-6.7,0
+				L45.3,43.8z"/>
+			<path id="XMLID_3026_" style="fill:#FFFFFF;" d="M55,45.8l-2.5,0l0-2l7.3,0l0,2l-2.5,0l0,6.3l-2.3,0L55,45.8z"/>
+			<path id="XMLID_3022_" style="fill:#FFFFFF;" d="M39,54l4.3,0c1,0,1.8,0.3,2.3,0.7c0.3,0.3,0.5,0.8,0.5,1.4v0
+				c0,1-0.5,1.5-1.3,1.9c1,0.3,1.6,0.9,1.6,2v0c0,1.4-1.2,2.3-3.1,2.3l-4.3,0L39,54z M43.8,56.6c0-0.5-0.4-0.7-1-0.7l-1.5,0l0,1.5
+				l1.4,0C43.4,57.3,43.8,57.1,43.8,56.6L43.8,56.6z M43,59l-1.8,0l0,1.5H43c0.7,0,1.1-0.3,1.1-0.8v0C44.1,59.2,43.7,59,43,59z"/>
+			<path id="XMLID_3019_" style="fill:#FFFFFF;" d="M46.8,54l3.9,0c1.3,0,2.1,0.3,2.7,0.9c0.5,0.5,0.7,1.1,0.7,1.9v0
+				c0,1.3-0.7,2.1-1.7,2.6l2,2.9l-2.6,0l-1.7-2.5h-1l0,2.5l-2.3,0L46.8,54z M50.6,58c0.8,0,1.2-0.4,1.2-1v0c0-0.7-0.5-1-1.2-1
+				l-1.5,0v2H50.6z"/>
+			<path id="XMLID_3016_" style="fill:#FFFFFF;" d="M56.8,54l2.2,0l3.5,8.4l-2.5,0l-0.6-1.5l-3.2,0l-0.6,1.5l-2.4,0L56.8,54z
+				 M58.8,59l-0.9-2.3L57,59L58.8,59z"/>
+			<path id="XMLID_3014_" style="fill:#FFFFFF;" d="M62.8,54l2.3,0l0,8.3l-2.3,0L62.8,54z"/>
+			<path id="XMLID_3012_" style="fill:#FFFFFF;" d="M65.7,54l2.1,0l3.4,4.4l0-4.4l2.3,0l0,8.3l-2,0L68,57.8l0,4.6l-2.3,0L65.7,54z"
+				/>
+			<path id="XMLID_3010_" style="fill:#FFFFFF;" d="M73.7,61.1l1.3-1.5c0.8,0.7,1.7,1,2.7,1c0.6,0,1-0.2,1-0.6v0
+				c0-0.4-0.3-0.5-1.4-0.8c-1.8-0.4-3.1-0.9-3.1-2.6v0c0-1.5,1.2-2.7,3.2-2.7c1.4,0,2.5,0.4,3.4,1.1l-1.2,1.6
+				c-0.8-0.5-1.6-0.8-2.3-0.8c-0.6,0-0.8,0.2-0.8,0.5v0c0,0.4,0.3,0.5,1.4,0.8c1.9,0.4,3.1,1,3.1,2.6v0c0,1.7-1.3,2.7-3.4,2.7
+				C76.1,62.5,74.7,62,73.7,61.1z"/>
+		</g>
+	</g>
+</g>
+</svg>

+ 20 - 0
externs.js

@@ -17,7 +17,12 @@ dummy.getRangeAt = function() {};
 dummy.getElementsByClassName = function() {};
 dummy.getElementsByClassName = function() {};
 dummy.containsNode = function() {};
 dummy.containsNode = function() {};
 dummy.select = function() {};
 dummy.select = function() {};
+dummy.search = function() {};
+dummy.add = function() {};
+dummy.remove = function() {};
+dummy.closest = function () {};
 dummy.setAttribute = function() {};
 dummy.setAttribute = function() {};
+dummy.getAttribute = function() {};
 dummy.font = function() {};
 dummy.font = function() {};
 dummy.measureText = function() {};
 dummy.measureText = function() {};
 dummy.fillStyle = function() {};
 dummy.fillStyle = function() {};
@@ -50,3 +55,18 @@ dummy.values = function() {};
 // Do we really need those?
 // Do we really need those?
 dummy.filter = function() {};
 dummy.filter = function() {};
 dummy.concat = function() {};
 dummy.concat = function() {};
+dummy.diff_main = function() {};
+dummy.patch_make = function() {};
+dummy.patch_apply = function() {};
+
+/**
+ * @typedef {{
+ *     recursive: (undefined | boolean),
+ * }}
+ */
+var openDirectoryOptions;
+/**
+ * @param {(undefined | openDirectoryOptions)} options
+ * @param {function} cb
+ */
+var openDirectory = function(options, cb) {};

+ 46 - 1
gulpfile.js

@@ -1,4 +1,5 @@
 const fs = require('fs')
 const fs = require('fs')
+const cp = require('child_process')
 const path = require('path')
 const path = require('path')
 const gulp = require('gulp')
 const gulp = require('gulp')
 const postcss = require('gulp-postcss')
 const postcss = require('gulp-postcss')
@@ -62,7 +63,7 @@ const css = {
 
 
 const common = {
 const common = {
   clean () {
   clean () {
-    return del(outputPath)
+    return del(['./static/**/*', '!./static/yarn.lock', '!./static/node_modules'])
   },
   },
 
 
   syncResourceFile () {
   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.clean = common.clean
 exports.watch = gulp.parallel(common.keepSyncResourceFile, css.watchCSS)
 exports.watch = gulp.parallel(common.keepSyncResourceFile, css.watchCSS)
 exports.build = gulp.series(common.clean, common.syncResourceFile, css.buildCSS)
 exports.build = gulp.series(common.clean, common.syncResourceFile, css.buildCSS)

+ 24 - 7
package.json

@@ -2,6 +2,7 @@
     "name": "logseq",
     "name": "logseq",
     "version": "0.0.1",
     "version": "0.0.1",
     "private": true,
     "private": true,
+    "main": "static/electron.js",
     "devDependencies": {
     "devDependencies": {
         "@tailwindcss/ui": "0.7.2",
         "@tailwindcss/ui": "0.7.2",
         "@types/gulp": "^4.0.7",
         "@types/gulp": "^4.0.7",
@@ -18,26 +19,32 @@
         "postcss-cli": "8.3.0",
         "postcss-cli": "8.3.0",
         "postcss-nested": "^5.0.1",
         "postcss-nested": "^5.0.1",
         "purgecss": "3.0.0",
         "purgecss": "3.0.0",
-        "shadow-cljs": "2.8.81",
+        "shadow-cljs": "2.11.11",
         "stylelint": "^13.8.0",
         "stylelint": "^13.8.0",
         "stylelint-config-standard": "^20.0.0",
         "stylelint-config-standard": "^20.0.0",
         "tailwindcss": "2.0.1"
         "tailwindcss": "2.0.1"
     },
     },
     "scripts": {
     "scripts": {
         "watch": "run-p gulp:build gulp:watch cljs:watch",
         "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",
         "release": "run-s gulp:build cljs:release",
         "watch-app": "run-p gulp:watch cljs:watch-app",
         "watch-app": "run-p gulp:watch cljs:watch-app",
         "release-app": "run-s gulp:build cljs:release-app",
         "release-app": "run-s gulp:build cljs:release-app",
         "release-publishing": "run-s gulp:build cljs:release-publishing",
         "release-publishing": "run-s gulp:build cljs:release-publishing",
         "dev-release-app": "run-s gulp:build cljs:dev-release-app",
         "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",
         "clean": "gulp clean",
         "test": "run-s cljs:test cljs:run-test",
         "test": "run-s cljs:test cljs:run-test",
         "report": "run-s cljs:report",
         "report": "run-s cljs:report",
         "style:lint": "stylelint \"src/**/*.css\" ",
         "style:lint": "stylelint \"src/**/*.css\" ",
         "gulp:watch": "gulp watch",
         "gulp:watch": "gulp watch",
         "gulp:build": "cross-env NODE_ENV=production gulp build",
         "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:test": "clojure -A:test compile test",
         "cljs:run-test": "node static/tests.js",
         "cljs:run-test": "node static/tests.js",
         "cljs:watch-app": "clojure -M:cljs watch app",
         "cljs:watch-app": "clojure -M:cljs watch app",
@@ -45,21 +52,31 @@
         "cljs:release-publishing": "clojure -M:cljs release publishing",
         "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: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: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": {
     "dependencies": {
+        "@kanru/rage-wasm": "^0.2.1",
+        "chokidar": "^3.5.1",
+        "chrono-node": "^2.2.1",
         "codemirror": "^5.58.1",
         "codemirror": "^5.58.1",
-        "diff": "^4.0.2",
-        "fuzzysort": "^1.1.4",
+        "diff": "5.0.0",
+        "diff-match-patch": "^1.0.5",
+        "electron": "^11.2.0",
+        "fs": "^0.0.1-security",
+        "fuse.js": "^6.4.6",
         "gulp-cached": "^1.1.1",
         "gulp-cached": "^1.1.1",
         "ignore": "^5.1.8",
         "ignore": "^5.1.8",
         "jszip": "^3.5.0",
         "jszip": "^3.5.0",
-        "mldoc": "^0.3.0",
+        "mldoc": "^0.5.0",
         "mousetrap": "^1.6.5",
         "mousetrap": "^1.6.5",
+        "path": "^0.12.7",
         "react": "^17.0.1",
         "react": "^17.0.1",
         "react-dom": "^17.0.1",
         "react-dom": "^17.0.1",
+        "react-resize-context": "^3.0.0",
         "react-textarea-autosize": "^8.0.1",
         "react-textarea-autosize": "^8.0.1",
         "react-transition-group": "^4.3.0",
         "react-transition-group": "^4.3.0",
+        "url": "^0.11.0",
         "yargs-parser": "^20.2.4"
         "yargs-parser": "^20.2.4"
     }
     }
 }
 }

文件差异内容过多而无法显示
+ 0 - 0
public/index.html


+ 135 - 64
resources/css/common.css

@@ -1,6 +1,6 @@
 :root {
 :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-text-size: 1em;
   --ls-page-title-size: 36px;
   --ls-page-title-size: 36px;
   --ls-font-family: 'Inter';
   --ls-font-family: 'Inter';
@@ -36,8 +36,9 @@ html[data-theme=dark] {
   --ls-active-secondary-color: #d0e8e8;
   --ls-active-secondary-color: #d0e8e8;
   --ls-block-properties-background-color: #02222a;
   --ls-block-properties-background-color: #02222a;
   --ls-block-ref-link-text-color: #1a6376;
   --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-border-color: #0e5263;
+  --ls-secondary-border-color: #126277;
   --ls-guideline-color: #0b4a5a;
   --ls-guideline-color: #0b4a5a;
   --ls-menu-hover-color: var(--ls-secondary-background-color);
   --ls-menu-hover-color: var(--ls-secondary-background-color);
   --ls-primary-text-color: #a4b5b6;
   --ls-primary-text-color: #a4b5b6;
@@ -61,8 +62,8 @@ html[data-theme=dark] {
   --ls-page-blockquote-border-color: var(--ls-border-color);
   --ls-page-blockquote-border-color: var(--ls-border-color);
   --ls-page-inline-code-color: var(--ls-primary-text-color);
   --ls-page-inline-code-color: var(--ls-primary-text-color);
   --ls-page-inline-code-bg-color: #01222a;
   --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-scrollbar-thumb-hover-color: rgba(255, 255, 255, 0.2);
   --ls-head-text-color: var(--ls-link-text-color);
   --ls-head-text-color: var(--ls-link-text-color);
   --ls-icon-color: var(--ls-link-text-color);
   --ls-icon-color: var(--ls-link-text-color);
@@ -80,44 +81,45 @@ html[data-theme=dark] {
 .white-theme,
 .white-theme,
 html[data-theme=light] {
 html[data-theme=light] {
   --ls-primary-background-color: white;
   --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-table-tr-even-background-color: #f4f5f7;
   --ls-active-primary-color: rgb(4, 85, 145);
   --ls-active-primary-color: rgb(4, 85, 145);
   --ls-active-secondary-color: #003761;
   --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-block-ref-link-text-color: #D8E1E8;
   --ls-search-background-color: var(--ls-primary-background-color);
   --ls-search-background-color: var(--ls-primary-background-color);
   --ls-border-color: #ccc;
   --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-menu-hover-color: var(--ls-a-chosen-bg);
-  --ls-primary-text-color: #24292e;
+  --ls-primary-text-color: #433F38;
   --ls-secondary-text-color: #161e2e;
   --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-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-block-highlight-color: #c0e6fd;
   --ls-selection-background-color: #e4f2ff;
   --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-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-page-inline-code-color: var(--ls-primary-text-color);
   --ls-scrollbar-foreground-color: rgba(0, 0, 0, 0.1);
   --ls-scrollbar-foreground-color: rgba(0, 0, 0, 0.1);
   --ls-scrollbar-background-color: rgba(0, 0, 0, 0.05);
   --ls-scrollbar-background-color: rgba(0, 0, 0, 0.05);
   --ls-scrollbar-thumb-hover-color: rgba(0, 0, 0, 0.2);
   --ls-scrollbar-thumb-hover-color: rgba(0, 0, 0, 0.2);
   --ls-head-text-color: var(--ls-link-text-color);
   --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-search-icon-color: var(--ls-icon-color);
   --ls-a-chosen-bg: #f4f5f7;
   --ls-a-chosen-bg: #f4f5f7;
   --ls-right-sidebar-code-bg-color: var(--ls-secondary-background-color);
   --ls-right-sidebar-code-bg-color: var(--ls-secondary-background-color);
@@ -260,7 +262,7 @@ summary {
 }
 }
 
 
 iframe {
 iframe {
-  /* width: 100%; */
+  width: 100%;
   margin: 1rem 0;
   margin: 1rem 0;
 }
 }
 
 
@@ -443,6 +445,10 @@ li p:last-child,
   opacity: 0.6;
   opacity: 0.6;
 }
 }
 
 
+.opacity-30 {
+    opacity: 0.3;
+}
+
 .opacity-70 {
 .opacity-70 {
   opacity: 0.7;
   opacity: 0.7;
 }
 }
@@ -540,22 +546,6 @@ li p:last-child,
   overflow-y: auto;
   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 {
 .heading-bg {
   border-radius: 50%;
   border-radius: 50%;
   width: 12px;
   width: 12px;
@@ -565,19 +555,11 @@ li p:last-child,
 /** endregion **/
 /** endregion **/
 
 
 /* region FIXME: override elements (?) */
 /* region FIXME: override elements (?) */
-a.block-control,
-a.block-control:hover {
-  text-decoration: none;
-  cursor: pointer;
-  font-size: 14px;
-  min-width: 10px;
-  color: initial;
-}
-
 h1.title {
 h1.title {
   margin-bottom: 1.5rem;
   margin-bottom: 1.5rem;
   color: var(--ls-title-text-color, #222);
   color: var(--ls-title-text-color, #222);
   font-size: var(--ls-page-title-size, 36px);
   font-size: var(--ls-page-title-size, 36px);
+  font-weight: 700;
 }
 }
 
 
 .block-highlight,
 .block-highlight,
@@ -587,11 +569,6 @@ h1.title {
   padding: -1px;
   padding: -1px;
 }
 }
 
 
-.content img {
-  margin-top: 0.5rem;
-  margin-bottom: 0.5rem;
-}
-
 span.timestamp {
 span.timestamp {
   margin: 0 0.25rem;
   margin: 0 0.25rem;
 }
 }
@@ -648,10 +625,6 @@ a.login:hover {
   color: var(--ls-link-text-hover-color, #000);
   color: var(--ls-link-text-hover-color, #000);
 }
 }
 
 
-a.marker-switch:hover {
-  opacity: 1;
-}
-
 a.tooltip-priority {
 a.tooltip-priority {
   display: contents;
   display: contents;
   position: absolute;
   position: absolute;
@@ -684,12 +657,17 @@ img.small {
 }
 }
 
 
 a.tag {
 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 {
 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);
   color: var(--ls-tag-text-hover-color, #045591);
 }
 }
 
 
@@ -720,3 +698,96 @@ hr {
   margin: 2rem 0;
   margin: 2rem 0;
   border-color: var(--ls-border-color, #ccc);
   border-color: var(--ls-border-color, #ccc);
 }
 }
+
+.resize {
+    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;
+}
+
+#head {
+    background: none;
+}
+
+/* < > buttons */
+
+a.navigation {
+    border-radius: 3px;
+    transition: .3s;
+}
+
+/* text mark/highlight */
+
+mark {
+    padding: 2px 4px;
+    border-radius: 3px;
+    font-size: 14px;
+}
+
+/* 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;
+}
+
+.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;
+}

+ 39 - 39
resources/css/inter.css

@@ -3,16 +3,16 @@
   font-style:  normal;
   font-style:  normal;
   font-weight: 100;
   font-weight: 100;
   font-display: swap;
   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-face {
   font-family: 'Inter';
   font-family: 'Inter';
   font-style:  italic;
   font-style:  italic;
   font-weight: 100;
   font-weight: 100;
   font-display: swap;
   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 {
 @font-face {
@@ -20,16 +20,16 @@
   font-style:  normal;
   font-style:  normal;
   font-weight: 200;
   font-weight: 200;
   font-display: swap;
   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-face {
   font-family: 'Inter';
   font-family: 'Inter';
   font-style:  italic;
   font-style:  italic;
   font-weight: 200;
   font-weight: 200;
   font-display: swap;
   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 {
 @font-face {
@@ -37,16 +37,16 @@
   font-style:  normal;
   font-style:  normal;
   font-weight: 300;
   font-weight: 300;
   font-display: swap;
   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-face {
   font-family: 'Inter';
   font-family: 'Inter';
   font-style:  italic;
   font-style:  italic;
   font-weight: 300;
   font-weight: 300;
   font-display: swap;
   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 {
 @font-face {
@@ -54,16 +54,16 @@
   font-style:  normal;
   font-style:  normal;
   font-weight: 400;
   font-weight: 400;
   font-display: swap;
   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-face {
   font-family: 'Inter';
   font-family: 'Inter';
   font-style:  italic;
   font-style:  italic;
   font-weight: 400;
   font-weight: 400;
   font-display: swap;
   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 {
 @font-face {
@@ -71,16 +71,16 @@
   font-style:  normal;
   font-style:  normal;
   font-weight: 500;
   font-weight: 500;
   font-display: swap;
   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-face {
   font-family: 'Inter';
   font-family: 'Inter';
   font-style:  italic;
   font-style:  italic;
   font-weight: 500;
   font-weight: 500;
   font-display: swap;
   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 {
 @font-face {
@@ -88,16 +88,16 @@
   font-style:  normal;
   font-style:  normal;
   font-weight: 600;
   font-weight: 600;
   font-display: swap;
   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-face {
   font-family: 'Inter';
   font-family: 'Inter';
   font-style:  italic;
   font-style:  italic;
   font-weight: 600;
   font-weight: 600;
   font-display: swap;
   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 {
 @font-face {
@@ -105,16 +105,16 @@
   font-style:  normal;
   font-style:  normal;
   font-weight: 700;
   font-weight: 700;
   font-display: swap;
   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-face {
   font-family: 'Inter';
   font-family: 'Inter';
   font-style:  italic;
   font-style:  italic;
   font-weight: 700;
   font-weight: 700;
   font-display: swap;
   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 {
 @font-face {
@@ -122,16 +122,16 @@
   font-style:  normal;
   font-style:  normal;
   font-weight: 800;
   font-weight: 800;
   font-display: swap;
   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-face {
   font-family: 'Inter';
   font-family: 'Inter';
   font-style:  italic;
   font-style:  italic;
   font-weight: 800;
   font-weight: 800;
   font-display: swap;
   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 {
 @font-face {
@@ -139,16 +139,16 @@
   font-style:  normal;
   font-style:  normal;
   font-weight: 900;
   font-weight: 900;
   font-display: swap;
   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-face {
   font-family: 'Inter';
   font-family: 'Inter';
   font-style:  italic;
   font-style:  italic;
   font-weight: 900;
   font-weight: 900;
   font-display: swap;
   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-display: swap;
   font-style: normal;
   font-style: normal;
   font-named-instance: 'Regular';
   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-face {
   font-family: 'Inter var';
   font-family: 'Inter var';
@@ -174,7 +174,7 @@ Usage:
   font-display: swap;
   font-display: swap;
   font-style: italic;
   font-style: italic;
   font-named-instance: '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-weight: 100 900;
   font-display: swap;
   font-display: swap;
   font-style: oblique 0deg 10deg;
   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");
 }
 }

+ 2 - 2
resources/css/style.css

@@ -9,6 +9,6 @@
 @import "./table.css";
 @import "./table.css";
 @import "./datepicker.css";
 @import "./datepicker.css";
 @import "./highlight.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 "./common.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 */

+ 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>

+ 109 - 0
resources/electron-dev.html

@@ -0,0 +1,109 @@
+<!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="./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>
+</body>
+</html>

+ 110 - 0
resources/electron.html

@@ -0,0 +1,110 @@
+<!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>
+</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>

+ 52 - 0
resources/forge.config.js

@@ -0,0 +1,52 @@
+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']
+    }
+  ],
+
+  publishers: [
+    {
+      name: '@electron-forge/publisher-github',
+      config: {
+        repository: {
+          owner: 'logseq',
+          name: 'logseq'
+        },
+        prerelease: true
+      }
+    }
+  ]
+}

二进制
resources/icons/logseq.icns


二进制
resources/icons/logseq.ico


二进制
resources/icons/logseq.png


二进制
resources/icons/logseq_big_sur.icns


二进制
resources/icons/logseq_big_sur.ico


二进制
resources/icons/logseq_big_sur.png


二进制
resources/img/dmg-bg.png


文件差异内容过多而无法显示
+ 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 });
+
+})));

文件差异内容过多而无法显示
+ 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(
 importScripts(
   // Batched optimization
   // 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
   // Fixed a bug
-  "/static/js/magic_portal.js"
+  "./magic_portal.js"
 );
 );
 
 
 const detect = () => {
 const detect = () => {

+ 33 - 0
resources/package.json

@@ -0,0 +1,33 @@
+{
+  "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": "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 ./

+ 20 - 5
shadow-cljs.edn

@@ -9,16 +9,20 @@
    :modules {:main {:init-fn frontend.core/init}
    :modules {:main {:init-fn frontend.core/init}
              :code-editor
              :code-editor
              {:entries [frontend.extensions.code]
              {:entries [frontend.extensions.code]
+              :depends-on #{:main}}
+             :age-encryption
+             {:entries [frontend.extensions.age-encryption]
               :depends-on #{:main}}}
               :depends-on #{:main}}}
 
 
    :output-dir "./static/js"
    :output-dir "./static/js"
    :asset-path "/static/js"
    :asset-path "/static/js"
    :release {:asset-path "https://asset.logseq.com/static/js"}
    :release {:asset-path "https://asset.logseq.com/static/js"}
    :compiler-options {:infer-externs :auto
    :compiler-options {:infer-externs :auto
-                      :output-feature-set :es6
-                      :source-map true
+                      :output-feature-set :es-next
+                      ;; :source-map true
                       :externs ["datascript/externs.js"
                       :externs ["datascript/externs.js"
-                                "externs.js"]}
+                                "externs.js"]
+                      :warnings {:fn-deprecated false}}
    :closure-defines {goog.debug.LOGGING_ENABLED true
    :closure-defines {goog.debug.LOGGING_ENABLED true
                      frontend.config/GITHUB_APP_NAME #shadow/env "GITHUB_APP2_NAME"}
                      frontend.config/GITHUB_APP_NAME #shadow/env "GITHUB_APP2_NAME"}
 
 
@@ -32,6 +36,13 @@
     :watch-path   "static"
     :watch-path   "static"
     :preloads     [devtools.preload]}}
     :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
   :test
   {:target :node-test
   {:target :node-test
    :output-to "static/tests.js"
    :output-to "static/tests.js"
@@ -44,6 +55,9 @@
    :modules {:main {:init-fn frontend.publishing/init}
    :modules {:main {:init-fn frontend.publishing/init}
              :code-editor
              :code-editor
              {:entries [frontend.extensions.code]
              {:entries [frontend.extensions.code]
+              :depends-on #{:main}}
+             :age-encryption
+             {:entries [frontend.extensions.age-encryption]
               :depends-on #{:main}}}
               :depends-on #{:main}}}
 
 
    :output-dir "./static/js/publishing"
    :output-dir "./static/js/publishing"
@@ -53,9 +67,10 @@
                      goog.debug.LOGGING_ENABLED true}
                      goog.debug.LOGGING_ENABLED true}
 
 
    :compiler-options {:infer-externs :auto
    :compiler-options {:infer-externs :auto
-                      :output-feature-set :es6
+                      :output-feature-set :es-next
                       :externs ["datascript/externs.js"
                       :externs ["datascript/externs.js"
-                                "externs.js"]}
+                                "externs.js"]
+                      :warnings {:fn-deprecated false}}
    :devtools
    :devtools
    ;; before live-reloading any code call this function
    ;; before live-reloading any code call this function
    {:before-load frontend.core/stop
    {:before-load frontend.core/stop

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

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

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

@@ -0,0 +1,133 @@
+(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]))
+
+(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
+  "create main app window"
+  []
+  (let [win-opts {:width         980
+                  :height        700
+                  :frame         win32?
+                  :autoHideMenuBar win32?
+                  :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))))
+
+(defn main
+  []
+  (.on app "window-all-closed" #(when-not mac? (.quit app)))
+  (.on app "ready"
+       (fn []
+         (let [^js win (create-main-window)
+               *win (atom 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" #(if (or @*quitting? (not mac?))
+                               (reset! *win nil)
+                               (do (.preventDefault ^js/Event %)
+                                   (.hide win))))
+           (.on app "before-quit" #(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)))

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

@@ -0,0 +1,154 @@
+(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]))
+
+(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))
+
+(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)))

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

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

@@ -0,0 +1,26 @@
+(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]))
+
+(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!
+  []
+  (listen-to-open-dir!)
+  (run-dirs-watcher!))

+ 33 - 2
src/main/frontend/commands.cljs

@@ -3,6 +3,7 @@
             [frontend.date :as date]
             [frontend.date :as date]
             [frontend.state :as state]
             [frontend.state :as state]
             [frontend.search :as search]
             [frontend.search :as search]
+            [frontend.config :as config]
             [clojure.string :as string]
             [clojure.string :as string]
             [goog.dom :as gdom]
             [goog.dom :as gdom]
             [goog.object :as gobj]
             [goog.object :as gobj]
@@ -123,8 +124,18 @@
                   [:editor/search-template]]]
                   [:editor/search-template]]]
      ;; same as link
      ;; same as link
      ["Image Link" link-steps]
      ["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]]])
        ["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
      ["Embed Youtube Video" [[:editor/input "{{youtube }}" {:last-pattern slash
                                                             :backward-pos 2}]]]
                                                             :backward-pos 2}]]]
      ["Html Inline " (->inline "html")]
      ["Html Inline " (->inline "html")]
@@ -253,7 +264,27 @@
     (state/set-block-content-and-last-pos! id new-value new-pos)
     (state/set-block-content-and-last-pos! id new-value new-pos)
     (util/move-cursor-to input new-pos)
     (util/move-cursor-to input new-pos)
     (when check-fn
     (when check-fn
-      (check-fn new-value (dec (count prefix))))))
+      (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!
 (defn simple-replace!
   [id value selected
   [id value selected

+ 311 - 158
src/main/frontend/components/block.cljs

@@ -2,6 +2,8 @@
   (:refer-clojure :exclude [range])
   (:refer-clojure :exclude [range])
   (:require [frontend.config :as config]
   (:require [frontend.config :as config]
             [cljs.core.match :refer-macros [match]]
             [cljs.core.match :refer-macros [match]]
+            [promesa.core :as p]
+            [frontend.fs :as fs]
             [clojure.string :as string]
             [clojure.string :as string]
             [frontend.util :as util]
             [frontend.util :as util]
             [rum.core :as rum]
             [rum.core :as rum]
@@ -46,17 +48,23 @@
             [frontend.commands :as commands]
             [frontend.commands :as commands]
             [lambdaisland.glogi :as log]
             [lambdaisland.glogi :as log]
             [frontend.context.i18n :as i18n]
             [frontend.context.i18n :as i18n]
+            [frontend.template :as template]
             [frontend.filtering :as filtering]))
             [frontend.filtering :as filtering]))
 
 
+;; TODO: remove rum/with-context because it'll make reactive queries not working
+
 (defn safe-read-string
 (defn safe-read-string
-  [s]
-  (try
-    (reader/read-string s)
-    (catch js/Error e
-      (println "read-string error:")
-      (js/console.error e)
-      [:div.warning {:title "read-string failed"}
-       s])))
+  ([s]
+   (safe-read-string s true))
+  ([s warn?]
+   (try
+     (reader/read-string s)
+     (catch js/Error e
+       (println "read-string error:")
+       (js/console.error e)
+       (when warn?
+         [:div.warning {:title "read-string failed"}
+          s])))))
 
 
 ;; local state
 ;; local state
 (defonce *block-children
 (defonce *block-children
@@ -155,17 +163,93 @@
                 parts (remove #(string/blank? %) parts)]
                 parts (remove #(string/blank? %) parts)]
             (string/join "/" (reverse parts))))))))
             (string/join "/" (reverse parts))))))))
 
 
+(defonce *resizing-image? (atom false))
+(rum/defcs resizable-image <
+  (rum/local nil ::size)
+  {:will-unmount (fn [state]
+                   (reset! *resizing-image? false)
+                   state)}
+  [state config title src metadata full_text local?]
+  (rum/with-context [[t] i18n/*tongue-context*]
+    (let [size (get state ::size)]
+      (ui/resize-provider
+       (ui/resize-consumer
+        (cond->
+         {:className "resize"
+          :onSizeChanged (fn [value]
+                           (when (and (not @*resizing-image?)
+                                      (some? @size)
+                                      (not= value @size))
+                             (reset! *resizing-image? true))
+                           (reset! size value))
+          :onMouseUp (fn []
+                       (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))))
+                       (when @*resizing-image?
+                         ;; TODO: need a better way to prevent the clicking to edit current block
+                         (js/setTimeout #(reset! *resizing-image? false) 200)))
+          :onClick (fn [e]
+                     (when @*resizing-image? (util/stop e)))}
+          (and (:width metadata) (not (util/mobile?)))
+          (assoc :style {:width (:width metadata)}))
+        [:div.asset-container
+         [:img.rounded-sm.shadow-xl.relative
+          (merge
+           {:loading "lazy"
+            :src     src
+            :title   title}
+           metadata)]
+         [:span.ctl
+          [:a.delete
+           {:title "Delete this image"
+            :on-click
+            (fn [e]
+              (when-let [block-id (:block/uuid config)]
+                (let [confirm-fn (ui/make-confirm-modal
+                                  {:title         (t :asset/confirm-delete (.toLocaleLowerCase (t :text/image)))
+                                   :sub-title     (if local? :asset/physical-delete "")
+                                   :sub-checkbox? local?
+                                   :on-confirm    (fn [e {:keys [close-fn sub-selected]}]
+                                                    (close-fn)
+                                                    (editor-handler/delete-asset-of-block!
+                                                     {:block-id    block-id
+                                                      :local?      local?
+                                                      :repo        (state/get-current-repo)
+                                                      :href        src
+                                                      :title       title
+                                                      :full-text   full_text}))})]
+                  (state/set-modal! confirm-fn)
+                  (util/stop e))))}
+           svg/trash-sm]]])))))
+
+(rum/defcs asset-link < rum/reactive
+  (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)])
+        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
+      (resizable-image config title @src metadata full_text true))))
+
 ;; TODO: safe encoding asciis
 ;; TODO: safe encoding asciis
 ;; TODO: image link to another link
 ;; TODO: image link to another link
-(defn image-link [config url href label]
-  (let [href (if (util/starts-with? href "http")
-               href
-               (get-file-absolute-path config href))]
-    [:img.rounded-sm.shadow-xl
-     {:loading "lazy"
-      ;; :on-error (fn [])
-      :src href
-      :title (second (first label))}]))
+(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 (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
+                   (get-file-absolute-path config href))]
+        (resizable-image config title href metadata full_text false)))))
 
 
 (defn repetition-to-string
 (defn repetition-to-string
   [[[kind] [duration] n]]
   [[[kind] [duration] n]]
@@ -246,30 +330,39 @@
   [{:keys [html-export? label children contents-page?] :as config} page]
   [{:keys [html-export? label children contents-page?] :as config} page]
   (when-let [page-name (:page/name page)]
   (when-let [page-name (:page/name page)]
     (let [source-page (model/get-alias-source-page (state/get-current-repo)
     (let [source-page (model/get-alias-source-page (state/get-current-repo)
-                                                   page-name)
+                                                   (string/lower-case page-name))
           original-page-name (get page :page/original-name page-name)
           original-page-name (get page :page/original-name page-name)
           original-page-name (if (date/valid-journal-title? original-page-name)
           original-page-name (if (date/valid-journal-title? original-page-name)
                                (string/capitalize original-page-name)
                                (string/capitalize original-page-name)
                                original-page-name)
                                original-page-name)
           page (string/lower-case page-name)
           page (string/lower-case page-name)
-          source-page-name (or (when source-page (:page/name source-page))
+          redirect-page-name (cond
+                               (:page/alias? config)
+                               page
+
+                               (db/page-empty? (state/get-current-repo) page-name)
+                               (or (when source-page (:page/name source-page))
+                                   page)
+
+                               :else
                                page)
                                page)
           href (if html-export?
           href (if html-export?
                  (util/encode-str page)
                  (util/encode-str page)
-                 (rfe/href :page {:name source-page-name}))]
+                 (rfe/href :page {:name redirect-page-name}))]
       [:a.page-ref
       [:a.page-ref
-       {:href href
+       {:data-ref page-name
+        :href href
         :on-click (fn [e]
         :on-click (fn [e]
                     (util/stop e)
                     (util/stop e)
                     (if (gobj/get e "shiftKey")
                     (if (gobj/get e "shiftKey")
-                      (when-let [page-entity (db/entity [:page/name source-page-name])]
+                      (when-let [page-entity (db/entity [:page/name redirect-page-name])]
                         (state/sidebar-add-block!
                         (state/sidebar-add-block!
                          (state/get-current-repo)
                          (state/get-current-repo)
                          (:db/id page-entity)
                          (:db/id page-entity)
                          :page
                          :page
                          {:page page-entity}))
                          {:page page-entity}))
                       (route-handler/redirect! {:to :page
                       (route-handler/redirect! {:to :page
-                                                :path-params {:name source-page-name}}))
+                                                :path-params {:name redirect-page-name}}))
                     (when (and contents-page?
                     (when (and contents-page?
                                (state/get-left-sidebar-open?))
                                (state/get-left-sidebar-open?))
                       (ui-handler/close-left-sidebar!)))}
                       (ui-handler/close-left-sidebar!)))}
@@ -287,6 +380,12 @@
            label
            label
            original-page-name))])))
            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)]))
+
 (rum/defc page-reference < rum/reactive
 (rum/defc page-reference < rum/reactive
   [html-export? s config label]
   [html-export? s config label]
   (let [show-brackets? (state/show-brackets?)
   (let [show-brackets? (state/show-brackets?)
@@ -296,7 +395,7 @@
      (when (and (or show-brackets? nested-link?)
      (when (and (or show-brackets? nested-link?)
                 (not html-export?)
                 (not html-export?)
                 (not contents-page?))
                 (not contents-page?))
-       [:span.text-gray-500 "[["])
+       [:span.text-gray-500.bracket "[["])
      (if (string/ends-with? s ".excalidraw")
      (if (string/ends-with? s ".excalidraw")
        [:a.page-ref
        [:a.page-ref
         {:on-click (fn [e]
         {:on-click (fn [e]
@@ -312,7 +411,7 @@
      (when (and (or show-brackets? nested-link?)
      (when (and (or show-brackets? nested-link?)
                 (not html-export?)
                 (not html-export?)
                 (not contents-page?))
                 (not contents-page?))
-       [:span.text-gray-500 "]]"])]))
+       [:span.text-gray-500.bracket "]]"])]))
 
 
 (defn- latex-environment-content
 (defn- latex-environment-content
   [name option content]
   [name option content]
@@ -337,7 +436,6 @@
 (rum/defc page-embed < rum/reactive db-mixins/query
 (rum/defc page-embed < rum/reactive db-mixins/query
   [config page-name]
   [config page-name]
   (let [page-name (string/lower-case page-name)
   (let [page-name (string/lower-case page-name)
-        page-original-name (:page/original-name (db/entity [:page/name page-name]))
         current-page (state/get-current-page)]
         current-page (state/get-current-page)]
     [:div.color-level.embed.embed-page.bg-base-2
     [:div.color-level.embed.embed-page.bg-base-2
      {:class (if (:sidebar? config) "in-sidebar")}
      {:class (if (:sidebar? config) "in-sidebar")}
@@ -373,14 +471,15 @@
     (util/format "{{{%s %s}}}" name (string/join ", " arguments))
     (util/format "{{{%s %s}}}" name (string/join ", " arguments))
     (util/format "{{{%s}}}" name)))
     (util/format "{{{%s}}}" name)))
 
 
-(defn block-reference
+(declare block-content)
+(rum/defc block-reference < rum/reactive
   [config id]
   [config id]
   (when-not (string/blank? id)
   (when-not (string/blank? id)
     (let [block (and (util/uuid-string? id)
     (let [block (and (util/uuid-string? id)
                      (db/pull-block (uuid id)))]
                      (db/pull-block (uuid id)))]
       (if block
       (if block
         [:span
         [:span
-         [:a
+         [:div.block-ref-wrap
           {:on-click (fn [e]
           {:on-click (fn [e]
                        (util/stop e)
                        (util/stop e)
                        (if (gobj/get e "shiftKey")
                        (if (gobj/get e "shiftKey")
@@ -389,12 +488,17 @@
                           (:db/id block)
                           (:db/id block)
                           :block-ref
                           :block-ref
                           {:block block})
                           {:block block})
-                         (route-handler/redirect! {:to :page
+                         (route-handler/redirect! {:to          :page
                                                    :path-params {:name id}})))}
                                                    :path-params {:name id}})))}
 
 
-          (->elem
-           :span.block-ref
-           (map-inline config (:block/title block)))]]
+          (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"}
         [:span.warning.mr-1 {:title "Block ref invalid"}
          (util/format "((%s))" id)]))))
          (util/format "((%s))" id)]))))
 
 
@@ -460,17 +564,18 @@
     (->elem :sub (map-inline config l))
     (->elem :sub (map-inline config l))
     ["Tag" s]
     ["Tag" s]
     (if (and s (util/tag-valid? s))
     (if (and s (util/tag-valid? s))
-      [:a.tag.mr-1 {: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")
-                                    (state/sidebar-add-block!
-                                     repo
-                                     (:db/id page)
-                                     :page
-                                     {:page page}))))}
+      [:a.tag {:data-ref s
+               :href (rfe/href :page {:name s})
+               :on-click (fn [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")
+                               (state/sidebar-add-block!
+                                repo
+                                (:db/id page)
+                                :page
+                                {:page page})
+                               (.preventDefault e))))}
        (str "#" s)]
        (str "#" s)]
       [:span.warning.mr-1 {:title "Invalid tag, tags only accept alphanumeric characters, \"-\", \"_\", \"@\" and \"%\"."}
       [:span.warning.mr-1 {:title "Invalid tag, tags only accept alphanumeric characters, \"-\", \"_\", \"@\" and \"%\"."}
        (str "#" s)])
        (str "#" s)])
@@ -516,26 +621,35 @@
     (nested-link config html-export? link)
     (nested-link config html-export? link)
 
 
     ["Link" link]
     ["Link" link]
-    (let [{:keys [url label title]} link
+    (let [{:keys [url label title metadata full_text]} link
           img-formats (set (map name (config/img-formats)))]
           img-formats (set (map name (config/img-formats)))]
       (match url
       (match url
         ["Search" s]
         ["Search" s]
         (cond
         (cond
+          (string/blank? s)
+          [:span.warning {:title "Invalid link"} full_text]
+
           ;; image
           ;; image
           (some (fn [fmt] (re-find (re-pattern (str "(?i)\\." fmt)) s)) img-formats)
           (some (fn [fmt] (re-find (re-pattern (str "(?i)\\." fmt)) s)) img-formats)
-          (image-link config url s label)
+          (image-link config url s label metadata full_text)
 
 
           (= \# (first s))
           (= \# (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
           ;; FIXME: same headline, see more https://orgmode.org/manual/Internal-Links.html
           (and (= \* (first s))
           (and (= \* (first s))
                (not= \* (last 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)
           (re-find #"(?i)^http[s]?://" s)
-          (->elem :a {:href s}
+          (->elem :a {:href s
+                      :data-href s
+                      :target "_blank"}
                   (map-inline config label))
                   (map-inline config label))
 
 
+          (and (util/electron?) (config/local-asset? s))
+          (asset-reference (second (first label)) s)
+
           :else
           :else
           (page-reference html-export? s config label))
           (page-reference html-export? s config label))
 
 
@@ -555,7 +669,7 @@
 
 
             (= protocol "file")
             (= protocol "file")
             (if (some (fn [fmt] (re-find (re-pattern (str "(?i)\\." fmt)) href)) img-formats)
             (if (some (fn [fmt] (re-find (re-pattern (str "(?i)\\." fmt)) href)) img-formats)
-              (image-link config url href label)
+              (image-link config url href label metadata full_text)
               (let [label-text (get-label-text label)
               (let [label-text (get-label-text label)
                     page (if (string/blank? label-text)
                     page (if (string/blank? label-text)
                            {:page/name (db/get-file-page (string/replace href "file:" ""))}
                            {:page/name (db/get-file-page (string/replace href "file:" ""))}
@@ -571,14 +685,16 @@
                   (->elem
                   (->elem
                    :a
                    :a
                    (cond->
                    (cond->
-                    {:href href}
+                    {:href      (str "file://" href)
+                     :data-href href
+                     :target    "_blank"}
                      title
                      title
                      (assoc :title title))
                      (assoc :title title))
                    (map-inline config label)))))
                    (map-inline config label)))))
 
 
             ;; image
             ;; image
             (some (fn [fmt] (re-find (re-pattern (str "(?i)\\." fmt)) href)) img-formats)
             (some (fn [fmt] (re-find (re-pattern (str "(?i)\\." fmt)) href)) img-formats)
-            (image-link config url href label)
+            (image-link config url href label metadata full_text)
 
 
             :else
             :else
             (->elem
             (->elem
@@ -645,7 +761,7 @@
       [:sup.fn
       [:sup.fn
        [:a {:id (str "fnr." encode-name)
        [:a {:id (str "fnr." encode-name)
             :class "footref"
             :class "footref"
-            :href (str "#fn." encode-name)}
+            :on-click #(route-handler/jump-to-anchor! (str "fn." encode-name))}
         name]])
         name]])
 
 
     ["Macro" options]
     ["Macro" options]
@@ -660,38 +776,59 @@
       (cond
       (cond
         (= name "query")
         (= name "query")
         [:div.dsl-query
         [:div.dsl-query
-         (let [query (string/join "," arguments)]
+         (let [query (string/join ", " arguments)]
            (custom-query (assoc config :dsl-query? true)
            (custom-query (assoc config :dsl-query? true)
                          {:title [:code.p-1 (str "Query: " query)]
                          {:title [:code.p-1 (str "Query: " query)]
                           :query query}))]
                           :query query}))]
 
 
         (= name "youtube")
         (= name "youtube")
         (let [url (first arguments)]
         (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}])))))
+
+        ;; 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)
               (let [width (min (- (util/get-width) 96)
                                560)
                                560)
                     height (int (* width (/ 315 560)))]
                     height (int (* width (/ 315 560)))]
                 [:iframe
                 [: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")
         (= name "embed")
         (let [a (first arguments)]
         (let [a (first arguments)]
           (cond
           (cond
+            (nil? a) ; empty embed
+            nil
+
             (and (string/starts-with? a "[[")
             (and (string/starts-with? a "[[")
                  (string/ends-with? a "]]"))
                  (string/ends-with? a "]]"))
             (let [page-name (-> (string/replace a "[[" "")
             (let [page-name (-> (string/replace a "[[" "")
@@ -729,11 +866,15 @@
                                (get (state/get-macros) (keyword name)))
                                (get (state/get-macros) (keyword name)))
                 macro-content (if (and (seq arguments) macro-content)
                 macro-content (if (and (seq arguments) macro-content)
                                 (block/macro-subs macro-content arguments)
                                 (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))
             (render-macro config name arguments macro-content format))
 
 
           (when-let [macro-txt (macro->text name arguments)]
           (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))))))
               (render-macro config name arguments macro-txt format))))))
 
 
     :else
     :else
@@ -800,13 +941,17 @@
      [:a (if (not dummy?)
      [:a (if (not dummy?)
            {:href (rfe/href :page {:name uuid})
            {:href (rfe/href :page {:name uuid})
             :on-click (fn [e]
             :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
       [:span.bullet-container.cursor
        {:id (str "dot-" uuid)
        {:id (str "dot-" uuid)
         :draggable true
         :draggable true
@@ -948,16 +1093,18 @@
     (->elem
     (->elem
      :span
      :span
      {:class "block-tags"}
      {:class "block-tags"}
-     (mapv (fn [{:keys [db/id tag/name]}]
-             (if (util/tag-valid? name)
-               [:a.tag.mx-1 {:key (str "tag-" id)
-                             :href (rfe/href :page {:name name})}
-                (str "#" name)]
-               [:span.warning.mx-1 {:title "Invalid tag, tags only accept alphanumeric characters, \"-\", \"_\", \"@\" and \"%\"."}
-                (str "#" name)]))
+     (mapv (fn [tag]
+             (when-let [page (db/entity (:db/id tag))]
+               (let [tag (:page/name page)]
+                 [:a.tag.mx-1 {:data-ref tag
+                               :key (str "tag-" (:db/id tag))
+                               :href (rfe/href :page {:name tag})}
+                  (str "#" tag)])))
            tags))))
            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]
   [{: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}]
                                 :as t}]
   (let [config (assoc config :block t)
   (let [config (assoc config :block t)
@@ -991,25 +1138,17 @@
             {:style {:background-color bg-color
             {:style {:background-color bg-color
                      :padding-left 6
                      :padding-left 6
                      :padding-right 6
                      :padding-right 6
-                     :color "#FFFFFF"}}))
+                     :color "#FFFFFF"}
+             :class "with-bg-color"}))
          (remove-nils
          (remove-nils
           (concat
           (concat
            [(when-not slide? checkbox)
            [(when-not slide? checkbox)
             (when-not slide? marker-switch)
             (when-not slide? marker-switch)
             marker-cp
             marker-cp
             priority]
             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])))))))
            [tags])))))))
 
 
 (defn dnd-same-block?
 (defn dnd-same-block?
@@ -1051,12 +1190,11 @@
                       [:span (t :page/edit-properties-placeholder)]
                       [:span (t :page/edit-properties-placeholder)]
                       (markup-elements-cp (assoc config :block/format format) ast))]]
                       (markup-elements-cp (assoc config :block/format format) ast))]]
       (if slide?
       (if slide?
-        [:div [:h1 (:page-name config)]
-         block-cp]
+        [:div [:h1 (:page-name config)]]
         block-cp))))
         block-cp))))
 
 
 (rum/defc properties-cp
 (rum/defc properties-cp
-  [block]
+  [config block]
   (let [properties (apply dissoc (:block/properties block) text/hidden-properties)]
   (let [properties (apply dissoc (:block/properties block) text/hidden-properties)]
     (when (seq properties)
     (when (seq properties)
       [:div.blocks-properties.text-sm.opacity-80.my-1.p-2
       [:div.blocks-properties.text-sm.opacity-80.my-1.p-2
@@ -1065,7 +1203,16 @@
          [:div.my-1
          [:div.my-1
           [:b k]
           [:b k]
           [:span.mr-1 ":"]
           [:span.mr-1 ":"]
-          (inline-text (:block/format block) (str v))])])))
+          (if (coll? v)
+            (let [v (->> (remove string/blank? v)
+                         (filter string?))
+                  vals (for [v-item v]
+                         (page-cp config {:page/name v-item}))]
+              (interpose [:span ", "] vals))
+            (let [page-name (string/lower-case (str v))]
+              (if (db/entity [:page/name page-name])
+                (page-cp config {:page/name page-name})
+                (inline-text (:block/format block) (str v)))))])])))
 
 
 (rum/defcs timestamp-cp < rum/reactive
 (rum/defcs timestamp-cp < rum/reactive
   (rum/local false ::show?)
   (rum/local false ::show?)
@@ -1102,7 +1249,7 @@
           (datetime-comp/date-picker nil nil ts)]))]))
           (datetime-comp/date-picker nil nil ts)]))]))
 
 
 (rum/defc block-content < rum/reactive
 (rum/defc block-content < rum/reactive
-  [config {:block/keys [uuid title level body meta content marker dummy? page format repo children pre-block? properties collapsed? idx block-refs-count scheduled scheduled-ast deadline deadline-ast repeated?] :as block} edit-input-id block-id slide?]
+  [config {:block/keys [uuid title level body meta content marker dummy? page format repo children pre-block? properties collapsed? idx container block-refs-count scheduled scheduled-ast deadline deadline-ast repeated?] :as block} edit-input-id block-id slide?]
   (let [dragging? (rum/react *dragging?)
   (let [dragging? (rum/react *dragging?)
         attrs {:blockid       (str uuid)
         attrs {:blockid       (str uuid)
                ;; FIXME: Click to copy a selection instead of click first and then copy
                ;; FIXME: Click to copy a selection instead of click first and then copy
@@ -1120,7 +1267,8 @@
                                       (let [cursor-range (util/caret-range (gdom/getElement block-id))
                                       (let [cursor-range (util/caret-range (gdom/getElement block-id))
                                             properties-hidden? (text/properties-hidden? properties)
                                             properties-hidden? (text/properties-hidden? properties)
                                             content (text/remove-level-spaces content format)
                                             content (text/remove-level-spaces content format)
-                                            content (if properties-hidden? (text/remove-properties! content) content)]
+                                            content (if properties-hidden? (text/remove-properties! content) content)
+                                            block (db/pull [:block/uuid (:block/uuid block)])]
                                         (state/set-editing!
                                         (state/set-editing!
                                          edit-input-id
                                          edit-input-id
                                          content
                                          content
@@ -1155,7 +1303,8 @@
 
 
       (if pre-block?
       (if pre-block?
         (pre-block-cp config (string/trim content) format)
         (pre-block-cp config (string/trim content) format)
-        (build-block-part config block))
+        (when (seq title)
+          (build-block-title config block)))
 
 
       (when (and dragging? (not slide?))
       (when (and dragging? (not slide?))
         (dnd-separator block 0 -4 false true))
         (dnd-separator block 0 -4 false true))
@@ -1170,19 +1319,20 @@
                  (let [hidden? (text/properties-hidden? properties)]
                  (let [hidden? (text/properties-hidden? properties)]
                    (not hidden?))
                    (not hidden?))
                  (not (:slide? config)))
                  (not (:slide? config)))
-        (properties-cp block))
+        (properties-cp config block))
 
 
       (when (and (not pre-block?) (seq body))
       (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))
      (when (and block-refs-count (> block-refs-count 0))
        [:div
        [:div
-        [:a.block.py-0.px-2.rounded.bg-base-2.opacity-50.hover:opacity-100
+        [:a.open-block-ref-link.bg-base-2
          {:title "Open block references"
          {:title "Open block references"
           :style {:margin-top -1}
           :style {:margin-top -1}
           :on-click (fn []
           :on-click (fn []
@@ -1246,9 +1396,9 @@
        (not @*dragging?)))
        (not @*dragging?)))
 
 
 (defn block-parents
 (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)
    (let [parents (db/get-block-parents repo block-id 3)
          page (db/get-block-page repo block-id)
          page (db/get-block-page repo block-id)
          page-name (:page/name page)]
          page-name (:page/name page)]
@@ -1266,16 +1416,10 @@
                           [:span.mx-2.opacity-50 "➤"])
                           [:span.mx-2.opacity-50 "➤"])
 
 
                         (when (seq parents)
                         (when (seq parents)
-                          (let [parents (for [{:block/keys [uuid content]} parents]
-                                          (let [title (->> (take 24
-                                                                 (-> (string/split content #"\n")
-                                                                     first
-                                                                     (text/remove-level-spaces format)))
-                                                           (apply str))]
-                                            (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)]
                                 parents (remove nil? parents)]
                             (reset! parents-atom parents)
                             (reset! parents-atom parents)
                             (when (seq parents)
                             (when (seq parents)
@@ -1295,7 +1439,7 @@
    :should-update (fn [old-state new-state]
    :should-update (fn [old-state new-state]
                     (not= (:block/content (second (:rum/args old-state)))
                     (not= (:block/content (second (:rum/args old-state)))
                           (:block/content (second (:rum/args new-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))
   (let [ref? (boolean (:ref? config))
         breadcrumb-show? (:breadcrumb-show? config)
         breadcrumb-show? (:breadcrumb-show? config)
         sidebar? (boolean (:sidebar? config))
         sidebar? (boolean (:sidebar? config))
@@ -1344,7 +1488,8 @@
                           (reset! *dragging-block nil)
                           (reset! *dragging-block nil)
                           (editor-handler/unhighlight-block!))
                           (editor-handler/unhighlight-block!))
                :on-mouse-move (fn [e]
                :on-mouse-move (fn [e]
-                                (when (non-dragging? e)
+                                (when (and (non-dragging? e)
+                                           (not @*resizing-image?))
                                   (state/into-selection-mode!)))
                                   (state/into-selection-mode!)))
                :on-mouse-down (fn [e]
                :on-mouse-down (fn [e]
                                 (when (and
                                 (when (and
@@ -1376,10 +1521,20 @@
                                (when doc-mode?
                                (when doc-mode?
                                  (when-let [parent (gdom/getElement block-id)]
                                  (when-let [parent (gdom/getElement block-id)]
                                    (when-let [node (.querySelector parent ".bullet-container")]
                                    (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
     [:div.ls-block.flex.flex-col.rounded-sm
      (cond->
      (cond->
       {:id block-id
       {:id block-id
+       :data-refs data-refs
+       :data-refs-self data-refs-self
        :style {:position "relative"}
        :style {:position "relative"}
        :class (str uuid
        :class (str uuid
                    (when dummy? " dummy")
                    (when dummy? " dummy")
@@ -1393,7 +1548,7 @@
        (merge attrs))
        (merge attrs))
 
 
      (when (and ref? breadcrumb-show?)
      (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]))
          [:div.my-2.opacity-50.ml-4 comp]))
 
 
      (dnd-separator-wrapper block slide? (zero? idx))
      (dnd-separator-wrapper block slide? (zero? idx))
@@ -1405,7 +1560,7 @@
       (block-content-or-editor config block edit-input-id block-id slide?)]
       (block-content-or-editor config block edit-input-id block-id slide?)]
 
 
      (when (seq children)
      (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" "")}}
                                      :display (if collapsed? "none" "")}}
         (for [child children]
         (for [child children]
           (when (map? child)
           (when (map? child)
@@ -1620,9 +1775,11 @@
             (and (seq result)
             (and (seq result)
                  (or only-blocks? blocks-grouped-by-page?))
                  (or only-blocks? blocks-grouped-by-page?))
             (->hiccup result (cond-> (assoc config
             (->hiccup result (cond-> (assoc config
-                                            ;; :editor-box editor/box
                                             :custom-query? true
                                             :custom-query? true
-                                            :group-by-page? blocks-grouped-by-page?)
+                                            ;; :breadcrumb-show? true
+                                            :group-by-page? blocks-grouped-by-page?
+                                            ;; :ref? true
+)
                                children?
                                children?
                                (assoc :ref? true))
                                (assoc :ref? true))
                       {:style {:margin-top "0.25rem"
                       {:style {:margin-top "0.25rem"
@@ -1682,24 +1839,20 @@
       ["Properties" m]
       ["Properties" m]
       [:div.properties
       [:div.properties
        (let [format (:block/format config)]
        (let [format (:block/format config)]
-         (for [[k v] m]
+         (for [[k v] (dissoc m :roam_alias :roam_tags)]
            (when (and (not (and (= k :macros) (empty? v))) ; empty macros
            (when (and (not (and (= k :macros) (empty? v))) ; empty macros
                       (not (= k :title))
                       (not (= k :title))
                       (not (= k :filter)))
                       (not (= k :filter)))
              [:div.property
              [:div.property
               [:span.font-medium.mr-1 (str (name k) ": ")]
               [:span.font-medium.mr-1 (str (name k) ": ")]
               (if (coll? v)
               (if (coll? v)
-                (for [item v]
-                  (if (or (= k :tags)
-                          (= k :alias))
-                    (if (string/includes? item "[[")
-                      (inline-text format item)
-                      (let [p (-> item
-                                  (string/replace "[" "")
-                                  (string/replace "]" ""))]
-                        [:a.mr-1 {:href (rfe/href :page {:name p})}
-                         p]))
-                    (inline-text format item)))
+                (let [vals (for [item v]
+                             (if (coll? v)
+                               (let [config (if (= k :alias)
+                                              (assoc config :page/alias? true))]
+                                 (page-cp config {:page/name item}))
+                               (inline-text format item)))]
+                  (interpose [:span ", "] vals))
                 (inline-text format v))])))]
                 (inline-text format v))])))]
 
 
       ["Paragraph" l]
       ["Paragraph" l]
@@ -1830,7 +1983,7 @@
            [:a.ml-1 {:id (str "fn." id)
            [:a.ml-1 {:id (str "fn." id)
                      :style {:font-size 14}
                      :style {:font-size 14}
                      :class "footnum"
                      :class "footnum"
-                     :href (str "#fnr." id)}
+                     :on-click #(route-handler/jump-to-anchor! (str "fnr." id))}
             [:sup.fn (str name "↩︎")]])]])
             [:sup.fn (str name "↩︎")]])]])
 
 
       :else
       :else
@@ -1901,13 +2054,10 @@
                       (rest blocks)
                       (rest blocks)
                       blocks)
                       blocks)
              first-id (:block/uuid (first blocks))]
              first-id (:block/uuid (first blocks))]
-         (for [item blocks]
+         (for [[idx item] (medley/indexed blocks)]
            (let [item (-> (if (:block/dummy? item)
            (let [item (-> (if (:block/dummy? item)
                             item
                             item
                             (dissoc item :block/meta)))
                             (dissoc item :block/meta)))
-                 item (if (= first-id (:block/uuid item))
-                        (assoc item :block/idx 0)
-                        item)
                  config (assoc config :block/uuid (:block/uuid item))]
                  config (assoc config :block/uuid (:block/uuid item))]
              (rum/with-key
              (rum/with-key
                (block-container config item)
                (block-container config item)
@@ -1924,14 +2074,17 @@
      (assoc :class "doc-mode"))
      (assoc :class "doc-mode"))
    (if (:group-by-page? config)
    (if (:group-by-page? config)
      [:div.flex.flex-col
      [:div.flex.flex-col
-      (for [[page blocks] blocks]
-        (if (not-empty blocks)
-          (let [page (db/entity (:db/id page))]
+      (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))}
             [:div.my-2 (cond-> {:key (str "page-" (:db/id page))}
                          (:ref? config)
                          (:ref? config)
-                         (assoc :class "bg-base-2 px-7 py-2 rounded"))
+                         (assoc :class "color-level px-7 py-2 rounded"))
              (ui/foldable
              (ui/foldable
-              (page-cp config page)
+              [:div
+               (page-cp config page)
+               (when alias? [:span.text-sm.font-medium.opacity-50 " Alias"])]
               (blocks-container blocks config))])))]
               (blocks-container blocks config))])))]
      (blocks-container blocks config))])
      (blocks-container blocks config))])
 
 

+ 108 - 6
src/main/frontend/components/block.css

@@ -10,9 +10,81 @@
 
 
   img {
   img {
     max-width: 100%;
     max-width: 100%;
+
+    /* FIXME: img macros */
+
+    &.left {
+      float: left;
+    }
+
+    &.right {
+      float: right;
+    }
+
+    &.loading-asset {
+      width: 9px;
+    }
+  }
+
+  .asset-container {
+    display: inline-block;
+    position: relative;
+    margin-top: .5rem;
+
+    .ctl {
+      position: absolute;
+      top: 0;
+      right: 0;
+      padding: 5px;
+      z-index: 1;
+      display: none;
+
+      > a {
+        padding: 3px;
+        border-radius: 4px;
+        opacity: .4;
+        user-select: none;
+        background: var(--ls-primary-background-color);
+
+        &.delete {
+          svg {
+            color: var(--ls-primary-text-color);
+
+            opacity: .5;
+            font-weight: normal;
+          }
+        }
+
+        &:hover {
+          opacity: 1;
+        }
+
+        &:active {
+          opacity: 1;
+        }
+      }
+    }
+
+    &:hover {
+      .ctl {
+        display: flex;
+      }
+    }
+  }
+
+  .resize {
+    display: flex;
   }
   }
 }
 }
 
 
+.open-block-ref-link {
+  @apply py-0 px-1 rounded opacity-50 hover:opacity-100;
+  font-size: 12px;
+  line-height: 1em;
+  position: relative;
+  right: -4px;
+}
+
 .block-body {
 .block-body {
   blockquote:first-child,
   blockquote:first-child,
   pre:first-child {
   pre:first-child {
@@ -22,17 +94,39 @@
 }
 }
 
 
 .block-children {
 .block-children {
-  border-left: 2px solid;
+  border-left: 1px solid;
   border-left-color: var(--ls-guideline-color, #ddd);
   border-left-color: var(--ls-guideline-color, #ddd);
+
+  padding-top: 2px;
+  padding-bottom: 3px;
+
+  > .ls-block {
+    &:last-child {
+      margin-bottom: -5px;
+    }
+  }
+}
+
+.block-control,
+.block-control:hover {
+  text-decoration: none;
+  cursor: pointer;
+  font-size: 14px;
+  min-width: 10px;
+  color: initial;
+  user-select: none;
 }
 }
 
 
 .block-ref {
 .block-ref {
-  color: var(--ls-link-text-color);
   padding-bottom: 2px;
   padding-bottom: 2px;
   border-bottom: 0.5px solid;
   border-bottom: 0.5px solid;
   border-bottom-color: var(--ls-block-ref-link-text-color);
   border-bottom-color: var(--ls-block-ref-link-text-color);
   cursor: alias;
   cursor: alias;
 
 
+  &-wrap {
+    display: inline-block;
+  }
+
   &:hover {
   &:hover {
     color: var(--ls-link-text-hover-color)
     color: var(--ls-link-text-hover-color)
   }
   }
@@ -81,6 +175,7 @@
 
 
   &:hover {
   &:hover {
     color: var(--ls-link-text-hover-color);
     color: var(--ls-link-text-hover-color);
+    opacity: 1;
   }
   }
 }
 }
 
 
@@ -192,8 +287,8 @@
 
 
 .bullet-container {
 .bullet-container {
   display: flex;
   display: flex;
-  height: 13px;
-  width: 13px;
+  height: 12px;
+  width: 12px;
   border-radius: 50%;
   border-radius: 50%;
   justify-content: center;
   justify-content: center;
   align-items: center;
   align-items: center;
@@ -204,9 +299,16 @@
 
 
   .bullet {
   .bullet {
     border-radius: 50%;
     border-radius: 50%;
-    width: 5px;
-    height: 5px;
+    width: 6px;
+    height: 6px;
     background-color: var(--ls-block-bullet-color, #394b59);
     background-color: var(--ls-block-bullet-color, #394b59);
+    transition: transform .2s;
+  }
+
+  &:hover {
+    .bullet {
+      transform: scale(1.2);
+    }
   }
   }
 
 
   &.bullet-closed {
   &.bullet-closed {

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

@@ -4,6 +4,8 @@
             [frontend.handler.repo :as repo-handler]
             [frontend.handler.repo :as repo-handler]
             [frontend.state :as state]
             [frontend.state :as state]
             [frontend.mixins :as mixins]
             [frontend.mixins :as mixins]
+            [frontend.handler.notification :as notification]
+            [promesa.core :as p]
             [goog.dom :as gdom]
             [goog.dom :as gdom]
             [goog.object :as gobj]))
             [goog.object :as gobj]))
 
 
@@ -11,7 +13,9 @@
   []
   []
   (let [value (gobj/get (gdom/getElement "commit-message") "value")]
   (let [value (gobj/get (gdom/getElement "commit-message") "value")]
     (when (and value (>= (count value) 1))
     (when (and value (>= (count value) 1))
-      (repo-handler/git-commit-and-push! value)
+      (-> (repo-handler/git-commit-and-push! value)
+          (p/catch (fn [error]
+                     (notification/show! error :error false))))
       (state/close-modal!))))
       (state/close-modal!))))
 
 
 (rum/defcs add-commit-message <
 (rum/defcs add-commit-message <

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

@@ -62,30 +62,52 @@
       :on-click editor-handler/bulk-make-todos}
       :on-click editor-handler/bulk-make-todos}
      (str "Make " (state/get-preferred-todo) "s"))]])
      (str "Make " (state/get-preferred-todo) "s"))]])
 
 
+;; FIXME: Make it configurable
 (def block-background-colors
 (def block-background-colors
-  ["rgb(83, 62, 125)"
-   "rgb(73, 125, 70)"
-   "rgb(120, 127, 151)"
-   "rgb(151, 134, 38)"
-   "rgb(73, 118, 123)"
-   "rgb(38, 76, 155)"
-   "rgb(121, 62, 62)"])
+  ["#533e7d"
+   "#497d46"
+   "#787f97"
+   "#978626"
+   "#49767b"
+   "#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 false ::edit?)
   (rum/local "" ::input)
   (rum/local "" ::input)
+  {:will-unmount (fn [state]
+                   (reset! *including-parent? nil)
+                   state)}
   [state block-id]
   [state block-id]
   (let [edit? (get state ::edit?)
   (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?
     (if @edit?
       (do
       (do
         (state/clear-edit!)
         (state/clear-edit!)
         [:div.px-4.py-2 {:on-click (fn [e] (util/stop e))}
         [:div.px-4.py-2 {:on-click (fn [e] (util/stop e))}
          [:p "What's the template's name?"]
          [: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
           {:auto-focus true
            :on-change (fn [e]
            :on-change (fn [e]
                         (reset! input (util/evalue e)))}]
                         (reset! input (util/evalue e)))}]
+         (when has-children?
+           (template-checkbox including-parent?))
          (ui/button "Submit"
          (ui/button "Submit"
                     :on-click (fn []
                     :on-click (fn []
                                 (let [title (string/trim @input)]
                                 (let [title (string/trim @input)]
@@ -96,6 +118,8 @@
                                        :error)
                                        :error)
                                       (do
                                       (do
                                         (editor-handler/set-block-property! block-id "template" title)
                                         (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!)))))))])
                                         (state/hide-custom-context-menu!)))))))])
       (ui/menu-link
       (ui/menu-link
        {:key "Make template"
        {:key "Make template"
@@ -130,7 +154,9 @@
           (ui/menu-link
           (ui/menu-link
            {:key "Convert heading"
            {:key "Convert heading"
             :on-click (fn [_e]
             :on-click (fn [_e]
-                        (editor-handler/set-block-as-a-heading! block-id (not heading?)))}
+                        (if heading?
+                          (editor-handler/remove-block-property! block-id "heading")
+                          (editor-handler/set-block-as-a-heading! block-id true)))}
            (if heading?
            (if heading?
              "Convert back to a block"
              "Convert back to a block"
              "Convert to a heading"))
              "Convert to a heading"))
@@ -147,7 +173,7 @@
                                     content (:block/content block)
                                     content (:block/content block)
                                     content (cond
                                     content (cond
                                               empty-properties?
                                               empty-properties?
-                                              (text/rejoin-properties content {"" ""} false)
+                                              (text/rejoin-properties content {"" ""} {:remove-blank? false})
                                               all-hidden?
                                               all-hidden?
                                               (let [idx (string/index-of content "\n:END:")]
                                               (let [idx (string/index-of content "\n:END:")]
                                                 (str
                                                 (str

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

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

+ 121 - 474
src/main/frontend/components/editor.cljs

@@ -3,23 +3,19 @@
             [frontend.components.svg :as svg]
             [frontend.components.svg :as svg]
             [frontend.config :as config]
             [frontend.config :as config]
             [frontend.handler.editor :as editor-handler :refer [get-state]]
             [frontend.handler.editor :as editor-handler :refer [get-state]]
+            [frontend.handler.editor.lifecycle :as lifecycle]
             [frontend.util :as util :refer-macros [profile]]
             [frontend.util :as util :refer-macros [profile]]
-            [frontend.handler.file :as file]
             [frontend.handler.block :as block-handler]
             [frontend.handler.block :as block-handler]
             [frontend.handler.page :as page-handler]
             [frontend.handler.page :as page-handler]
-            [frontend.handler.editor.keyboards :as keyboards-handler]
             [frontend.components.datetime :as datetime-comp]
             [frontend.components.datetime :as datetime-comp]
-            [promesa.core :as p]
             [frontend.state :as state]
             [frontend.state :as state]
             [frontend.mixins :as mixins]
             [frontend.mixins :as mixins]
             [frontend.ui :as ui]
             [frontend.ui :as ui]
             [frontend.db :as db]
             [frontend.db :as db]
-            [frontend.config :as config]
             [dommy.core :as d]
             [dommy.core :as d]
             [goog.object :as gobj]
             [goog.object :as gobj]
             [goog.dom :as gdom]
             [goog.dom :as gdom]
             [clojure.string :as string]
             [clojure.string :as string]
-            [clojure.set :as set]
             [frontend.commands :as commands
             [frontend.commands :as commands
              :refer [*show-commands
              :refer [*show-commands
                      *matched-commands
                      *matched-commands
@@ -27,9 +23,6 @@
                      *angle-bracket-caret-pos
                      *angle-bracket-caret-pos
                      *matched-block-commands
                      *matched-block-commands
                      *show-block-commands]]
                      *show-block-commands]]
-            [medley.core :as medley]
-            [cljs-drag-n-drop.core :as dnd]
-            [frontend.text :as text]
             ["/frontend/utils" :as utils]))
             ["/frontend/utils" :as utils]))
 
 
 (rum/defc commands < rum/reactive
 (rum/defc commands < rum/reactive
@@ -56,7 +49,7 @@
                        (editor-handler/insert-command! id command-steps
                        (editor-handler/insert-command! id command-steps
                                                        format
                                                        format
                                                        {:restore? restore-slash?})))
                                                        {:restore? restore-slash?})))
-        :class "black"}))))
+        :class     "black"}))))
 
 
 (rum/defc block-commands < rum/reactive
 (rum/defc block-commands < rum/reactive
   [id format]
   [id format]
@@ -69,7 +62,7 @@
                      (editor-handler/insert-command! id (get (into {} matched) chosen)
                      (editor-handler/insert-command! id (get (into {} matched) chosen)
                                                      format
                                                      format
                                                      {:last-pattern commands/angle-bracket}))
                                                      {:last-pattern commands/angle-bracket}))
-        :class "black"}))))
+        :class     "black"}))))
 
 
 (rum/defc page-search < rum/reactive
 (rum/defc page-search < rum/reactive
   {:will-unmount (fn [state] (reset! editor-handler/*selected-text nil) state)}
   {:will-unmount (fn [state] (reset! editor-handler/*selected-text nil) state)}
@@ -88,92 +81,44 @@
                  (when (> (count edit-content) current-pos)
                  (when (> (count edit-content) current-pos)
                    (util/safe-subs edit-content pos current-pos)))
                    (util/safe-subs edit-content pos current-pos)))
               matched-pages (when-not (string/blank? q)
               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
           (ui/auto-complete
            matched-pages
            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"]
             :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?)
   (when (state/sub :editor/show-block-search?)
     (let [pos (:editor/last-saved-cursor @state/state)
     (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
       (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
           (ui/auto-complete
            matched-blocks
            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]}]
             :item-render (fn [{:block/keys [content]}]
                            (subs content 0 64))
                            (subs content 0 64))
-            :class "black"}))))))
+            :class       "black"}))))))
 
 
 (rum/defc template-search < rum/reactive
 (rum/defc template-search < rum/reactive
   {:will-unmount (fn [state] (reset! editor-handler/*selected-text nil) state)}
   {:will-unmount (fn [state] (reset! editor-handler/*selected-text nil) state)}
@@ -190,43 +135,16 @@
                    (subs edit-content pos current-pos))
                    (subs edit-content pos current-pos))
                  "")
                  "")
               matched-templates (editor-handler/get-matched-templates q)
               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]
               non-exist-handler (fn [_state]
                                   (state/set-editor-show-template-search! false))]
                                   (state/set-editor-show-template-search! false))]
           (ui/auto-complete
           (ui/auto-complete
            matched-templates
            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]]
             :item-render (fn [[template _block-db-id]]
                            template)
                            template)
-            :class "black"}))))))
+            :class       "black"}))))))
 
 
 (rum/defc mobile-bar < rum/reactive
 (rum/defc mobile-bar < rum/reactive
   [parent-state parent-id]
   [parent-state parent-id]
@@ -246,12 +164,28 @@
    [:button.bottom-action
    [:button.bottom-action
     {:on-click #(commands/simple-insert! parent-id "\n" {})}
     {:on-click #(commands/simple-insert! parent-id "\n" {})}
     svg/multi-line-input]
     svg/multi-line-input]
+   [:button.bottom-action
+    {:on-click #(commands/insert-before! parent-id "TODO " {})}
+    svg/checkbox]
    [:button.font-extrabold.bottom-action.-mt-1
    [:button.font-extrabold.bottom-action.-mt-1
-    {:on-click #(commands/simple-insert! parent-id "[[]]" {:backward-pos 2})}
+    {: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]))})}
     "[[]]"]
     "[[]]"]
    [:button.font-extrabold.bottom-action.-mt-1
    [:button.font-extrabold.bottom-action.-mt-1
-    {:on-click #(commands/simple-insert! parent-id "(())" {:backward-pos 2})}
-    "(())"]])
+    {: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]))})}
+    "(())"]
+   [:button.font-extrabold.bottom-action.-mt-1
+    {:on-click #(commands/simple-insert! parent-id "/" {})}
+    "/"]])
 
 
 (rum/defcs input < rum/reactive
 (rum/defcs input < rum/reactive
   (rum/local {} ::input-value)
   (rum/local {} ::input-value)
@@ -294,11 +228,11 @@
               [:input.form-input.block.w-full.pl-2.sm:text-sm.sm:leading-5
               [:input.form-input.block.w-full.pl-2.sm:text-sm.sm:leading-5
                (merge
                (merge
                 (cond->
                 (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")}
                   :auto-complete (if (util/chrome?) "chrome-off" "off")}
                   placeholder
                   placeholder
                   (assoc :placeholder placeholder))
                   (assoc :placeholder placeholder))
@@ -345,22 +279,31 @@
     (when-let [pos (rum/react pos)]
     (when-let [pos (rum/react pos)]
       (ui/css-transition
       (ui/css-transition
        {:class-names "fade"
        {:class-names "fade"
-        :timeout {:enter 500
-                  :exit 300}}
+        :timeout     {:enter 500
+                      :exit  300}}
        (absolute-modal cp set-default-width? pos)))))
        (absolute-modal cp set-default-width? pos)))))
 
 
 (rum/defc image-uploader < rum/reactive
 (rum/defc image-uploader < rum/reactive
+  {:did-mount    (fn [state]
+                   (let [[id format] (:rum/args state)]
+                     (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-asset id #js[f] format editor-handler/*asset-uploading? true))))
+                   state)
+   :will-unmount (fn [state]
+                   (remove-watch editor-handler/*asset-pending-file ::pending-asset))}
   [id format]
   [id format]
   [:div.image-uploader
   [:div.image-uploader
    [:input
    [:input
-    {:id "upload-file"
-     :type "file"
+    {:id        "upload-file"
+     :type      "file"
      :on-change (fn [e]
      :on-change (fn [e]
                   (let [files (.-files (.-target 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
        (transition-cp
         [:div.flex.flex-row.align-center.rounded-md.shadow-sm.bg-base-2.px-1.py-1
         [:div.flex.flex-row.align-center.rounded-md.shadow-sm.bg-base-2.px-1.py-1
          (ui/loading
          (ui/loading
@@ -368,355 +311,59 @@
         false
         false
         *slash-caret-pos)))])
         *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)
+(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/slash))
-                  (do
-                    (reset! *slash-caret-pos nil)
-                    (reset! *show-commands false))
-
-                  (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]
   [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)]
   (let [content (state/get-edit-content)]
     [:div.editor-inner {:class (if block "block-editor" "non-block-editor")}
     [:div.editor-inner {:class (if block "block-editor" "non-block-editor")}
      (when config/mobile? (mobile-bar state id))
      (when config/mobile? (mobile-bar state id))
      (ui/ls-textarea
      (ui/ls-textarea
-      {:id id
+      {:id                id
+       :class             "mousetrap"
        :cacheMeasurements true
        :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))))
-       :auto-focus false})
+       :default-value     (or content "")
+       :minRows           (if (state/enable-grammarly?) 2 1)
+       :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?
      ;; TODO: how to render the transitions asynchronously?
      (transition-cp
      (transition-cp

+ 8 - 0
src/main/frontend/components/editor.css

@@ -42,6 +42,14 @@
   }
   }
 }
 }
 
 
+.is-mobile {
+  .absolute-modal {
+    &.is-overflow-vw-x {
+      transform: translateX(-1%);
+    }
+  }
+}
+
 .non-block-editor textarea,
 .non-block-editor textarea,
 pre {
 pre {
   display: block;
   display: block;

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

@@ -0,0 +1,177 @@
+(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.text-gray-900
+          "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.text-gray-900
+          {:on-click (fn []
+                       (when (not @reveal-secret-phrase?)
+                         (reset! reveal-secret-phrase? true)))}
+          [:div.font-medium.text-gray-900 "Public Key:"]
+          [:div public-key]
+          (if @reveal-secret-phrase?
+            [:div
+             [:div.mt-1.font-medium.text-gray-900 "Private Key:"]
+             [:div private-key]]
+            [:div.text-gray-500 "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.text-gray-900.font-bold
+          "Enter a password"]]]
+
+       (ui/admonition
+        :warning
+        [:div.text-gray-700
+         "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
+         :style {:color "#000"}
+         :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"
+         :style {:color "#000"}
+         :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.text-gray-900
+        "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.text-gray-900
+          "Enter your password"]]]
+
+       [:input.form-input.block.w-full.sm:text-sm.sm:leading-5.my-2
+        {:type "password"
+         :auto-focus true
+         :style {:color "#000"}
+         :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))))

+ 8 - 7
src/main/frontend/components/file.cljs

@@ -71,7 +71,7 @@
   (let [path (get-path state)
   (let [path (get-path state)
         format (format/get-format path)
         format (format/get-format path)
         page (db/get-file-page 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*]
     (rum/with-context [[tongue] i18n/*tongue-context*]
       [:div.file {:id (str "file-" path)}
       [:div.file {:id (str "file-" path)}
        [:h1.title
        [:h1.title
@@ -81,20 +81,21 @@
           [:a.bg-base-2.p-1.ml-1 {:style {:border-radius 4}
           [:a.bg-base-2.p-1.ml-1 {:style {:border-radius 4}
                                   :href (rfe/href :page {:name page})
                                   :href (rfe/href :page {:name page})
                                   :on-click (fn [e]
                                   :on-click (fn [e]
-                                              (.preventDefault e)
                                               (when (gobj/get e "shiftKey")
                                               (when (gobj/get e "shiftKey")
                                                 (when-let [page (db/entity [:page/name (string/lower-case page)])]
                                                 (when-let [page (db/entity [:page/name (string/lower-case page)])]
                                                   (state/sidebar-add-block!
                                                   (state/sidebar-add-block!
                                                    (state/get-current-repo)
                                                    (state/get-current-repo)
                                                    (:db/id page)
                                                    (:db/id page)
                                                    :page
                                                    :page
-                                                   {:page page}))))}
+                                                   {:page page}))
+                                                (util/stop e)))}
            page]])
            page]])
 
 
-       [:p.text-sm.ml-1.mb-4
-        (svg/warning {:style {:width "1em"
-                              :display "inline-block"}})
-        [:span.ml-1 "Please don't remove the page's title property (you can still modify it)."]]
+       (when (and page (not (string/starts-with? page "logseq/")))
+         [:p.text-sm.ml-1.mb-4
+          (svg/warning {:style {:width "1em"
+                                :display "inline-block"}})
+          [:span.ml-1 "Please don't remove the page's title property (you can still modify it)."]])
 
 
        (when (and config? (state/logged?))
        (when (and config? (state/logged?))
          [:a.mb-8.block {:on-click (fn [_e] (project/sync-project-settings!))}
          [:a.mb-8.block {:on-click (fn [_e] (project/sync-project-settings!))}

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

@@ -1,4 +1,6 @@
 .file {
 .file {
+  max-width: 86vw;
+
   textarea, pre {
   textarea, pre {
     margin: 0;
     margin: 0;
   }
   }

+ 128 - 103
src/main/frontend/components/header.cljs

@@ -9,12 +9,14 @@
             [frontend.storage :as storage]
             [frontend.storage :as storage]
             [frontend.config :as config]
             [frontend.config :as config]
             [frontend.context.i18n :as i18n]
             [frontend.context.i18n :as i18n]
+            [frontend.handler.ui :as ui-handler]
             [frontend.handler.user :as user-handler]
             [frontend.handler.user :as user-handler]
             [frontend.handler.export :as export]
             [frontend.handler.export :as export]
             [frontend.components.svg :as svg]
             [frontend.components.svg :as svg]
             [frontend.components.repo :as repo]
             [frontend.components.repo :as repo]
             [frontend.components.search :as search]
             [frontend.components.search :as search]
             [frontend.handler.project :as project-handler]
             [frontend.handler.project :as project-handler]
+            [frontend.handler.page :as page-handler]
             [frontend.handler.web.nfs :as nfs]
             [frontend.handler.web.nfs :as nfs]
             [goog.dom :as gdom]
             [goog.dom :as gdom]
             [goog.object :as gobj]))
             [goog.object :as gobj]))
@@ -22,7 +24,7 @@
 (rum/defc logo < rum/reactive
 (rum/defc logo < rum/reactive
   [{:keys [white?]}]
   [{:keys [white?]}]
   [:a.cp__header-logo
   [:a.cp__header-logo
-   {:href "/"
+   {:href     (rfe/href :home)
     :on-click (fn []
     :on-click (fn []
                 (util/scroll-to-top)
                 (util/scroll-to-top)
                 (state/set-journals-length! 1))}
                 (state/set-journals-length! 1))}
@@ -31,6 +33,29 @@
      [:img.cp__header-logo-img {:src logo}]
      [:img.cp__header-logo-img {:src logo}]
      (svg/logo (not white?)))])
      (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
 (rum/defc left-menu-button < rum/reactive
   [{:keys [on-click]}]
   [{:keys [on-click]}]
   [:button#left-menu.cp__header-left-menu
   [:button#left-menu.cp__header-left-menu
@@ -45,93 +70,83 @@
 
 
 (rum/defc dropdown-menu < rum/reactive
 (rum/defc dropdown-menu < rum/reactive
   [{:keys [me current-repo t default-home]}]
   [{: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
     (ui/dropdown-with-links
      (fn [{:keys [toggle-fn]}]
      (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}
         {: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? (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)
          {:title (t :settings)
-          :options {:href (rfe/href :settings)}
-          :icon svg/settings-sm}
-
-         (when (and logged? 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)])
+          :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/defc header
   < rum/reactive
   < rum/reactive
@@ -141,35 +156,47 @@
                    (remove #(= (:url %) config/local-repo)))]
                    (remove #(= (:url %) config/local-repo)))]
     (rum/with-context [[t] i18n/*tongue-context*]
     (rum/with-context [[t] i18n/*tongue-context*]
       [:div.cp__header#head
       [: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 []
        (left-menu-button {:on-click (fn []
                                       (open-fn)
                                       (open-fn)
                                       (state/set-left-sidebar-open! true))})
                                       (state/set-left-sidebar-open! true))})
 
 
        (logo {:white? white?})
        (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
        (if current-repo
          (search/search)
          (search/search)
          [:div.flex-1])
          [:div.flex-1])
 
 
        (new-block-mode)
        (new-block-mode)
 
 
-       (when (and (not logged?)
-                  (not config/publishing?))
-         [:a.text-sm.font-medium.login.opacity-70.hover:opacity-100
-          {:href "/login/github"
-           :on-click (fn []
-                       (storage/remove :git/current-repo))}
-          (t :login-github)])
+       (when-not (util/electron?)
+         (login logged?))
 
 
-       (repo/sync-status)
+       (repo/sync-status current-repo)
 
 
        [:div.repos.hidden.md:block
        [:div.repos.hidden.md:block
-        (repo/repos-dropdown true)]
+        (repo/repos-dropdown true nil)]
 
 
-       (when (and (nfs/supported?) (empty? repos))
+       (when (and (nfs/supported?) (empty? repos)
+                  (not config/publishing?))
          [:a.text-sm.font-medium.opacity-70.hover:opacity-100.ml-3.block
          [:a.text-sm.font-medium.opacity-70.hover:opacity-100.ml-3.block
           {:on-click (fn []
           {:on-click (fn []
-                       (nfs/ls-dir-files))}
+                       (page-handler/ls-dir-files!))}
           [:div.flex.flex-row.text-center
           [:div.flex.flex-row.text-center
            [:span.inline-block svg/folder-add]
            [:span.inline-block svg/folder-add]
            (when-not config/mobile?
            (when-not config/mobile?
@@ -178,14 +205,12 @@
 
 
        (if config/publishing?
        (if config/publishing?
          [:a.text-sm.font-medium.ml-3 {:href (rfe/href :graph)}
          [: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-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%;
   width: 100%;
   top: 0;
   top: 0;
   left: 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 {
 .cp__header-left-menu {
@@ -32,8 +46,11 @@
 
 
 .cp__header-logo,
 .cp__header-logo,
 .cp__right-menu-button {
 .cp__right-menu-button {
-  opacity: 0.7;
-  display: none;
+  opacity: 0.3;
+}
+
+.cp__header-logo {
+    display: none;
 }
 }
 
 
 .cp__header-logo:hover,
 .cp__header-logo:hover,
@@ -50,6 +67,10 @@
   @apply ml-3;
   @apply ml-3;
 }
 }
 
 
+.cp__right-menu-button {
+    display: block;
+}
+
 @screen sm {
 @screen sm {
   .cp__header {
   .cp__header {
     @apply shadow-none;
     @apply shadow-none;
@@ -63,8 +84,4 @@
     display: flex;
     display: flex;
     align-items: center;
     align-items: center;
   }
   }
-
-  .cp__right-menu-button {
-    display: block;
-  }
 }
 }

+ 20 - 13
src/main/frontend/components/journal.cljs

@@ -1,5 +1,6 @@
 (ns frontend.components.journal
 (ns frontend.components.journal
   (:require [rum.core :as rum]
   (:require [rum.core :as rum]
+            [reitit.frontend.easy :as rfe]
             [frontend.util :as util :refer-macros [profile]]
             [frontend.util :as util :refer-macros [profile]]
             [frontend.config :as config]
             [frontend.config :as config]
             [frontend.date :as date]
             [frontend.date :as date]
@@ -8,6 +9,7 @@
             [frontend.handler.page :as page-handler]
             [frontend.handler.page :as page-handler]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.editor :as editor-handler]
             [frontend.db :as db]
             [frontend.db :as db]
+            [frontend.db.model :as model]
             [frontend.state :as state]
             [frontend.state :as state]
             [frontend.ui :as ui]
             [frontend.ui :as ui]
             [frontend.config :as config]
             [frontend.config :as config]
@@ -20,7 +22,8 @@
             [frontend.components.onboarding :as onboarding]
             [frontend.components.onboarding :as onboarding]
             [goog.object :as gobj]
             [goog.object :as gobj]
             [clojure.string :as string]
             [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
 (rum/defc blocks-inner < rum/static
   {:did-mount (fn [state]
   {:did-mount (fn [state]
@@ -73,27 +76,34 @@
         intro? (and (not (state/logged?))
         intro? (and (not (state/logged?))
                     (not (config/local-db? repo))
                     (not (config/local-db? repo))
                     (not config/publishing?)
                     (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
      (ui/foldable
       [:a.initial-color.title
       [:a.initial-color.title
-       {:href (str "/page/" encoded-page-name)
+       {:href     (rfe/href :page {:name page})
         :on-click (fn [e]
         :on-click (fn [e]
-                    (.preventDefault e)
                     (when (gobj/get e "shiftKey")
                     (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/sidebar-add-block!
                          (state/get-current-repo)
                          (state/get-current-repo)
                          (:db/id page)
                          (:db/id page)
                          :page
                          :page
-                         {:page page
-                          :journal? true}))))}
+                         {:page     page
+                          :journal? true}))
+                      (.preventDefault e)))}
        [:h1.title
        [:h1.title
         (util/capitalize-all title)]]
         (util/capitalize-all title)]]
 
 
       (blocks-cp repo page format))
       (blocks-cp repo page format))
 
 
-     (when intro? (widgets/add-repo))
+     (when intro? (widgets/add-graph))
 
 
      (page/today-queries repo today? false)
      (page/today-queries repo today? false)
 
 
@@ -101,10 +111,7 @@
 
 
      (when intro? (onboarding/intro))]))
      (when intro? (onboarding/intro))]))
 
 
-(rum/defc journals <
-  {:did-mount (fn [state]
-                (editor-handler/open-last-block! true)
-                state)}
+(rum/defc journals
   [latest-journals]
   [latest-journals]
   [:div#journals
   [:div#journals
    (ui/infinite-list
    (ui/infinite-list

+ 9 - 3
src/main/frontend/components/onboarding.cljs

@@ -176,6 +176,10 @@
        [:a {:href "https://logseq.com/blog/about"
        [:a {:href "https://logseq.com/blog/about"
             :target "_blank"}
             :target "_blank"}
         (t :help/about)]]
         (t :help/about)]]
+      [:li
+       [:a {:href "https://trello.com/b/8txSM12G/roadmap"
+            :target "_blank"}
+        (t :help/roadmap)]]
       [:li
       [:li
        [:a {:href "https://github.com/logseq/logseq/issues/new?assignees=&labels=&template=bug_report.md&title="
        [:a {:href "https://github.com/logseq/logseq/issues/new?assignees=&labels=&template=bug_report.md&title="
             :target "_blank"}
             :target "_blank"}
@@ -237,8 +241,8 @@
          [:tr [:td (t :help/new-line-in-block)] [:td "Shift-Enter"]]
          [:tr [:td (t :help/new-line-in-block)] [:td "Shift-Enter"]]
          [:tr [:td (t :undo)] [:td (util/->platform-shortcut "Ctrl-z")]]
          [:tr [:td (t :undo)] [:td (util/->platform-shortcut "Ctrl-z")]]
          [:tr [:td (t :redo)] [:td (util/->platform-shortcut "Ctrl-y")]]
          [:tr [:td (t :redo)] [:td (util/->platform-shortcut "Ctrl-y")]]
-         [:tr [:td (t :help/zoom-in)] [:td (util/->platform-shortcut "Alt-Right")]]
-         [:tr [:td (t :help/zoom-out)] [:td (util/->platform-shortcut "Alt-left")]]
+         [:tr [:td (t :help/zoom-in)] [:td (util/->platform-shortcut (if util/mac? "Alt-." "Alt-Right"))]]
+         [:tr [:td (t :help/zoom-out)] [:td (util/->platform-shortcut (if util/mac? "Alt-," "Alt-left"))]]
          [:tr [:td (t :help/follow-link-under-cursor)] [:td (util/->platform-shortcut "Ctrl-o")]]
          [:tr [:td (t :help/follow-link-under-cursor)] [:td (util/->platform-shortcut "Ctrl-o")]]
          [:tr [:td (t :help/open-link-in-sidebar)] [:td (util/->platform-shortcut "Ctrl-shift-o")]]
          [:tr [:td (t :help/open-link-in-sidebar)] [:td (util/->platform-shortcut "Ctrl-shift-o")]]
          [:tr [:td (t :expand)] [:td (util/->platform-shortcut "Ctrl-Down")]]
          [:tr [:td (t :expand)] [:td (util/->platform-shortcut "Ctrl-Down")]]
@@ -258,11 +262,13 @@
          [:tr [:td (t :help/open-link-in-sidebar)] [:td "Shift-Click"]]
          [:tr [:td (t :help/open-link-in-sidebar)] [:td "Shift-Click"]]
          [:tr [:td (t :help/context-menu)] [:td "Right Click"]]
          [:tr [:td (t :help/context-menu)] [:td "Right Click"]]
          [:tr [:td (t :help/fold-unfold)] [:td "Tab"]]
          [: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-doc-mode)] [:td "t d"]]
          [:tr [:td (t :help/toggle-theme)] [:td "t t"]]
          [:tr [:td (t :help/toggle-theme)] [:td "t t"]]
          [:tr [:td (t :help/toggle-right-sidebar)] [:td "t r"]]
          [: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/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
        [:table
         [:thead
         [:thead
          [:tr
          [:tr

+ 92 - 53
src/main/frontend/components/page.cljs

@@ -1,6 +1,7 @@
 (ns frontend.components.page
 (ns frontend.components.page
   (:require [rum.core :as rum]
   (:require [rum.core :as rum]
             [frontend.util :as util :refer-macros [profile]]
             [frontend.util :as util :refer-macros [profile]]
+            [frontend.tools.html-export :as html-export]
             [frontend.handler.file :as file]
             [frontend.handler.file :as file]
             [frontend.handler.page :as page-handler]
             [frontend.handler.page :as page-handler]
             [frontend.handler.ui :as ui-handler]
             [frontend.handler.ui :as ui-handler]
@@ -21,6 +22,8 @@
             [frontend.components.project :as project]
             [frontend.components.project :as project]
             [frontend.config :as config]
             [frontend.config :as config]
             [frontend.db :as db]
             [frontend.db :as db]
+            [frontend.db.model :as model]
+            [frontend.db.utils :as db-utils]
             [frontend.mixins :as mixins]
             [frontend.mixins :as mixins]
             [frontend.db-mixins :as db-mixins]
             [frontend.db-mixins :as db-mixins]
             [goog.dom :as gdom]
             [goog.dom :as gdom]
@@ -34,6 +37,7 @@
             [cljs.pprint :as pprint]
             [cljs.pprint :as pprint]
             [frontend.context.i18n :as i18n]
             [frontend.context.i18n :as i18n]
             [reitit.frontend.easy :as rfe]
             [reitit.frontend.easy :as rfe]
+            [frontend.text :as text]
             [frontend.handler.block :as block-handler]))
             [frontend.handler.block :as block-handler]))
 
 
 (defn- get-page-name
 (defn- get-page-name
@@ -54,6 +58,9 @@
   db-mixins/query
   db-mixins/query
   [repo page file-path page-name page-original-name encoded-page-name sidebar? journal? block? block-id format]
   [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)
   (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
         page-blocks (block-handler/with-dummy-block raw-page-blocks format
                       (if (empty? raw-page-blocks)
                       (if (empty? raw-page-blocks)
                         (let [content (db/get-file repo file-path)]
                         (let [content (db/get-file repo file-path)]
@@ -73,11 +80,23 @@
                        :editor-box editor/box}
                        :editor-box editor/box}
         hiccup-config (common-handler/config-with-document-mode hiccup-config)
         hiccup-config (common-handler/config-with-document-mode hiccup-config)
         hiccup (block/->hiccup page-blocks 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
 (defn contents-page
   [{:page/keys [name original-name file] :as contents}]
   [{:page/keys [name original-name file] :as contents}]
@@ -157,7 +176,7 @@
   [state page-name close-fn]
   [state page-name close-fn]
   (let [input (get state ::input)]
   (let [input (get state ::input)]
     (rum/with-context [[t] i18n/*tongue-context*]
     (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.sm:flex.sm:items-start
         [:div.mt-3.text-center.sm:mt-0.sm:text-left
         [: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.text-gray-900
@@ -202,18 +221,13 @@
          [:ul.mt-2
          [:ul.mt-2
           (for [[original-name name] pages]
           (for [[original-name name] pages]
             [:li {:key (str "tagged-page-" name)}
             [: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
 ;; A page is just a logical block
 (rum/defcs page < rum/reactive
 (rum/defcs page < rum/reactive
   {:did-mount (fn [state]
   {:did-mount (fn [state]
                 (ui-handler/scroll-and-highlight! 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)
                 state)
    :did-update (fn [state]
    :did-update (fn [state]
                  (ui-handler/scroll-and-highlight! state)
                  (ui-handler/scroll-and-highlight! state)
@@ -227,10 +241,13 @@
         path-page-name page-name
         path-page-name page-name
         marker-page? (util/marker? page-name)
         marker-page? (util/marker? page-name)
         priority-page? (contains? #{"a" "b" "c"} 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? (util/uuid-string? page-name)
         block-id (and block? (uuid 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)]
         sidebar? (:sidebar? option)]
     (rum/with-context [[t] i18n/*tongue-context*]
     (rum/with-context [[t] i18n/*tongue-context*]
       (cond
       (cond
@@ -268,23 +285,35 @@
               developer-mode? (state/sub [:ui/developer-mode?])
               developer-mode? (state/sub [:ui/developer-mode?])
               published? (= "true" (:published properties))
               published? (= "true" (:published properties))
               public? (= "true" (:public 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
            [:div.relative
             (when (and (not block?)
             (when (and (not block?)
                        (not sidebar?)
                        (not sidebar?)
                        (not config/publishing?))
                        (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)
                             {:title   (t :page/action-publish)
                              :options {:on-click
                              :options {:on-click
                                        (fn []
                                        (fn []
@@ -294,23 +323,35 @@
                                              (mapv (fn [{:keys [title options]}]
                                              (mapv (fn [{:keys [title options]}]
                                                      (when title
                                                      (when title
                                                        [:div.it
                                                        [:div.it
-                                                        {:on-click #(state/close-modal!)}
                                                         (apply (partial ui/button title) (flatten (seq options)))]))
                                                         (apply (partial ui/button title) (flatten (seq options)))]))
-                                                   [{:title   (t :page/publish)
-                                                     :options {:on-click (fn []
-                                                                           (page-handler/publish-page! page-name project/add-project))}}
-                                                    {:title   (t :page/publish-as-slide)
-                                                     :options {:on-click (fn []
-                                                                           (page-handler/publish-page-as-slide! page-name project/add-project))}}
-                                                    (when published?
+                                                   [(if published?
                                                       {:title   (t :page/unpublish)
                                                       {:title   (t :page/unpublish)
                                                        :options {:on-click (fn []
                                                        :options {:on-click (fn []
-                                                                             (page-handler/unpublish-page! page-name))}})
+                                                                             (page-handler/unpublish-page! page-name))}}
+                                                      {:title   (t :page/publish)
+                                                       :options {:on-click (fn []
+                                                                             (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
+                                                                              html-export/export-page))}})
                                                     {:title   (t (if public? :page/make-private :page/make-public))
                                                     {:title   (t (if public? :page/make-private :page/make-public))
                                                      :options {:background (if public? "gray" "indigo")
                                                      :options {:background (if public? "gray" "indigo")
-                                                               :on-click #(page-handler/update-public-attribute!
-                                                                           page-name
-                                                                           (if public? false true))}}])])))}}
+                                                               :on-click (fn []
+                                                                           (page-handler/update-public-attribute!
+                                                                            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?
                             (when developer-mode?
                               {:title "(Dev) Show page data"
                               {:title "(Dev) Show page data"
                                :options {:on-click (fn []
                                :options {:on-click (fn []
@@ -324,6 +365,7 @@
                                                                     :on-click #(.writeText js/navigator.clipboard page-data))]
                                                                     :on-click #(.writeText js/navigator.clipboard page-data))]
                                                         :success
                                                         :success
                                                         false)))}})]
                                                         false)))}})]
+                           (flatten)
                            (remove nil?))]
                            (remove nil?))]
                 (when (seq links)
                 (when (seq links)
                   (ui/dropdown-with-links
                   (ui/dropdown-with-links
@@ -371,12 +413,11 @@
                  {:key "page-file"}
                  {:key "page-file"}
                  [:span.opacity-50 {:style {:margin-top 2}} (t :file/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
                  [: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]
                   file-path]
 
 
-                 (when (and (not config/mobile?)
-                            (not journal?))
+                 (when (not config/mobile?)
                    (presentation repo page))])]
                    (presentation repo page))])]
 
 
              (when (and repo (not block?))
              (when (and repo (not block?))
@@ -385,12 +426,14 @@
                    [:div.text-sm.ml-1.mb-4 {:key "page-file"}
                    [:div.text-sm.ml-1.mb-4 {:key "page-file"}
                     [:span.opacity-50 "Alias: "]
                     [:span.opacity-50 "Alias: "]
                     (for [item 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])])))
                        item])])))
 
 
              (when (and block? (not sidebar?))
              (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
              ;; blocks
              (page-blocks-cp repo page file-path page-name page-original-name encoded-page-name sidebar? journal? block? block-id format)]]
              (page-blocks-cp repo page file-path page-name page-original-name encoded-page-name sidebar? journal? block? block-id format)]]
@@ -432,7 +475,7 @@
            {:width (if (and (> width 1280) sidebar-open?)
            {:width (if (and (> width 1280) sidebar-open?)
                      (- width 24 600)
                      (- width 24 600)
                      (- width 24))
                      (- width 24))
-            :height (- height 120)
+            :height height
             :ref (fn [v] (reset! graph-ref v))
             :ref (fn [v] (reset! graph-ref v))
             :ref-atom graph-ref}))
             :ref-atom graph-ref}))
          [:div.ls-center.mt-20
          [:div.ls-center.mt-20
@@ -471,11 +514,10 @@
               [:th (t :page/name)]
               [:th (t :page/name)]
               [:th (t :file/last-modified-at)]]]
               [:th (t :file/last-modified-at)]]]
             [:tbody
             [:tbody
-             (for [[page modified-at] pages]
+             (for [page pages]
                (let [encoded-page (util/encode-str page)]
                (let [encoded-page (util/encode-str page)]
                  [:tr {:key encoded-page}
                  [:tr {:key encoded-page}
                   [:td [:a {:on-click (fn [e]
                   [:td [:a {:on-click (fn [e]
-                                        (.preventDefault e)
                                         (let [repo (state/get-current-repo)
                                         (let [repo (state/get-current-repo)
                                               page (db/pull repo '[*] [:page/name (string/lower-case page)])]
                                               page (db/pull repo '[*] [:page/name (string/lower-case page)])]
                                           (when (gobj/get e "shiftKey")
                                           (when (gobj/get e "shiftKey")
@@ -487,10 +529,7 @@
                             :href (rfe/href :page {:name encoded-page})}
                             :href (rfe/href :page {:name encoded-page})}
                         page]]
                         page]]
                   [:td [:span.text-gray-500.text-sm
                   [: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/defcs new < rum/reactive
   (rum/local "" ::title)
   (rum/local "" ::title)

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

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

+ 5 - 2
src/main/frontend/components/project.cljs

@@ -1,7 +1,9 @@
 (ns frontend.components.project
 (ns frontend.components.project
   (:require [rum.core :as rum]
   (:require [rum.core :as rum]
             [frontend.util :as util :refer-macros [profile]]
             [frontend.util :as util :refer-macros [profile]]
-            [frontend.handler.project :as project-handler]))
+            [frontend.handler.project :as project-handler]
+            [frontend.handler.config :as config-handler]
+            [clojure.string :as string]))
 
 
 (rum/defcs add-project <
 (rum/defcs add-project <
   (rum/local "" ::project)
   (rum/local "" ::project)
@@ -40,7 +42,8 @@
          :on-click (fn []
          :on-click (fn []
                      (let [value @project]
                      (let [value @project]
                        (when (and value (>= (count value) 2))
                        (when (and value (>= (count value) 2))
-                         (project-handler/add-project! value))))}
+                         (project-handler/add-project! value
+                                                       config-handler/set-project!))))}
         "Submit"]]
         "Submit"]]
       [:span.mt-3.flex.w-full.rounded-md.shadow-sm.sm:mt-0.sm:w-auto
       [: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
        [: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

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

@@ -93,7 +93,8 @@
           (ui/foldable
           (ui/foldable
            [:div.flex.flex-row.flex-1.justify-between
            [:div.flex.flex-row.flex-1.justify-between
             [:h2.font-bold.opacity-50 (let []
             [:h2.font-bold.opacity-50 (let []
-                                        (str n-ref " Linked References"))]
+                                        (str n-ref " Linked Reference"
+                                             (if (> n-ref 1) "s")))]
             [:a {:title "Filter"
             [:a {:title "Filter"
                  :on-click #(state/set-modal! (filter-dialog references page-name))}
                  :on-click #(state/set-modal! (filter-dialog references page-name))}
               (svg/filter-icon (cond
               (svg/filter-icon (cond
@@ -101,6 +102,7 @@
                                  (every? true? (vals filter-state)) "text-green-500"
                                  (every? true? (vals filter-state)) "text-green-500"
                                  (every? false? (vals filter-state)) "text-red-500"
                                  (every? false? (vals filter-state)) "text-red-500"
                                  :else "text-yellow-200"))]]
                                  :else "text-yellow-200"))]]
+
            [:div.references-blocks
            [:div.references-blocks
             (let [ref-hiccup (block/->hiccup filtered-ref-blocks
             (let [ref-hiccup (block/->hiccup filtered-ref-blocks
                                              {:id page-name
                                              {:id page-name
@@ -145,7 +147,8 @@
           (ui/foldable
           (ui/foldable
            [:h2.font-bold {:style {:opacity "0.3"}}
            [:h2.font-bold {:style {:opacity "0.3"}}
             (if @n-ref
             (if @n-ref
-              (str @n-ref " Unlinked References")
+              (str @n-ref " Unlinked Reference" (if (> @n-ref 1)
+                                                  "s"))
               "Unlinked References")]
               "Unlinked References")]
            (fn [] (unlinked-references-aux page-name n-ref))
            (fn [] (unlinked-references-aux page-name n-ref))
            true)]]))))
            true)]]))))

+ 63 - 36
src/main/frontend/components/repo.cljs

@@ -4,23 +4,32 @@
             [frontend.ui :as ui]
             [frontend.ui :as ui]
             [frontend.state :as state]
             [frontend.state :as state]
             [frontend.db :as db]
             [frontend.db :as db]
+            [frontend.encrypt :as e]
             [frontend.handler.repo :as repo-handler]
             [frontend.handler.repo :as repo-handler]
             [frontend.handler.common :as common-handler]
             [frontend.handler.common :as common-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.export :as export-handler]
             [frontend.handler.export :as export-handler]
             [frontend.handler.web.nfs :as nfs-handler]
             [frontend.handler.web.nfs :as nfs-handler]
+            [frontend.handler.page :as page-handler]
             [frontend.util :as util]
             [frontend.util :as util]
             [frontend.config :as config]
             [frontend.config :as config]
             [reitit.frontend.easy :as rfe]
             [reitit.frontend.easy :as rfe]
             [frontend.version :as version]
             [frontend.version :as version]
             [frontend.components.commit :as commit]
             [frontend.components.commit :as commit]
             [frontend.components.svg :as svg]
             [frontend.components.svg :as svg]
+            [frontend.components.encryption :as encryption]
             [frontend.context.i18n :as i18n]
             [frontend.context.i18n :as i18n]
-            [clojure.string :as string]))
+            [clojure.string :as string]
+            [clojure.string :as str]))
 
 
 (rum/defc add-repo
 (rum/defc add-repo
-  []
-  (widgets/add-repo))
+  [args]
+  (if-let [graph-types (get-in args [:query-params :graph-types])]
+    (let [graph-types-s (->> (str/split graph-types #",")
+                             (mapv keyword))]
+      (when (seq graph-types-s)
+        (widgets/add-graph :graph-types graph-types-s)))
+    (widgets/add-graph)))
 
 
 (rum/defc repos < rum/reactive
 (rum/defc repos < rum/reactive
   []
   []
@@ -29,20 +38,25 @@
         repos (util/distinct-by :url repos)]
         repos (util/distinct-by :url repos)]
     (rum/with-context [[t] i18n/*tongue-context*]
     (rum/with-context [[t] i18n/*tongue-context*]
       (if (seq repos)
       (if (seq repos)
-        [:div#repos
-         [:h1.title "All 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
           [:div.flex.flex-row.my-4
            (when (nfs-handler/supported?)
            (when (nfs-handler/supported?)
              [:div.mr-8
              [:div.mr-8
               (ui/button
               (ui/button
                (t :open-a-directory)
                (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
              (ui/button
               "Add another git repo"
               "Add another git repo"
-              :href (rfe/href :repo-add)))]
+              :href (rfe/href :repo-add nil {:graph-types "github"})
+              :intent "logseq"))]
           (for [{:keys [id url] :as repo} repos]
           (for [{:keys [id url] :as repo} repos]
             (let [local? (config/local-db? url)]
             (let [local? (config/local-db? url)]
               [:div.flex.justify-between.mb-1 {:key id}
               [:div.flex.justify-between.mb-1 {:key id}
@@ -53,45 +67,44 @@
                       :href url}
                       :href url}
                   (db/get-repo-path url)])
                   (db/get-repo-path url)])
                [:div.controls
                [: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/refresh! 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 []
                                   :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))}
                                                     (repo-handler/remove-repo! repo))}
                  "Unlink"]]]))]
                  "Unlink"]]]))]
 
 
          [:a#download-as-json.hidden]]
          [:a#download-as-json.hidden]]
-        (widgets/add-repo)))))
+        (widgets/add-graph)))))
 
 
 (rum/defc sync-status < rum/reactive
 (rum/defc sync-status < rum/reactive
   {:did-mount (fn [state]
   {:did-mount (fn [state]
                 (js/setTimeout common-handler/check-changed-files-status 1000)
                 (js/setTimeout common-handler/check-changed-files-status 1000)
                 state)}
                 state)}
-  []
-  (when-let [repo (state/get-current-repo)]
+  [repo]
+  (when repo
     (let [nfs-repo? (config/local-db? repo)]
     (let [nfs-repo? (config/local-db? repo)]
       (when-not (= repo config/local-repo)
       (when-not (= repo config/local-repo)
         (if (and nfs-repo? (nfs-handler/supported?))
         (if (and nfs-repo? (nfs-handler/supported?))
           (let [syncing? (state/sub :graph/syncing?)]
           (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
              [:a
               {:on-click #(nfs-handler/refresh! repo
               {:on-click #(nfs-handler/refresh! repo
                                                 repo-handler/create-today-journal!)
                                                 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)}
                            version/version)}
               svg/refresh]])
               svg/refresh]])
           (let [changed-files (state/sub [:repo/changed-files repo])
           (let [changed-files (state/sub [:repo/changed-files repo])
@@ -99,6 +112,13 @@
                 git-status (state/sub [:git/status repo])
                 git-status (state/sub [:git/status repo])
                 pushing? (= :pushing git-status)
                 pushing? (= :pushing git-status)
                 pulling? (= :pulling git-status)
                 pulling? (= :pulling git-status)
+                git-failed? (contains?
+                             #{:push-failed
+                               :clone-failed
+                               :checkout-failed
+                               :fetch-failed
+                               :merge-failed}
+                             git-status)
                 push-failed? (= :push-failed git-status)
                 push-failed? (= :push-failed git-status)
                 last-pulled-at (db/sub-key-value repo :git/last-pulled-at)
                 last-pulled-at (db/sub-key-value repo :git/last-pulled-at)
                 ;; db-persisted? (state/sub [:db/persisted? repo])
                 ;; db-persisted? (state/sub [:db/persisted? repo])
@@ -109,7 +129,7 @@
               (fn [{:keys [toggle-fn]}]
               (fn [{:keys [toggle-fn]}]
                 [:div.cursor.w-2.h-2.sync-status.mr-2
                 [:div.cursor.w-2.h-2.sync-status.mr-2
                  {:class (cond
                  {:class (cond
-                           push-failed?
+                           git-failed?
                            "bg-red-500"
                            "bg-red-500"
                            (or
                            (or
                             ;; (not db-persisted?)
                             ;; (not db-persisted?)
@@ -185,8 +205,12 @@
           (> (count repos) 1)
           (> (count repos) 1)
           (ui/dropdown-with-links
           (ui/dropdown-with-links
            (fn [{:keys [toggle-fn]}]
            (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"}}]])
               [:span.dropdown-caret.ml-1 {:style {:border-top-color "#6b7280"}}]])
            (mapv
            (mapv
             (fn [{:keys [id url]}]
             (fn [{:keys [id url]}]
@@ -208,8 +232,11 @@
           (and current-repo (not local-repo?))
           (and current-repo (not local-repo?))
           (let [repo-name (get-repo-name current-repo)]
           (let [repo-name (get-repo-name current-repo)]
             (if (config/local-db? 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
                {:href current-repo
                 :target "_blank"}
                 :target "_blank"}
                repo-name]))
                repo-name]))

+ 94 - 66
src/main/frontend/components/right_sidebar.cljs

@@ -11,6 +11,7 @@
             [frontend.handler.graph :as graph-handler]
             [frontend.handler.graph :as graph-handler]
             [frontend.state :as state]
             [frontend.state :as state]
             [frontend.db :as db]
             [frontend.db :as db]
+            [frontend.db.model :as db-model]
             [frontend.util :as util]
             [frontend.util :as util]
             [frontend.date :as date]
             [frontend.date :as date]
             [medley.core :as medley]
             [medley.core :as medley]
@@ -27,16 +28,16 @@
 (rum/defc block-cp < rum/reactive
 (rum/defc block-cp < rum/reactive
   [repo idx block]
   [repo idx block]
   (let [id (:block/uuid 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
                 :sidebar/idx idx
-                :repo repo})))
+                :repo        repo})))
 
 
 (rum/defc page-cp < rum/reactive
 (rum/defc page-cp < rum/reactive
   [repo page-name]
   [repo page-name]
   (page/page {:parameters {:path {:name page-name}}
   (page/page {:parameters {:path {:name page-name}}
-              :sidebar? true
-              :repo repo}))
+              :sidebar?   true
+              :repo       repo}))
 
 
 (rum/defc page-graph < db-mixins/query
 (rum/defc page-graph < db-mixins/query
   [page]
   [page]
@@ -50,7 +51,7 @@
        (graph-2d/graph
        (graph-2d/graph
         (graph/build-graph-opts
         (graph/build-graph-opts
          graph dark? false
          graph dark? false
-         {:width 600
+         {:width  600
           :height 600}))])))
           :height 600}))])))
 
 
 (defn recent-pages
 (defn recent-pages
@@ -59,34 +60,19 @@
     [:div.recent-pages.text-sm.flex-col.flex.ml-3.mt-2
     [:div.recent-pages.text-sm.flex-col.flex.ml-3.mt-2
      (if (seq pages)
      (if (seq pages)
        (for [page 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]
+                               (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}))
+                                 (.preventDefault e)))}
           page]))]))
           page]))]))
 
 
-(rum/defcs foldable-list <
-  (rum/local false ::fold?)
-  [state page l]
-  (let [fold? (get state ::fold?)]
-    [:div
-     [:div.flex.flex-row.items-center.mb-1
-      [:a.control.opacity-50.hover:opacity-100
-       {:on-click #(swap! fold? not)
-        :style {:width "0.75rem"}}
-       (when (seq l)
-         (if @fold?
-           svg/arrow-down-v2
-           svg/arrow-right-v2))]
-
-      [:a.ml-2 {:key (str "contents-" page)
-                :href (rfe/href :page {:name page})}
-       (util/capitalize-all page)]]
-     (when (seq l)
-       [:div.contents-list.ml-4 {:class (if @fold? "hidden" "initial")}
-        (for [{:keys [page list]} l]
-          (rum/with-key
-            (foldable-list page list)
-            (str "toc-item-" page)))])]))
-
 (rum/defc contents < rum/reactive db-mixins/query
 (rum/defc contents < rum/reactive db-mixins/query
   []
   []
   [:div.contents.flex-col.flex.ml-3
   [:div.contents.flex-col.flex.ml-3
@@ -101,7 +87,7 @@
                       (util/stop e)
                       (util/stop e)
                       (if-not (db/entity [:page/name "contents"])
                       (if-not (db/entity [:page/name "contents"])
                         (page-handler/create! "contents")
                         (page-handler/create! "contents")
-                        (route-handler/redirect! {:to :page
+                        (route-handler/redirect! {:to          :page
                                                   :path-params {:name "contents"}})))}
                                                   :path-params {:name "contents"}})))}
       (t :right-side-bar/contents)]
       (t :right-side-bar/contents)]
      (contents)]
      (contents)]
@@ -113,7 +99,7 @@
     [(t :right-side-bar/help) (onboarding/help)]
     [(t :right-side-bar/help) (onboarding/help)]
 
 
     :page-graph
     :page-graph
-    [(str (t :right-side-bar/graph-ref) (util/capitalize-all block-data))
+    [(str (t :right-side-bar/graph-ref) (db-model/get-page-original-name block-data))
      (page-graph block-data)]
      (page-graph block-data)]
 
 
     :block-ref
     :block-ref
@@ -123,7 +109,8 @@
              block-id (:block/uuid block)
              block-id (:block/uuid block)
              format (:block/format block)]
              format (:block/format block)]
          [[:div.ml-2.mt-1
          [[: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
           [:div.ml-2
            (block-cp repo idx block)]])])
            (block-cp repo idx block)]])])
 
 
@@ -131,14 +118,18 @@
     (when-let [block (db/entity repo [:block/uuid (:block/uuid block-data)])]
     (when-let [block (db/entity repo [:block/uuid (:block/uuid block-data)])]
       (let [block-id (:block/uuid block-data)
       (let [block-id (:block/uuid block-data)
             format (:block/format 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
          [:div.ml-2
           (block-cp repo idx block-data)]]))
           (block-cp repo idx block-data)]]))
 
 
     :page
     :page
     (let [page-name (:page/name block-data)]
     (let [page-name (:page/name block-data)]
-      [[:a {:href (rfe/href :page {:name (util/url-encode page-name)})}
-        (util/capitalize-all page-name)]
+      [[:a {:href     (rfe/href :page {:name page-name})
+            :on-click (fn [e]
+                        (when (gobj/get e "shiftKey")
+                          (.preventDefault e)))}
+        (db-model/get-page-original-name page-name)]
        [:div.ml-2
        [:div.ml-2
         (page-cp repo page-name)]])
         (page-cp repo page-name)]])
 
 
@@ -149,13 +140,13 @@
           blocks (if journal?
           blocks (if journal?
                    (rest blocks)
                    (rest blocks)
                    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
                                                        :start-level 2
-                                                       :slide? true
-                                                       :sidebar? true
-                                                       :page-name page-name})]
-      [[:a {:href (str "/page/" (util/url-encode page-name))}
-        (util/capitalize-all 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
        [:div.ml-2.slide.mt-2
         (slide/slide sections)]])
         (slide/slide sections)]])
 
 
@@ -217,9 +208,41 @@
         theme (:ui/theme @state/state)]
         theme (:ui/theme @state/state)]
     (get-page match)))
     (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
 (rum/defcs sidebar < rum/reactive
   [state]
   [state]
   (let [blocks (state/sub :sidebar/blocks)
   (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?)
         sidebar-open? (state/sub :ui/sidebar-open?)
         repo (state/sub :git/current-repo)
         repo (state/sub :git/current-repo)
         match (state/sub :route-match)
         match (state/sub :route-match)
@@ -230,31 +253,36 @@
        {:class (if sidebar-open? "is-open")}
        {:class (if sidebar-open? "is-open")}
        (if sidebar-open?
        (if sidebar-open?
          [:div.cp__right-sidebar-inner
          [: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)]]
+          (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))}
 
 
-           [: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)]]
+              (t :right-side-bar/recent)]]
 
 
-           (when config/publishing?
-             [:div.ml-4.text-sm
-              [:a {:href (rfe/href :all-pages)}
-               (t :all-pages)]])
+            [: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 []
-                                                  (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)]
           (for [[idx [repo db-id block-type block-data]] (medley/indexed blocks)]
             (rum/with-key
             (rum/with-key

+ 77 - 25
src/main/frontend/components/search.cljs

@@ -31,11 +31,46 @@
   (let [switch (reductions not= true (map pred? coll (rest coll)))]
   (let [switch (reductions not= true (map pred? coll (rest coll)))]
     (map (partial map first) (partition-by second (map list coll switch)))))
     (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
 (rum/defc highlight-fuzzy
   [content indexes]
   [content indexes]
   (let [n (count content)
   (let [n (count content)
-        max-hightlighted-len 64
-        max-surrounding-len 32
+        max-hightlighted-len 512
+        max-surrounding-len 512
 
 
         first-index (first indexes)
         first-index (first indexes)
         last-index (nth indexes (dec (count indexes)))
         last-index (nth indexes (dec (count indexes)))
@@ -86,19 +121,27 @@
 
 
 (defn- leave-focus
 (defn- leave-focus
   []
   []
-  (when-let [input (gdom/getElement "search_field")]
+  (when-let [input (gdom/getElement "search-field")]
     (.blur input)))
     (.blur input)))
 
 
+(defonce search-timeout (atom nil))
+
 (rum/defc search-auto-complete
 (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*]
   (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)))
                      (when (contains? config/mldoc-support-formats (keyword (string/lower-case ext)))
                        [{:type :new-file}]))
                        [{:type :new-file}]))
           pages (map (fn [page] {:type :page :data page}) pages)
           pages (map (fn [page] {:type :page :data page}) pages)
           files (map (fn [file] {:type :file :data file}) files)
           files (map (fn [file] {:type :file :data file}) files)
           blocks (map (fn [block] {:type :block :data block}) blocks)
           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?
           result (if config/publishing?
                    (concat pages files blocks)
                    (concat pages files blocks)
                    (concat new-page pages new-file files blocks))]
                    (concat new-page pages new-file files blocks))]
@@ -129,9 +172,10 @@
 
 
                         :block
                         :block
                         (let [block-uuid (uuid (:block/uuid data))
                         (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))
                         nil))
          :on-shift-chosen (fn [{:keys [type data]}]
          :on-shift-chosen (fn [{:keys [type data]}]
                             (case type
                             (case type
@@ -176,15 +220,17 @@
                            data]
                            data]
 
 
                           :block
                           :block
-                          (let [{:block/keys [page content indexes]} data]
-                            (let [page (:page/original-name page)]
-                              [:div.flex-1
-                               [:div.text-sm.font-medium page]
-                               (highlight-fuzzy content indexes)]))
+                          (let [{:block/keys [page content indexes]} data
+                                page (or (:page/original-name page)
+                                         (:page/name page))]
+                            [:div.flex-1
+                             [:div.text-sm.font-medium (str "-> " page)]
+                             (highlight-exact-query content search-q)])
 
 
                           nil))})])))
                           nil))})])))
 
 
-(rum/defc search < rum/reactive
+(rum/defcs search < rum/reactive
+  (rum/local false ::inside-box?)
   (mixins/event-mixin
   (mixins/event-mixin
    (fn [state]
    (fn [state]
      (mixins/hide-when-esc-or-outside
      (mixins/hide-when-esc-or-outside
@@ -192,36 +238,42 @@
       :on-hide (fn []
       :on-hide (fn []
                  (search-handler/clear-search!)
                  (search-handler/clear-search!)
                  (leave-focus)))))
                  (leave-focus)))))
-  []
+  [state]
   (let [search-result (state/sub :search/result)
   (let [search-result (state/sub :search/result)
         search-q (state/sub :search/q)
         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*]
     (rum/with-context [[t] i18n/*tongue-context*]
       [:div#search.flex-1.flex
       [: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#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
           [:svg.h-5.w-5
            {:view-box "0 0 20 20", :fill "currentColor"}
            {:view-box "0 0 20 20", :fill "currentColor"}
            [:path
            [:path
             {:d
             {: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"}]]]
              :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"}
           {:style {:padding-left "2rem"}
            :placeholder (t :search)
            :placeholder (t :search)
            :auto-complete (if (util/chrome?) "chrome-off" "off") ; off not working here
            :auto-complete (if (util/chrome?) "chrome-off" "off") ; off not working here
            :default-value ""
            :default-value ""
            :on-change (fn [e]
            :on-change (fn [e]
+                        (when @search-timeout
+                          (js/clearTimeout @search-timeout))
                         (let [value (util/evalue e)]
                         (let [value (util/evalue e)]
                           (if (string/blank? value)
                           (if (string/blank? value)
                             (search-handler/clear-search!)
                             (search-handler/clear-search!)
                             (do
                             (do
                               (state/set-q! value)
                               (state/set-q! value)
-                              (search-handler/search value)))))}]
+                              (reset! search-timeout
+                                      (js/setTimeout
+                                       #(search-handler/search value)
+                                       timeout))))))}]
          (when-not (string/blank? search-q)
          (when-not (string/blank? search-q)
            (ui/css-transition
            (ui/css-transition
             {:class-names "fade"
             {: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 {
 #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);
   background-color: var(--ls-search-background-color, #fff);
   color: var(--ls-secondary-text-color, #161e2e);
   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);
+}

+ 299 - 158
src/main/frontend/components/settings.cljs

@@ -1,18 +1,22 @@
 (ns frontend.components.settings
 (ns frontend.components.settings
   (:require [rum.core :as rum]
   (:require [rum.core :as rum]
             [frontend.ui :as ui]
             [frontend.ui :as ui]
+            [frontend.components.svg :as svg]
             [frontend.handler.notification :as notification]
             [frontend.handler.notification :as notification]
             [frontend.handler.user :as user-handler]
             [frontend.handler.user :as user-handler]
             [frontend.handler.ui :as ui-handler]
             [frontend.handler.ui :as ui-handler]
             [frontend.handler.repo :as repo-handler]
             [frontend.handler.repo :as repo-handler]
             [frontend.handler.config :as config-handler]
             [frontend.handler.config :as config-handler]
+            [frontend.handler.page :as page-handler]
             [frontend.state :as state]
             [frontend.state :as state]
+            [frontend.version :refer [version]]
             [frontend.util :as util]
             [frontend.util :as util]
             [frontend.config :as config]
             [frontend.config :as config]
             [frontend.dicts :as dicts]
             [frontend.dicts :as dicts]
             [clojure.string :as string]
             [clojure.string :as string]
             [goog.object :as gobj]
             [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)
 (rum/defcs set-email < (rum/local "" ::email)
   [state]
   [state]
@@ -23,8 +27,8 @@
        [:div
        [:div
         [:h1.title.mb-1
         [:h1.title.mb-1
          "Your email address:"]
          "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
           {:autoFocus true
            :on-change (fn [e]
            :on-change (fn [e]
                         (reset! email (util/evalue e)))}]]]]
                         (reset! email (util/evalue e)))}]]]]
@@ -47,8 +51,8 @@
        [:div
        [:div
         [:h1.title.mb-1
         [:h1.title.mb-1
          "Your cors address:"]
          "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
           {:autoFocus true
            :on-change (fn [e]
            :on-change (fn [e]
                         (reset! cors (util/evalue e)))}]]]]
                         (reset! cors (util/evalue e)))}]]]]
@@ -62,179 +66,316 @@
 
 
       [:span.pl-1.opacity-70 "Git commit requires the cors address."]]]))
       [:span.pl-1.opacity-70 "Git commit requires the cors address."]]]))
 
 
+(defn toggle
+  [label-for name state on-toggle]
+  [: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 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
 (rum/defcs settings < rum/reactive
   []
   []
-  (let [preferred-format (keyword (state/sub [:me :preferred_format]))
-        preferred-workflow (keyword (state/sub [:me :preferred_workflow]))
+  (let [preferred-format (state/get-preferred-format)
+        preferred-workflow (state/get-preferred-workflow)
         preferred-language (state/sub [:preferred-language])
         preferred-language (state/sub [:preferred-language])
         enable-timetracking? (state/enable-timetracking?)
         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?)
         enable-block-time? (state/enable-block-time?)
         show-brackets? (state/show-brackets?)
         show-brackets? (state/show-brackets?)
         github-token (state/sub [:me :access-token])
         github-token (state/sub [:me :access-token])
         cors-proxy (state/sub [:me :cors_proxy])
         cors-proxy (state/sub [:me :cors_proxy])
         logged? (state/logged?)
         logged? (state/logged?)
-        current-repo (state/get-current-repo)
         developer-mode? (state/sub [:ui/developer-mode?])
         developer-mode? (state/sub [:ui/developer-mode?])
         theme (state/sub :ui/theme)
         theme (state/sub :ui/theme)
         dark? (= "dark" theme)
         dark? (= "dark" theme)
         switch-theme (if dark? "white" "dark")]
         switch-theme (if dark? "white" "dark")]
     (rum/with-context [[t] i18n/*tongue-context*]
     (rum/with-context [[t] i18n/*tongue-context*]
-      [:div#settings
+      [:div#settings.cp__settings-main
        [:h1.title (t :settings)]
        [: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.max-w-lg.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.max-w-lg.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
         (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")])]]]]
-
-         [: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.opacity-70
-           {:for "enable_timetracking"}
-           (t :settings-page/enable-timetracking)]
-          [:div.mt-1.sm:mt-0.sm:col-span-2
-           [:div.max-w-lg.rounded-md.sm:max-w-xs
-            (ui/toggle enable-timetracking?
-                       (fn []
-                         (let [value (not enable-timetracking?)]
-                           (config-handler/set-config! :feature/enable-timetracking? value))))]]]
-
-         [: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.opacity-70
-           {:for "enable_block_time"}
-           (t :settings-page/enable-block-time)]
-          [:div.mt-1.sm:mt-0.sm:col-span-2
-           [:div.max-w-lg.rounded-md.sm:max-w-xs
-            (ui/toggle enable-block-time?
-                       (fn []
-                         (let [value (not enable-block-time?)]
-                           (config-handler/set-config! :feature/enable-block-time? 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]
+          [: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]
+                          (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.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]
+
+       [: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.max-w-lg.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")]
                                  (let [k (gobj/get event "key")]
                                    (if (= "Enter" k)
                                    (if (= "Enter" k)
                                      (when-let [server (util/evalue event)]
                                      (when-let [server (util/evalue event)]
                                        (user-handler/set-cors! server)
                                        (user-handler/set-cors! server)
                                        (notification/show! "Custom CORS proxy updated successfully!" :success)))))}]]]]
                                        (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))))]]]])]])))

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

@@ -0,0 +1,107 @@
+.cp__settings {
+  &-main {
+    padding: 24px;
+
+    > 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;
+        }
+      }
+    }
+  }
+}

+ 41 - 28
src/main/frontend/components/sidebar.cljs

@@ -17,6 +17,7 @@
             [frontend.storage :as storage]
             [frontend.storage :as storage]
             [frontend.util :as util]
             [frontend.util :as util]
             [frontend.state :as state]
             [frontend.state :as state]
+            [frontend.handler.ui :as ui-handler]
             [frontend.handler.user :as user-handler]
             [frontend.handler.user :as user-handler]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.route :as route-handler]
@@ -28,7 +29,8 @@
             [goog.object :as gobj]
             [goog.object :as gobj]
             [frontend.context.i18n :as i18n]
             [frontend.context.i18n :as i18n]
             [reitit.frontend.easy :as rfe]
             [reitit.frontend.easy :as rfe]
-            [goog.dom :as gdom]))
+            [goog.dom :as gdom]
+            [frontend.handler.web.nfs :as nfs-handler]))
 
 
 (defn nav-item
 (defn nav-item
   [title href svg-d active? close-modal-fn]
   [title href svg-d active? close-modal-fn]
@@ -54,16 +56,16 @@
         left-sidebar? (state/sub :ui/left-sidebar-open?)]
         left-sidebar? (state/sub :ui/left-sidebar-open?)]
     (when left-sidebar?
     (when left-sidebar?
       [:nav.flex-1.left-sidebar-inner
       [: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"
                  "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)
                  (active? :home)
                  close-modal-fn)
                  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"
                  "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)
                  (active? :all-pages)
                  close-modal-fn)
                  close-modal-fn)
        (when-not config/publishing?
        (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"
                    "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)
                    (active? :all-files)
                    close-modal-fn))
                    close-modal-fn))
@@ -76,7 +78,7 @@
 (rum/defc sidebar-mobile-sidebar < rum/reactive
 (rum/defc sidebar-mobile-sidebar < rum/reactive
   [{:keys [open? close-fn route-match]}]
   [{:keys [open? close-fn route-match]}]
   [:div.md:hidden
   [:div.md:hidden
-   [:div.fixed.inset-0.z-30.bg-gray-600.opacity-0.pointer-events-none.transition-opacity.ease-linear.duration-300
+   [:div.fixed.inset-0.z-30.bg-gray-600.pointer-events-none.ease-linear.duration-300
     {:class (if @open?
     {:class (if @open?
               "opacity-75 pointer-events-auto"
               "opacity-75 pointer-events-auto"
               "opacity-0 pointer-events-none")
               "opacity-0 pointer-events-none")
@@ -85,20 +87,19 @@
     {:class (if @open?
     {:class (if @open?
               "translate-x-0"
               "translate-x-0"
               "-translate-x-full")
               "-translate-x-full")
-     :style {:background-color "#002b36"
-             :max-width "15rem"}}
+     :style {:max-width "86vw"}}
     (if @open?
     (if @open?
       [:div.absolute.top-0.right-0.p-1
       [:div.absolute.top-0.right-0.p-1
-       [:button#close-left-bar.flex.items-center.justify-center.h-12.w-12.rounded-full.focus:outline-none.focus:bg-gray-600
+       [:button#close-left-bar.close-panel-btn.flex.items-center.justify-center.h-12.w-12.rounded-full.focus:outline-none.focus:bg-gray-600
         {:on-click close-fn}
         {:on-click close-fn}
-        [:svg.h-6.w-6.text-white
+        [:svg.h-6.w-6
          {:viewBox "0 0 24 24", :fill "none", :stroke "currentColor"}
          {:viewBox "0 0 24 24", :fill "none", :stroke "currentColor"}
          [:path
          [:path
           {:d "M6 18L18 6M6 6l12 12"
           {:d "M6 18L18 6M6 6l12 12"
            :stroke-width "2"
            :stroke-width "2"
            :stroke-linejoin "round"
            :stroke-linejoin "round"
            :stroke-linecap "round"}]]]])
            :stroke-linecap "round"}]]]])
-    [:div.flex-shrink-0.flex.items-center.px-4.h-16 {:style {:background-color "#002b36"}}
+    [:div.flex-shrink-0.flex.items-center.px-4.h-16.head-wrap
      (repo/repos-dropdown false nil)]
      (repo/repos-dropdown false nil)]
     [:div.flex-1.h-0.overflow-y-auto
     [:div.flex-1.h-0.overflow-y-auto
      (sidebar-nav route-match close-fn)]]])
      (sidebar-nav route-match close-fn)]]])
@@ -143,6 +144,7 @@
 
 
 (defonce sidebar-inited? (atom false))
 (defonce sidebar-inited? (atom false))
 ;; TODO: simplify logic
 ;; TODO: simplify logic
+
 (rum/defc main-content < rum/reactive db-mixins/query
 (rum/defc main-content < rum/reactive db-mixins/query
   {:init (fn [state]
   {:init (fn [state]
            (when-not @sidebar-inited?
            (when-not @sidebar-inited?
@@ -205,7 +207,7 @@
          (journal/journals latest-journals)
          (journal/journals latest-journals)
 
 
          (and logged? (empty? (:repos me)))
          (and logged? (empty? (:repos me)))
-         (widgets/add-repo)
+         (widgets/add-graph)
 
 
          ;; FIXME: why will this happen?
          ;; FIXME: why will this happen?
          :else
          :else
@@ -242,6 +244,21 @@
                     (state/sidebar-add-block! (state/get-current-repo) "help" :help nil))}
                     (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 <
 (rum/defcs sidebar <
   (mixins/modal :modal/show?)
   (mixins/modal :modal/show?)
   rum/reactive
   rum/reactive
@@ -257,6 +274,7 @@
                         (editor-handler/clear-selection! e)
                         (editor-handler/clear-selection! e)
                         (state/set-selection-start-block! nil))))
                         (state/set-selection-start-block! nil))))
 
 
+     ;; TODO: move to keyboards
      (mixins/on-key-down
      (mixins/on-key-down
       state
       state
       {;; esc
       {;; esc
@@ -274,22 +292,10 @@
        ;; ?
        ;; ?
        191 (fn [state e]
        191 (fn [state e]
              (when-not (util/input? (gobj/get e "target"))
              (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)))))})))
+               (state/sidebar-add-block! (state/get-current-repo) "help" :help nil)))})))
   {:did-mount (fn [state]
   {:did-mount (fn [state]
                 (keyboards/bind-shortcuts!)
                 (keyboards/bind-shortcuts!)
                 state)}
                 state)}
-  (mixins/keyboards-mixin keyboards/keyboards)
   [state route-match main-content]
   [state route-match main-content]
   (let [{:keys [open? close-fn open-fn]} state
   (let [{:keys [open? close-fn open-fn]} state
         close-fn (fn []
         close-fn (fn []
@@ -297,9 +303,11 @@
                    (state/set-left-sidebar-open! false))
                    (state/set-left-sidebar-open! false))
         me (state/sub :me)
         me (state/sub :me)
         current-repo (state/sub :git/current-repo)
         current-repo (state/sub :git/current-repo)
+        granted? (state/sub [:nfs/user-granted? (state/get-current-repo)])
         theme (state/sub :ui/theme)
         theme (state/sub :ui/theme)
         white? (= "white" (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])
         route-name (get-in route-match [:data :name])
         global-graph-pages? (= :graph route-name)
         global-graph-pages? (= :graph route-name)
         logged? (:name me)
         logged? (:name me)
@@ -310,8 +318,11 @@
         default-home (get-default-home-if-valid)]
         default-home (get-default-home-if-valid)]
     (rum/with-context [[t] i18n/*tongue-context*]
     (rum/with-context [[t] i18n/*tongue-context*]
       (theme/container
       (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
        [:div.theme-inner
         (sidebar-mobile-sidebar
         (sidebar-mobile-sidebar
@@ -319,7 +330,8 @@
           :close-fn    close-fn
           :close-fn    close-fn
           :route-match route-match})
           :route-match route-match})
         [:div.#app-container.cp__sidebar-layout
         [: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
          (header/header {:open-fn        open-fn
                          :white?         white?
                          :white?         white?
                          :current-repo   current-repo
                          :current-repo   current-repo
@@ -342,6 +354,7 @@
 
 
         (ui/notification)
         (ui/notification)
         (ui/modal)
         (ui/modal)
+        (settings-modal settings-open?)
         (custom-context-menu)
         (custom-context-menu)
         [:a#download.hidden]
         [:a#download.hidden]
         (when
         (when

+ 45 - 5
src/main/frontend/components/sidebar.css

@@ -33,20 +33,42 @@
 }
 }
 
 
 #left-bar {
 #left-bar {
+  background-color: var(--ls-primary-background-color);
+
+  > .head-wrap {
+    background-color: var(--ls-search-background-color);
+  }
+
+  .close-panel-btn {
+    color: var(--ls-active-primary-color);
+  }
+
   .left-sidebar-inner {
   .left-sidebar-inner {
     padding-right: 15px;
     padding-right: 15px;
   }
   }
 
 
-  a {
+  nav > a {
     color: var(--ls-icon-color);
     color: var(--ls-icon-color);
   }
   }
 }
 }
 
 
+.settings-modal {
+  background-color: var(--ls-primary-background-color);
+
+  max-height: 80vh;
+  overflow: auto;
+  margin: -25px;
+  padding: 20px;
+
+  @screen sm {
+    width: 768px;
+  }
+}
+
 .cp__sidebar-layout {
 .cp__sidebar-layout {
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
   min-height: 100vh;
   min-height: 100vh;
-  padding-bottom: 30px;
 }
 }
 
 
 .cp__sidebar-main-layout {
 .cp__sidebar-main-layout {
@@ -57,7 +79,7 @@
 
 
 .cp__sidebar-layout.is-right-sidebar-open {
 .cp__sidebar-layout.is-right-sidebar-open {
   .cp__sidebar-main-layout {
   .cp__sidebar-main-layout {
-    margin-right: 40%;
+    margin-right: var(--ls-right-sidebar-width);
   }
   }
 }
 }
 
 
@@ -71,7 +93,7 @@
 }
 }
 
 
 .cp__sidebar-main-content {
 .cp__sidebar-main-content {
-  padding: 6rem 1.5rem;
+  padding: 5rem 1.5rem;
   max-width: var(--ls-main-content-max-width);
   max-width: var(--ls-main-content-max-width);
   min-height: 100vh;
   min-height: 100vh;
   flex: 1;
   flex: 1;
@@ -129,12 +151,26 @@
 
 
   &-inner {
   &-inner {
     padding: 15px;
     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 {
   &-settings {
     @apply flex flex-row mb-2;
     @apply flex flex-row mb-2;
     margin: -15px;
     margin: -15px;
     margin-bottom: 0;
     margin-bottom: 0;
+    margin-top: 0;
     overflow: auto;
     overflow: auto;
 
 
     &-btn {
     &-btn {
@@ -144,9 +180,13 @@
     }
     }
   }
   }
 
 
+  .close-arrow svg {
+    transform: scale(0.8);
+  }
+
   &.is-open {
   &.is-open {
     display: block;
     display: block;
-    width: 40%;
+    width: var(--ls-right-sidebar-width);
     opacity: 1;
     opacity: 1;
   }
   }
 
 

+ 64 - 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"
     {:d "M5 11L0 6l1.5-1.5L5 8.25 8.5 4.5 10 6l-5 5z"
      :fill-rule "evenodd"}]])
      :fill-rule "evenodd"}]])
 
 
-(rum/defc arrow-right
+(rum/defc arrow-right-2
   []
   []
   [:svg
   [:svg
    {:aria-hidden "true"
    {: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"
     {:d "M7.5 8l-5 5L1 11.5 4.75 8 1 4.5 2.5 3l5 5z"
      :fill-rule "evenodd"}]])
      :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
 (rum/defc big-arrow-right
   []
   []
   [:svg
   [:svg
@@ -83,6 +103,22 @@
    [:path.opacity-75 {:fill "currentColor"
    [: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"}]])
                       :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
 (defn- hero-icon
   ([d]
   ([d]
    (hero-icon d {}))
    (hero-icon d {}))
@@ -148,6 +184,16 @@
      :stroke-linejoin "round"
      :stroke-linejoin "round"
      :stroke-linecap "round"}]])
      :stroke-linecap "round"}]])
 
 
+(def folder-add-large
+  [:svg
+   {:stroke "currentColor", :view-box "0 0 24 24", :fill "none" :width 64 :height 64 :display "inline-block"}
+   [:path
+    {:d
+     "M9 13h6m-3-3v6m-9 1V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z"
+     :stroke-width "2"
+     :stroke-linejoin "round"
+     :stroke-linecap "round"}]])
+
 (def folder (hero-icon "M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"))
 (def folder (hero-icon "M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"))
 (def folder-sm (hero-icon "M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" {:height "16" :width "16"}))
 (def folder-sm (hero-icon "M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" {:height "16" :width "16"}))
 (def pages-sm [:svg {:viewBox "0 0 20 20", :fill "currentColor", :height "16", :width "16"}
 (def pages-sm [:svg {:viewBox "0 0 20 20", :fill "currentColor", :height "16", :width "16"}
@@ -172,6 +218,9 @@
 (defn vertical-dots
 (defn vertical-dots
   [options]
   [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))
   (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
 (def external-link
   [:svg {:fill "none", :view-box "0 0 24 24", :height "21", :width "21"
   [:svg {:fill "none", :view-box "0 0 24 24", :height "21", :width "21"
          :stroke "currentColor"}
          :stroke "currentColor"}
@@ -424,6 +473,17 @@
      :stroke-linejoin "round"
      :stroke-linejoin "round"
      :stroke-linecap "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
 (def page
   [:svg.h-5.w-4 {:viewBox "0 0 24 24", :fill "none", :xmlns "http://www.w3.org/2000/svg"}
   [: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)"}]
    [: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)"}]
@@ -443,3 +503,6 @@
     :class class }
     :class class }
    [:path
    [:path
     {:d "M.53.53h15l-5 7v8h-5v-8z" :stroke-width "1.06" :stroke-linejoin "round"}]])
     {: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
 (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
 (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
   [:div
-   {:class (str theme "-theme")
+   {:class    (str theme "-theme")
     :on-click on-click}
     :on-click on-click}
    child])
    child])

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

@@ -8,9 +8,11 @@
   --ls-z-index-level-3: 999;
   --ls-z-index-level-3: 999;
   --ls-z-index-level-4: 9999;
   --ls-z-index-level-4: 9999;
   --ls-z-index-level-5: 99999;
   --ls-z-index-level-5: 99999;
+
+  --ls-right-sidebar-width: 40%;
 }
 }
 
 
-html:not(.is-mac) {
+html {
   ::-webkit-scrollbar-thumb {
   ::-webkit-scrollbar-thumb {
     background-color: var(--ls-scrollbar-foreground-color);
     background-color: var(--ls-scrollbar-foreground-color);
   }
   }
@@ -26,7 +28,6 @@ html:not(.is-mac) {
   ::-webkit-scrollbar {
   ::-webkit-scrollbar {
     width: 8px;
     width: 8px;
     height: 8px;
     height: 8px;
-    -webkit-border-radius: 100px;
   }
   }
 
 
   ::-webkit-scrollbar-thumb {
   ::-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] {
 html[data-theme=dark] {
   background-color: var(--ls-primary-background-color);
   background-color: var(--ls-primary-background-color);
 
 
-  input {
-    color: var(--ls-secondary-text-color);
-  }
-
   input.form-input {
   input.form-input {
     background: none;
     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] {
 html[data-theme=light] {
@@ -91,3 +92,126 @@ html[data-theme=light] {
     display: none;
     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;
+  }
+}

+ 93 - 52
src/main/frontend/components/widgets.cljs

@@ -3,21 +3,16 @@
             [frontend.util :as util]
             [frontend.util :as util]
             [frontend.handler.user :as user-handler]
             [frontend.handler.user :as user-handler]
             [frontend.handler.repo :as repo-handler]
             [frontend.handler.repo :as repo-handler]
-            [frontend.handler.route :as route-handler]
-            [frontend.handler.export :as export-handler]
             [frontend.handler.notification :as notification]
             [frontend.handler.notification :as notification]
             [frontend.handler.web.nfs :as nfs]
             [frontend.handler.web.nfs :as nfs]
+            [frontend.handler.page :as page-handler]
             [frontend.state :as state]
             [frontend.state :as state]
-            [frontend.config :as config]
             [clojure.string :as string]
             [clojure.string :as string]
             [frontend.ui :as ui]
             [frontend.ui :as ui]
-            [frontend.db :as db]
-            [frontend.version :as version]
-            [frontend.components.commit :as commit]
             [frontend.context.i18n :as i18n]
             [frontend.context.i18n :as i18n]
-            [reitit.frontend.easy :as rfe]))
+            [frontend.handler.web.nfs :as nfs]))
 
 
-(rum/defcs choose-preferred-format
+(rum/defc choose-preferred-format
   []
   []
   (rum/with-context [[t] i18n/*tongue-context*]
   (rum/with-context [[t] i18n/*tongue-context*]
     [:div
     [:div
@@ -37,54 +32,100 @@
        :on-click
        :on-click
        #(user-handler/set-preferred-format! :org))]]))
        #(user-handler/set-preferred-format! :org))]]))
 
 
-(rum/defcs add-repo <
+(rum/defcs add-github-repo <
   (rum/local "" ::repo)
   (rum/local "" ::repo)
+  (rum/local "" ::branch)
   [state]
   [state]
-  (let [repo (get state ::repo)]
+  (let [repo (get state ::repo)
+        branch (get state ::branch)]
     (rum/with-context [[t] i18n/*tongue-context*]
     (rum/with-context [[t] i18n/*tongue-context*]
-      [:div.p-8.flex.items-center.justify-center.flex-col
-       (let [nfs-supported? (nfs/supported?)]
-         [:div.cp__widgets-open-local-directory
-          [:div.select-file-wrap.cursor
-           (when nfs-supported?
-             {:on-click nfs/ls-dir-files})
+      [:div.flex.flex-col
+       [:div.w-full.mx-auto
+        [:div
+         [:div
+          [:h1.title
+           (t :git/add-repo-prompt)]
+          [:div.mt-4.mb-4.relative.rounded-md.shadow-sm.max-w-xs
+           [:input#repo.form-input.block.w-full.sm:text-sm.sm:leading-5
+            {:autoFocus true
+             :placeholder "https://github.com/username/repo"
+             :on-change (fn [e]
+                          (reset! repo (util/evalue e)))}]]
+          [:label.font-medium "Default Branch (make sure it's matched with your setting on Github): "]
+          [:div.mt-2.mb-4.relative.rounded-md.shadow-sm.max-w-xs
+           [:input#branch.form-input.block.w-full.sm:text-sm.sm:leading-5
+            {:value @branch
+             :placeholder "e.g. master"
+             :on-change (fn [e]
+                          (reset! branch (util/evalue e)))}]]]]
 
 
-           [:div
-            [:h1.title "Open a local directory"]
-            [:p.text-sm
-             "Your data will be stored only in your device."]
-            (when-not nfs-supported?
-              (ui/admonition :warning
-                             [:p "It seems that your browser doesn't support the "
+        (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))
 
 
-                              [:a {:href "https://web.dev/file-system-access/"
-                                   :target "_blank"}
-                               "new native filesystem API"]
-                              [:span ", please use any chromium 86+ browser like Chrome, Vivaldi, Edge, Brave, etc."]]))]]])
-       (when (state/logged?)
-         [:div.w-full.mx-auto.mt-10
-          [:h1.title "Or you can"]
-          [:div
-           [:div
-            [:h1.title.mb-1
-             (t :git/add-repo-prompt)]
-            [:div.mt-4.mb-4.relative.rounded-md.shadow-sm.max-w-xs
-             [:input#repo.form-input.block.w-full.sm:text-sm.sm:leading-5
-              {:autoFocus true
-               :placeholder "https://github.com/username/repo"
-               :on-change (fn [e]
-                            (reset! repo (util/evalue e)))}]]]]
+                   (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)))))))]])))
 
 
-          (ui/button
-           (t :git/add-repo-prompt-confirm)
-           :on-click
-           (fn []
-             (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))
+(rum/defcs add-local-directory
+  []
+  (rum/with-context [[t] i18n/*tongue-context*]
+    [:div.flex.flex-col
+     [:h1.title "Add a graph"]
+     (let [nfs-supported? (nfs/supported?)]
+       [:div.cp__widgets-open-local-directory
+        [:div.select-file-wrap.cursor
+         (when nfs-supported?
+           {:on-click page-handler/ls-dir-files!})
+         [:div
+          [:h1.title "Open a local directory"]
+          [: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 "
+
+                            [:a {:href "https://web.dev/file-system-access/"
+                                 :target "_blank"}
+                             "new native filesystem API"]
+                            [:span ", please use any chromium 86+ browser like Chrome, Vivaldi, Edge, Brave, etc."]]))]]])]))
 
 
-                 (notification/show!
-                  [:p "Please input a valid repo url, e.g. https://github.com/username/repo"]
-                  :error
-                  false)))))])])))
+(rum/defcs add-graph <
+  [state & {:keys [graph-types]
+            :or {graph-types [:local :github]}
+            :as opts}]
+  (let [github-authed? (state/github-authed?)
+        generate-f (fn [x]
+                     (case x
+                       :github
+                       (when (and github-authed? (not (util/electron?)))
+                         (rum/with-key (add-github-repo)
+                           "add-github-repo"))
+
+                       :local
+                       (rum/with-key (add-local-directory)
+                         "add-local-directory")
+
+                       nil))
+        available-graph (->> (set graph-types)
+                             (keep generate-f)
+                             (vec)
+                             (interpose [:b.mt-10.mb-5.opacity-50 "OR"]))]
+    (rum/with-context [[t] i18n/*tongue-context*]
+      [:div.p-8.flex.flex-col available-graph])))

文件差异内容过多而无法显示
+ 5 - 3
src/main/frontend/config.cljs


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

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

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

@@ -7,7 +7,13 @@
             [cljs-bean.core :as bean]
             [cljs-bean.core :as bean]
             [frontend.util :as util]
             [frontend.util :as util]
             [clojure.string :as string]
             [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
 (defn format
   [date]
   [date]
@@ -127,21 +133,7 @@
      (gobj/get js/window.navigator "language")
      (gobj/get js/window.navigator "language")
      (bean/->js {:hour "2-digit"
      (bean/->js {:hour "2-digit"
                  :minute "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?
 (defn valid?
   [s]
   [s]
@@ -156,7 +148,7 @@
 (defn valid-journal-title?
 (defn valid-journal-title?
   [title]
   [title]
   (and title
   (and title
-       (valid? (string/capitalize title))))
+       (valid? (util/capitalize-all title))))
 
 
 (defn journal-title->
 (defn journal-title->
   [journal-title then-fn]
   [journal-title then-fn]
@@ -164,7 +156,7 @@
     (when-let [time (->> (map
     (when-let [time (->> (map
                           (fn [formatter]
                           (fn [formatter]
                             (try
                             (try
-                              (tf/parse (tf/formatter formatter) journal-title)
+                              (tf/parse (tf/formatter formatter) (util/capitalize-all journal-title))
                               (catch js/Error _e
                               (catch js/Error _e
                                 nil)))
                                 nil)))
                           (journal-title-formatters))
                           (journal-title-formatters))
@@ -174,7 +166,9 @@
 
 
 (defn journal-title->int
 (defn journal-title->int
   [journal-title]
   [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
 (defn journal-title->long
   [journal-title]
   [journal-title]

+ 16 - 9
src/main/frontend/db.cljs

@@ -10,6 +10,7 @@
             [frontend.state :as state]
             [frontend.state :as state]
             [promesa.core :as p]
             [promesa.core :as p]
             [frontend.db-schema :as db-schema]
             [frontend.db-schema :as db-schema]
+            [frontend.db.default :as default-db]
             [clojure.core.async :as async]
             [clojure.core.async :as async]
             [frontend.idb :as idb]))
             [frontend.idb :as idb]))
 
 
@@ -37,7 +38,7 @@
  [frontend.db.model
  [frontend.db.model
   add-properties! block-and-children-transform blocks-count blocks-count-cache clean-export!  cloned? delete-blocks
   add-properties! block-and-children-transform blocks-count blocks-count-cache clean-export!  cloned? delete-blocks
   delete-file! delete-file-blocks! delete-file-pages! delete-file-tx delete-files delete-pages-by-files
   delete-file! delete-file-blocks! delete-file-pages! delete-file-tx delete-files delete-pages-by-files
-  filter-only-public-pages-and-blocks get-all-block-contents get-all-tagged-pages get-all-tags
+  filter-only-public-pages-and-blocks get-all-block-contents get-all-tagged-pages
   get-all-templates get-block-and-children get-block-and-children-no-cache get-block-by-uuid get-block-children
   get-all-templates get-block-and-children get-block-and-children-no-cache get-block-by-uuid get-block-children
   get-block-children-ids get-block-content get-block-file get-block-immediate-children get-block-page
   get-block-children-ids get-block-content get-block-file get-block-immediate-children get-block-page
   get-block-page-end-pos get-block-parent get-block-parents get-block-referenced-blocks get-block-refs-count
   get-block-page-end-pos get-block-parent get-block-parents get-block-referenced-blocks get-block-refs-count
@@ -50,10 +51,11 @@
   get-page-properties-content get-page-referenced-blocks get-page-referenced-pages get-page-unlinked-references
   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
   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
   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]
+  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
  [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!
   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
   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]
   query-state query-components query-entity-in-component remove-custom-query! set-new-result! sub-key-value]
@@ -65,10 +67,12 @@
 (defn persist! [repo]
 (defn persist! [repo]
   (let [file-key (datascript-files-db repo)
   (let [file-key (datascript-files-db repo)
         non-file-key (datascript-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}
     (p/let [_ (idb/set-batch! [{:key file-key :value file-db-str}
                                {:key non-file-key :value non-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))
       (state/set-last-persist-transact-id! repo true (get-max-tx-id file-db))
@@ -146,7 +150,8 @@
          (p/let [stored (idb/get-item db-name)
          (p/let [stored (idb/get-item db-name)
                  _ (when stored
                  _ (when stored
                      (let [stored-db (string->db 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)))
                        (conn/reset-conn! db-conn attached-db)))
                  db-name (datascript-db repo)
                  db-name (datascript-db repo)
                  db-conn (d/create-conn db-schema/schema)
                  db-conn (d/create-conn db-schema/schema)
@@ -155,7 +160,9 @@
                  stored (idb/get-item db-name)
                  stored (idb/get-item db-name)
                  _ (if stored
                  _ (if stored
                      (let [stored-db (string->db 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))
                        (conn/reset-conn! db-conn attached-db))
                      (when logged?
                      (when logged?
                        (d/transact! db-conn [(me-tx (d/db db-conn) me)])))]
                        (d/transact! db-conn [(me-tx (d/db db-conn) me)])))]

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

@@ -2,6 +2,7 @@
   "Contains db connections."
   "Contains db connections."
   (:require [clojure.string :as string]
   (:require [clojure.string :as string]
             [frontend.db-schema :as db-schema]
             [frontend.db-schema :as db-schema]
+            [frontend.db.default :as default-db]
             [frontend.util :as util]
             [frontend.util :as util]
             [frontend.state :as state]
             [frontend.state :as state]
             [frontend.config :as config]
             [frontend.config :as config]
@@ -85,6 +86,8 @@
      (when me
      (when me
        (d/transact! db-conn [(me-tx (d/db db-conn) me)]))
        (d/transact! db-conn [(me-tx (d/db db-conn) me)]))
 
 
+     (d/transact! db-conn default-db/built-in-pages)
+
      (when listen-handler (listen-handler repo)))))
      (when listen-handler (listen-handler repo)))))
 
 
 (defn destroy-all!
 (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"}))

+ 203 - 96
src/main/frontend/db/model.cljs

@@ -12,7 +12,6 @@
             [clojure.set :as set]
             [clojure.set :as set]
             [frontend.utf8 :as utf8]
             [frontend.utf8 :as utf8]
             [frontend.config :as config]
             [frontend.config :as config]
-            [frontend.format.block :as block]
             [cljs.reader :as reader]
             [cljs.reader :as reader]
             [cljs-time.core :as t]
             [cljs-time.core :as t]
             [cljs-time.coerce :as tc]
             [cljs-time.coerce :as tc]
@@ -22,6 +21,31 @@
 ;; TODO: extract to specific models and move data transform logic to the
 ;; TODO: extract to specific models and move data transform logic to the
 ;; correponding handlers.
 ;; 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!
 (defn transact-files-db!
   ([tx-data]
   ([tx-data]
    (db-utils/transact! (state/get-current-repo) tx-data))
    (db-utils/transact! (state/get-current-repo) tx-data))
@@ -48,60 +72,46 @@
        react
        react
        ffirst))))
        ffirst))))
 
 
-(defn get-all-tags
-  []
-  (let [repo (state/get-current-repo)]
-    (when (conn/get-conn repo)
-      (some->>
-       (react/q repo [:tags] {}
-                '[:find ?name ?h ?p
-                  :where
-                  [?t :tag/name ?name]
-                  (or
-                   [?h :block/tags ?t]
-                   [?p :page/tags ?t])])
-       react
-       (seq)))))
-
 (defn get-tag-pages
 (defn get-tag-pages
   [repo tag-name]
   [repo tag-name]
-  (d/q '[:find ?original-name ?name
-         :in $ ?tag
-         :where
-         [?e :tag/name ?tag]
-         [?page :page/tags ?e]
-         [?page :page/original-name ?original-name]
-         [?page :page/name ?name]]
-       (conn/get-conn repo)
-       tag-name))
+  (when tag-name
+    (d/q '[:find ?original-name ?name
+           :in $ ?tag
+           :where
+           [?e :page/name ?tag]
+           [?page :page/tags ?e]
+           [?page :page/original-name ?original-name]
+           [?page :page/name ?name]]
+         (conn/get-conn repo)
+         (string/lower-case tag-name))))
 
 
 (defn get-all-tagged-pages
 (defn get-all-tagged-pages
   [repo]
   [repo]
   (d/q '[:find ?page-name ?tag
   (d/q '[:find ?page-name ?tag
          :where
          :where
          [?page :page/tags ?e]
          [?page :page/tags ?e]
-         [?e :tag/name ?tag]
-         [_ :page/name ?tag]
+         [?e :page/name ?tag]
          [?page :page/name ?page-name]]
          [?page :page/name ?page-name]]
        (conn/get-conn repo)))
        (conn/get-conn repo)))
 
 
 (defn get-pages
 (defn get-pages
   [repo]
   [repo]
   (->> (d/q
   (->> (d/q
-        '[:find ?page-name
+        '[:find ?page-original-name
           :where
           :where
-          [?page :page/original-name ?page-name]]
+          [?page :page/name ?page-name]
+          [(get-else $ ?page :page/original-name ?page-name) ?page-original-name]]
         (conn/get-conn repo))
         (conn/get-conn repo))
        (map first)))
        (map first)))
 
 
 (defn get-modified-pages
 (defn get-modified-pages
   [repo]
   [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
 (defn get-page-alias
   [repo page-name]
   [repo page-name]
@@ -141,13 +151,15 @@
   [repo]
   [repo]
   (when-let [conn (conn/get-conn repo)]
   (when-let [conn (conn/get-conn repo)]
     (->> (d/q
     (->> (d/q
-          '[:find ?path ?modified-at
+          '[:find ?path
+             ;; ?modified-at
             :where
             :where
             [?file :file/path ?path]
             [?file :file/path ?path]
-            [(get-else $ ?file :file/last-modified-at 0) ?modified-at]]
+            ;; [?file :file/last-modified-at ?modified-at]
+]
           conn)
           conn)
          (seq)
          (seq)
-         (sort-by last)
+         ;; (sort-by last)
          (reverse))))
          (reverse))))
 
 
 (defn get-files-blocks
 (defn get-files-blocks
@@ -217,7 +229,7 @@
 (defn set-file-last-modified-at!
 (defn set-file-last-modified-at!
   [repo path last-modified-at]
   [repo path last-modified-at]
   (when (and 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
       (d/transact! conn
                    [{:file/path path
                    [{:file/path path
                      :file/last-modified-at last-modified-at}]))))
                      :file/last-modified-at last-modified-at}]))))
@@ -225,7 +237,7 @@
 (defn get-file-last-modified-at
 (defn get-file-last-modified-at
   [repo path]
   [repo path]
   (when (and 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])
       (-> (d/entity (d/db conn) [:file/path path])
           :file/last-modified-at))))
           :file/last-modified-at))))
 
 
@@ -272,7 +284,8 @@
 
 
 (defn get-custom-css
 (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
 (defn get-file-no-sub
   ([path]
   ([path]
@@ -280,20 +293,15 @@
   ([repo path]
   ([repo path]
    (when (and repo path)
    (when (and repo path)
      (when-let [conn (conn/get-files-conn repo)]
      (when-let [conn (conn/get-files-conn repo)]
-       (->
-        (d/q
-         '[:find ?content
-           :in $ ?path
-           :where
-           [?file :file/path ?path]
-           [?file :file/content ?content]]
-         @conn
-         path)
-        ffirst)))))
+       (:file/content (d/entity (d/db conn) [:file/path path]))))))
 
 
 (defn get-block-by-uuid
 (defn get-block-by-uuid
-  [uuid]
-  (db-utils/entity [:block/uuid uuid]))
+  [id]
+  (db-utils/entity [:block/uuid (if (uuid? id) id (uuid id))]))
+
+(defn query-block-by-uuid
+  [id]
+  (db-utils/pull [:block/uuid (if (uuid? id) id (uuid id))]))
 
 
 (defn get-page-format
 (defn get-page-format
   [page-name]
   [page-name]
@@ -323,12 +331,29 @@
      (set)
      (set)
      (set/union #{page-id}))))
      (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
 (defn get-page-alias-names
   [repo page-name]
   [repo page-name]
   (let [alias-ids (page-alias-set repo page-name)]
   (let [alias-ids (page-alias-set repo page-name)]
     (when (seq alias-ids)
     (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
            distinct
            (remove #(= (string/lower-case %) (string/lower-case page-name)))))))
            (remove #(= (string/lower-case %) (string/lower-case page-name)))))))
 
 
@@ -361,7 +386,7 @@
 (defn sort-blocks
 (defn sort-blocks
   [blocks]
   [blocks]
   (let [pages-ids (map (comp :db/id :block/page) 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)
         pages-map (reduce (fn [acc p] (assoc acc (:db/id p) p)) {} pages)
         blocks (map
         blocks (map
                 (fn [block]
                 (fn [block]
@@ -484,7 +509,8 @@
 (defn get-block-parent
 (defn get-block-parent
   [repo block-id]
   [repo block-id]
   (when-let [conn (conn/get-conn repo)]
   (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
 ;; non recursive query
 (defn get-block-parents
 (defn get-block-parents
@@ -548,8 +574,15 @@
              (string/join "\n"))
              (string/join "\n"))
 
 
         :markdown
         :markdown
-        (str (subs content 0 (string/last-index-of content "---\n\n"))
-             "---\n\n")
+        (let [[m leading-spaces first-dashes] (re-find #"(\s*)(---\n)" content)]
+          (if m
+            (let [begin (count leading-spaces)
+                  begin-inner (+ begin (count first-dashes))
+                  second-dashes "\n---\n"
+                  end-inner (string/index-of content second-dashes begin-inner)
+                  end (if end-inner (+ end-inner (count second-dashes)) begin)]
+              (subs content begin end))
+            ""))
 
 
         content))))
         content))))
 
 
@@ -571,17 +604,12 @@
   (when-let [conn (conn/get-conn repo)]
   (when-let [conn (conn/get-conn repo)]
     (let [eid (:db/id (db-utils/entity repo [:block/uuid block-uuid]))]
     (let [eid (:db/id (db-utils/entity repo [:block/uuid block-uuid]))]
       (->> (d/q
       (->> (d/q
-            '[:find ?e1
-              :in $ ?e2 %
-              :where (parent ?e2 ?e1)]
+            '[:find ?c
+              :in $ ?p %
+              :where (parent ?p ?c)]
             conn
             conn
             eid
             eid
-             ;; recursive rules
-            '[[(parent ?e2 ?e1)
-               [?e2 :block/children ?e1]]
-              [(parent ?e2 ?e1)
-               [?t :block/children ?e1]
-               (parent ?e2 ?t)]])
+            rules)
            (apply concat)))))
            (apply concat)))))
 
 
 (defn get-block-immediate-children
 (defn get-block-immediate-children
@@ -704,30 +732,40 @@
     (db-utils/entity [:block/uuid (uuid page-name)])
     (db-utils/entity [:block/uuid (uuid page-name)])
     (db-utils/entity [:page/name page-name])))
     (db-utils/entity [:page/name page-name])))
 
 
+(defn- heading-block?
+  [block]
+  (and
+   (vector? block)
+   (= "Heading" (first block))))
+
 (defn get-page-name
 (defn get-page-name
   [file ast]
   [file ast]
   ;; headline
   ;; headline
   (let [ast (map first ast)]
   (let [ast (map first ast)]
-    (if (util/starts-with? file "pages/contents.")
+    (if (string/includes? file "pages/contents.")
       "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))
             property-name (when (and (= "Properties" (ffirst ast))
                                      (not (string/blank? (:title (last (first ast))))))
                                      (not (string/blank? (:title (last (first ast))))))
                             (: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 #"/"))]
             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
         (or property-name
             (if (= (state/page-name-order) "file")
             (if (= (state/page-name-order) "file")
               (or file-name first-block-name)
               (or file-name first-block-name)
               (or first-block-name file-name)))))))
               (or first-block-name file-name)))))))
 
 
+(defn get-page-original-name
+  [page-name]
+  (when page-name
+    (let [page (db-utils/pull [:page/name (string/lower-case page-name)])]
+      (or (:page/original-name page)
+          (:page/name page)))))
+
 (defn get-block-content
 (defn get-block-content
   [utf8-content block]
   [utf8-content block]
   (let [meta (:block/meta block)]
   (let [meta (:block/meta block)]
@@ -812,6 +850,10 @@
      (db-utils/seq-flatten)
      (db-utils/seq-flatten)
      (distinct))))
      (distinct))))
 
 
+(defn page-empty?
+  [repo page]
+  (nil? (:page/file (db-utils/entity repo [:page/name (string/lower-case page)]))))
+
 (defn get-pages-relation
 (defn get-pages-relation
   [repo with-journal?]
   [repo with-journal?]
   (when-let [conn (conn/get-conn repo)]
   (when-let [conn (conn/get-conn repo)]
@@ -854,6 +896,15 @@
                                db-utils/seq-flatten)]
                                db-utils/seq-flatten)]
       (mapv (fn [page] [page (get-page-alias repo page)]) mentioned-pages))))
       (mapv (fn [page] [page (get-page-alias repo page)]) mentioned-pages))))
 
 
+(defn- remove-children!
+  [blocks]
+  (let [childrens (->> (mapcat :block/children blocks)
+                       (map :db/id)
+                       (set))]
+    (if (seq childrens)
+      (remove (fn [block] (contains? childrens (:db/id block))) blocks)
+      blocks)))
+
 (defn get-page-referenced-blocks
 (defn get-page-referenced-blocks
   ([page]
   ([page]
    (get-page-referenced-blocks (state/get-current-repo) page))
    (get-page-referenced-blocks (state/get-current-repo) page))
@@ -884,14 +935,20 @@
                                        :where
                                        :where
                                        [?block :block/ref-pages ?ref-page]
                                        [?block :block/ref-pages ?ref-page]
                                        [(contains? ?pages ?ref-page)]]
                                        [(contains? ?pages ?ref-page)]]
-                                     pages))]
-         (->> query-result
-              react
-              db-utils/seq-flatten
-              (remove (fn [block]
-                        (= page-id (:db/id (:block/page block)))))
-              sort-blocks
-              db-utils/group-by-page))))))
+                                     pages))
+             result (->> query-result
+                         react
+                         db-utils/seq-flatten
+                         (remove (fn [block]
+                                   (= page-id (:db/id (:block/page block)))))
+                         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)]))))]
+         result)))))
 
 
 (defn get-date-scheduled-or-deadlines
 (defn get-date-scheduled-or-deadlines
   [journal-title]
   [journal-title]
@@ -909,9 +966,7 @@
              react
              react
              db-utils/seq-flatten
              db-utils/seq-flatten
              sort-blocks
              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
 (defn get-files-that-referenced-page
   [page-id]
   [page-id]
@@ -953,7 +1008,9 @@
                                 ref-pages
                                 ref-pages
                                 pages))))))
                                 pages))))))
              sort-blocks
              sort-blocks
-             db-utils/group-by-page)))))
+             db-utils/group-by-page
+             (map (fn [[k blocks]]
+                    [k (remove-children! blocks)])))))))
 
 
 (defn get-block-referenced-blocks
 (defn get-block-referenced-blocks
   [block-uuid]
   [block-uuid]
@@ -1074,9 +1131,10 @@
   ([cache?]
   ([cache?]
    (if (and cache? @blocks-count-cache)
    (if (and cache? @blocks-count-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
 ;; block/uuid and block/content
 (defn get-all-block-contents
 (defn get-all-block-contents
@@ -1103,10 +1161,9 @@
 
 
 (defn filter-only-public-pages-and-blocks
 (defn filter-only-public-pages-and-blocks
   [db]
   [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)
     (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"} %)
             page-or-block? #(contains? #{"page" "block" "me" "recent" "file"} %)
             filtered-db (d/filter db
             filtered-db (d/filter db
                                   (fn [db datom]
                                   (fn [db datom]
@@ -1159,3 +1216,53 @@
                    (remove nil?))]
                    (remove nil?))]
     (when (seq pages)
     (when (seq pages)
       (mapv (fn [page] [:db.fn/retractEntity [:page/name page]]) (map string/lower-case pages)))))
       (mapv (fn [page] [:db.fn/retractEntity [:page/name page]]) (map string/lower-case pages)))))
+
+(defn remove-all-aliases!
+  [repo]
+  (let [page-ids (->>
+                  (d/q '[:find ?e
+                         :where
+                         [?e :page/alias]]
+                       (conn/get-conn repo))
+                  (apply concat)
+                  (distinct))
+        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))

+ 53 - 28
src/main/frontend/db/query_custom.cljs

@@ -10,7 +10,9 @@
             [frontend.extensions.sci :as sci]
             [frontend.extensions.sci :as sci]
             [lambdaisland.glogi :as log]
             [lambdaisland.glogi :as log]
             [frontend.util :as util]
             [frontend.util :as util]
-            [frontend.db.react :as react]))
+            [frontend.db.react :as react]
+            [frontend.text :as text]
+            [clojure.walk :as walk]))
 
 
 (defn- resolve-input
 (defn- resolve-input
   [input]
   [input]
@@ -34,42 +36,65 @@
           days (util/parse-int (subs input 0 (dec (count input))))]
           days (util/parse-int (subs input 0 (dec (count input))))]
       (date->int (t/plus (t/today) (t/days days))))
       (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
     :else
     input))
     input))
 
 
 (defn custom-query-result-transform
 (defn custom-query-result-transform
   [query-result remove-blocks q]
   [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)))))
+  (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
 (defn react-query
   [repo {:keys [query inputs] :as query'} query-opts]
   [repo {:keys [query inputs] :as query'} query-opts]
   (try
   (try
-    (let [inputs (map resolve-input inputs)
+    (let [query (resolve-query query)
+          inputs (map resolve-input inputs)
           repo (or repo (state/get-current-repo))
           repo (or repo (state/get-current-repo))
           k [:custom query']]
           k [:custom query']]
       (apply react/q repo k query-opts query inputs))
       (apply react/q repo k query-opts query inputs))

+ 70 - 42
src/main/frontend/db/query_dsl.cljs

@@ -13,7 +13,9 @@
             [frontend.util :as util]
             [frontend.util :as util]
             [medley.core :as medley]
             [medley.core :as medley]
             [clojure.walk :as walk]
             [clojure.walk :as walk]
-            [clojure.core]))
+            [clojure.core]
+            [clojure.set :as set]
+            [frontend.template :as template]))
 
 
 ;; Query fields:
 ;; Query fields:
 
 
@@ -28,6 +30,7 @@
 ;; property (block)
 ;; property (block)
 ;; todo (block)
 ;; todo (block)
 ;; priority (block)
 ;; priority (block)
+;; page
 ;; page-property (page)
 ;; page-property (page)
 ;; page-tags (page)
 ;; page-tags (page)
 ;; all-page-tags
 ;; all-page-tags
@@ -119,12 +122,12 @@
                  t/days)]
                  t/days)]
         (tc/to-long (t/plus (t/today) (tf duration)))))))
         (tc/to-long (t/plus (t/today) (tf duration)))))))
 
 
-#_(defn uniq-symbol
-    [counter prefix]
-    (let [result (symbol (str prefix (when-not (zero? @counter)
-                                       @counter)))]
-      (swap! counter inc)
-      result))
+(defn uniq-symbol
+  [counter prefix]
+  (let [result (symbol (str prefix (when-not (zero? @counter)
+                                     @counter)))]
+    (swap! counter inc)
+    result))
 
 
 (defn build-query
 (defn build-query
   ([repo e env]
   ([repo e env]
@@ -135,17 +138,16 @@
          page-ref? (text/page-ref? e)]
          page-ref? (text/page-ref? e)]
      (when (or (and page-ref?
      (when (or (and page-ref?
                     (not (contains? #{'page-property 'page-tags} (:current-filter env))))
                     (not (contains? #{'page-property 'page-tags} (:current-filter env))))
-               (contains? #{'between 'property 'todo 'priority 'sort-by} fe))
+               (contains? #{'between 'property 'todo 'priority 'sort-by 'page} fe))
        (reset! blocks? true))
        (reset! blocks? true))
      (cond
      (cond
        (nil? e)
        (nil? e)
        nil
        nil
 
 
        page-ref?
        page-ref?
-       (let [page-name (text/page-ref-un-brackets! e)]
-         (when (and (not (string/blank? page-name))
-                    (some? (db-utils/entity repo [:page/name page-name])))
-           [['?b :block/ref-pages [:page/name page-name]]]))
+       (let [page-name (-> (text/page-ref-un-brackets! e)
+                           (string/lower-case))]
+         [['?b :block/path-ref-pages [:page/name page-name]]])
 
 
        (contains? #{'and 'or 'not} fe)
        (contains? #{'and 'or 'not} fe)
        (let [clauses (->> (map (fn [form]
        (let [clauses (->> (map (fn [form]
@@ -209,8 +211,8 @@
              (when (and start end)
              (when (and start end)
                (let [[start end] (sort [start end])
                (let [[start end] (sort [start end])
                      sym '?v]
                      sym '?v]
-                 [['?b :block/properties '?p]
-                  [(list 'get '?p k) sym]
+                 [['?b :block/properties '?prop]
+                  [(list 'get '?prop k) sym]
                   [(list '>= sym start)]
                   [(list '>= sym start)]
                   [(list '< sym end)]])))))
                   [(list '< sym end)]])))))
 
 
@@ -218,9 +220,11 @@
             (= 3 (count e)))
             (= 3 (count e)))
        (let [v (some-> (name (nth e 2))
        (let [v (some-> (name (nth e 2))
                        (text/page-ref-un-brackets!))
                        (text/page-ref-un-brackets!))
-             sym '?v]
-         [['?b :block/properties '?p]
-          [(list 'get '?p (name (nth e 1))) sym]
+             sym (if (= current-filter 'or)
+                   '?v
+                   (uniq-symbol counter "?v"))]
+         [['?b :block/properties '?prop]
+          [(list 'get '?prop (name (nth e 1))) sym]
           (list
           (list
            'or
            'or
            [(list '= sym v)]
            [(list '= sym v)]
@@ -228,8 +232,8 @@
 
 
        (and (= 'property fe)
        (and (= 'property fe)
             (= 2 (count e)))
             (= 2 (count e)))
-       [['?b :block/properties '?p]
-        [(list 'get '?p (name (nth e 1)))]]
+       [['?b :block/properties '?prop]
+        [(list 'get '?prop (name (nth e 1)))]]
 
 
        (= 'todo fe)
        (= 'todo fe)
        (let [markers (if (coll? (first (rest e)))
        (let [markers (if (coll? (first (rest e)))
@@ -267,6 +271,11 @@
                                                   comp))))
                                                   comp))))
              nil)))
              nil)))
 
 
+       (= 'page fe)
+       (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)
        (= 'page-property fe)
        (let [[k v] (rest e)]
        (let [[k v] (rest e)]
          (if v
          (if v
@@ -283,18 +292,21 @@
             [(list 'get '?prop (keyword (nth e 1)))]]))
             [(list 'get '?prop (keyword (nth e 1)))]]))
 
 
        (= 'page-tags fe)
        (= 'page-tags fe)
-       (let [tags (if (coll? (first (rest e)))
-                    (first (rest e))
-                    (rest e))]
-         (when (seq tags)
-           (let [tags (set (map (comp text/page-ref-un-brackets! name) tags))]
-             [['?p :page/tags '?t]
-              ['?t :tag/name '?tag]
-              [(list 'contains? tags '?tag)]])))
+       (do
+         (let [tags (if (coll? (first (rest e)))
+                      (first (rest e))
+                      (rest e))
+               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))]
+               (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)
        (= 'all-page-tags fe)
-       [['?t :tag/name '?tag]
-        ['?p :page/name '?tag]]
+       [['?e :page/tags '?p]]
 
 
        :else
        :else
        nil))))
        nil))))
@@ -315,6 +327,26 @@
                                                  (string/join " ")
                                                  (string/join " ")
                                                  (util/format "(between %s)"))))))
                                                  (util/format "(between %s)"))))))
 
 
+(defn- add-bindings!
+  [q]
+  (let [syms ['?b '?p 'not]
+        [b? p? not?] (-> (set/intersection (set syms) (set (flatten q)))
+                         (map syms))]
+    (if not?
+      (cond
+        (and b? p?)
+        (concat [['?b :block/uuid] ['?p :page/name] ['?b :block/page '?p]] q)
+
+        b?
+        (concat [['?b :block/uuid]] q)
+
+        p?
+        (concat [['?p :page/name]] q)
+
+        :else
+        q)
+      q)))
+
 (defn parse
 (defn parse
   [repo s]
   [repo s]
   (when (and (string? s)
   (when (and (string? s)
@@ -332,18 +364,13 @@
               result (when (seq result)
               result (when (seq result)
                        (let [key (if (coll? (first result))
                        (let [key (if (coll? (first result))
                                    (keyword (ffirst result))
                                    (keyword (ffirst result))
-                                   (keyword (first result)))]
-                         (case key
-                           :and
-                           (rest result)
-
-                           :not
-                           (cons ['?b :block/uuid] result)
-
-                           :or
-                           result
+                                   (keyword (first result)))
+                             result (case key
+                                      :and
+                                      (rest result)
 
 
-                           result)))]
+                                      result)]
+                         (add-bindings! result)))]
           {:query result
           {:query result
            :sort-by @sort-by
            :sort-by @sort-by
            :blocks? (boolean @blocks?)})
            :blocks? (boolean @blocks?)})
@@ -352,8 +379,9 @@
 
 
 (defn query
 (defn query
   [repo query-string]
   [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
       (when query
         (let [query (query-wrapper query blocks?)]
         (let [query (query-wrapper query blocks?)]
           (query-custom/react-query repo
           (query-custom/react-query repo

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

@@ -165,7 +165,6 @@
   []
   []
   (let [match (:route-match @state/state)
   (let [match (:route-match @state/state)
         route-name (get-in match [:data :name])
         route-name (get-in match [:data :name])
-        tag? (= route-name :tag)
         page (case route-name
         page (case route-name
                :page
                :page
                (get-in match [:path-params :name])
                (get-in match [:path-params :name])
@@ -173,15 +172,10 @@
                :file
                :file
                (get-in match [:path-params :path])
                (get-in match [:path-params :path])
 
 
-               :tag
-               (get-in match [:path-params :name])
-
                (date/journal-name))]
                (date/journal-name))]
     (when page
     (when page
       (let [page-name (util/url-decode (string/lower-case page))]
       (let [page-name (util/url-decode (string/lower-case page))]
-        (db-utils/entity (if tag?
-                           [:tag/name page-name]
-                           [:page/name page-name]))))))
+        (db-utils/entity [:page/name page-name])))))
 
 
 (defn get-current-priority
 (defn get-current-priority
   []
   []
@@ -297,7 +291,7 @@
                               (conn/get-files-conn repo-url)
                               (conn/get-files-conn repo-url)
                               (conn/get-conn repo-url false)))]
                               (conn/get-conn repo-url false)))]
         (when (and (seq tx-data) (get-conn))
         (when (and (seq tx-data) (get-conn))
-          (let [tx-result (profile "Transact!" (d/transact! (get-conn) (vec tx-data)))
+          (let [tx-result (d/transact! (get-conn) (vec tx-data))
                 db (:db-after tx-result)
                 db (:db-after tx-result)
                 handler-keys (get-handler-keys handler-opts)]
                 handler-keys (get-handler-keys handler-opts)]
             (doseq [handler-key handler-keys]
             (doseq [handler-key handler-keys]
@@ -345,18 +339,3 @@
      (-> (q repo-url [:kv key] {} key key)
      (-> (q repo-url [:kv key] {} key key)
          react
          react
          key))))
          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}))))

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

@@ -35,8 +35,12 @@
 (defn group-by-page
 (defn group-by-page
   [blocks]
   [blocks]
   (some->> 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]
 (defn get-tx-id [tx-report]
   (get-in tx-report [:tempids :db/current-tx]))
   (get-in tx-report [:tempids :db/current-tx]))
@@ -72,8 +76,8 @@
    (when-let [conn (conn/get-conn repo)]
    (when-let [conn (conn/get-conn repo)]
      (try
      (try
        (d/pull conn
        (d/pull conn
-         selector
-         eid)
+               selector
+               eid)
        (catch js/Error e
        (catch js/Error e
          nil)))))
          nil)))))
 
 
@@ -95,7 +99,7 @@
   ([repo-url tx-data]
   ([repo-url tx-data]
    (when-not config/publishing?
    (when-not config/publishing?
      (let [tx-data (->> (util/remove-nils tx-data)
      (let [tx-data (->> (util/remove-nils tx-data)
-                     (remove nil?))]
+                        (remove nil?))]
        (when (seq tx-data)
        (when (seq tx-data)
          (when-let [conn (conn/get-conn repo-url false)]
          (when-let [conn (conn/get-conn repo-url false)]
            (d/transact! conn (vec tx-data))))))))
            (d/transact! conn (vec tx-data))))))))
@@ -106,4 +110,4 @@
   ([repo-url key]
   ([repo-url key]
    (when-let [db (conn/get-conn repo-url)]
    (when-let [db (conn/get-conn repo-url)]
      (some-> (d/entity db key)
      (some-> (d/entity db key)
-       key))))
+             key))))

+ 13 - 11
src/main/frontend/db_schema.cljs

@@ -5,7 +5,6 @@
 (def files-db-schema
 (def files-db-schema
   {:file/path {:db/unique :db.unique/identity}
   {:file/path {:db/unique :db.unique/identity}
    :file/content {}
    :file/content {}
-   :file/last-modified-at {}
    :file/size {}
    :file/size {}
    :file/handle {}})
    :file/handle {}})
 
 
@@ -16,7 +15,8 @@
   {:schema/version  {}
   {:schema/version  {}
    :db/type         {}
    :db/type         {}
    :db/ident        {:db/unique :db.unique/identity}
    :db/ident        {:db/unique :db.unique/identity}
-
+   :db/encrypted?    {}
+   :db/encryption-keys {}
    ;; user
    ;; user
    :me/name  {}
    :me/name  {}
    :me/email {}
    :me/email {}
@@ -50,8 +50,6 @@
                      :db/cardinality :db.cardinality/many}
                      :db/cardinality :db.cardinality/many}
    :page/journal?   {}
    :page/journal?   {}
    :page/journal-day {}
    :page/journal-day {}
-   :page/created-at {}
-   :page/last-modified-at {}
 
 
    ;; block
    ;; block
    :block/uuid   {:db/unique      :db.unique/identity}
    :block/uuid   {:db/unique      :db.unique/identity}
@@ -64,6 +62,16 @@
    ;; referenced pages
    ;; referenced pages
    :block/ref-pages {:db/valueType   :db.type/ref
    :block/ref-pages {:db/valueType   :db.type/ref
                      :db/cardinality :db.cardinality/many}
                      :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:
+   ;; Markdown tags will be only stored in :block/ref-pages
+   :block/tags {:db/valueType   :db.type/ref
+                :db/cardinality :db.cardinality/many}
+
    ;; referenced blocks
    ;; referenced blocks
    :block/ref-blocks {:db/valueType   :db.type/ref
    :block/ref-blocks {:db/valueType   :db.type/ref
                       :db/cardinality :db.cardinality/many}
                       :db/cardinality :db.cardinality/many}
@@ -76,9 +84,6 @@
    :block/marker {}
    :block/marker {}
    :block/priority {}
    :block/priority {}
    :block/level {}
    :block/level {}
-   :block/tags {:db/valueType   :db.type/ref
-                :db/cardinality :db.cardinality/many
-                :db/isComponent true}
    ;; :start-pos :end-pos
    ;; :start-pos :end-pos
    :block/meta {}
    :block/meta {}
    :block/properties {}
    :block/properties {}
@@ -92,7 +97,4 @@
    :block/scheduled-ast {}
    :block/scheduled-ast {}
    :block/deadline {}
    :block/deadline {}
    :block/deadline-ast {}
    :block/deadline-ast {}
-   :block/repeated? {}
-
-   ;; For pages
-   :tag/name       {:db/unique :db.unique/identity}})
+   :block/repeated? {}})

+ 287 - 17
src/main/frontend/dicts.cljs

@@ -17,7 +17,7 @@ title: $today
 :heading: true
 :heading: true
 :END:
 :END:
 ## Logseq is a _privacy-first_, _open-source_ platform for _knowledge_ sharing and management.
 ## 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!
 ## Here are some tips might be useful.
 ## Here are some tips might be useful.
 #+BEGIN_TIP
 #+BEGIN_TIP
 Click to edit any block.
 Click to edit any block.
@@ -115,6 +115,7 @@ title: How to take dummy notes?
         :on-boarding/sci-desc " - Small Clojure Interpreter"
         :on-boarding/sci-desc " - Small Clojure Interpreter"
         :on-boarding/isomorphic-git-desc " - A pure JavaScript implementation of git for node and browsers!"
         :on-boarding/isomorphic-git-desc " - A pure JavaScript implementation of git for node and browsers!"
         :help/about "About Logseq"
         :help/about "About Logseq"
+        :help/roadmap "Roadmap"
         :help/bug "Bug report"
         :help/bug "Bug report"
         :help/feature "Feature request"
         :help/feature "Feature request"
         :help/changelog "Changelog"
         :help/changelog "Changelog"
@@ -138,10 +139,11 @@ title: How to take dummy notes?
         :help/move-block-down "Move Block Down"
         :help/move-block-down "Move Block Down"
         :help/create-new-block "Create New Block"
         :help/create-new-block "Create New Block"
         :help/new-line-in-block "New Line in Block"
         :help/new-line-in-block "New Line in Block"
+        :help/select-nfs-browser "Please use another browser (like latest chrome) which support NFS features to open local directory."
         :undo "Undo"
         :undo "Undo"
         :redo "Redo"
         :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/follow-link-under-cursor "Follow link under cursor"
         :help/open-link-in-sidebar "Open link in Sidebar"
         :help/open-link-in-sidebar "Open link in Sidebar"
         :expand "Expand"
         :expand "Expand"
@@ -156,8 +158,10 @@ title: How to take dummy notes?
         :help/context-menu "Context Menu"
         :help/context-menu "Context Menu"
         :help/fold-unfold "Fold/Unfold blocks (when not in edit mode)"
         :help/fold-unfold "Fold/Unfold blocks (when not in edit mode)"
         :help/toggle-doc-mode "Toggle document mode"
         :help/toggle-doc-mode "Toggle document mode"
+        :help/toggle-contents "Toggle Contents"
         :help/toggle-theme "Toggle between dark/light theme"
         :help/toggle-theme "Toggle between dark/light theme"
         :help/toggle-right-sidebar "Toggle right sidebar"
         :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/toggle-insert-new-block "Toggle Enter/Alt+Enter for inserting new block"
         :help/jump-to-journals "Jump to Journals"
         :help/jump-to-journals "Jump to Journals"
         :formatting "Formatting"
         :formatting "Formatting"
@@ -215,16 +219,18 @@ title: How to take dummy notes?
         :page/presentation-mode "Presentation mode (Powered by Reveal.js)"
         :page/presentation-mode "Presentation mode (Powered by Reveal.js)"
         :page/edit-properties-placeholder "Click here to edit this page's properties"
         :page/edit-properties-placeholder "Click here to edit this page's properties"
         :page/delete-success "Page {1} was deleted successfully!"
         :page/delete-success "Page {1} was deleted successfully!"
-        :page/delete-confirmation "Are you sure you want to delete this page?"
+        :page/delete-confirmation "Are you sure you want to delete this page and its file?"
         :page/rename-to "Rename \"{1}\" to:"
         :page/rename-to "Rename \"{1}\" to:"
         :page/priority "Priority \"{1}\""
         :page/priority "Priority \"{1}\""
         :page/re-index "Re-index this page"
         :page/re-index "Re-index this page"
         :page/copy-to-json "Copy the whole page as JSON"
         :page/copy-to-json "Copy the whole page as JSON"
         :page/rename "Rename page"
         :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/action-publish "Publish"
         :page/make-public "Publish it when exporting to an html file"
         :page/make-public "Publish it when exporting to an html file"
         :page/make-private "Make it private"
         :page/make-private "Make it private"
-        :page/delete "Delete page (will delete the file too)"
+        :page/delete "Delete page"
         :page/publish "Publish this page on Logseq"
         :page/publish "Publish this page on Logseq"
         :page/cancel-publishing "Cancel publishing on Logseq"
         :page/cancel-publishing "Cancel publishing on Logseq"
         :page/publish-as-slide "Publish this page as a slide on Logseq"
         :page/publish-as-slide "Publish this page as a slide on Logseq"
@@ -265,6 +271,9 @@ title: How to take dummy notes?
         :draw/delete "Delete"
         :draw/delete "Delete"
         :draw/more-options "More options"
         :draw/more-options "More options"
         :draw/back-to-logseq "Back to logseq"
         :draw/back-to-logseq "Back to logseq"
+        :text/image "Image"
+        :asset/confirm-delete "Are you sure you want to delete this {1}?"
+        :asset/physical-delete "Remove the file too (notice it can't be restored)"
         :content/copy "Copy"
         :content/copy "Copy"
         :content/cut "Cut"
         :content/cut "Cut"
         :content/make-todos "Make {1}s"
         :content/make-todos "Make {1}s"
@@ -278,6 +287,9 @@ title: How to take dummy notes?
         :settings-page/preferred-file-format "Preferred file format"
         :settings-page/preferred-file-format "Preferred file format"
         :settings-page/preferred-workflow "Preferred workflow"
         :settings-page/preferred-workflow "Preferred workflow"
         :settings-page/enable-timetracking "Enable timetracking"
         :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/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 "
         :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 "
         :settings-page/custom-cors-proxy-server "Custom CORS proxy server"
         :settings-page/custom-cors-proxy-server "Custom CORS proxy server"
@@ -285,14 +297,17 @@ title: How to take dummy notes?
         :settings-page/enable-developer-mode "Enable developer mode"
         :settings-page/enable-developer-mode "Enable developer mode"
         :settings-page/disable-developer-mode "Disable 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/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"
         :logseq "Logseq"
         :dot-mode "Dot mode"
         :dot-mode "Dot mode"
         :on "ON"
         :on "ON"
         :more-options "More options"
         :more-options "More options"
         :to "to"
         :to "to"
         :yes "Yes"
         :yes "Yes"
+        :no "No"
         :submit "Submit"
         :submit "Submit"
         :cancel "Cancel"
         :cancel "Cancel"
+        :close "Close"
         :re-index "Re-index"
         :re-index "Re-index"
         :export-json "Export as JSON"
         :export-json "Export as JSON"
         :unlink "unlink"
         :unlink "unlink"
@@ -302,9 +317,9 @@ title: How to take dummy notes?
         :new-page "New page"
         :new-page "New page"
         :new-file "New file"
         :new-file "New file"
         :graph "Graph"
         :graph "Graph"
+        :graph-view "View Graph"
         :publishing "Publishing"
         :publishing "Publishing"
         :export "Export public pages"
         :export "Export public pages"
-        :all-repos "All repos"
         :all-graphs "All graphs"
         :all-graphs "All graphs"
         :all-pages "All pages"
         :all-pages "All pages"
         :all-files "All files"
         :all-files "All files"
@@ -322,6 +337,8 @@ title: How to take dummy notes?
         :parsing-files "Parsing files"
         :parsing-files "Parsing files"
         :loading-files "Loading files"
         :loading-files "Loading files"
         :login-github "Login with Github"
         :login-github "Login with Github"
+        :login-google "Login with Google"
+        :login "Login"
         :go-to "Go to "
         :go-to "Go to "
         :or "or"
         :or "or"
         :download "Download"
         :download "Download"
@@ -331,7 +348,239 @@ title: How to take dummy notes?
         :dark "Dark"
         :dark "Dark"
         :remove-background "Remove background"
         :remove-background "Remove background"
         :open "Open"
         :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"
    :fr {:help/about "A propos de Logseq"
         :help/bug "Signaler une anomalie"
         :help/bug "Signaler une anomalie"
@@ -357,6 +606,7 @@ title: How to take dummy notes?
         :help/move-block-down "Déplacer un bloc en dessous"
         :help/move-block-down "Déplacer un bloc en dessous"
         :help/create-new-block "Créer un nouveau bloc"
         :help/create-new-block "Créer un nouveau bloc"
         :help/new-line-in-block "Aller à la ligne dans un bloc"
         :help/new-line-in-block "Aller à la ligne dans un bloc"
+        :help/select-nfs-browser "Please use another browser (like latest chrome) which support NFS features to open local directory."
         :undo "Annuler"
         :undo "Annuler"
         :redo "Redo"
         :redo "Redo"
         :help/zoom-in "Zoomer"
         :help/zoom-in "Zoomer"
@@ -518,7 +768,6 @@ title: How to take dummy notes?
         :new-file "Nouveau fichier"
         :new-file "Nouveau fichier"
         :graph "Graphe"
         :graph "Graphe"
         :publishing "Publication"
         :publishing "Publication"
-        :all-repos "Tous les répertoires"
         :all-pages "Toutes les pages"
         :all-pages "Toutes les pages"
         :all-files "Tous les fichiers"
         :all-files "Tous les fichiers"
         :all-journals "Tous les journaux"
         :all-journals "Tous les journaux"
@@ -534,6 +783,7 @@ title: How to take dummy notes?
         :parsing-files "Analyse des fichiers"
         :parsing-files "Analyse des fichiers"
         :loading-files "Chargement des fichiers"
         :loading-files "Chargement des fichiers"
         :login-github "S'authentifier avec Github"
         :login-github "S'authentifier avec Github"
+        :login-google "S'authentifier avec Google"
         :go-to "Aller à "
         :go-to "Aller à "
         :or "ou"
         :or "ou"
         :download "Télécharger"
         :download "Télécharger"
@@ -613,10 +863,14 @@ title: How to take dummy notes?
            :help/move-block-down "向下移动块"
            :help/move-block-down "向下移动块"
            :help/create-new-block "创建块"
            :help/create-new-block "创建块"
            :help/new-line-in-block "块中新建行"
            :help/new-line-in-block "块中新建行"
+           :help/select-nfs-browser "请选择支持nfs的浏览来使用logseq本地文件夹功能, 如最新的Chrome浏览器."
+           :text/image "图片"
+           :asset/confirm-delete "确定要删除{1}吗?"
+           :asset/physical-delete "同时删除本地文件(目前不可撤销)"
            :undo "撤销"
            :undo "撤销"
            :redo "重做"
            :redo "重做"
            :help/zoom-in "聚焦"
            :help/zoom-in "聚焦"
-           :help/zoom-out "推出聚焦"
+           :help/zoom-out "退出聚焦"
            :help/follow-link-under-cursor "跟随光标下的链接"
            :help/follow-link-under-cursor "跟随光标下的链接"
            :help/open-link-in-sidebar "在侧边栏打开"
            :help/open-link-in-sidebar "在侧边栏打开"
            :expand "展开"
            :expand "展开"
@@ -631,8 +885,10 @@ title: How to take dummy notes?
            :help/context-menu "右键菜单"
            :help/context-menu "右键菜单"
            :help/fold-unfold "折叠/展开方块(不在编辑模式中)"
            :help/fold-unfold "折叠/展开方块(不在编辑模式中)"
            :help/toggle-doc-mode "切换文档模式"
            :help/toggle-doc-mode "切换文档模式"
+           :help/toggle-contents "打开/关闭目录"
            :help/toggle-theme "“在暗色/亮色主题之间切换”"
            :help/toggle-theme "“在暗色/亮色主题之间切换”"
            :help/toggle-right-sidebar "启用/关闭右侧栏"
            :help/toggle-right-sidebar "启用/关闭右侧栏"
+           :help/toggle-settings "显示/关闭设置"
            :help/toggle-insert-new-block "切换 Enter/Alt+Enter 以插入新块"
            :help/toggle-insert-new-block "切换 Enter/Alt+Enter 以插入新块"
            :help/jump-to-journals "跳转到日记"
            :help/jump-to-journals "跳转到日记"
            :formatting "格式化"
            :formatting "格式化"
@@ -690,16 +946,18 @@ title: How to take dummy notes?
            :page/edit-properties-placeholder "点击这里编辑当前页面的属性 (标签,别名等)"
            :page/edit-properties-placeholder "点击这里编辑当前页面的属性 (标签,别名等)"
            :page/presentation-mode "演讲模式 (由 Reveal.js 驱动)"
            :page/presentation-mode "演讲模式 (由 Reveal.js 驱动)"
            :page/delete-success "页面 {1} 删除成功!"
            :page/delete-success "页面 {1} 删除成功!"
-           :page/delete-confirmation "您确定要删除此页面吗?"
+           :page/delete-confirmation "您确定要删除此页面和文件吗?"
            :page/rename-to "重命名 \"{1}\" 至:"
            :page/rename-to "重命名 \"{1}\" 至:"
            :page/priority "优先级 \"{1}\""
            :page/priority "优先级 \"{1}\""
            :page/re-index "对此页面重新建立索引"
            :page/re-index "对此页面重新建立索引"
            :page/copy-to-json "将整页以 JSON 格式复制"
            :page/copy-to-json "将整页以 JSON 格式复制"
            :page/rename "重命名本页"
            :page/rename "重命名本页"
+           :page/open-in-finder "打开文件对应目录"
+           :page/open-with-default-app "用默认应用打开文件"
            :page/action-publish "发布"
            :page/action-publish "发布"
            :page/make-public "导出 HTML 时发布本页面"
            :page/make-public "导出 HTML 时发布本页面"
            :page/make-private "导出 HTML 时取消发布本页面"
            :page/make-private "导出 HTML 时取消发布本页面"
-           :page/delete "删除本页(并删除文件)"
+           :page/delete "删除本页"
            :page/publish "将本页发布至 Logseq"
            :page/publish "将本页发布至 Logseq"
            :page/cancel-publishing "撤回本页在 Logseq 上的发布"
            :page/cancel-publishing "撤回本页在 Logseq 上的发布"
            :page/publish-as-slide "将本页作为幻灯片发布至 Logseq"
            :page/publish-as-slide "将本页作为幻灯片发布至 Logseq"
@@ -753,6 +1011,9 @@ title: How to take dummy notes?
            :settings-page/preferred-file-format "首选文件格式"
            :settings-page/preferred-file-format "首选文件格式"
            :settings-page/preferred-workflow "首选工作流"
            :settings-page/preferred-workflow "首选工作流"
            :settings-page/enable-timetracking "开启 timetracking"
            :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/enable-block-time "记录 block 创建/修改时间"
            :settings-page/dont-use-other-peoples-proxy-servers "不要使用其他人的代理服务器。这非常危险,可能会使您的令牌和笔记被盗。 如果您使用其他人的代理服务器,Logseq 将不会对此损失负责。您可以自己部署它,请查阅 "
            :settings-page/dont-use-other-peoples-proxy-servers "不要使用其他人的代理服务器。这非常危险,可能会使您的令牌和笔记被盗。 如果您使用其他人的代理服务器,Logseq 将不会对此损失负责。您可以自己部署它,请查阅 "
            :settings-page/custom-cors-proxy-server "自定义 CORS 代理服务器"
            :settings-page/custom-cors-proxy-server "自定义 CORS 代理服务器"
@@ -760,6 +1021,7 @@ title: How to take dummy notes?
            :settings-page/enable-developer-mode "启用开发者模式"
            :settings-page/enable-developer-mode "启用开发者模式"
            :settings-page/disable-developer-mode "禁用开发者模式"
            :settings-page/disable-developer-mode "禁用开发者模式"
            :settings-page/developer-mode-desc "开发者模式帮助贡献者和扩展开发者更有效地测试他们与 Logseq 的集成。"
            :settings-page/developer-mode-desc "开发者模式帮助贡献者和扩展开发者更有效地测试他们与 Logseq 的集成。"
+           :settings-page/current-version "当前版本"
            :logseq "Logseq"
            :logseq "Logseq"
            :dot-mode "点模式"
            :dot-mode "点模式"
            :on "已打开"
            :on "已打开"
@@ -777,9 +1039,9 @@ title: How to take dummy notes?
            :new-page "新页面"
            :new-page "新页面"
            :new-file "新文件"
            :new-file "新文件"
            :graph "图谱"
            :graph "图谱"
+           :graph-view "全局图谱"
            :publishing "发布"
            :publishing "发布"
            :export "导出公开页面"
            :export "导出公开页面"
-           :all-repos "所有库"
            :all-graphs "所有库"
            :all-graphs "所有库"
            :all-pages "所有页面"
            :all-pages "所有页面"
            :all-files "所有文件"
            :all-files "所有文件"
@@ -790,12 +1052,14 @@ title: How to take dummy notes?
            :sponsor-us "赞助我们!"
            :sponsor-us "赞助我们!"
            :discord-title "我们的 Discord 社群!"
            :discord-title "我们的 Discord 社群!"
            :sign-out "登出"
            :sign-out "登出"
-           :help-shortcut-title "点此查看快捷方式和更多游泳帮助"
+           :help-shortcut-title "点此查看快捷方式和更多有用帮助"
            :loading "加载中"
            :loading "加载中"
            :cloning "Clone 中"
            :cloning "Clone 中"
            :parsing-files "正在解析文件"
            :parsing-files "正在解析文件"
            :loading-files "正在加载文件"
            :loading-files "正在加载文件"
            :login-github "用 Github 登录"
            :login-github "用 Github 登录"
+           :login-google "用 Google 登录"
+           :login "登录"
            :go-to "转到"
            :go-to "转到"
            :or "或"
            :or "或"
            :download "下载"
            :download "下载"
@@ -805,7 +1069,10 @@ title: How to take dummy notes?
            :dark "暗黑"
            :dark "暗黑"
            :remove-background "去除背景"
            :remove-background "去除背景"
            :open "打开"
            :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!"
    :zh-Hant {:on-boarding/title "你好,歡迎使用 Logseq!"
              :on-boarding/sharing "分享"
              :on-boarding/sharing "分享"
@@ -877,6 +1144,7 @@ title: How to take dummy notes?
              :help/move-block-down "向下移動塊"
              :help/move-block-down "向下移動塊"
              :help/create-new-block "創建塊"
              :help/create-new-block "創建塊"
              :help/new-line-in-block "塊中新建行"
              :help/new-line-in-block "塊中新建行"
+             :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 "聚焦"
              :help/zoom-in "聚焦"
@@ -1035,7 +1303,6 @@ title: How to take dummy notes?
              :new-page "新頁面"
              :new-page "新頁面"
              :graph "圖譜"
              :graph "圖譜"
              :publishing "發布/下載 HTML 文件"
              :publishing "發布/下載 HTML 文件"
-             :all-repos "所有庫"
              :all-pages "所有頁面"
              :all-pages "所有頁面"
              :all-files "所有文件"
              :all-files "所有文件"
              :my-publishing "My publishing"
              :my-publishing "My publishing"
@@ -1044,12 +1311,13 @@ title: How to take dummy notes?
              :join-community "加入社區"
              :join-community "加入社區"
              :discord-title "我們的 Discord 社群!"
              :discord-title "我們的 Discord 社群!"
              :sign-out "登出"
              :sign-out "登出"
-             :help-shortcut-title "點此查看快捷方式和更多游泳幫助"
+             :help-shortcut-title "點此查看快捷方式和更多有用幫助"
              :loading "加載中"
              :loading "加載中"
              :cloning "Clone 中"
              :cloning "Clone 中"
              :parsing-files "正在解析文件"
              :parsing-files "正在解析文件"
              :loading-files "正在加載文件"
              :loading-files "正在加載文件"
              :login-github "用 Github 登錄"
              :login-github "用 Github 登錄"
+             :login-google "用 Google 登錄"
              :go-to "轉到"
              :go-to "轉到"
              :or "或"
              :or "或"
              :download "下載"
              :download "下載"
@@ -1128,6 +1396,7 @@ title: How to take dummy notes?
         :help/move-block-down "Skuif Blok Ondertoe"
         :help/move-block-down "Skuif Blok Ondertoe"
         :help/create-new-block "Skep 'n nuwe blok"
         :help/create-new-block "Skep 'n nuwe blok"
         :help/new-line-in-block "Nuwe lyn in blok"
         :help/new-line-in-block "Nuwe lyn in blok"
+        :help/select-nfs-browser "Please use another browser (like latest chrome) which support NFS features to open local directory."
         :undo "Ontdoen"
         :undo "Ontdoen"
         :redo "Herdoen"
         :redo "Herdoen"
         :help/zoom-in "Zoem in"
         :help/zoom-in "Zoem in"
@@ -1281,7 +1550,6 @@ title: How to take dummy notes?
         :search "Soek"
         :search "Soek"
         :new-page "Nuwe bladsy"
         :new-page "Nuwe bladsy"
         :graph "Grafiek"
         :graph "Grafiek"
-        :all-repos "Alle stoorplekke"
         :all-pages "Alle blaaie"
         :all-pages "Alle blaaie"
         :all-files "Alle lêers"
         :all-files "Alle lêers"
         :settings "Verstellings"
         :settings "Verstellings"
@@ -1295,6 +1563,7 @@ title: How to take dummy notes?
         :parsing-files "Lêer ontleding"
         :parsing-files "Lêer ontleding"
         :loading-files "Laai lêers"
         :loading-files "Laai lêers"
         :login-github "Aantekening deur Github"
         :login-github "Aantekening deur Github"
+        :login-google "Aantekening deur Google"
         :go-to "Gaan na "
         :go-to "Gaan na "
         :or "of"
         :or "of"
         :download "Laai af"
         :download "Laai af"
@@ -1306,6 +1575,7 @@ title: How to take dummy notes?
 
 
 (def languages [{:label "English" :value :en}
 (def languages [{:label "English" :value :en}
                 {:label "Français" :value :fr}
                 {:label "Français" :value :fr}
+                {:label "Deutsch" :value :de}
                 {:label "简体中文" :value :zh-CN}
                 {:label "简体中文" :value :zh-CN}
                 {:label "繁體中文" :value :zh-Hant}
                 {:label "繁體中文" :value :zh-Hant}
                 {:label "Afrikaans" :value :af}])
                 {:label "Afrikaans" :value :af}])

+ 22 - 3
src/main/frontend/diff.cljs

@@ -1,15 +1,34 @@
 (ns frontend.diff
 (ns frontend.diff
   (:require [clojure.string :as string]
   (:require [clojure.string :as string]
             ["diff" :as jsdiff]
             ["diff" :as jsdiff]
+            ["diff-match-patch" :as diff-match-patch]
             [goog.object :as gobj]
             [goog.object :as gobj]
             [lambdaisland.glogi :as log]
             [lambdaisland.glogi :as log]
             [cljs-bean.core :as bean]))
             [cljs-bean.core :as bean]))
 
 
+;; TODO: replace with diff-match-patch
 (defn diff
 (defn diff
   [s1 s2]
   [s1 s2]
   (-> ((gobj/get jsdiff "diffLines") s1 s2)
   (-> ((gobj/get jsdiff "diffLines") s1 s2)
       bean/->clj))
       bean/->clj))
 
 
+(defonce dmp (diff-match-patch.))
+
+(defn diffs
+  [s1 s2]
+  (.diff_main dmp s1 s2 true))
+
+(defn get-patches
+  [s1 s2 diffs]
+  (.patch_make dmp s1 s2 diffs))
+
+(defn apply-patches!
+  [text patches]
+  (if (seq patches)
+    (let [result (.patch_apply dmp patches text)]
+      (nth result 0))
+    text))
+
 ;; (find-position "** hello _w_" "hello w")
 ;; (find-position "** hello _w_" "hello w")
 (defn find-position
 (defn find-position
   [markup text]
   [markup text]
@@ -32,6 +51,6 @@
 
 
           :else
           :else
           (recur r1 t2 (inc i1) i2))))
           (recur r1 t2 (inc i1) i2))))
-      (catch js/Error e
-        (log/error :diff/find-position {:error e})
-        (count markup))))
+    (catch js/Error e
+      (log/error :diff/find-position {:error e})
+      (count markup))))

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

@@ -0,0 +1,103 @@
+(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]
+  (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))

+ 1 - 0
src/main/frontend/extensions/code.cljs

@@ -14,6 +14,7 @@
             ["codemirror/addon/edit/matchbrackets"]
             ["codemirror/addon/edit/matchbrackets"]
             ["codemirror/addon/edit/closebrackets"]
             ["codemirror/addon/edit/closebrackets"]
             ["codemirror/mode/clojure/clojure"]
             ["codemirror/mode/clojure/clojure"]
+            ["codemirror/mode/powershell/powershell"]
             ["codemirror/mode/javascript/javascript"]
             ["codemirror/mode/javascript/javascript"]
             ["codemirror/mode/clike/clike"]
             ["codemirror/mode/clike/clike"]
             ["codemirror/mode/vue/vue"]
             ["codemirror/mode/vue/vue"]

+ 2 - 1
src/main/frontend/extensions/code.css

@@ -11,7 +11,8 @@
   > .CodeMirror {
   > .CodeMirror {
     z-index: 0;
     z-index: 0;
     height: auto;
     height: auto;
-    margin: 6px 0 0 0;
+    padding: 6px 0 0 0;
     font-family: Fira Code, Monaco, Menlo, Consolas, 'COURIER NEW', monospace;
     font-family: Fira Code, Monaco, Menlo, Consolas, 'COURIER NEW', monospace;
+    max-width: 86vw;
   }
   }
 }
 }

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

@@ -29,28 +29,33 @@
               (bean/->js
               (bean/->js
                {:embedded true
                {:embedded true
                 :controls true
                 :controls true
-                :history true
+                :history false
                 :center true
                 :center true
                 :transition "slide"}))]
                 :transition "slide"}))]
     (.initialize deck)))
     (.initialize deck)))
 
 
 (defn slide-content
 (defn slide-content
   [loading? style sections]
   [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))
                          (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
 (rum/defc slide < rum/reactive
   {:did-mount (fn [state]
   {:did-mount (fn [state]

+ 0 - 0
src/main/frontend/external.cljs → src/main/frontend/external.cljc


+ 2 - 2
src/main/frontend/external/protocol.cljs → src/main/frontend/external/protocol.cljc

@@ -2,9 +2,9 @@
 
 
 (defprotocol External
 (defprotocol External
   (toMarkdownFiles [this content config]
   (toMarkdownFiles [this content config]
-    "Should return a map of markdown's file name to contents.")
+    "Should return a map of markdown's file name to contents."))
 
 
   ;; Long-term goal:
   ;; Long-term goal:
   ;; (toMldocAst [this content])
   ;; (toMldocAst [this content])
   ;; (fromMldocAst [this ast])
   ;; (fromMldocAst [this ast])
-  )
+

+ 20 - 5
src/main/frontend/external/roam.cljs → src/main/frontend/external/roam.cljc

@@ -1,6 +1,7 @@
 (ns frontend.external.roam
 (ns frontend.external.roam
-  (:require [frontend.external.protocol :as protocol]
-            [cljs-bean.core :as bean]
+  (:require #?(:cljs [cljs-bean.core :as bean]
+               :clj [cheshire.core :as json])
+            [frontend.external.protocol :as protocol]
             [medley.core :as medley]
             [medley.core :as medley]
             [clojure.walk :as walk]
             [clojure.walk :as walk]
             [clojure.string :as string]
             [clojure.string :as string]
@@ -42,6 +43,15 @@
                                              (util/format "{{%s %s}}" name arg))
                                              (util/format "{{%s %s}}" name arg))
                                            original)))))
                                            original)))))
 
 
+(defn- fenced-code-transform
+  [text]
+  (string/replace text
+                  #"```([a-z]*\n[\s\S]*?\n*)```"
+                  (fn [[_ match]]
+                    (str "```"
+                         (str match "\n")
+                         "```"))))
+
 (defn load-all-refed-uids!
 (defn load-all-refed-uids!
   [data]
   [data]
   (let [full-text (atom "")]
   (let [full-text (atom "")]
@@ -65,7 +75,8 @@
       (string/replace "{{[[TODO]]}}" "TODO")
       (string/replace "{{[[TODO]]}}" "TODO")
       (string/replace "{{[[DONE]]}}" "DONE")
       (string/replace "{{[[DONE]]}}" "DONE")
       (uid-transform)
       (uid-transform)
-      (macro-transform)))
+      (macro-transform)
+      (fenced-code-transform)))
 
 
 (declare children->text)
 (declare children->text)
 (defn child->text
 (defn child->text
@@ -119,11 +130,15 @@
                    (apply str))))
                    (apply str))))
      files)))
      files)))
 
 
+(defn json->edn
+  [raw-string]
+  #?(:cljs (-> raw-string js/JSON.parse bean/->clj)
+     :clj (-> raw-string json/parse-string clojure.walk/keywordize-keys)))
+
 (defrecord Roam []
 (defrecord Roam []
   protocol/External
   protocol/External
   (toMarkdownFiles [this content _config]
   (toMarkdownFiles [this content _config]
-    (let [data (bean/->clj (js/JSON.parse content))]
-      (->files data))))
+    (-> content json->edn ->files)))
 
 
 (comment
 (comment
   (defonce test-roam-json (frontend.db/get-file "same.json"))
   (defonce test-roam-json (frontend.db/get-file "same.json"))

+ 136 - 61
src/main/frontend/format/block.cljs

@@ -9,7 +9,9 @@
             [datascript.core :as d]
             [datascript.core :as d]
             [frontend.date :as date]
             [frontend.date :as date]
             [frontend.text :as text]
             [frontend.text :as text]
-            [medley.core :as medley]))
+            [medley.core :as medley]
+            [frontend.state :as state]
+            [frontend.db :as db]))
 
 
 (defn heading-block?
 (defn heading-block?
   [block]
   [block]
@@ -34,10 +36,12 @@
                    (= typ "Search")
                    (= typ "Search")
                    ;; FIXME: alert error
                    ;; FIXME: alert error
                    (not (contains? #{\# \* \/ \[} (first (second (:url (second block))))))
                    (not (contains? #{\# \* \/ \[} (first (second (:url (second block))))))
-                   (let [page (second (:url (second block)))]
-                     (when (and (not (util/starts-with? page "http"))
-                                (not (util/starts-with? page "file"))
-                                (not (string/ends-with? page ".html")))
+                   (let [page (second (:url (second block)))
+                         ext (some-> (util/get-file-ext page) keyword)]
+                     (when (and (not (util/starts-with? page "http:"))
+                                (not (util/starts-with? page "https:"))
+                                (not (util/starts-with? page "file:"))
+                                (not (contains? (config/supported-formats) ext)))
                        page)))
                        page)))
 
 
                   (and
                   (and
@@ -141,22 +145,45 @@
       (update "created_at" util/safe-parse-int)
       (update "created_at" util/safe-parse-int)
       (update "last_modified_at" util/safe-parse-int)))
       (update "last_modified_at" util/safe-parse-int)))
 
 
+(defonce non-parsing-properties
+  (atom #{"background_color"}))
+
 (defn extract-properties
 (defn extract-properties
-  [[_ properties] start-pos end-pos]
-  (let [properties (->> (into {} properties)
+  [[_ properties] _start-pos _end-pos]
+  (let [properties (into {} properties)
+        page-refs (->>
+                   (map (fn [v]
+                          (when v
+                            (->> (re-seq text/page-ref-re v)
+                                 (map second)
+                                 (map string/lower-case))))
+                        (vals properties))
+                   (apply concat)
+                   (distinct))
+        properties (->> properties
                         (medley/map-kv (fn [k v]
                         (medley/map-kv (fn [k v]
-                                         (let [k' (and k (string/trim (string/lower-case k)))
-                                               v' (and v (string/trim v))
-                                               v' (if (and k' v'
-                                                           (contains? config/markers k')
-                                                           (util/safe-parse-int v'))
-                                                    (util/safe-parse-int v')
-                                                    (text/split-page-refs-without-brackets v'))]
-                                           [k' v'])))
+                                         (let [v (string/trim v)]
+                                           (cond
+                                             (and (= "\"" (first v) (last v))) ; wrapped in ""
+                                             [(string/lower-case k) (string/trim (subs v 1 (dec (count v))))]
+
+                                             (contains? @non-parsing-properties (string/lower-case k))
+                                             [(string/lower-case k) v]
+
+                                             :else
+                                             (let [k' (and k (string/trim (string/lower-case k)))
+                                                   v' v
+                                                   ;; built-in collections
+                                                   comma? (contains? #{"tags" "alias"} k)
+                                                   v' (if (and k' v'
+                                                               (contains? config/markers k')
+                                                               (util/safe-parse-int v'))
+                                                        (util/safe-parse-int v')
+                                                        (text/split-page-refs-without-brackets v' comma?))]
+                                               [k' v'])))))
                         (->schema-properties))]
                         (->schema-properties))]
     {:properties properties
     {:properties properties
-     :start-pos start-pos
-     :end-pos end-pos}))
+     :page-refs page-refs}))
 
 
 (defn- paragraph-timestamp-block?
 (defn- paragraph-timestamp-block?
   [block]
   [block]
@@ -192,10 +219,19 @@
                               (assoc :repeated? true))))))]
                               (assoc :repeated? true))))))]
     (apply merge m)))
     (apply merge m)))
 
 
+(defn block-tags->pages
+  [{:keys [tags] :as block}]
+  (if (seq tags)
+    (assoc block :tags (map (fn [tag]
+                              [:page/name (string/lower-case tag)]) tags))
+    block))
+
 (defn with-page-refs
 (defn with-page-refs
-  [{:keys [title body tags] :as block}]
-  (let [tags (mapv :tag/name (util/->tags (map :tag/name tags)))
-        ref-pages (atom tags)]
+  [{:keys [title body tags ref-pages] :as block}]
+  (let [ref-pages (->> (concat tags ref-pages)
+                       (remove string/blank?)
+                       (distinct))
+        ref-pages (atom ref-pages)]
     (walk/postwalk
     (walk/postwalk
      (fn [form]
      (fn [form]
        (when-let [page (get-page-reference form)]
        (when-let [page (get-page-reference form)]
@@ -254,11 +290,42 @@
          (block-keywordize (util/remove-nils block)))
          (block-keywordize (util/remove-nils block)))
        blocks))
        blocks))
 
 
-(defn collect-block-tags
-  [{:keys [title body tags] :as block}]
-  (cond-> block
-    (seq tags)
-    (assoc :tags (util/->tags tags))))
+(defn with-path-refs
+  [blocks]
+  (loop [blocks blocks
+         acc []
+         parents []]
+    (if (empty? blocks)
+      acc
+      (let [block (first blocks)
+            cur-level (:block/level block)
+            level-diff (- cur-level
+                          (get (last parents) :block/level 0))
+            [path-refs parents]
+            (cond
+              (zero? level-diff)            ; sibling
+              (let [path-refs (mapcat :block/ref-pages (drop-last parents))
+                    parents (conj (vec (butlast parents)) block)]
+                [path-refs parents])
+
+              (> level-diff 0)              ; child
+              (let [path-refs (mapcat :block/ref-pages parents)]
+                [path-refs (conj parents block)])
+
+              (< level-diff 0)              ; new parent
+              (let [parents (vec (take-while (fn [p] (< (:block/level p) cur-level)) parents))
+                    path-refs (mapcat :block/ref-pages parents)]
+                [path-refs (conj parents block)]))
+            path-ref-pages (->> path-refs
+                                (concat (:block/ref-pages block))
+                                (remove string/blank?)
+                                (map string/lower-case)
+                                (distinct)
+                                (map (fn [p]
+                                       {:page/name p})))]
+        (recur (rest blocks)
+               (conj acc (assoc block :block/path-ref-pages path-ref-pages))
+               parents)))))
 
 
 (defn extract-blocks
 (defn extract-blocks
   [blocks last-pos encoded-content]
   [blocks last-pos encoded-content]
@@ -293,6 +360,7 @@
                                  (when (util/uuid-string? custom-id)
                                  (when (util/uuid-string? custom-id)
                                    (uuid custom-id))))
                                    (uuid custom-id))))
                              (d/squuid))
                              (d/squuid))
+                      ref-pages-in-properties (:page-refs properties)
                       block (second block)
                       block (second block)
                       level (:level block)
                       level (:level block)
                       [children current-block-children]
                       [children current-block-children]
@@ -313,16 +381,18 @@
                                        :uuid id
                                        :uuid id
                                        :body (vec (reverse block-body))
                                        :body (vec (reverse block-body))
                                        :properties (:properties properties)
                                        :properties (:properties properties)
+                                       :ref-pages ref-pages-in-properties
                                        :children (or current-block-children []))
                                        :children (or current-block-children []))
                                 (assoc-in [:meta :start-pos] start_pos)
                                 (assoc-in [:meta :start-pos] start_pos)
                                 (assoc-in [:meta :end-pos] last-pos))
                                 (assoc-in [:meta :end-pos] last-pos))
                       block (if (seq timestamps)
                       block (if (seq timestamps)
                               (merge block (timestamps->scheduled-and-deadline timestamps))
                               (merge block (timestamps->scheduled-and-deadline timestamps))
                               block)
                               block)
-                      block (collect-block-tags block)
-                      block (with-page-refs block)
-                      block (with-block-refs block)
-                      block (update-src-pos-meta! block)
+                      block (-> block
+                                with-page-refs
+                                with-block-refs
+                                block-tags->pages
+                                update-src-pos-meta!)
                       last-pos' (get-in block [:meta :start-pos])]
                       last-pos' (get-in block [:meta :start-pos])]
                   (recur (conj headings block) [] (rest blocks) {} {} last-pos' (:level block) children))
                   (recur (conj headings block) [] (rest blocks) {} {} last-pos' (:level block) children))
 
 
@@ -332,35 +402,36 @@
             (-> (reverse headings)
             (-> (reverse headings)
                 safe-blocks)))]
                 safe-blocks)))]
     (let [first-block (first blocks)
     (let [first-block (first blocks)
-          first-block-start-pos (get-in first-block [:block/meta :start-pos])]
-      (if (and
-           (not (string/blank? encoded-content))
-           (or (empty? blocks)
-               (> first-block-start-pos 1)))
-        (cons
-         (merge
-          (let [content (utf8/substring encoded-content 0 first-block-start-pos)
-                uuid (d/squuid)]
-            (->
-             {:uuid uuid
-              :content content
-              :anchor (str uuid)
-              :level 2
-              :meta {:start-pos 0
-                     :end-pos (or first-block-start-pos
-                                  (utf8/length encoded-content))}
-              :body (take-while (fn [block] (not (heading-block? block))) blocks)
-              :pre-block? true}
-             (block-keywordize)))
-          (select-keys first-block [:block/file :block/format :block/page]))
-         blocks)
-        blocks))))
+          first-block-start-pos (get-in first-block [:block/meta :start-pos])
+          blocks (if (and
+                      (not (string/blank? encoded-content))
+                      (or (empty? blocks)
+                          (> first-block-start-pos 1)))
+                   (cons
+                    (merge
+                     (let [content (utf8/substring encoded-content 0 first-block-start-pos)
+                           uuid (d/squuid)]
+                       (->
+                        {:uuid uuid
+                         :content content
+                         :anchor (str uuid)
+                         :level 2
+                         :meta {:start-pos 0
+                                :end-pos (or first-block-start-pos
+                                             (utf8/length encoded-content))}
+                         :body (take-while (fn [block] (not (heading-block? block))) blocks)
+                         :pre-block? true}
+                        (block-keywordize)))
+                     (select-keys first-block [:block/file :block/format :block/page]))
+                    blocks)
+                   blocks)]
+      (with-path-refs blocks))))
 
 
 (defn- page-with-journal
 (defn- page-with-journal
   [original-page-name]
   [original-page-name]
   (when original-page-name
   (when original-page-name
     (let [page-name (string/lower-case original-page-name)]
     (let [page-name (string/lower-case original-page-name)]
-      (if-let [d (date/journal-title->int (string/capitalize page-name))]
+      (if-let [d (date/journal-title->int page-name)]
         {:page/name page-name
         {:page/name page-name
          :page/original-name original-page-name
          :page/original-name original-page-name
          :page/journal? true
          :page/journal? true
@@ -379,10 +450,17 @@
            content-length (utf8/length encoded-content)
            content-length (utf8/length encoded-content)
            blocks (extract-blocks ast content-length encoded-content)
            blocks (extract-blocks ast content-length encoded-content)
            ref-pages-atom (atom [])
            ref-pages-atom (atom [])
+           parent-ref-pages (->> (db/get-block-parent (state/get-current-repo) uuid)
+                                 :block/path-ref-pages
+                                 (map :db/id))
            blocks (doall
            blocks (doall
                    (map-indexed
                    (map-indexed
                     (fn [idx {:block/keys [ref-pages ref-blocks meta] :as block}]
                     (fn [idx {:block/keys [ref-pages ref-blocks meta] :as block}]
-                      (let [block (collect-block-tags block)
+                      (let [path-ref-pages (->> ref-pages
+                                                (remove string/blank?)
+                                                (map string/lower-case)
+                                                (map (fn [p] [:page/name p]))
+                                                (concat parent-ref-pages))
                             block (merge
                             block (merge
                                    block
                                    block
                                    {:block/meta meta
                                    {:block/meta meta
@@ -393,12 +471,10 @@
                                     :block/page page
                                     :block/page page
                                     :block/content (utf8/substring encoded-content
                                     :block/content (utf8/substring encoded-content
                                                                    (:start-pos meta)
                                                                    (:start-pos meta)
-                                                                   (:end-pos meta))}
+                                                                   (:end-pos meta))
+                                    :block/path-ref-pages path-ref-pages}
                                    ;; Preserve the original block id
                                    ;; Preserve the original block id
-                                   (when (and (zero? idx)
-                                              ;; not custom-id
-                                              (not (get-in block [:block/properties "custom_id"]))
-                                              (not (get-in block [:block/properties "id"])))
+                                   (when (zero? idx)
                                      {:block/uuid uuid})
                                      {:block/uuid uuid})
                                    (when (seq ref-pages)
                                    (when (seq ref-pages)
                                      {:block/ref-pages
                                      {:block/ref-pages
@@ -443,5 +519,4 @@
 
 
 (defn trim-break-lines!
 (defn trim-break-lines!
   [ast]
   [ast]
-  (->> (drop-while break-line-paragraph? ast)
-       (take-while (complement break-line-paragraph?))))
+  (drop-while break-line-paragraph? ast))

+ 33 - 10
src/main/frontend/format/mldoc.cljs

@@ -66,6 +66,17 @@
         (recur (rest ast)))
         (recur (rest ast)))
       nil)))
       nil)))
 
 
+(defn- ->vec
+  [s]
+  (if (string? s) [s] s))
+
+(defn- ->vec-concat
+  [& coll]
+  (->> (map ->vec coll)
+       (remove nil?)
+       (apply concat)
+       (distinct)))
+
 (defn collect-page-properties
 (defn collect-page-properties
   [ast]
   [ast]
   (if (seq ast)
   (if (seq ast)
@@ -75,9 +86,10 @@
           properties (->> (take-while directive? ast)
           properties (->> (take-while directive? ast)
                           (map (fn [[_ k v]]
                           (map (fn [[_ k v]]
                                  (let [k (keyword (string/lower-case k))
                                  (let [k (keyword (string/lower-case k))
-                                       v (if (contains? #{:title :description} k)
+                                       comma? (contains? #{:tags :alias :roam_tags} k)
+                                       v (if (contains? #{:title :description :roam_tags} k)
                                            v
                                            v
-                                           (text/split-page-refs-without-brackets v))]
+                                           (text/split-page-refs-without-brackets v comma?))]
                                    [k v])))
                                    [k v])))
                           (into {}))
                           (into {}))
           macro-properties (filter (fn [x] (= :macro (first x))) properties)
           macro-properties (filter (fn [x] (= :macro (first x))) properties)
@@ -94,23 +106,33 @@
                    {})
                    {})
           properties (->> (remove (fn [x] (= :macro (first x))) properties)
           properties (->> (remove (fn [x] (= :macro (first x))) properties)
                           (into {}))
                           (into {}))
-          properties (if (:roam_alias properties)
-                       (assoc properties :alias (:roam_alias properties))
-                       properties)
           properties (if (seq properties)
           properties (if (seq properties)
                        (cond-> properties
                        (cond-> properties
                          (:roam_key properties)
                          (:roam_key properties)
                          (assoc :key (:roam_key properties)))
                          (assoc :key (:roam_key properties)))
                        properties)
                        properties)
           definition-tags (get-tags-from-definition ast)
           definition-tags (get-tags-from-definition ast)
-          properties (if definition-tags
-                       (update properties :tags (fn [tags]
-                                                  (-> (concat tags definition-tags)
-                                                      distinct)))
-                       properties)
           properties (cond-> properties
           properties (cond-> properties
                        (seq macros)
                        (seq macros)
                        (assoc :macros macros))
                        (assoc :macros macros))
+          alias (->vec-concat (:roam_alias properties) (:alias properties))
+          filetags (if-let [org-file-tags (:filetags properties)]
+                     (->> (string/split org-file-tags ":")
+                          (remove string/blank?)))
+          roam-tags (if-let [org-roam-tags (:roam_tags properties)]
+                      (let [pat #"\"(.*?)\"" ;; note: lazy, capturing group
+                            quoted (map second (re-seq pat org-roam-tags))
+                            rest   (string/replace org-roam-tags pat "")
+                            rest (->> (string/split rest " ")
+                                      (remove string/blank?))]
+                        (concat quoted rest)))
+          tags (->vec-concat roam-tags (:tags properties) definition-tags filetags)
+          properties (assoc properties :tags tags :alias alias)
+          properties (-> properties
+                         (update :roam_alias ->vec)
+                         (update :roam_tags (constantly roam-tags))
+                         (update :filetags (constantly filetags)))
+          properties (medley/filter-kv (fn [k v] (not (empty? v))) properties)
           other-ast (drop-while (fn [[item _pos]] (directive? item)) original-ast)]
           other-ast (drop-while (fn [[item _pos]] (directive? item)) original-ast)]
       (if (seq properties)
       (if (seq properties)
         (cons [["Properties" properties] nil] other-ast)
         (cons [["Properties" properties] nil] other-ast)
@@ -161,6 +183,7 @@
   (let [ast (->> (->edn content
   (let [ast (->> (->edn content
                         (default-config format))
                         (default-config format))
                  (map first))
                  (map first))
+        properties (collect-page-properties ast)
         properties (let [properties (and (seq ast)
         properties (let [properties (and (seq ast)
                                          (= "Properties" (ffirst ast))
                                          (= "Properties" (ffirst ast))
                                          (last (first ast)))]
                                          (last (first ast)))]

+ 44 - 0
src/main/frontend/format/mldoc_test.cljs

@@ -0,0 +1,44 @@
+(ns frontend.format.mldoc-test
+  (:require [frontend.format.mldoc :refer [parse-properties]]
+            [clojure.string :as string]
+            [cljs.test :refer [deftest are is testing]]))
+
+(deftest test-parse-org-properties
+  []
+  (testing "just title"
+    (let [content "#+TITLE:   some title   "
+          props (parse-properties content "org")]
+      (are [x y] (= x y)
+        ;; TODO: should we trim in parse-properties?
+        "some title" (string/trim (:title props)))))
+
+  (testing "filetags"
+    (let [content "
+#+FILETAGS:   :tag1:tag_2:@tag:
+#+ROAM_TAGS:  roamtag
+body"
+          props (parse-properties content "org")]
+      (are [x y] (= x y)
+        (list "@tag" "tag1" "tag_2") (sort (:filetags props))
+        ["roamtag"] (:roam_tags props)
+        (list "@tag" "roamtag" "tag1" "tag_2") (sort (:tags props)))))
+
+  (testing "roam tags"
+    (let [content "
+#+FILETAGS: filetag
+#+ROAM_TAGS: roam1 roam2
+body
+"
+          props (parse-properties content "org")]
+      (are [x y] (= x y)
+        ["roam1" "roam2"] (:roam_tags props)
+        (list "filetag" "roam1" "roam2") (sort (:tags props)))))
+
+  (testing "quoted roam tags"
+    (let [content "
+#+ROAM_TAGS: \"why would\"  you use \"spaces\" xxx
+body
+"
+          props (parse-properties content "org")]
+      ;; TODO maybe need to sort or something
+      (is (= ["why would" "spaces" "you" "use" "xxx"] (:roam_tags props))))))

+ 101 - 260
src/main/frontend/fs.cljs

@@ -1,290 +1,138 @@
 (ns frontend.fs
 (ns frontend.fs
   (:require [frontend.util :as util :refer-macros [profile]]
   (:require [frontend.util :as util :refer-macros [profile]]
             [frontend.config :as config]
             [frontend.config :as config]
-            [frontend.state :as state]
             [clojure.string :as string]
             [clojure.string :as string]
-            [frontend.idb :as idb]
-            [frontend.db :as db]
             [promesa.core :as p]
             [promesa.core :as p]
-            [goog.object :as gobj]
-            [clojure.set :as set]
             [lambdaisland.glogi :as log]
             [lambdaisland.glogi :as log]
-            ["/frontend/utils" :as utils]))
-
-;; We need to cache the file handles in the memory so that
-;; the browser will not keep asking permissions.
-(defonce nfs-file-handles-cache (atom {}))
-
-(defn get-nfs-file-handle
-  [handle-path]
-  (get @nfs-file-handles-cache handle-path))
-
-(defn add-nfs-file-handle!
-  [handle-path handle]
-  (swap! nfs-file-handles-cache assoc handle-path handle))
-
-(defn remove-nfs-file-handle!
-  [handle-path]
-  (swap! nfs-file-handles-cache dissoc handle-path))
+            [frontend.fs.protocol :as protocol]
+            [frontend.fs.nfs :as nfs]
+            [frontend.fs.bfs :as bfs]
+            [frontend.fs.node :as node]
+            [frontend.db :as db]
+            [cljs-bean.core :as bean]
+            [frontend.state :as state]
+            [frontend.encrypt :as encrypt]))
 
 
-;; TODO:
-;; We need to support several platforms:
-;; 1. Chrome native file system API (lighting-fs wip)
-;; 2. IndexedDB (lighting-fs)
-;; 3. NodeJS
-#_(defprotocol Fs
-    (mkdir! [this dir])
-    (readdir! [this dir])
-    (unlink! [this path opts])
-    (rename! [this old-path new-path])
-    (rmdir! [this dir])
-    (read-file [dir path option])
-    (write-file! [dir path content])
-    (stat [dir path]))
+(defonce nfs-record (nfs/->Nfs))
+(defonce bfs-record (bfs/->Bfs))
+(defonce node-record (node/->Node))
 
 
 (defn local-db?
 (defn local-db?
   [dir]
   [dir]
   (and (string? dir)
   (and (string? dir)
        (config/local-db? (subs dir 1))))
        (config/local-db? (subs dir 1))))
 
 
-(defn mkdir
+(defn get-fs
   [dir]
   [dir]
-  (cond
-    (local-db? dir)
-    (let [[root new-dir] (rest (string/split dir "/"))
-          root-handle (str "handle/" root)]
-      (->
-       (p/let [handle (idb/get-item root-handle)
-               _ (when handle (utils/verifyPermission handle true))]
-         (when (and handle new-dir
-                    (not (string/blank? new-dir)))
-           (p/let [handle (.getDirectoryHandle ^js handle new-dir
-                                               #js {:create true})
-                   handle-path (str root-handle "/" new-dir)
-                   _ (idb/set-item! handle-path handle)]
-             (add-nfs-file-handle! handle-path handle)
-             (println "Stored handle: " (str root-handle "/" new-dir)))))
-       (p/catch (fn [error]
-                  (println "mkdir error: " error ", dir: " dir)
-                  (js/console.error error)))))
+  (let [bfs-local? (or (string/starts-with? dir "/local")
+                       (string/starts-with? dir "local"))
+        current-repo (state/get-current-repo)
+        git-repo? (and current-repo
+                       (string/starts-with? current-repo "https://"))]
+    (cond
+      (and (util/electron?) (not bfs-local?) (not git-repo?))
+      node-record
 
 
-    (and dir js/window.pfs)
-    (js/window.pfs.mkdir dir)
+      (local-db? dir)
+      nfs-record
 
 
-    :else
-    (println (str "mkdir " dir " failed"))))
+      :else
+      bfs-record)))
 
 
-(defn readdir
+(defn mkdir!
   [dir]
   [dir]
-  (cond
-    (local-db? dir)
-    (let [prefix (str "handle/" dir)
-          cached-files (keys @nfs-file-handles-cache)]
-      (p/resolved
-       (->> (filter #(string/starts-with? % (str prefix "/")) cached-files)
-            (map (fn [path]
-                   (string/replace path prefix ""))))))
+  (protocol/mkdir! (get-fs dir) dir))
 
 
-    (and dir js/window.pfs)
-    (js/window.pfs.readdir dir)
-
-    :else
-    nil))
+(defn readdir
+  [dir]
+  (protocol/readdir (get-fs dir) dir))
 
 
-(defn unlink
+(defn unlink!
   [path opts]
   [path opts]
-  (cond
-    (local-db? path)
-    (let [[dir basename] (util/get-dir-and-basename path)
-          handle-path (str "handle" path)]
-      (->
-       (p/let [handle (idb/get-item (str "handle" dir))
-               _ (idb/remove-item! handle-path)]
-         (when handle
-           (.removeEntry ^js handle basename))
-         (remove-nfs-file-handle! handle-path))
-       (p/catch (fn [error]
-                  (log/error :unlink/path {:path path
-                                           :error error})))))
+  (protocol/unlink! (get-fs path) path opts))
 
 
-    :else
-    (js/window.pfs.unlink path opts)))
-
-(defn rmdir
-  "Remove the directory recursively."
+(defn rmdir!
+  "Remove the directory recursively.
+   Warning: only run it for browser cache."
   [dir]
   [dir]
-  (cond
-    (local-db? dir)
-    nil
-
-    :else
-    (js/window.workerThread.rimraf dir)))
+  (protocol/rmdir! (get-fs dir) dir))
+
+(defn write-file!
+  [repo dir path content opts]
+  (when content
+    (let [fs-record (get-fs dir)]
+      (p/let [metadata-or-css? (or (string/ends-with? path config/metadata-file)
+                                  (string/ends-with? path config/custom-css-file))
+             content (if metadata-or-css? content (encrypt/encrypt content))]
+       (->
+        (p/let [_ (protocol/write-file! (get-fs dir) repo dir path content opts)]
+          (when (= bfs-record fs-record)
+            (db/set-file-last-modified-at! repo (config/get-file-path repo path) (js/Date.))))
+        (p/catch (fn [error]
+                   (log/error :file/write-failed {:dir dir
+                                                  :path path
+                                                  :error error})
+                   ;; Disable this temporarily
+                   ;; (js/alert "Current file can't be saved! Please copy its content to your local file system and click the refresh button.")
+                   )))))))
 
 
 (defn read-file
 (defn read-file
   ([dir path]
   ([dir path]
-   (read-file dir path (clj->js {:encoding "utf8"})))
-  ([dir path option]
-   (cond
-     (local-db? dir)
-     (let [handle-path (str "handle" dir "/" path)]
-       (p/let [handle (idb/get-item handle-path)
-               local-file (and handle (.getFile handle))]
-         (and local-file (.text local-file))))
-
-     :else
-     (js/window.pfs.readFile (str dir "/" path) option))))
-
-(defn nfs-saved-handler
-  [repo path file]
-  (when-let [last-modified (gobj/get file "lastModified")]
-    ;; TODO: extract
-    (let [path (if (= \/ (first path))
-                 (subs path 1)
-                 path)]
-      (db/set-file-last-modified-at! repo path last-modified))))
-
-(defn write-file
-  ([repo dir path content]
-   (write-file repo dir path content nil))
-  ([repo dir path content {:keys [old-content last-modified-at]}]
-   (->
-    (cond
-      (local-db? dir)
-      (let [parts (string/split path "/")
-            basename (last parts)
-            sub-dir (->> (butlast parts)
-                         (remove string/blank?)
-                         (string/join "/"))
-            sub-dir-handle-path (str "handle/"
-                                     (subs dir 1)
-                                     (if sub-dir
-                                       (str "/" sub-dir)))
-            handle-path (if (= "/" (last sub-dir-handle-path))
-                          (subs sub-dir-handle-path 0 (dec (count sub-dir-handle-path)))
-                          sub-dir-handle-path)
-            basename-handle-path (str handle-path "/" basename)]
-        (p/let [file-handle (idb/get-item basename-handle-path)]
-          (when file-handle
-            (add-nfs-file-handle! basename-handle-path file-handle))
-          (if file-handle
-            (p/let [local-file (.getFile file-handle)
-                    local-content (.text local-file)
-                    local-last-modified-at (gobj/get local-file "lastModified")
-                    current-time (util/time-ms)
-                    new? (> current-time local-last-modified-at)
-                    new-created? (nil? last-modified-at)
-                    not-changed? (= last-modified-at local-last-modified-at)
-                    format (-> (util/get-file-ext path)
-                               (config/get-file-format))
-                    pending-writes (state/get-write-chan-length)]
-              ;; (println {:last-modified-at last-modified-at
-              ;;           :local-last-modified-at local-last-modified-at
-              ;;           :not-changed? not-changed?
-              ;;           :new-created? new-created?
-              ;;           :pending-writes pending-writes
-              ;;           :local-content local-content
-              ;;           :old-content old-content
-              ;;           :new? new?})
-              (if (and local-content old-content new?
-                       (or
-                        (> pending-writes 0)
-                        not-changed?
-                        new-created?))
-                (do
-                  (p/let [_ (utils/verifyPermission file-handle true)
-                          _ (utils/writeFile file-handle content)
-                          file (.getFile file-handle)]
-                    (when file
-                      (nfs-saved-handler repo path file))))
-                (do
-                  (js/alert (str "The file has been modified in your local disk! File path: " path
-                                 ", save your changes and click the refresh button to reload it.")))))
-            ;; create file handle
-            (->
-             (p/let [handle (idb/get-item handle-path)]
-               (if handle
-                 (do
-                   (p/let [_ (utils/verifyPermission handle true)
-                           file-handle (.getFileHandle ^js handle basename #js {:create true})
-                           _ (idb/set-item! basename-handle-path file-handle)
-                           _ (utils/writeFile file-handle content)
-                           file (.getFile file-handle)]
-                     (when file
-                       (nfs-saved-handler repo path file))))
-                 (println "Error: directory handle not exists: " handle-path)))
-             (p/catch (fn [error]
-                        (println "Write local file failed: " {:path path})
-                        (js/console.error error)))))))
-
-      js/window.pfs
-      (js/window.pfs.writeFile (str dir "/" path) content)
-
-      :else
-      nil)
-    (p/catch (fn [error]
-               (log/error :file/write-failed? {:dir dir
-                                               :path path
-                                               :error error})
-               ;; Disable this temporarily
-               ;; (js/alert "Current file can't be saved! Please copy its content to your local file system and click the refresh button.")
-)))))
-
-(defn rename
+   (let [fs (get-fs dir)
+         options (if (= fs bfs-record)
+                   {:encoding "utf8"}
+                   {})]
+     (read-file dir path options)))
+  ([dir path options]
+   (p/chain (protocol/read-file (get-fs dir) dir path options)
+            encrypt/decrypt)))
+
+(defn rename!
   [repo old-path new-path]
   [repo old-path new-path]
   (cond
   (cond
-    (local-db? old-path)
-    ;; create new file
-    ;; delete old file
-    (p/let [[dir basename] (util/get-dir-and-basename old-path)
-            [_ new-basename] (util/get-dir-and-basename new-path)
-            parts (->> (string/split new-path "/")
-                       (remove string/blank?))
-            dir (str "/" (first parts))
-            new-path (->> (rest parts)
-                          (string/join "/"))
-            handle (idb/get-item (str "handle" old-path))
-            file (.getFile handle)
-            content (.text file)
-            _ (write-file repo dir new-path content)]
-      (unlink old-path nil))
+    ; See https://github.com/isomorphic-git/lightning-fs/issues/41
+    (= old-path new-path)
+    (p/resolved nil)
 
 
     :else
     :else
-    (js/window.pfs.rename old-path new-path)))
+    (protocol/rename! (get-fs old-path) repo old-path new-path)))
 
 
 (defn stat
 (defn stat
   [dir path]
   [dir path]
-  (let [append-path (if path
-                      (str "/"
-                           (if (= \/ (first path))
-                             (subs path 1)
-                             path))
-                      "")]
-    (cond
-      (local-db? dir)
-      (if-let [file (get-nfs-file-handle (str "handle/"
-                                              (string/replace-first dir "/" "")
-                                              append-path))]
-        (p/let [file (.getFile file)]
-          (let [get-attr #(gobj/get file %)]
-            {:file/last-modified-at (get-attr "lastModified")
-             :file/size (get-attr "size")
-             :file/type (get-attr "type")}))
-        (p/rejected "File not exists"))
-
-      :else
-      (do
-        (js/window.pfs.stat (str dir append-path))))))
+  (protocol/stat (get-fs dir) dir path))
+
+(defn open-dir
+  [ok-handler]
+  (let [record (if (util/electron?) node-record nfs-record)]
+    (p/let [result (protocol/open-dir record ok-handler)]
+      (if (util/electron?)
+        (let [[dir & paths] (bean/->clj result)]
+          [(:path dir) paths])
+        result))))
+
+(defn get-files
+  [path-or-handle ok-handler]
+  (let [record (if (util/electron?) node-record nfs-record)]
+    (p/let [result (protocol/get-files record path-or-handle ok-handler)]
+      (if (util/electron?)
+        (let [result (bean/->clj result)]
+          (rest result))
+        result))))
+
+(defn watch-dir!
+  [dir]
+  (protocol/watch-dir! node-record dir))
 
 
 (defn mkdir-if-not-exists
 (defn mkdir-if-not-exists
   [dir]
   [dir]
-  (when dir
-    (let [local? (config/local-db? dir)]
-      (when (or local? js/window.pfs)
-        (util/p-handle
-         (stat dir nil)
-         (fn [_stat])
-         (fn [error]
-           (mkdir dir)))))))
+  (->
+   (when dir
+     (util/p-handle
+      (stat dir nil)
+      (fn [_stat])
+      (fn [error]
+        (mkdir! dir))))
+   (p/catch (fn [_error] nil))))
 
 
 (defn create-if-not-exists
 (defn create-if-not-exists
   ([repo dir path]
   ([repo dir path]
@@ -294,11 +142,11 @@
                 path
                 path
                 (str "/" path))]
                 (str "/" path))]
      (->
      (->
-      (p/let [_ (stat dir path)]
+      (p/let [stat (stat dir path)]
         true)
         true)
       (p/catch
       (p/catch
        (fn [_error]
        (fn [_error]
-         (p/let [_ (write-file repo dir path initial-content)]
+         (p/let [_ (write-file! repo dir path initial-content nil)]
            false)))))))
            false)))))))
 
 
 (defn file-exists?
 (defn file-exists?
@@ -307,10 +155,3 @@
    (stat dir path)
    (stat dir path)
    (fn [_stat] true)
    (fn [_stat] true)
    (fn [_e] false)))
    (fn [_e] false)))
-
-(defn check-directory-permission!
-  [repo]
-  (when (config/local-db? repo)
-    (p/let [handle (idb/get-item (str "handle/" repo))]
-      (when handle
-        (utils/verifyPermission handle true)))))

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