瀏覽代碼

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
 
 .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 - && \
     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?
 
-[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).
 
@@ -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)
 
 ## Feature requests
+
 Please go to https://discuss.logseq.com/c/feature-requests/7.
 
 ## How can I use it?
 
-1. Make sure you have registered a [GitHub account](https://github.com/join) and already created a repository (could be an old one). _Currently we only support GitHub, but more sync  options (e.g. Gitlab, Dropbox, Google Drive, WebDAV, etc.) will be added soon._
+1. Make sure you have registered a [GitHub account](https://github.com/join) and already created a repository (could be an old one). _Currently we only support GitHub, but more sync options (e.g. Gitlab, Dropbox, Google Drive, WebDAV, etc.) will be added soon._
 
 2. Visit our website <https://logseq.com/>.
 
@@ -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
 - 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.
 
@@ -68,11 +70,11 @@ The following is for developers and designers who want to build and run Logseq l
 ### 1. Requirements
 
 - [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
 
-``` bash
+```bash
 git clone https://github.com/logseq/logseq
 yarn
 yarn watch
@@ -84,35 +86,75 @@ Open <http://localhost:3001>.
 
 ### 4. Build a release
 
-``` bash
+```bash
 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
 
+Basically it just pre-installs Java, Clojure and NodeJS for your convenience.
+
 ### 1. Fetch sources
 
-``` bash
+```bash
 git clone https://github.com/logseq/logseq
 ```
 
 ### 2. Build Docker image
 
-``` bash
+```bash
 cd logseq
 docker build -t logseq-docker .
 ```
 
 ### 3. Run Docker container
 
-``` bash
+```bash
 docker run -v $(pwd):/home/logseq/logseq -p 3001:3001 -p 9630:9630 -p 8701:8701 --rm -it logseq-docker /bin/bash
 ```
 
 ### 4. Inside the container compile as described above
 
-``` bash
+```bash
 cd logseq
 yarn
 yarn watch
 ```
+
+## Thanks
+
+[![JetBrains](docs/assets/jetbrains.svg)](https://www.jetbrains.com/?from=logseq)

+ 17 - 13
deps.edn

@@ -1,13 +1,14 @@
 {:paths ["src/main"]
  :deps
  {org.clojure/clojure         {:mvn/version "1.10.0"}
+  cheshire/cheshire {:mvn/version "5.10.0"}
   rum/rum                     {:mvn/version "0.12.3"}
   ;; rum                         {:local/root "/home/tienson/codes/source/clj/rum"}
   ;; persistent-sorted-set       {:mvn/version "0.1.2"}
   ;; FIXME: doesn't work on my archlinux laptop (tienson)
   ;; The required namespace "datascript.core" is not available, it was required by "frontend/db.cljs".
   datascript/datascript       {:git/url "https://github.com/tiensonqin/datascript",
-                               :sha "7c2822565d9a114c7d8604c335af89de4640e2e5"}
+                               :sha "efde8d389e6703b6f60ca3538f484a579b0d6de0"}
   ;; datascript                  {:mvn/version "1.0.1"}
   datascript-transit/datascript-transit
   {:mvn/version "0.3.0"
@@ -20,7 +21,9 @@
   cljs-bean/cljs-bean         {:mvn/version "1.5.0"}
   prismatic/dommy             {:mvn/version "1.1.0"}
   org.clojure/core.match      {:mvn/version "1.0.0"}
-  com.andrewmcveigh/cljs-time {:mvn/version "0.5.2"}
+  ;; fork
+  com.andrewmcveigh/cljs-time {:git/url "https://github.com/logseq/cljs-time",
+                               :sha "5704fbf48d3478eedcf24d458c8964b3c2fd59a9"}
   cljs-drag-n-drop/cljs-drag-n-drop
   {:mvn/version "0.1.0"}
   borkdude/sci                {:mvn/version "0.1.1-alpha.6"}
@@ -28,25 +31,26 @@
   hiccups/hiccups             {:mvn/version "0.3.0"}
   tongue/tongue               {:mvn/version "0.2.9"}
   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"}
-  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"}
-                                cider/cider-nrepl           {:mvn/version "0.23.0-SNAPSHOT"}}
+                                cider/cider-nrepl           {:mvn/version "0.25.5"}}
                   :main-opts ["-m" "shadow.cljs.devtools.cli"]}
            :test
            {:extra-paths ["src/test/"]
-            :extra-deps  {org.clojure/clojurescript {:mvn/version "1.10.520"}
+            :extra-deps  {org.clojure/clojurescript {:mvn/version "1.10.764"}
                           org.clojure/test.check {:mvn/version "RELEASE"}}
             :main-opts   ["-m" "shadow.cljs.devtools.cli"]}
-           :runner
-           {:extra-deps
+
+           :test-clj
+           {:extra-paths ["src/test/"]
+            :extra-deps
             {com.cognitect/test-runner
              {:git/url "https://github.com/cognitect-labs/test-runner",
               :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.containsNode = function() {};
 dummy.select = function() {};
+dummy.search = function() {};
+dummy.add = function() {};
+dummy.remove = function() {};
+dummy.closest = function () {};
 dummy.setAttribute = function() {};
+dummy.getAttribute = function() {};
 dummy.font = function() {};
 dummy.measureText = function() {};
 dummy.fillStyle = function() {};
@@ -50,3 +55,18 @@ dummy.values = function() {};
 // Do we really need those?
 dummy.filter = 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 cp = require('child_process')
 const path = require('path')
 const gulp = require('gulp')
 const postcss = require('gulp-postcss')
@@ -62,7 +63,7 @@ const css = {
 
 const common = {
   clean () {
-    return del(outputPath)
+    return del(['./static/**/*', '!./static/yarn.lock', '!./static/node_modules'])
   },
 
   syncResourceFile () {
@@ -74,6 +75,50 @@ const common = {
   }
 }
 
+exports.electron = () => {
+  if (!fs.existsSync(path.join(outputPath, 'node_modules'))) {
+    cp.execSync('yarn', {
+      cwd: outputPath,
+      stdio: 'inherit'
+    })
+  }
+
+  cp.execSync('yarn electron:dev', {
+    cwd: outputPath,
+    stdio: 'inherit'
+  })
+}
+
+exports.electronMaker = async () => {
+  cp.execSync('yarn cljs:electron-release', {
+    stdio: 'inherit'
+  })
+
+  const pkgPath = path.join(outputPath, 'package.json')
+  const pkg = require(pkgPath)
+  const version = fs.readFileSync(path.join(__dirname, 'src/main/frontend/version.cljs'))
+    .toString().match(/[0-9.]{3,}/)[0]
+
+  if (!version) {
+    throw new Error('release version error in src/**/*/version.cljs')
+  }
+
+  pkg.version = version
+  fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2))
+
+  if (!fs.existsSync(path.join(outputPath, 'node_modules'))) {
+    cp.execSync('yarn', {
+      cwd: outputPath,
+      stdio: 'inherit'
+    })
+  }
+
+  cp.execSync('yarn electron:make', {
+    cwd: outputPath,
+    stdio: 'inherit'
+  })
+}
+
 exports.clean = common.clean
 exports.watch = gulp.parallel(common.keepSyncResourceFile, css.watchCSS)
 exports.build = gulp.series(common.clean, common.syncResourceFile, css.buildCSS)

+ 24 - 7
package.json

@@ -2,6 +2,7 @@
     "name": "logseq",
     "version": "0.0.1",
     "private": true,
+    "main": "static/electron.js",
     "devDependencies": {
         "@tailwindcss/ui": "0.7.2",
         "@types/gulp": "^4.0.7",
@@ -18,26 +19,32 @@
         "postcss-cli": "8.3.0",
         "postcss-nested": "^5.0.1",
         "purgecss": "3.0.0",
-        "shadow-cljs": "2.8.81",
+        "shadow-cljs": "2.11.11",
         "stylelint": "^13.8.0",
         "stylelint-config-standard": "^20.0.0",
         "tailwindcss": "2.0.1"
     },
     "scripts": {
         "watch": "run-p gulp:build gulp:watch cljs:watch",
+        "electron-watch": "run-p gulp:build gulp:watch cljs:electron-watch",
         "release": "run-s gulp:build cljs:release",
         "watch-app": "run-p gulp:watch cljs:watch-app",
         "release-app": "run-s gulp:build cljs:release-app",
         "release-publishing": "run-s gulp:build cljs:release-publishing",
         "dev-release-app": "run-s gulp:build cljs:dev-release-app",
+        "dev-electron-app": "gulp electron",
+        "release-electron": "gulp build && gulp electronMaker",
+        "debug-electron": "cd static/ && yarn electron:debug",
         "clean": "gulp clean",
         "test": "run-s cljs:test cljs:run-test",
         "report": "run-s cljs:report",
         "style:lint": "stylelint \"src/**/*.css\" ",
         "gulp:watch": "gulp watch",
         "gulp:build": "cross-env NODE_ENV=production gulp build",
-        "cljs:watch": "clojure -M:cljs watch app publishing",
-        "cljs:release": "clojure -M:cljs release app publishing",
+        "cljs:watch": "clojure -M:cljs watch app publishing electron",
+        "cljs:electron-watch": "clojure -M:cljs watch app electron",
+        "cljs:release": "clojure -M:cljs release app publishing electron",
+        "cljs:electron-release": "clojure -M:cljs release app publishing electron --config-merge '{:asset-path \"./js\"}'",
         "cljs:test": "clojure -A:test compile test",
         "cljs:run-test": "node static/tests.js",
         "cljs:watch-app": "clojure -M:cljs watch app",
@@ -45,21 +52,31 @@
         "cljs:release-publishing": "clojure -M:cljs release publishing",
         "cljs:dev-release-app": "clojure -M:cljs release app --config-merge '{:closure-defines {frontend.config/DEV-RELEASE true}}'",
         "cljs:debug": "clojure -M:cljs release app --debug",
-        "cljs:report": "clojure -M:cljs run shadow.cljs.build-report app report.html"
+        "cljs:report": "clojure -M:cljs run shadow.cljs.build-report app report.html",
+        "cljs:build-electron": "clojure -A:cljs compile app electron"
     },
     "dependencies": {
+        "@kanru/rage-wasm": "^0.2.1",
+        "chokidar": "^3.5.1",
+        "chrono-node": "^2.2.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",
         "ignore": "^5.1.8",
         "jszip": "^3.5.0",
-        "mldoc": "^0.3.0",
+        "mldoc": "^0.5.0",
         "mousetrap": "^1.6.5",
+        "path": "^0.12.7",
         "react": "^17.0.1",
         "react-dom": "^17.0.1",
+        "react-resize-context": "^3.0.0",
         "react-textarea-autosize": "^8.0.1",
         "react-transition-group": "^4.3.0",
+        "url": "^0.11.0",
         "yargs-parser": "^20.2.4"
     }
 }

文件差異過大導致無法顯示
+ 0 - 0
public/index.html


+ 135 - 64
resources/css/common.css

@@ -1,6 +1,6 @@
 :root {
-  --ls-tag-text-opacity: 0.6;
-  --ls-tag-text-hover-opacity: 0.8;
+  --ls-tag-text-opacity: 0.8;
+  --ls-tag-text-hover-opacity: 1;
   --ls-page-text-size: 1em;
   --ls-page-title-size: 36px;
   --ls-font-family: 'Inter';
@@ -36,8 +36,9 @@ html[data-theme=dark] {
   --ls-active-secondary-color: #d0e8e8;
   --ls-block-properties-background-color: #02222a;
   --ls-block-ref-link-text-color: #1a6376;
-  --ls-search-background-color: var(--ls-primary-background-color);
+  --ls-search-background-color: linear-gradient(to right,#021c23 0,#021b21 200px,#002b36 100%);
   --ls-border-color: #0e5263;
+  --ls-secondary-border-color: #126277;
   --ls-guideline-color: #0b4a5a;
   --ls-menu-hover-color: var(--ls-secondary-background-color);
   --ls-primary-text-color: #a4b5b6;
@@ -61,8 +62,8 @@ html[data-theme=dark] {
   --ls-page-blockquote-border-color: var(--ls-border-color);
   --ls-page-inline-code-color: var(--ls-primary-text-color);
   --ls-page-inline-code-bg-color: #01222a;
-  --ls-scrollbar-foreground-color: rgba(255, 255, 255, 0.1);
-  --ls-scrollbar-background-color: rgba(255, 255, 255, 0.05);
+  --ls-scrollbar-foreground-color: rgba(255, 255, 255, 0.05);
+  --ls-scrollbar-background-color: rgba(30, 60, 67, 0.9);
   --ls-scrollbar-thumb-hover-color: rgba(255, 255, 255, 0.2);
   --ls-head-text-color: var(--ls-link-text-color);
   --ls-icon-color: var(--ls-link-text-color);
@@ -80,44 +81,45 @@ html[data-theme=dark] {
 .white-theme,
 html[data-theme=light] {
   --ls-primary-background-color: white;
-  --ls-secondary-background-color: #dee9f2;
-  --ls-tertiary-background-color: #f0f8ff;
-  --ls-quaternary-background-color: #e1f0fe;
+  --ls-secondary-background-color: #f7f6f4;
+  --ls-tertiary-background-color: #f1eee8;
+  --ls-quaternary-background-color: #e8e5de;
   --ls-table-tr-even-background-color: #f4f5f7;
   --ls-active-primary-color: rgb(4, 85, 145);
   --ls-active-secondary-color: #003761;
-  --ls-block-properties-background-color: var(--ls-tertiary-background-color);
+  --ls-block-properties-background-color: #f7f6f4;
   --ls-block-ref-link-text-color: #D8E1E8;
   --ls-search-background-color: var(--ls-primary-background-color);
   --ls-border-color: #ccc;
-  --ls-guideline-color: var(--ls-border-color);
+  --ls-secondary-border-color: #e2e2e2;
+  --ls-guideline-color: rgba(46, 27, 5, 0.08);
   --ls-menu-hover-color: var(--ls-a-chosen-bg);
-  --ls-primary-text-color: #24292e;
+  --ls-primary-text-color: #433F38;
   --ls-secondary-text-color: #161e2e;
-  --ls-title-text-color: #222;
-  --ls-link-text-color: var(--ls-active-primary-color);
-  --ls-link-text-hover-color: var(--ls-active-secondary-color);
-  --ls-link-ref-text-color: var(--ls-link-text-color);
-  --ls-link-ref-text-hover-color: var(--ls-link-text-hover-color);
-  --ls-tag-text-color: var(--ls-link-text-color);
-  --ls-tag-text-hover-color: var(--ls-link-text-hover-color);
+  --ls-title-text-color: var(--ls-primary-text-color);
+  --ls-link-text-color: #106BA3;
+  --ls-link-text-hover-color: #1a537c;
+  --ls-link-ref-text-color: #106BA3;
+  --ls-link-ref-text-hover-color: #1a537c;
+  --ls-tag-text-color: var(--ls-link-ref-text-color);
+  --ls-tag-text-hover-color: var(--ls-link-ref-text-hover-color);
   --ls-slide-background-color: #fff;
-  --ls-block-bullet-border-color: var(--ls-border-color);
-  --ls-block-bullet-color: #394b59;
+  --ls-block-bullet-border-color: #dedede;
+  --ls-block-bullet-color: rgba(67, 63, 56, 0.25);
   --ls-block-highlight-color: #c0e6fd;
   --ls-selection-background-color: #e4f2ff;
-  --ls-page-checkbox-color: var(--ls-active-primary-color);
-  --ls-page-checkbox-border-color: #8c8c8c;
+  --ls-page-checkbox-color: #9dbbd8;
+  --ls-page-checkbox-border-color: var(--ls-page-checkbox-color);
   --ls-page-blockquote-color: var(--ls-primary-text-color);
-  --ls-page-blockquote-bg-color: var(--ls-secondary-background-color);
-  --ls-page-blockquote-border-color: var(--ls-active-primary-color);
-  --ls-page-inline-code-bg-color: var(--ls-secondary-background-color);
+  --ls-page-blockquote-bg-color: #fbfaf8;
+  --ls-page-blockquote-border-color: #799bbc;
+  --ls-page-inline-code-bg-color: #f7f6f4;
   --ls-page-inline-code-color: var(--ls-primary-text-color);
   --ls-scrollbar-foreground-color: rgba(0, 0, 0, 0.1);
   --ls-scrollbar-background-color: rgba(0, 0, 0, 0.05);
   --ls-scrollbar-thumb-hover-color: rgba(0, 0, 0, 0.2);
   --ls-head-text-color: var(--ls-link-text-color);
-  --ls-icon-color: #6b7280;
+  --ls-icon-color: #c1bdb7;
   --ls-search-icon-color: var(--ls-icon-color);
   --ls-a-chosen-bg: #f4f5f7;
   --ls-right-sidebar-code-bg-color: var(--ls-secondary-background-color);
@@ -260,7 +262,7 @@ summary {
 }
 
 iframe {
-  /* width: 100%; */
+  width: 100%;
   margin: 1rem 0;
 }
 
@@ -443,6 +445,10 @@ li p:last-child,
   opacity: 0.6;
 }
 
+.opacity-30 {
+    opacity: 0.3;
+}
+
 .opacity-70 {
   opacity: 0.7;
 }
@@ -540,22 +546,6 @@ li p:last-child,
   overflow-y: auto;
 }
 
-.marker-switch {
-  font-size: 85%;
-  margin-right: 6px;
-  margin-left: 2px;
-  border-radius: 3px;
-  font-weight: 500;
-  display: inline-block;
-  text-align: center;
-  width: 16px;
-  height: 18px;
-  opacity: 0.5;
-  padding: 0 2px 0 2px;
-  border: 1px solid;
-  line-height: 1.3;
-}
-
 .heading-bg {
   border-radius: 50%;
   width: 12px;
@@ -565,19 +555,11 @@ li p:last-child,
 /** endregion **/
 
 /* 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 {
   margin-bottom: 1.5rem;
   color: var(--ls-title-text-color, #222);
   font-size: var(--ls-page-title-size, 36px);
+  font-weight: 700;
 }
 
 .block-highlight,
@@ -587,11 +569,6 @@ h1.title {
   padding: -1px;
 }
 
-.content img {
-  margin-top: 0.5rem;
-  margin-bottom: 0.5rem;
-}
-
 span.timestamp {
   margin: 0 0.25rem;
 }
@@ -648,10 +625,6 @@ a.login:hover {
   color: var(--ls-link-text-hover-color, #000);
 }
 
-a.marker-switch:hover {
-  opacity: 1;
-}
-
 a.tooltip-priority {
   display: contents;
   position: absolute;
@@ -684,12 +657,17 @@ img.small {
 }
 
 a.tag {
-  opacity: var(--ls-tag-text-opacity, 0.6);
-  color: var(--ls-tag-text-color, #045591);
+    font-size: 13px;
+    text-align: center;
+    text-decoration: none;
+    display: inline-block;
+    cursor: pointer;
+    color: var(--ls-tag-text-color, #045591);
+    opacity: var(--ls-tag-text-opacity, 0.8);
 }
 
 a.tag:hover {
-  opacity: var(--ls-tag-text-hover-opacity, 0.8);
+  opacity: var(--ls-tag-text-hover-opacity, 1);
   color: var(--ls-tag-text-hover-color, #045591);
 }
 
@@ -720,3 +698,96 @@ hr {
   margin: 2rem 0;
   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-weight: 100;
   font-display: swap;
-  src: url("/static/fonts/inter/Inter-Thin.woff2?v=3.15") format("woff2"),
-       url("/static/fonts/inter/Inter-Thin.woff?v=3.15") format("woff");
+  src: url("../fonts/inter/Inter-Thin.woff2?v=3.15") format("woff2"),
+       url("../fonts/inter/Inter-Thin.woff?v=3.15") format("woff");
 }
 @font-face {
   font-family: 'Inter';
   font-style:  italic;
   font-weight: 100;
   font-display: swap;
-  src: url("/static/fonts/inter/Inter-ThinItalic.woff2?v=3.15") format("woff2"),
-       url("/static/fonts/inter/Inter-ThinItalic.woff?v=3.15") format("woff");
+  src: url("../fonts/inter/Inter-ThinItalic.woff2?v=3.15") format("woff2"),
+       url("../fonts/inter/Inter-ThinItalic.woff?v=3.15") format("woff");
 }
 
 @font-face {
@@ -20,16 +20,16 @@
   font-style:  normal;
   font-weight: 200;
   font-display: swap;
-  src: url("/static/fonts/inter/Inter-ExtraLight.woff2?v=3.15") format("woff2"),
-       url("/static/fonts/inter/Inter-ExtraLight.woff?v=3.15") format("woff");
+  src: url("../fonts/inter/Inter-ExtraLight.woff2?v=3.15") format("woff2"),
+       url("../fonts/inter/Inter-ExtraLight.woff?v=3.15") format("woff");
 }
 @font-face {
   font-family: 'Inter';
   font-style:  italic;
   font-weight: 200;
   font-display: swap;
-  src: url("/static/fonts/inter/Inter-ExtraLightItalic.woff2?v=3.15") format("woff2"),
-       url("/static/fonts/inter/Inter-ExtraLightItalic.woff?v=3.15") format("woff");
+  src: url("../fonts/inter/Inter-ExtraLightItalic.woff2?v=3.15") format("woff2"),
+       url("../fonts/inter/Inter-ExtraLightItalic.woff?v=3.15") format("woff");
 }
 
 @font-face {
@@ -37,16 +37,16 @@
   font-style:  normal;
   font-weight: 300;
   font-display: swap;
-  src: url("/static/fonts/inter/Inter-Light.woff2?v=3.15") format("woff2"),
-       url("/static/fonts/inter/Inter-Light.woff?v=3.15") format("woff");
+  src: url("../fonts/inter/Inter-Light.woff2?v=3.15") format("woff2"),
+       url("../fonts/inter/Inter-Light.woff?v=3.15") format("woff");
 }
 @font-face {
   font-family: 'Inter';
   font-style:  italic;
   font-weight: 300;
   font-display: swap;
-  src: url("/static/fonts/inter/Inter-LightItalic.woff2?v=3.15") format("woff2"),
-       url("/static/fonts/inter/Inter-LightItalic.woff?v=3.15") format("woff");
+  src: url("../fonts/inter/Inter-LightItalic.woff2?v=3.15") format("woff2"),
+       url("../fonts/inter/Inter-LightItalic.woff?v=3.15") format("woff");
 }
 
 @font-face {
@@ -54,16 +54,16 @@
   font-style:  normal;
   font-weight: 400;
   font-display: swap;
-  src: url("/static/fonts/inter/Inter-Regular.woff2?v=3.15") format("woff2"),
-       url("/static/fonts/inter/Inter-Regular.woff?v=3.15") format("woff");
+  src: url("../fonts/inter/Inter-Regular.woff2?v=3.15") format("woff2"),
+       url("../fonts/inter/Inter-Regular.woff?v=3.15") format("woff");
 }
 @font-face {
   font-family: 'Inter';
   font-style:  italic;
   font-weight: 400;
   font-display: swap;
-  src: url("/static/fonts/inter/Inter-Italic.woff2?v=3.15") format("woff2"),
-       url("/static/fonts/inter/Inter-Italic.woff?v=3.15") format("woff");
+  src: url("../fonts/inter/Inter-Italic.woff2?v=3.15") format("woff2"),
+       url("../fonts/inter/Inter-Italic.woff?v=3.15") format("woff");
 }
 
 @font-face {
@@ -71,16 +71,16 @@
   font-style:  normal;
   font-weight: 500;
   font-display: swap;
-  src: url("/static/fonts/inter/Inter-Medium.woff2?v=3.15") format("woff2"),
-       url("/static/fonts/inter/Inter-Medium.woff?v=3.15") format("woff");
+  src: url("../fonts/inter/Inter-Medium.woff2?v=3.15") format("woff2"),
+       url("../fonts/inter/Inter-Medium.woff?v=3.15") format("woff");
 }
 @font-face {
   font-family: 'Inter';
   font-style:  italic;
   font-weight: 500;
   font-display: swap;
-  src: url("/static/fonts/inter/Inter-MediumItalic.woff2?v=3.15") format("woff2"),
-       url("/static/fonts/inter/Inter-MediumItalic.woff?v=3.15") format("woff");
+  src: url("../fonts/inter/Inter-MediumItalic.woff2?v=3.15") format("woff2"),
+       url("../fonts/inter/Inter-MediumItalic.woff?v=3.15") format("woff");
 }
 
 @font-face {
@@ -88,16 +88,16 @@
   font-style:  normal;
   font-weight: 600;
   font-display: swap;
-  src: url("/static/fonts/inter/Inter-SemiBold.woff2?v=3.15") format("woff2"),
-       url("/static/fonts/inter/Inter-SemiBold.woff?v=3.15") format("woff");
+  src: url("../fonts/inter/Inter-SemiBold.woff2?v=3.15") format("woff2"),
+       url("../fonts/inter/Inter-SemiBold.woff?v=3.15") format("woff");
 }
 @font-face {
   font-family: 'Inter';
   font-style:  italic;
   font-weight: 600;
   font-display: swap;
-  src: url("/static/fonts/inter/Inter-SemiBoldItalic.woff2?v=3.15") format("woff2"),
-       url("/static/fonts/inter/Inter-SemiBoldItalic.woff?v=3.15") format("woff");
+  src: url("../fonts/inter/Inter-SemiBoldItalic.woff2?v=3.15") format("woff2"),
+       url("../fonts/inter/Inter-SemiBoldItalic.woff?v=3.15") format("woff");
 }
 
 @font-face {
@@ -105,16 +105,16 @@
   font-style:  normal;
   font-weight: 700;
   font-display: swap;
-  src: url("/static/fonts/inter/Inter-Bold.woff2?v=3.15") format("woff2"),
-       url("/static/fonts/inter/Inter-Bold.woff?v=3.15") format("woff");
+  src: url("../fonts/inter/Inter-Bold.woff2?v=3.15") format("woff2"),
+       url("../fonts/inter/Inter-Bold.woff?v=3.15") format("woff");
 }
 @font-face {
   font-family: 'Inter';
   font-style:  italic;
   font-weight: 700;
   font-display: swap;
-  src: url("/static/fonts/inter/Inter-BoldItalic.woff2?v=3.15") format("woff2"),
-       url("/static/fonts/inter/Inter-BoldItalic.woff?v=3.15") format("woff");
+  src: url("../fonts/inter/Inter-BoldItalic.woff2?v=3.15") format("woff2"),
+       url("../fonts/inter/Inter-BoldItalic.woff?v=3.15") format("woff");
 }
 
 @font-face {
@@ -122,16 +122,16 @@
   font-style:  normal;
   font-weight: 800;
   font-display: swap;
-  src: url("/static/fonts/inter/Inter-ExtraBold.woff2?v=3.15") format("woff2"),
-       url("/static/fonts/inter/Inter-ExtraBold.woff?v=3.15") format("woff");
+  src: url("../fonts/inter/Inter-ExtraBold.woff2?v=3.15") format("woff2"),
+       url("../fonts/inter/Inter-ExtraBold.woff?v=3.15") format("woff");
 }
 @font-face {
   font-family: 'Inter';
   font-style:  italic;
   font-weight: 800;
   font-display: swap;
-  src: url("/static/fonts/inter/Inter-ExtraBoldItalic.woff2?v=3.15") format("woff2"),
-       url("/static/fonts/inter/Inter-ExtraBoldItalic.woff?v=3.15") format("woff");
+  src: url("../fonts/inter/Inter-ExtraBoldItalic.woff2?v=3.15") format("woff2"),
+       url("../fonts/inter/Inter-ExtraBoldItalic.woff?v=3.15") format("woff");
 }
 
 @font-face {
@@ -139,16 +139,16 @@
   font-style:  normal;
   font-weight: 900;
   font-display: swap;
-  src: url("/static/fonts/inter/Inter-Black.woff2?v=3.15") format("woff2"),
-       url("/static/fonts/inter/Inter-Black.woff?v=3.15") format("woff");
+  src: url("../fonts/inter/Inter-Black.woff2?v=3.15") format("woff2"),
+       url("../fonts/inter/Inter-Black.woff?v=3.15") format("woff");
 }
 @font-face {
   font-family: 'Inter';
   font-style:  italic;
   font-weight: 900;
   font-display: swap;
-  src: url("/static/fonts/inter/Inter-BlackItalic.woff2?v=3.15") format("woff2"),
-       url("/static/fonts/inter/Inter-BlackItalic.woff?v=3.15") format("woff");
+  src: url("../fonts/inter/Inter-BlackItalic.woff2?v=3.15") format("woff2"),
+       url("../fonts/inter/Inter-BlackItalic.woff?v=3.15") format("woff");
 }
 
 /* -------------------------------------------------------
@@ -166,7 +166,7 @@ Usage:
   font-display: swap;
   font-style: normal;
   font-named-instance: 'Regular';
-  src: url("/static/fonts/inter/Inter-roman.var.woff2?v=3.15") format("woff2");
+  src: url("../fonts/inter/Inter-roman.var.woff2?v=3.15") format("woff2");
 }
 @font-face {
   font-family: 'Inter var';
@@ -174,7 +174,7 @@ Usage:
   font-display: swap;
   font-style: italic;
   font-named-instance: 'Italic';
-  src: url("/static/fonts/inter/Inter-italic.var.woff2?v=3.15") format("woff2");
+  src: url("../fonts/inter/Inter-italic.var.woff2?v=3.15") format("woff2");
 }
 
 
@@ -196,5 +196,5 @@ explicitly, e.g.
   font-weight: 100 900;
   font-display: swap;
   font-style: oblique 0deg 10deg;
-  src: url("/static/fonts/inter/Inter.var.woff2?v=3.15") format("woff2");
+  src: url("../fonts/inter/Inter.var.woff2?v=3.15") format("woff2");
 }

+ 2 - 2
resources/css/style.css

@@ -9,6 +9,6 @@
 @import "./table.css";
 @import "./datepicker.css";
 @import "./highlight.css";
-@import "../../static/css/tailwind.core.css"; /* Build by gulp. Check `_buildTailwind` for more detail */
+@import "./tailwind.core.css"; /* Build by gulp. Check `_buildTailwind` for more detail */
 @import "./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(
   // Batched optimization
-  "/static/js/lightning-fs.min.js?v=0.0.2.3",
-  "https://cdn.jsdelivr.net/npm/isomorphic-git@1.7.4/index.umd.min.js",
-  "https://cdn.jsdelivr.net/npm/[email protected]/http/web/index.umd.js",
+  "./lightning-fs.min.js?v=0.0.2.3",
+  "./isomorphic-git/1.7.4/index.umd.min.js",
+  "./isomorphic-git/1.7.4/http-web-index.umd.js",
   // Fixed a bug
-  "/static/js/magic_portal.js"
+  "./magic_portal.js"
 );
 
 const detect = () => {

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

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

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

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

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

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

+ 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.state :as state]
             [frontend.search :as search]
+            [frontend.config :as config]
             [clojure.string :as string]
             [goog.dom :as gdom]
             [goog.object :as gobj]
@@ -123,8 +124,18 @@
                   [:editor/search-template]]]
      ;; same as link
      ["Image Link" link-steps]
-     (when (state/logged?)
+     (cond
+       (and (util/electron?) (config/local-db? (state/get-current-repo)))
+
+       ["Upload an asset (image, pdf, docx, etc.)" [[:editor/click-hidden-file-input :id]]]
+
+       (state/logged?)
        ["Upload an image" [[:editor/click-hidden-file-input :id]]])
+
+     (when (util/zh-CN-supported?)
+       ["Embed Bilibili Video" [[:editor/input "{{bilibili }}" {:last-pattern slash
+                                                                :backward-pos 2}]]])
+
      ["Embed Youtube Video" [[:editor/input "{{youtube }}" {:last-pattern slash
                                                             :backward-pos 2}]]]
      ["Html Inline " (->inline "html")]
@@ -253,7 +264,27 @@
     (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 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!
   [id value selected

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

@@ -2,6 +2,8 @@
   (:refer-clojure :exclude [range])
   (:require [frontend.config :as config]
             [cljs.core.match :refer-macros [match]]
+            [promesa.core :as p]
+            [frontend.fs :as fs]
             [clojure.string :as string]
             [frontend.util :as util]
             [rum.core :as rum]
@@ -46,17 +48,23 @@
             [frontend.commands :as commands]
             [lambdaisland.glogi :as log]
             [frontend.context.i18n :as i18n]
+            [frontend.template :as template]
             [frontend.filtering :as filtering]))
 
+;; TODO: remove rum/with-context because it'll make reactive queries not working
+
 (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
 (defonce *block-children
@@ -155,17 +163,93 @@
                 parts (remove #(string/blank? %) 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: 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
   [[[kind] [duration] n]]
@@ -246,30 +330,39 @@
   [{:keys [html-export? label children contents-page?] :as config} page]
   (when-let [page-name (:page/name page)]
     (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 (if (date/valid-journal-title? original-page-name)
                                (string/capitalize original-page-name)
                                original-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)
           href (if html-export?
                  (util/encode-str page)
-                 (rfe/href :page {:name source-page-name}))]
+                 (rfe/href :page {:name redirect-page-name}))]
       [:a.page-ref
-       {:href href
+       {:data-ref page-name
+        :href href
         :on-click (fn [e]
                     (util/stop e)
                     (if (gobj/get e "shiftKey")
-                      (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/get-current-repo)
                          (:db/id page-entity)
                          :page
                          {:page page-entity}))
                       (route-handler/redirect! {:to :page
-                                                :path-params {:name source-page-name}}))
+                                                :path-params {:name redirect-page-name}}))
                     (when (and contents-page?
                                (state/get-left-sidebar-open?))
                       (ui-handler/close-left-sidebar!)))}
@@ -287,6 +380,12 @@
            label
            original-page-name))])))
 
+(rum/defc asset-reference
+  [title path]
+  (let [repo-path (config/get-repo-dir (state/get-current-repo))
+        full-path (.. util/node-path (join repo-path (config/get-local-asset-absolute-path path)))]
+    [:a.asset-ref {:target "_blank" :href full-path} (or title path)]))
+
 (rum/defc page-reference < rum/reactive
   [html-export? s config label]
   (let [show-brackets? (state/show-brackets?)
@@ -296,7 +395,7 @@
      (when (and (or show-brackets? nested-link?)
                 (not html-export?)
                 (not contents-page?))
-       [:span.text-gray-500 "[["])
+       [:span.text-gray-500.bracket "[["])
      (if (string/ends-with? s ".excalidraw")
        [:a.page-ref
         {:on-click (fn [e]
@@ -312,7 +411,7 @@
      (when (and (or show-brackets? nested-link?)
                 (not html-export?)
                 (not contents-page?))
-       [:span.text-gray-500 "]]"])]))
+       [:span.text-gray-500.bracket "]]"])]))
 
 (defn- latex-environment-content
   [name option content]
@@ -337,7 +436,6 @@
 (rum/defc page-embed < rum/reactive db-mixins/query
   [config 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)]
     [:div.color-level.embed.embed-page.bg-base-2
      {:class (if (:sidebar? config) "in-sidebar")}
@@ -373,14 +471,15 @@
     (util/format "{{{%s %s}}}" name (string/join ", " arguments))
     (util/format "{{{%s}}}" name)))
 
-(defn block-reference
+(declare block-content)
+(rum/defc block-reference < rum/reactive
   [config id]
   (when-not (string/blank? id)
     (let [block (and (util/uuid-string? id)
                      (db/pull-block (uuid id)))]
       (if block
         [:span
-         [:a
+         [:div.block-ref-wrap
           {:on-click (fn [e]
                        (util/stop e)
                        (if (gobj/get e "shiftKey")
@@ -389,12 +488,17 @@
                           (:db/id block)
                           :block-ref
                           {:block block})
-                         (route-handler/redirect! {:to :page
+                         (route-handler/redirect! {:to          :page
                                                    :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"}
          (util/format "((%s))" id)]))))
 
@@ -460,17 +564,18 @@
     (->elem :sub (map-inline config l))
     ["Tag" 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)]
       [:span.warning.mr-1 {:title "Invalid tag, tags only accept alphanumeric characters, \"-\", \"_\", \"@\" and \"%\"."}
        (str "#" s)])
@@ -516,26 +621,35 @@
     (nested-link config html-export? 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)))]
       (match url
         ["Search" s]
         (cond
+          (string/blank? s)
+          [:span.warning {:title "Invalid link"} full_text]
+
           ;; image
           (some (fn [fmt] (re-find (re-pattern (str "(?i)\\." fmt)) s)) img-formats)
-          (image-link config url s label)
+          (image-link config url s label metadata full_text)
 
           (= \# (first s))
-          (->elem :a {:href (str "#" (mldoc/anchorLink (subs s 1)))} (map-inline config label))
+          (->elem :a {:on-click #(route-handler/jump-to-anchor! (mldoc/anchorLink (subs s 1)))} (subs s 1))
+
           ;; FIXME: same headline, see more https://orgmode.org/manual/Internal-Links.html
           (and (= \* (first s))
                (not= \* (last s)))
-          (->elem :a {:href (str "#" (mldoc/anchorLink (subs s 1)))} (map-inline config label))
+          (->elem :a {:on-click #(route-handler/jump-to-anchor! (mldoc/anchorLink (subs s 1)))} (subs s 1))
 
           (re-find #"(?i)^http[s]?://" s)
-          (->elem :a {:href s}
+          (->elem :a {:href s
+                      :data-href s
+                      :target "_blank"}
                   (map-inline config label))
 
+          (and (util/electron?) (config/local-asset? s))
+          (asset-reference (second (first label)) s)
+
           :else
           (page-reference html-export? s config label))
 
@@ -555,7 +669,7 @@
 
             (= protocol "file")
             (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)
                     page (if (string/blank? label-text)
                            {:page/name (db/get-file-page (string/replace href "file:" ""))}
@@ -571,14 +685,16 @@
                   (->elem
                    :a
                    (cond->
-                    {:href href}
+                    {:href      (str "file://" href)
+                     :data-href href
+                     :target    "_blank"}
                      title
                      (assoc :title title))
                    (map-inline config label)))))
 
             ;; image
             (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
             (->elem
@@ -645,7 +761,7 @@
       [:sup.fn
        [:a {:id (str "fnr." encode-name)
             :class "footref"
-            :href (str "#fn." encode-name)}
+            :on-click #(route-handler/jump-to-anchor! (str "fn." encode-name))}
         name]])
 
     ["Macro" options]
@@ -660,38 +776,59 @@
       (cond
         (= name "query")
         [:div.dsl-query
-         (let [query (string/join "," arguments)]
+         (let [query (string/join ", " arguments)]
            (custom-query (assoc config :dsl-query? true)
                          {:title [:code.p-1 (str "Query: " query)]
                           :query query}))]
 
         (= name "youtube")
         (let [url (first arguments)]
-          (when-let [youtube-id (cond
-                                  (string/starts-with? url "https://youtu.be/")
-                                  (string/replace url "https://youtu.be/" "")
-
-                                  (string? url)
-                                  url
-
-                                  :else
-                                  nil)]
-            (when-not (string/blank? youtube-id)
+          (let [YouTube-regex #"^((?:https?:)?//)?((?:www|m).)?((?:youtube.com|youtu.be))(/(?:[\w-]+\?v=|embed/|v/)?)([\w-]+)(\S+)?$"]
+            (when-let [youtube-id (cond
+                                    (== 11 (count url)) url
+                                    :else
+                                    (nth (re-find YouTube-regex url) 5))]
+              (when-not (string/blank? youtube-id)
+                (let [width (min (- (util/get-width) 96)
+                                 560)
+                      height (int (* width (/ 315 560)))]
+                  [:iframe
+                   {:allow-full-screen "allowfullscreen"
+                    :allow
+                    "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
+                    :frame-border "0"
+                    :src (str "https://www.youtube.com/embed/" youtube-id)
+                    :height height
+                    :width width}])))))
+
+        ;; TODO: support fullscreen mode, maybe we need a fullscreen dialog?
+        (= name "bilibili")
+        (let [url (first arguments)
+              id-regex #"https?://www\.bilibili\.com/video/([\w\W]+)"]
+          (when-let [id (cond
+                          (<= (count url) 15) url
+                          :else
+                          (last (re-find id-regex url)))]
+            (when-not (string/blank? id)
               (let [width (min (- (util/get-width) 96)
                                560)
                     height (int (* width (/ 315 560)))]
                 [:iframe
-                 {:allow-full-screen "allowfullscreen"
-                  :allow
-                  "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
-                  :frame-border "0"
-                  :src (str "https://www.youtube.com/embed/" youtube-id)
-                  :height height
-                  :width width}]))))
+                 {:allowfullscreen true
+                  :framespacing "0"
+                  :frameborder "no"
+                  :border "0"
+                  :scrolling "no"
+                  :src (str "https://player.bilibili.com/player.html?bvid=" id "&high_quality=1")
+                  :width width
+                  :height (max 500 height)}]))))
 
         (= name "embed")
         (let [a (first arguments)]
           (cond
+            (nil? a) ; empty embed
+            nil
+
             (and (string/starts-with? a "[[")
                  (string/ends-with? a "]]"))
             (let [page-name (-> (string/replace a "[[" "")
@@ -729,11 +866,15 @@
                                (get (state/get-macros) (keyword name)))
                 macro-content (if (and (seq arguments) macro-content)
                                 (block/macro-subs macro-content arguments)
-                                macro-content)]
+                                macro-content)
+                macro-content (when macro-content
+                                (template/resolve-dynamic-template! macro-content))]
             (render-macro config name arguments macro-content format))
 
           (when-let [macro-txt (macro->text name arguments)]
-            (let [format (get-in config [:block :block/format] :markdown)]
+            (let [macro-txt (when macro-txt
+                              (template/resolve-dynamic-template! macro-txt))
+                  format (get-in config [:block :block/format] :markdown)]
               (render-macro config name arguments macro-txt format))))))
 
     :else
@@ -800,13 +941,17 @@
      [:a (if (not dummy?)
            {:href (rfe/href :page {:name uuid})
             :on-click (fn [e]
-                        (.preventDefault e)
-                        (when (gobj/get e "shiftKey")
-                          (state/sidebar-add-block!
-                           (state/get-current-repo)
-                           (:db/id block)
-                           :block
-                           block)))})
+                        (if (gobj/get e "shiftKey")
+                          (do
+                            (state/sidebar-add-block!
+                             (state/get-current-repo)
+                             (:db/id block)
+                             :block
+                             block)
+                            (util/stop e))
+                          (when (:embed? config)
+                            (route-handler/redirect! {:to :page
+                                                      :path-params {:name (str uuid)}}))))})
       [:span.bullet-container.cursor
        {:id (str "dot-" uuid)
         :draggable true
@@ -948,16 +1093,18 @@
     (->elem
      :span
      {: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))))
 
-(defn build-block-part
+(declare block-content)
+
+(defn build-block-title
   [{:keys [slide?] :as config} {:block/keys [uuid title tags marker level priority anchor meta format content pre-block? dummy? block-refs-count page properties]
                                 :as t}]
   (let [config (assoc config :block t)
@@ -991,25 +1138,17 @@
             {:style {:background-color bg-color
                      :padding-left 6
                      :padding-right 6
-                     :color "#FFFFFF"}}))
+                     :color "#FFFFFF"}
+             :class "with-bg-color"}))
          (remove-nils
           (concat
            [(when-not slide? checkbox)
             (when-not slide? marker-switch)
             marker-cp
             priority]
-           (cond
-             dummy?
-             [[:span.opacity-50 "Click here to start writing"]]
-
-             ;; empty item
-             (and contents? (or
-                             (empty? title)
-                             (= title [["Plain" "[[]]"]])))
-             [[:span.opacity-50 "Click here to add a page, e.g. [[favorite-page]]"]]
-
-             :else
-             (map-inline config title))
+           (if title
+             (map-inline config title)
+             [[:span.opacity-50 "Click here to start writing"]])
            [tags])))))))
 
 (defn dnd-same-block?
@@ -1051,12 +1190,11 @@
                       [:span (t :page/edit-properties-placeholder)]
                       (markup-elements-cp (assoc config :block/format format) ast))]]
       (if slide?
-        [:div [:h1 (:page-name config)]
-         block-cp]
+        [:div [:h1 (:page-name config)]]
         block-cp))))
 
 (rum/defc properties-cp
-  [block]
+  [config block]
   (let [properties (apply dissoc (:block/properties block) text/hidden-properties)]
     (when (seq properties)
       [:div.blocks-properties.text-sm.opacity-80.my-1.p-2
@@ -1065,7 +1203,16 @@
          [:div.my-1
           [:b k]
           [: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/local false ::show?)
@@ -1102,7 +1249,7 @@
           (datetime-comp/date-picker nil nil ts)]))]))
 
 (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?)
         attrs {:blockid       (str uuid)
                ;; 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))
                                             properties-hidden? (text/properties-hidden? properties)
                                             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!
                                          edit-input-id
                                          content
@@ -1155,7 +1303,8 @@
 
       (if pre-block?
         (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?))
         (dnd-separator block 0 -4 false true))
@@ -1170,19 +1319,20 @@
                  (let [hidden? (text/properties-hidden? properties)]
                    (not hidden?))
                  (not (:slide? config)))
-        (properties-cp block))
+        (properties-cp config block))
 
       (when (and (not pre-block?) (seq body))
-        [:div.block-body {:style {:display (if collapsed? "none" "")}}
-         ;; TODO: consistent id instead of the idx (since it could be changed later)
-         (let [body (block/trim-break-lines! (:block/body block))]
-           (for [[idx child] (medley/indexed body)]
-             (when-let [block (markup-element-cp config child)]
-               (rum/with-key (block-child block)
-                 (str uuid "-" idx)))))])]
+        (do
+          [:div.block-body {:style {:display (if (and collapsed? (seq title)) "none" "")}}
+          ;; TODO: consistent id instead of the idx (since it could be changed later)
+          (let [body (block/trim-break-lines! (:block/body block))]
+            (for [[idx child] (medley/indexed body)]
+              (when-let [block (markup-element-cp config child)]
+                (rum/with-key (block-child block)
+                  (str uuid "-" idx)))))]))]
      (when (and block-refs-count (> block-refs-count 0))
        [:div
-        [:a.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"
           :style {:margin-top -1}
           :on-click (fn []
@@ -1246,9 +1396,9 @@
        (not @*dragging?)))
 
 (defn block-parents
-  ([repo block-id format]
-   (block-parents repo block-id format true))
-  ([repo block-id format show-page?]
+  ([config repo block-id format]
+   (block-parents config repo block-id format true))
+  ([config repo block-id format show-page?]
    (let [parents (db/get-block-parents repo block-id 3)
          page (db/get-block-page repo block-id)
          page-name (:page/name page)]
@@ -1266,16 +1416,10 @@
                           [:span.mx-2.opacity-50 "➤"])
 
                         (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)]
                             (reset! parents-atom parents)
                             (when (seq parents)
@@ -1295,7 +1439,7 @@
    :should-update (fn [old-state new-state]
                     (not= (:block/content (second (:rum/args old-state)))
                           (:block/content (second (:rum/args new-state)))))}
-  [config {:block/keys [uuid title level body meta content dummy? page format repo children collapsed? pre-block? idx properties] :as block}]
+  [config {:block/keys [uuid title level body meta content dummy? page format repo children collapsed? pre-block? idx properties refs-with-children] :as block}]
   (let [ref? (boolean (:ref? config))
         breadcrumb-show? (:breadcrumb-show? config)
         sidebar? (boolean (:sidebar? config))
@@ -1344,7 +1488,8 @@
                           (reset! *dragging-block nil)
                           (editor-handler/unhighlight-block!))
                :on-mouse-move (fn [e]
-                                (when (non-dragging? e)
+                                (when (and (non-dragging? e)
+                                           (not @*resizing-image?))
                                   (state/into-selection-mode!)))
                :on-mouse-down (fn [e]
                                 (when (and
@@ -1376,10 +1521,20 @@
                                (when doc-mode?
                                  (when-let [parent (gdom/getElement block-id)]
                                    (when-let [node (.querySelector parent ".bullet-container")]
-                                     (d/add-class! node "hide-inner-bullet")))))}]
+                                     (d/add-class! node "hide-inner-bullet")))))}
+        data-refs (let [refs (model/get-page-names-by-ids
+                              (->> (map :db/id refs-with-children)
+                                   (remove nil?)))]
+                    (text/build-data-value refs))
+        data-refs-self (let [refs  (model/get-page-names-by-ids
+                                    (->> (map :db/id (:block/ref-pages block))
+                                         (remove nil?)))]
+                         (text/build-data-value refs))]
     [:div.ls-block.flex.flex-col.rounded-sm
      (cond->
       {:id block-id
+       :data-refs data-refs
+       :data-refs-self data-refs-self
        :style {:position "relative"}
        :class (str uuid
                    (when dummy? " dummy")
@@ -1393,7 +1548,7 @@
        (merge attrs))
 
      (when (and ref? breadcrumb-show?)
-       (when-let [comp (block-parents repo uuid format false)]
+       (when-let [comp (block-parents config repo uuid format false)]
          [:div.my-2.opacity-50.ml-4 comp]))
 
      (dnd-separator-wrapper block slide? (zero? idx))
@@ -1405,7 +1560,7 @@
       (block-content-or-editor config block edit-input-id block-id slide?)]
 
      (when (seq children)
-       [:div.block-children {:style {:margin-left (if doc-mode? 12 22)
+       [:div.block-children {:style {:margin-left (if doc-mode? 12 21)
                                      :display (if collapsed? "none" "")}}
         (for [child children]
           (when (map? child)
@@ -1620,9 +1775,11 @@
             (and (seq result)
                  (or only-blocks? blocks-grouped-by-page?))
             (->hiccup result (cond-> (assoc config
-                                            ;; :editor-box editor/box
                                             :custom-query? true
-                                            :group-by-page? blocks-grouped-by-page?)
+                                            ;; :breadcrumb-show? true
+                                            :group-by-page? blocks-grouped-by-page?
+                                            ;; :ref? true
+)
                                children?
                                (assoc :ref? true))
                       {:style {:margin-top "0.25rem"
@@ -1682,24 +1839,20 @@
       ["Properties" m]
       [:div.properties
        (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
                       (not (= k :title))
                       (not (= k :filter)))
              [:div.property
               [:span.font-medium.mr-1 (str (name k) ": ")]
               (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))])))]
 
       ["Paragraph" l]
@@ -1830,7 +1983,7 @@
            [:a.ml-1 {:id (str "fn." id)
                      :style {:font-size 14}
                      :class "footnum"
-                     :href (str "#fnr." id)}
+                     :on-click #(route-handler/jump-to-anchor! (str "fnr." id))}
             [:sup.fn (str name "↩︎")]])]])
 
       :else
@@ -1901,13 +2054,10 @@
                       (rest blocks)
                       blocks)
              first-id (:block/uuid (first blocks))]
-         (for [item blocks]
+         (for [[idx item] (medley/indexed blocks)]
            (let [item (-> (if (:block/dummy? item)
                             item
                             (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))]
              (rum/with-key
                (block-container config item)
@@ -1924,14 +2074,17 @@
      (assoc :class "doc-mode"))
    (if (:group-by-page? config)
      [: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))}
                          (:ref? config)
-                         (assoc :class "bg-base-2 px-7 py-2 rounded"))
+                         (assoc :class "color-level px-7 py-2 rounded"))
              (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))])
 

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

@@ -10,9 +10,81 @@
 
   img {
     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 {
   blockquote:first-child,
   pre:first-child {
@@ -22,17 +94,39 @@
 }
 
 .block-children {
-  border-left: 2px solid;
+  border-left: 1px solid;
   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 {
-  color: var(--ls-link-text-color);
   padding-bottom: 2px;
   border-bottom: 0.5px solid;
   border-bottom-color: var(--ls-block-ref-link-text-color);
   cursor: alias;
 
+  &-wrap {
+    display: inline-block;
+  }
+
   &:hover {
     color: var(--ls-link-text-hover-color)
   }
@@ -81,6 +175,7 @@
 
   &:hover {
     color: var(--ls-link-text-hover-color);
+    opacity: 1;
   }
 }
 
@@ -192,8 +287,8 @@
 
 .bullet-container {
   display: flex;
-  height: 13px;
-  width: 13px;
+  height: 12px;
+  width: 12px;
   border-radius: 50%;
   justify-content: center;
   align-items: center;
@@ -204,9 +299,16 @@
 
   .bullet {
     border-radius: 50%;
-    width: 5px;
-    height: 5px;
+    width: 6px;
+    height: 6px;
     background-color: var(--ls-block-bullet-color, #394b59);
+    transition: transform .2s;
+  }
+
+  &:hover {
+    .bullet {
+      transform: scale(1.2);
+    }
   }
 
   &.bullet-closed {

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

@@ -4,6 +4,8 @@
             [frontend.handler.repo :as repo-handler]
             [frontend.state :as state]
             [frontend.mixins :as mixins]
+            [frontend.handler.notification :as notification]
+            [promesa.core :as p]
             [goog.dom :as gdom]
             [goog.object :as gobj]))
 
@@ -11,7 +13,9 @@
   []
   (let [value (gobj/get (gdom/getElement "commit-message") "value")]
     (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!))))
 
 (rum/defcs add-commit-message <

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

@@ -62,30 +62,52 @@
       :on-click editor-handler/bulk-make-todos}
      (str "Make " (state/get-preferred-todo) "s"))]])
 
+;; FIXME: Make it configurable
 (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 "" ::input)
+  {:will-unmount (fn [state]
+                   (reset! *including-parent? nil)
+                   state)}
   [state block-id]
   (let [edit? (get state ::edit?)
-        input (get state ::input)]
+        input (get state ::input)
+        including-parent? (rum/react *including-parent?)
+        block-id (if (string? block-id) (uuid block-id) block-id)
+        block (db/entity [:block/uuid block-id])
+        has-children? (seq (:block/children block))]
+    (when (and (nil? including-parent?) has-children?)
+      (reset! *including-parent? true))
+
     (if @edit?
       (do
         (state/clear-edit!)
         [:div.px-4.py-2 {:on-click (fn [e] (util/stop e))}
          [:p "What's the template's name?"]
-         [:input#new-template.form-input.block.w-full.sm:text-sm.sm:leading-5.my-2.text-gray-700
+         [:input#new-template.form-input.block.w-full.sm:text-sm.sm:leading-5.my-2
           {:auto-focus true
            :on-change (fn [e]
                         (reset! input (util/evalue e)))}]
+         (when has-children?
+           (template-checkbox including-parent?))
          (ui/button "Submit"
                     :on-click (fn []
                                 (let [title (string/trim @input)]
@@ -96,6 +118,8 @@
                                        :error)
                                       (do
                                         (editor-handler/set-block-property! block-id "template" title)
+                                        (when (false? including-parent?)
+                                          (editor-handler/set-block-property! block-id "including-parent" false))
                                         (state/hide-custom-context-menu!)))))))])
       (ui/menu-link
        {:key "Make template"
@@ -130,7 +154,9 @@
           (ui/menu-link
            {:key "Convert heading"
             :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?
              "Convert back to a block"
              "Convert to a heading"))
@@ -147,7 +173,7 @@
                                     content (:block/content block)
                                     content (cond
                                               empty-properties?
-                                              (text/rejoin-properties content {"" ""} false)
+                                              (text/rejoin-properties content {"" ""} {:remove-blank? false})
                                               all-hidden?
                                               (let [idx (string/index-of content "\n:END:")]
                                                 (str

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

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

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

@@ -3,23 +3,19 @@
             [frontend.components.svg :as svg]
             [frontend.config :as config]
             [frontend.handler.editor :as editor-handler :refer [get-state]]
+            [frontend.handler.editor.lifecycle :as lifecycle]
             [frontend.util :as util :refer-macros [profile]]
-            [frontend.handler.file :as file]
             [frontend.handler.block :as block-handler]
             [frontend.handler.page :as page-handler]
-            [frontend.handler.editor.keyboards :as keyboards-handler]
             [frontend.components.datetime :as datetime-comp]
-            [promesa.core :as p]
             [frontend.state :as state]
             [frontend.mixins :as mixins]
             [frontend.ui :as ui]
             [frontend.db :as db]
-            [frontend.config :as config]
             [dommy.core :as d]
             [goog.object :as gobj]
             [goog.dom :as gdom]
             [clojure.string :as string]
-            [clojure.set :as set]
             [frontend.commands :as commands
              :refer [*show-commands
                      *matched-commands
@@ -27,9 +23,6 @@
                      *angle-bracket-caret-pos
                      *matched-block-commands
                      *show-block-commands]]
-            [medley.core :as medley]
-            [cljs-drag-n-drop.core :as dnd]
-            [frontend.text :as text]
             ["/frontend/utils" :as utils]))
 
 (rum/defc commands < rum/reactive
@@ -56,7 +49,7 @@
                        (editor-handler/insert-command! id command-steps
                                                        format
                                                        {:restore? restore-slash?})))
-        :class "black"}))))
+        :class     "black"}))))
 
 (rum/defc block-commands < rum/reactive
   [id format]
@@ -69,7 +62,7 @@
                      (editor-handler/insert-command! id (get (into {} matched) chosen)
                                                      format
                                                      {:last-pattern commands/angle-bracket}))
-        :class "black"}))))
+        :class     "black"}))))
 
 (rum/defc page-search < rum/reactive
   {:will-unmount (fn [state] (reset! editor-handler/*selected-text nil) state)}
@@ -88,92 +81,44 @@
                  (when (> (count edit-content) current-pos)
                    (util/safe-subs edit-content pos current-pos)))
               matched-pages (when-not (string/blank? q)
-                              (editor-handler/get-matched-pages q))
-              chosen-handler (if (state/sub :editor/show-page-search-hashtag?)
-                               (fn [chosen _click?]
-                                 (state/set-editor-show-page-search! false)
-                                 (let [chosen (if (re-find #"\s+" chosen)
-                                                (util/format "[[%s]]" chosen)
-                                                chosen)]
-                                   (editor-handler/insert-command! id
-                                                                   (str "#" chosen)
-                                                                   format
-                                                                   {:last-pattern (str "#" (if @editor-handler/*selected-text "" q))})))
-                               (fn [chosen _click?]
-                                 (state/set-editor-show-page-search! false)
-                                 (let [page-ref-text (page-handler/get-page-ref-text chosen)]
-                                   (editor-handler/insert-command! id
-                                                                   page-ref-text
-                                                                   format
-                                                                   {:last-pattern (str "[[" (if @editor-handler/*selected-text "" q))
-                                                                    :postfix-fn (fn [s] (util/replace-first "]]" s ""))}))))
-              non-exist-page-handler (fn [_state]
-                                       (state/set-editor-show-page-search! false)
-                                       (if (state/org-mode-file-link? (state/get-current-repo))
-                                         (let [page-ref-text (page-handler/get-page-ref-text q)
-                                               value (gobj/get input "value")
-                                               old-page-ref (util/format "[[%s]]" q)
-                                               new-value (string/replace value
-                                                                         old-page-ref
-                                                                         page-ref-text)]
-                                           (state/set-edit-content! id new-value)
-                                           (let [new-pos (+ current-pos
-                                                            (- (count page-ref-text)
-                                                               (count old-page-ref))
-                                                            2)]
-                                             (util/move-cursor-to input new-pos)))
-                                         (util/cursor-move-forward input 2)))]
+                              (editor-handler/get-matched-pages q))]
           (ui/auto-complete
            matched-pages
-           {:on-chosen chosen-handler
-            :on-enter non-exist-page-handler
+           {:on-chosen (page-handler/on-chosen-handler input id q pos format)
+            :on-enter #(page-handler/page-not-exists-handler input id q current-pos)
             :empty-div [:div.text-gray-500.pl-4.pr-4 "Search for a page"]
-            :class "black"}))))))
+            :class     "black"}))))))
 
-(rum/defc block-search < rum/reactive
-  {:will-unmount (fn [state] (reset! editor-handler/*selected-text nil) state)}
-  [id format]
+(rum/defcs block-search < rum/reactive
+  {:will-unmount (fn [state]
+                   (reset! editor-handler/*selected-text nil)
+                   (state/clear-search-result!)
+                   state)}
+  [state id format]
   (when (state/sub :editor/show-block-search?)
     (let [pos (:editor/last-saved-cursor @state/state)
-          input (gdom/getElement id)]
+          input (gdom/getElement id)
+          [id format] (:rum/args state)
+          current-pos (:pos (util/get-caret-pos input))
+          edit-content (state/sub [:editor/content id])
+          edit-block (state/get-edit-block)
+          q (or
+             @editor-handler/*selected-text
+             (when (> (count edit-content) current-pos)
+               (subs edit-content pos current-pos)))
+          matched-blocks (when-not (string/blank? q)
+                           (editor-handler/get-matched-blocks q (:block/uuid edit-block)))]
       (when input
-        (let [current-pos (:pos (util/get-caret-pos input))
-              edit-content (state/sub [:editor/content id])
-              q (or
-                 @editor-handler/*selected-text
-                 (when (> (count edit-content) current-pos)
-                   (subs edit-content pos current-pos)))
-              matched-blocks (when-not (string/blank? q)
-                               (editor-handler/get-matched-blocks q))
-              chosen-handler (fn [chosen _click?]
-                               (state/set-editor-show-block-search! false)
-                               (let [uuid-string (str (:block/uuid chosen))]
-
-                                 ;; block reference
-                                 (editor-handler/insert-command! id
-                                                                 (util/format "((%s))" uuid-string)
-                                                                 format
-                                                                 {:last-pattern (str "((" (if @editor-handler/*selected-text "" q))
-                                                                  :postfix-fn (fn [s] (util/replace-first "))" s ""))})
-
-                                 ;; Save it so it'll be parsed correctly in the future
-                                 (editor-handler/set-block-property! (:block/uuid chosen)
-                                                                     "ID"
-                                                                     uuid-string)
-
-                                 (when-let [input (gdom/getElement id)]
-                                   (.focus input))))
-              non-exist-block-handler (fn [_state]
-                                        (state/set-editor-show-block-search! false)
-                                        (util/cursor-move-forward input 2))]
+        (let [chosen-handler (editor-handler/block-on-chosen-handler input id q format)
+              non-exist-block-handler (editor-handler/block-non-exist-handler input)]
           (ui/auto-complete
            matched-blocks
-           {:on-chosen chosen-handler
-            :on-enter non-exist-block-handler
-            :empty-div [:div.text-gray-500.pl-4.pr-4 "Search for a block"]
+           {:on-chosen   chosen-handler
+            :on-enter    non-exist-block-handler
+            :empty-div   [:div.text-gray-500.pl-4.pr-4 "Search for a block"]
             :item-render (fn [{:block/keys [content]}]
                            (subs content 0 64))
-            :class "black"}))))))
+            :class       "black"}))))))
 
 (rum/defc template-search < rum/reactive
   {:will-unmount (fn [state] (reset! editor-handler/*selected-text nil) state)}
@@ -190,43 +135,16 @@
                    (subs edit-content pos current-pos))
                  "")
               matched-templates (editor-handler/get-matched-templates q)
-              chosen-handler (fn [[template db-id] _click?]
-                               (if-let [block (db/entity db-id)]
-                                 (let [new-level (:block/level edit-block)
-                                       template-parent-level (:block/level block)
-                                       pattern (config/get-block-pattern format)
-                                       content
-                                       (block-handler/get-block-full-content
-                                        (state/get-current-repo)
-                                        (:block/uuid block)
-                                        (fn [{:block/keys [level content properties] :as block}]
-                                          (let [new-level (+ new-level (- level template-parent-level))
-                                                properties' (dissoc (into {} properties) "id" "custom_id" "template")]
-                                            (-> content
-                                                (string/replace-first (apply str (repeat level pattern))
-                                                                      (apply str (repeat new-level pattern)))
-                                                text/remove-properties!
-                                                (text/rejoin-properties properties')))))
-                                       content (if (string/includes? (string/trim edit-content) "\n")
-                                                 content
-                                                 (text/remove-level-spaces content format))]
-                                   (state/set-editor-show-template-search! false)
-                                   (editor-handler/insert-command! id
-                                                                   content
-                                                                   format
-                                                                   {})))
-                               (when-let [input (gdom/getElement id)]
-                                 (.focus input)))
               non-exist-handler (fn [_state]
                                   (state/set-editor-show-template-search! false))]
           (ui/auto-complete
            matched-templates
-           {:on-chosen chosen-handler
-            :on-enter non-exist-handler
-            :empty-div [:div.text-gray-500.pl-4.pr-4 "Search for a template"]
+           {:on-chosen   (editor-handler/template-on-chosen-handler input id q format edit-block edit-content)
+            :on-enter    non-exist-handler
+            :empty-div   [:div.text-gray-500.pl-4.pr-4 "Search for a template"]
             :item-render (fn [[template _block-db-id]]
                            template)
-            :class "black"}))))))
+            :class       "black"}))))))
 
 (rum/defc mobile-bar < rum/reactive
   [parent-state parent-id]
@@ -246,12 +164,28 @@
    [:button.bottom-action
     {:on-click #(commands/simple-insert! parent-id "\n" {})}
     svg/multi-line-input]
+   [:button.bottom-action
+    {:on-click #(commands/insert-before! parent-id "TODO " {})}
+    svg/checkbox]
    [:button.font-extrabold.bottom-action.-mt-1
-    {:on-click #(commands/simple-insert! parent-id "[[]]" {:backward-pos 2})}
+    {: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
-    {: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/local {} ::input-value)
@@ -294,11 +228,11 @@
               [:input.form-input.block.w-full.pl-2.sm:text-sm.sm:leading-5
                (merge
                 (cond->
-                 {:key (str "modal-input-" (name id))
-                  :id (str "modal-input-" (name id))
-                  :type (or type "text")
-                  :on-change (fn [e]
-                               (swap! input-value assoc id (util/evalue e)))
+                 {:key           (str "modal-input-" (name id))
+                  :id            (str "modal-input-" (name id))
+                  :type          (or type "text")
+                  :on-change     (fn [e]
+                                   (swap! input-value assoc id (util/evalue e)))
                   :auto-complete (if (util/chrome?) "chrome-off" "off")}
                   placeholder
                   (assoc :placeholder placeholder))
@@ -345,22 +279,31 @@
     (when-let [pos (rum/react pos)]
       (ui/css-transition
        {:class-names "fade"
-        :timeout {:enter 500
-                  :exit 300}}
+        :timeout     {:enter 500
+                      :exit  300}}
        (absolute-modal cp set-default-width? pos)))))
 
 (rum/defc image-uploader < rum/reactive
+  {:did-mount    (fn [state]
+                   (let [[id format] (:rum/args state)]
+                     (add-watch editor-handler/*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]
   [:div.image-uploader
    [:input
-    {:id "upload-file"
-     :type "file"
+    {:id        "upload-file"
+     :type      "file"
      :on-change (fn [e]
                   (let [files (.-files (.-target e))]
-                    (editor-handler/upload-image id files format editor-handler/*image-uploading? false)))
-     :hidden true}]
-   (when-let [uploading? (util/react editor-handler/*image-uploading?)]
-     (let [processing (util/react editor-handler/*image-uploading-process)]
+                    (editor-handler/upload-asset id files format editor-handler/*asset-uploading? false)))
+     :hidden    true}]
+   (when-let [uploading? (util/react editor-handler/*asset-uploading?)]
+     (let [processing (util/react editor-handler/*asset-uploading-process)]
        (transition-cp
         [:div.flex.flex-row.align-center.rounded-md.shadow-sm.bg-base-2.px-1.py-1
          (ui/loading
@@ -368,355 +311,59 @@
         false
         *slash-caret-pos)))])
 
-(rum/defcs box < rum/reactive
-  (mixins/event-mixin
-   (fn [state]
-     (let [{:keys [id format block]} (get-state state)
-           input-id id
-           input (gdom/getElement input-id)
-           repo (:block/repo block)]
-       (mixins/on-key-down
-        state
-        {;; enter
-         13 (fn [state e]
-              (when (and (not (gobj/get e "ctrlKey"))
-                         (not (gobj/get e "metaKey"))
-                         (not (editor-handler/in-auto-complete? input)))
-                (let [{:keys [block config]} (get-state state)]
-                  (when (and block
-                             (not (:ref? config))
-                             (not (:custom-query? config))) ; in reference section
-                    (let [content (state/get-edit-content)]
-                      (if (and
-                           (> (:block/level block) 2)
-                           (string/blank? content))
-                        (do
-                          (util/stop e)
-                          (editor-handler/adjust-block-level! state :left))
-                        (let [shortcut (state/get-new-block-shortcut)
-                              insert? (cond
-                                        config/mobile?
-                                        true
-
-                                        (and (= shortcut "alt+enter") (not (gobj/get e "altKey")))
-                                        false
-
-                                        (gobj/get e "shiftKey")
-                                        false
-
-                                        :else
-                                        true)]
-                          (when (and
-                                 insert?
-                                 (not (editor-handler/in-auto-complete? input)))
-                            (util/stop e)
-                            (profile
-                             "Insert block"
-                             (editor-handler/insert-new-block! state))))))))))
-         ;; up
-         38 (fn [state e]
-              (when (and
-                     (not (gobj/get e "ctrlKey"))
-                     (not (gobj/get e "metaKey"))
-                     (not (editor-handler/in-auto-complete? input)))
-                (editor-handler/on-up-down state e true)))
-         ;; down
-         40 (fn [state e]
-              (when (and
-                     (not (gobj/get e "ctrlKey"))
-                     (not (gobj/get e "metaKey"))
-                     (not (editor-handler/in-auto-complete? input)))
-                (editor-handler/on-up-down state e false)))
-         ;; backspace
-         8  (fn [state e]
-              (let [node (gdom/getElement input-id)
-                    current-pos (:pos (util/get-caret-pos node))
-                    value (gobj/get node "value")
-                    deleted (and (> current-pos 0)
-                                 (util/nth-safe value (dec current-pos)))
-                    selected-start (gobj/get node "selectionStart")
-                    selected-end (gobj/get node "selectionEnd")
-                    block-id (:block-id (first (:rum/args state)))
-                    page (state/get-current-page)]
-                (cond
-                  (not= selected-start selected-end)
-                  nil
-
-                  (and (zero? current-pos)
-                       ;; not the top block in a block page
-                       (not (and page
-                                 (util/uuid-string? page)
-                                 (= (medley/uuid page) block-id))))
-                  (editor-handler/delete-block! state repo e)
+(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]
-          :or {dummy? false}
-          :as option} id config]
+          :or   {dummy? false}
+          :as   option} id config]
   (let [content (state/get-edit-content)]
     [:div.editor-inner {:class (if block "block-editor" "non-block-editor")}
      (when config/mobile? (mobile-bar state id))
      (ui/ls-textarea
-      {:id id
+      {:id                id
+       :class             "mousetrap"
        :cacheMeasurements true
-       :default-value (or content "")
-       :minRows (if (state/enable-grammarly?) 2 1)
-       :on-click (fn [_e]
-                   (let [input (gdom/getElement id)
-                         current-pos (:pos (util/get-caret-pos input))]
-                     (state/set-edit-pos! current-pos)
-                     (editor-handler/close-autocomplete-if-outside input)))
-       :on-change (fn [e]
-                    (let [value (util/evalue e)
-                          current-pos (:pos (util/get-caret-pos (gdom/getElement id)))]
-                      (state/set-edit-content! id value false)
-                      (state/set-edit-pos! current-pos)
-                      (when-let [repo (or (:block/repo block)
-                                          (state/get-current-repo))]
-                        (state/set-editor-last-input-time! repo (util/time-ms))
-                        (db/clear-repo-persistent-job! repo))
-                      (let [input (gdom/getElement id)
-                            native-e (gobj/get e "nativeEvent")
-                            last-input-char (util/nth-safe value (dec current-pos))]
-                        (case last-input-char
-                          "/"
-                          ;; TODO: is it cross-browser compatible?
-                          (when (not= (gobj/get native-e "inputType") "insertFromPaste")
-                            (when-let [matched-commands (seq (editor-handler/get-matched-commands input))]
-                              (reset! *slash-caret-pos (util/get-caret-pos input))
-                              (reset! *show-commands true)))
-                          "<"
-                          (when-let [matched-commands (seq (editor-handler/get-matched-block-commands input))]
-                            (reset! *angle-bracket-caret-pos (util/get-caret-pos input))
-                            (reset! *show-block-commands true))
-                          nil))))
-       :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?
      (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,
 pre {
   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)
         format (format/get-format path)
         page (db/get-file-page path)
-        config? (= path (str config/app-name "/" config/config-file))]
+        config? (= path (config/get-config-path))]
     (rum/with-context [[tongue] i18n/*tongue-context*]
       [:div.file {:id (str "file-" path)}
        [:h1.title
@@ -81,20 +81,21 @@
           [:a.bg-base-2.p-1.ml-1 {:style {:border-radius 4}
                                   :href (rfe/href :page {:name page})
                                   :on-click (fn [e]
-                                              (.preventDefault e)
                                               (when (gobj/get e "shiftKey")
                                                 (when-let [page (db/entity [:page/name (string/lower-case page)])]
                                                   (state/sidebar-add-block!
                                                    (state/get-current-repo)
                                                    (:db/id page)
                                                    :page
-                                                   {:page page}))))}
+                                                   {:page page}))
+                                                (util/stop e)))}
            page]])
 
-       [: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?))
          [: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 {
+  max-width: 86vw;
+
   textarea, pre {
     margin: 0;
   }

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

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

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

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

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

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

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

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

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

@@ -1,6 +1,7 @@
 (ns frontend.components.page
   (:require [rum.core :as rum]
             [frontend.util :as util :refer-macros [profile]]
+            [frontend.tools.html-export :as html-export]
             [frontend.handler.file :as file]
             [frontend.handler.page :as page-handler]
             [frontend.handler.ui :as ui-handler]
@@ -21,6 +22,8 @@
             [frontend.components.project :as project]
             [frontend.config :as config]
             [frontend.db :as db]
+            [frontend.db.model :as model]
+            [frontend.db.utils :as db-utils]
             [frontend.mixins :as mixins]
             [frontend.db-mixins :as db-mixins]
             [goog.dom :as gdom]
@@ -34,6 +37,7 @@
             [cljs.pprint :as pprint]
             [frontend.context.i18n :as i18n]
             [reitit.frontend.easy :as rfe]
+            [frontend.text :as text]
             [frontend.handler.block :as block-handler]))
 
 (defn- get-page-name
@@ -54,6 +58,9 @@
   db-mixins/query
   [repo page file-path page-name page-original-name encoded-page-name sidebar? journal? block? block-id format]
   (let [raw-page-blocks (get-blocks repo page-name page-original-name block? block-id)
+        grouped-blocks-by-file (into {} (for [[k v] (db-utils/group-by-file raw-page-blocks)]
+                                          [(:file/path (db-utils/entity (:db/id k))) v]))
+        raw-page-blocks (get grouped-blocks-by-file file-path raw-page-blocks)
         page-blocks (block-handler/with-dummy-block raw-page-blocks format
                       (if (empty? raw-page-blocks)
                         (let [content (db/get-file repo file-path)]
@@ -73,11 +80,23 @@
                        :editor-box editor/box}
         hiccup-config (common-handler/config-with-document-mode hiccup-config)
         hiccup (block/->hiccup page-blocks hiccup-config {})]
-    (rum/with-key
-      (content/content page-name
-                       {:hiccup hiccup
-                        :sidebar? sidebar?})
-      (str encoded-page-name "-hiccup"))))
+    [:div.page-blocks-inner
+     (when (and (seq grouped-blocks-by-file)
+                (> (count grouped-blocks-by-file) 1))
+       (ui/admonition
+        :warning
+        [:div.text-sm
+         [:p.font-medium "Those pages have the same title, you might want to only keep one file."]
+         [:ol
+          (for [[file-path blocks] (into (sorted-map) grouped-blocks-by-file)]
+            [:li [:a {:key file-path
+                      :href (rfe/href :file {:path file-path})} file-path]])]]))
+
+     (rum/with-key
+       (content/content page-name
+                        {:hiccup   hiccup
+                         :sidebar? sidebar?})
+       (str encoded-page-name "-hiccup"))]))
 
 (defn contents-page
   [{:page/keys [name original-name file] :as contents}]
@@ -157,7 +176,7 @@
   [state page-name close-fn]
   (let [input (get state ::input)]
     (rum/with-context [[t] i18n/*tongue-context*]
-      [:div
+      [:div.w-full.sm:max-w-lg.sm:w-96
        [:div.sm:flex.sm:items-start
         [:div.mt-3.text-center.sm:mt-0.sm:text-left
          [:h3#modal-headline.text-lg.leading-6.font-medium.text-gray-900
@@ -202,18 +221,13 @@
          [:ul.mt-2
           (for [[original-name name] pages]
             [:li {:key (str "tagged-page-" name)}
-             [:a {:href (str "/page/" (util/encode-str name))}
-              original-name]])])]])))
+             [:a {:href (rfe/href :page {:name name})}
+              original-name]])] false)]])))
 
-(defonce last-route (atom :home))
 ;; A page is just a logical block
 (rum/defcs page < rum/reactive
   {:did-mount (fn [state]
                 (ui-handler/scroll-and-highlight! state)
-                ;; only when route changed
-                (when (not= @last-route (state/get-current-route))
-                  (editor-handler/open-last-block! false))
-                (reset! last-route (state/get-current-route))
                 state)
    :did-update (fn [state]
                  (ui-handler/scroll-and-highlight! state)
@@ -227,10 +241,13 @@
         path-page-name page-name
         marker-page? (util/marker? page-name)
         priority-page? (contains? #{"a" "b" "c"} page-name)
-        format (db/get-page-format page-name)
-        journal? (db/journal-page? page-name)
         block? (util/uuid-string? page-name)
         block-id (and block? (uuid page-name))
+        format (let [page (if block-id
+                            (:page/name (:block/page (db/entity [:block/uuid block-id])))
+                            page-name)]
+                 (db/get-page-format page))
+        journal? (db/journal-page? page-name)
         sidebar? (:sidebar? option)]
     (rum/with-context [[t] i18n/*tongue-context*]
       (cond
@@ -268,23 +285,35 @@
               developer-mode? (state/sub [:ui/developer-mode?])
               published? (= "true" (:published properties))
               public? (= "true" (:public properties))]
-          [:div.flex-1.page.relative
+          [:div.flex-1.page.relative (if (seq (:page/tags page))
+                                       (let [page-names (model/get-page-names-by-ids (map :db/id (:page/tags page)))]
+                                         {:data-page-tags (text/build-data-value page-names)})
+                                       {})
            [:div.relative
             (when (and (not block?)
                        (not sidebar?)
                        (not config/publishing?))
 
-              (let [links (->>
-                           [(when file
-                              {:title (t :page/re-index)
-                               :options {:on-click (fn []
-                                                     (file/re-index! file))}})
-                            {:title (t :page/add-to-contents)
-                             :options {:on-click (fn [] (page-handler/handle-add-page-to-contents! page-original-name))}}
-                            {:title (t :page/rename)
-                             :options {:on-click #(state/set-modal! (rename-page-dialog page-name))}}
-                            {:title (t :page/delete)
-                             :options {:on-click #(state/set-modal! (delete-page-dialog page-name))}}
+              (let [contents? (= (string/lower-case (str page-name)) "contents")
+                    links (->>
+                           [(when-not contents?
+                              {:title (t :page/add-to-contents)
+                               :options {:on-click (fn [] (page-handler/handle-add-page-to-contents! page-original-name))}})
+
+                            (when-not contents?
+                              {:title (t :page/rename)
+                               :options {:on-click #(state/set-modal! (rename-page-dialog page-name))}})
+
+                            (when (and file-path (util/electron?))
+                              [{:title   (t :page/open-in-finder)
+                                :options {:on-click #(js/window.apis.showItemInFolder file-path)}}
+                               {:title (t :page/open-with-default-app)
+                                :options {:on-click #(js/window.apis.openPath file-path)}}])
+
+                            (when-not contents?
+                              {:title (t :page/delete)
+                               :options {:on-click #(state/set-modal! (delete-page-dialog page-name))}})
+
                             {:title   (t :page/action-publish)
                              :options {:on-click
                                        (fn []
@@ -294,23 +323,35 @@
                                              (mapv (fn [{:keys [title options]}]
                                                      (when title
                                                        [:div.it
-                                                        {:on-click #(state/close-modal!)}
                                                         (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)
                                                        :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))
                                                      :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?
                               {:title "(Dev) Show page data"
                                :options {:on-click (fn []
@@ -324,6 +365,7 @@
                                                                     :on-click #(.writeText js/navigator.clipboard page-data))]
                                                         :success
                                                         false)))}})]
+                           (flatten)
                            (remove nil?))]
                 (when (seq links)
                   (ui/dropdown-with-links
@@ -371,12 +413,11 @@
                  {:key "page-file"}
                  [:span.opacity-50 {:style {:margin-top 2}} (t :file/file)]
                  [:a.bg-base-2.px-1.ml-1.mr-3 {:style {:border-radius 4
-                                                       :word-break "break-word"}
-                                               :href (str "/file/" (util/url-encode file-path))}
+                                                       :word-break    "break-word"}
+                                               :href  (rfe/href :file {:path file-path})}
                   file-path]
 
-                 (when (and (not config/mobile?)
-                            (not journal?))
+                 (when (not config/mobile?)
                    (presentation repo page))])]
 
              (when (and repo (not block?))
@@ -385,12 +426,14 @@
                    [:div.text-sm.ml-1.mb-4 {:key "page-file"}
                     [:span.opacity-50 "Alias: "]
                     (for [item alias]
-                      [:a.ml-1.mr-1 {:href (str "/page/" (util/encode-str item))}
+                      [:a.ml-1.mr-1 {:href (rfe/href :page {:name item})}
                        item])])))
 
              (when (and block? (not sidebar?))
-               [:div.mb-4
-                (block/block-parents repo block-id format)])
+               (let [config {:id "block-parent"
+                             :block? true}]
+                 [:div.mb-4
+                  (block/block-parents config repo block-id format)]))
 
              ;; blocks
              (page-blocks-cp repo page file-path page-name page-original-name encoded-page-name sidebar? journal? block? block-id format)]]
@@ -432,7 +475,7 @@
            {:width (if (and (> width 1280) sidebar-open?)
                      (- width 24 600)
                      (- width 24))
-            :height (- height 120)
+            :height height
             :ref (fn [v] (reset! graph-ref v))
             :ref-atom graph-ref}))
          [:div.ls-center.mt-20
@@ -471,11 +514,10 @@
               [:th (t :page/name)]
               [:th (t :file/last-modified-at)]]]
             [:tbody
-             (for [[page modified-at] pages]
+             (for [page pages]
                (let [encoded-page (util/encode-str page)]
                  [:tr {:key encoded-page}
                   [:td [:a {:on-click (fn [e]
-                                        (.preventDefault e)
                                         (let [repo (state/get-current-repo)
                                               page (db/pull repo '[*] [:page/name (string/lower-case page)])]
                                           (when (gobj/get e "shiftKey")
@@ -487,10 +529,7 @@
                             :href (rfe/href :page {:name encoded-page})}
                         page]]
                   [:td [:span.text-gray-500.text-sm
-                        (if (zero? modified-at)
-                          (t :file/no-data)
-                          (date/get-date-time-string
-                           (t/to-default-time-zone (tc/to-date-time modified-at))))]]]))]]))])))
+                        (t :file/no-data)]]]))]]))])))
 
 (rum/defcs new < rum/reactive
   (rum/local "" ::title)

+ 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
   (:require [rum.core :as rum]
             [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/local "" ::project)
@@ -40,7 +42,8 @@
          :on-click (fn []
                      (let [value @project]
                        (when (and value (>= (count value) 2))
-                         (project-handler/add-project! value))))}
+                         (project-handler/add-project! value
+                                                       config-handler/set-project!))))}
         "Submit"]]
       [: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

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

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

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

@@ -4,23 +4,32 @@
             [frontend.ui :as ui]
             [frontend.state :as state]
             [frontend.db :as db]
+            [frontend.encrypt :as e]
             [frontend.handler.repo :as repo-handler]
             [frontend.handler.common :as common-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.export :as export-handler]
             [frontend.handler.web.nfs :as nfs-handler]
+            [frontend.handler.page :as page-handler]
             [frontend.util :as util]
             [frontend.config :as config]
             [reitit.frontend.easy :as rfe]
             [frontend.version :as version]
             [frontend.components.commit :as commit]
             [frontend.components.svg :as svg]
+            [frontend.components.encryption :as encryption]
             [frontend.context.i18n :as i18n]
-            [clojure.string :as string]))
+            [clojure.string :as string]
+            [clojure.string :as str]))
 
 (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
   []
@@ -29,20 +38,25 @@
         repos (util/distinct-by :url repos)]
     (rum/with-context [[t] i18n/*tongue-context*]
       (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
            (when (nfs-handler/supported?)
              [:div.mr-8
               (ui/button
                (t :open-a-directory)
-               :on-click nfs-handler/ls-dir-files)])
-           (when (state/logged?)
+               :on-click page-handler/ls-dir-files!)])
+           (when (and (state/logged?) (not (util/electron?)))
              (ui/button
               "Add another git repo"
-              :href (rfe/href :repo-add)))]
+              :href (rfe/href :repo-add nil {:graph-types "github"})
+              :intent "logseq"))]
           (for [{:keys [id url] :as repo} repos]
             (let [local? (config/local-db? url)]
               [:div.flex.justify-between.mb-1 {:key id}
@@ -53,45 +67,44 @@
                       :href url}
                   (db/get-repo-path url)])
                [:div.controls
-                [:a.control {:title (if local?
-                                      "Sync with the local directory"
-                                      "Clone again and re-index the db")
-                             :on-click (fn []
-                                         (if local?
-                                           (nfs-handler/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 []
-                                              (export-handler/export-repo-as-json! (:url repo)))}
-                 "Export as JSON"]
-                [:a.text-gray-400.ml-4 {:on-click (fn []
+                                              (repo-handler/re-index! nfs-handler/rebuild-index!))}
+                 "Re-index"]
+                ;; [:a.control.ml-4 {:title "Export as JSON"
+                ;;                   :on-click (fn []
+                ;;                               (export-handler/export-repo-as-json! (:url repo)))}
+                ;;  "Export as JSON"]
+                [:a.text-gray-400.ml-4 {:title "No worries, unlink this graph will clear its cache only, it does not remove your files on the disk."
+                                        :on-click (fn []
                                                     (repo-handler/remove-repo! repo))}
                  "Unlink"]]]))]
 
          [:a#download-as-json.hidden]]
-        (widgets/add-repo)))))
+        (widgets/add-graph)))))
 
 (rum/defc sync-status < rum/reactive
   {:did-mount (fn [state]
                 (js/setTimeout common-handler/check-changed-files-status 1000)
                 state)}
-  []
-  (when-let [repo (state/get-current-repo)]
+  [repo]
+  (when repo
     (let [nfs-repo? (config/local-db? repo)]
       (when-not (= repo config/local-repo)
         (if (and nfs-repo? (nfs-handler/supported?))
           (let [syncing? (state/sub :graph/syncing?)]
-            [:div.ml-2.mr-1.opacity-70.hover:opacity-100 {:class (if syncing? "loader" "initial")}
+            [:div.ml-2.mr-2.opacity-30.refresh.hover:opacity-100 {:class (if syncing? "loader" "initial")}
              [:a
               {:on-click #(nfs-handler/refresh! repo
                                                 repo-handler/create-today-journal!)
-               :title (str "Sync files with the local directory: " (config/get-local-dir repo) ".\nVersion: "
+               :title (str "Import files from the local directory: " (config/get-local-dir repo) ".\nVersion: "
                            version/version)}
               svg/refresh]])
           (let [changed-files (state/sub [:repo/changed-files repo])
@@ -99,6 +112,13 @@
                 git-status (state/sub [:git/status repo])
                 pushing? (= :pushing 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)
                 last-pulled-at (db/sub-key-value repo :git/last-pulled-at)
                 ;; db-persisted? (state/sub [:db/persisted? repo])
@@ -109,7 +129,7 @@
               (fn [{:keys [toggle-fn]}]
                 [:div.cursor.w-2.h-2.sync-status.mr-2
                  {:class (cond
-                           push-failed?
+                           git-failed?
                            "bg-red-500"
                            (or
                             ;; (not db-persisted?)
@@ -185,8 +205,12 @@
           (> (count repos) 1)
           (ui/dropdown-with-links
            (fn [{:keys [toggle-fn]}]
-             [:a#repo-switch {:on-click toggle-fn}
-              [:span (get-repo-name current-repo)]
+             [:a#repo-switch.fade-link {:on-click toggle-fn}
+              (let [repo-name (get-repo-name current-repo)
+                    repo-name (if (util/electron?)
+                                (last (string/split repo-name #"/"))
+                                repo-name)]
+                [:span repo-name])
               [:span.dropdown-caret.ml-1 {:style {:border-top-color "#6b7280"}}]])
            (mapv
             (fn [{:keys [id url]}]
@@ -208,8 +232,11 @@
           (and current-repo (not local-repo?))
           (let [repo-name (get-repo-name current-repo)]
             (if (config/local-db? current-repo)
-              repo-name
-              [:a
+              [:span.fade-link
+               (if (util/electron?)
+                 (last (string/split repo-name #"/"))
+                 repo-name)]
+              [:a.fade-link
                {:href current-repo
                 :target "_blank"}
                repo-name]))

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

@@ -11,6 +11,7 @@
             [frontend.handler.graph :as graph-handler]
             [frontend.state :as state]
             [frontend.db :as db]
+            [frontend.db.model :as db-model]
             [frontend.util :as util]
             [frontend.date :as date]
             [medley.core :as medley]
@@ -27,16 +28,16 @@
 (rum/defc block-cp < rum/reactive
   [repo idx block]
   (let [id (:block/uuid block)]
-    (page/page {:parameters {:path {:name (str id)}}
-                :sidebar? true
+    (page/page {:parameters  {:path {:name (str id)}}
+                :sidebar?    true
                 :sidebar/idx idx
-                :repo repo})))
+                :repo        repo})))
 
 (rum/defc page-cp < rum/reactive
   [repo page-name]
   (page/page {:parameters {:path {:name page-name}}
-              :sidebar? true
-              :repo repo}))
+              :sidebar?   true
+              :repo       repo}))
 
 (rum/defc page-graph < db-mixins/query
   [page]
@@ -50,7 +51,7 @@
        (graph-2d/graph
         (graph/build-graph-opts
          graph dark? false
-         {:width 600
+         {:width  600
           :height 600}))])))
 
 (defn recent-pages
@@ -59,34 +60,19 @@
     [:div.recent-pages.text-sm.flex-col.flex.ml-3.mt-2
      (if (seq pages)
        (for [page pages]
-         [:a.mb-1 {:key (str "recent-page-" page)
-                   :href (rfe/href :page {:name page})}
+         [:a.mb-1 {:key      (str "recent-page-" page)
+                   :href     (rfe/href :page {:name page})
+                   :on-click (fn [e]
+                               (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]))]))
 
-(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
   []
   [:div.contents.flex-col.flex.ml-3
@@ -101,7 +87,7 @@
                       (util/stop e)
                       (if-not (db/entity [:page/name "contents"])
                         (page-handler/create! "contents")
-                        (route-handler/redirect! {:to :page
+                        (route-handler/redirect! {:to          :page
                                                   :path-params {:name "contents"}})))}
       (t :right-side-bar/contents)]
      (contents)]
@@ -113,7 +99,7 @@
     [(t :right-side-bar/help) (onboarding/help)]
 
     :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)]
 
     :block-ref
@@ -123,7 +109,8 @@
              block-id (:block/uuid block)
              format (:block/format block)]
          [[:div.ml-2.mt-1
-           (block/block-parents repo block-id format)]
+           (block/block-parents {:id     "block-parent"
+                                 :block? true} repo block-id format)]
           [:div.ml-2
            (block-cp repo idx block)]])])
 
@@ -131,14 +118,18 @@
     (when-let [block (db/entity repo [:block/uuid (:block/uuid block-data)])]
       (let [block-id (:block/uuid block-data)
             format (:block/format block-data)]
-        [(block/block-parents repo block-id format)
+        [(block/block-parents {:id     "block-parent"
+                               :block? true} repo block-id format)
          [:div.ml-2
           (block-cp repo idx block-data)]]))
 
     :page
     (let [page-name (:page/name block-data)]
-      [[:a {:href (rfe/href :page {:name (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
         (page-cp repo page-name)]])
 
@@ -149,13 +140,13 @@
           blocks (if journal?
                    (rest blocks)
                    blocks)
-          sections (block/build-slide-sections blocks {:id "slide-reveal-js"
+          sections (block/build-slide-sections blocks {:id          "slide-reveal-js"
                                                        :start-level 2
-                                                       :slide? true
-                                                       :sidebar? true
-                                                       :page-name page-name})]
-      [[:a {:href (str "/page/" (util/url-encode page-name))}
-        (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
         (slide/slide sections)]])
 
@@ -217,9 +208,41 @@
         theme (:ui/theme @state/state)]
     (get-page match)))
 
+(rum/defc sidebar-resizer
+  []
+  (let [el-ref (rum/use-ref nil)]
+    (rum/use-effect!
+     (fn []
+       (when-let [el (and (fn? js/window.interact) (rum/deref el-ref))]
+         (-> (js/interact el)
+             (.draggable
+              (bean/->js
+               {:listeners
+                {:move
+                 (fn [^js/MouseEvent e]
+                   (let [width js/document.documentElement.clientWidth
+                         offset (.-left (.-rect e))
+                         to-val (- 1 (.toFixed (/ offset width) 6))
+                         to-val (cond
+                                  (< to-val 0.2) 0.2
+                                  (> to-val 0.7) 0.7
+                                  :else to-val)]
+                     (.setProperty (.-style js/document.documentElement)
+                                   "--ls-right-sidebar-width"
+                                   (str (* to-val 100) "%"))))}}))
+             (.styleCursor false)
+             (.on "dragstart" #(.. js/document.documentElement -classList (add "is-resizing-buf")))
+             (.on "dragend" #(.. js/document.documentElement -classList (remove "is-resizing-buf")))))
+       #())
+     [])
+    [:span.resizer {:ref el-ref}]))
+
 (rum/defcs sidebar < rum/reactive
   [state]
   (let [blocks (state/sub :sidebar/blocks)
+        blocks (if (empty? blocks)
+                 [[(state/get-current-repo) "contents" :contents nil]]
+                 blocks)
         sidebar-open? (state/sub :ui/sidebar-open?)
         repo (state/sub :git/current-repo)
         match (state/sub :route-match)
@@ -230,31 +253,36 @@
        {:class (if sidebar-open? "is-open")}
        (if sidebar-open?
          [:div.cp__right-sidebar-inner
-          [:div.cp__right-sidebar-settings.hide-scrollbar {:key "right-sidebar-settings"}
-           [:div.ml-4.text-sm
-            [:a.cp__right-sidebar-settings-btn {:on-click (fn [e]
-                                                  (state/sidebar-add-block! repo "contents" :contents nil))}
-             (t :right-side-bar/contents)]]
+          (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)]
             (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)))]
     (map (partial map first) (partition-by second (map list coll switch)))))
 
+(defn highlight-exact-query
+  [content q]
+  (let [q-words (string/split q #" ")
+        lc-content (string/lower-case content)
+        lc-q (string/lower-case q)]
+    (if (or (string/includes? lc-content lc-q)
+            (not (re-find #" " q)))
+      (let [i (string/index-of lc-content lc-q)
+            [before after] [(subs content 0 i) (subs content (+ i (count q)))]]
+        [:p
+         (when-not (string/blank? before)
+           [:span before])
+         [:mark (subs content i (+ i (count q)))]
+         (when-not (string/blank? after)
+           [:span after])])
+      (let [elements (loop [words q-words
+                            content content
+                            result []]
+                       (if (and (seq words) content)
+                         (let [word (first words)
+                               lc-word (string/lower-case word)
+                               lc-content (string/lower-case content)]
+                           (if-let [i (string/index-of lc-content lc-word)]
+                             (recur (rest words)
+                                    (subs content (+ i (count word)))
+                                    (vec
+                                     (concat result
+                                             [[:span (subs content 0 i)]
+                                              [:mark (subs content i (+ i (count word)))]])))
+                             (recur nil
+                                    content
+                                    result)))
+                         (conj result [:span content])))]
+        [:p elements]))))
+
 (rum/defc highlight-fuzzy
   [content indexes]
   (let [n (count content)
-        max-hightlighted-len 64
-        max-surrounding-len 32
+        max-hightlighted-len 512
+        max-surrounding-len 512
 
         first-index (first indexes)
         last-index (nth indexes (dec (count indexes)))
@@ -86,19 +121,27 @@
 
 (defn- leave-focus
   []
-  (when-let [input (gdom/getElement "search_field")]
+  (when-let [input (gdom/getElement "search-field")]
     (.blur input)))
 
+(defonce search-timeout (atom nil))
+
 (rum/defc search-auto-complete
-  [{:keys [pages files blocks]} search-q]
+  [{:keys [pages files blocks] :as result} search-q]
   (rum/with-context [[t] i18n/*tongue-context*]
-    (let [new-page [{:type :new-page}]
-          new-file (when-let [ext (util/get-file-ext search-q)]
+    (let [new-file (when-let [ext (util/get-file-ext search-q)]
                      (when (contains? config/mldoc-support-formats (keyword (string/lower-case ext)))
                        [{:type :new-file}]))
           pages (map (fn [page] {:type :page :data page}) pages)
           files (map (fn [file] {:type :file :data file}) files)
           blocks (map (fn [block] {:type :block :data block}) blocks)
+          new-page (if (or
+                        (and (seq pages)
+                             (= (string/lower-case search-q)
+                                (string/lower-case (:data (first pages)))))
+                        (nil? result))
+                     []
+                     [{:type :new-page}])
           result (if config/publishing?
                    (concat pages files blocks)
                    (concat new-page pages new-file files blocks))]
@@ -129,9 +172,10 @@
 
                         :block
                         (let [block-uuid (uuid (:block/uuid data))
-                              page (:page/name (:block/page (db/entity [:block/uuid block-uuid])))
-                              path (str "/page/" (util/encode-str page) "#ls-block-" (:block/uuid data))]
-                          (route/redirect-with-fragment! path))
+                              page (:page/name (:block/page (db/entity [:block/uuid block-uuid])))]
+                          (route/redirect! {:to :page
+                                            :path-params {:name page}
+                                            :query-params {:anchor (str "ls-block-" (:block/uuid data))}}))
                         nil))
          :on-shift-chosen (fn [{:keys [type data]}]
                             (case type
@@ -176,15 +220,17 @@
                            data]
 
                           :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))})])))
 
-(rum/defc search < rum/reactive
+(rum/defcs search < rum/reactive
+  (rum/local false ::inside-box?)
   (mixins/event-mixin
    (fn [state]
      (mixins/hide-when-esc-or-outside
@@ -192,36 +238,42 @@
       :on-hide (fn []
                  (search-handler/clear-search!)
                  (leave-focus)))))
-  []
+  [state]
   (let [search-result (state/sub :search/result)
         search-q (state/sub :search/q)
-        show-result? (boolean (seq search-result))]
+        show-result? (boolean (seq search-result))
+        blocks-count (or (db/blocks-count) 0)
+        timeout (if (> blocks-count 2000) 500 100)]
     (rum/with-context [[t] i18n/*tongue-context*]
       [:div#search.flex-1.flex
-       [:div.flex.md:ml-0
-        [:label.sr-only {:for "search_field"} (t :search)]
+       [:div.inner
+        [:label.sr-only {:for "search-field"} (t :search)]
         [:div#search-wrapper.relative.w-full.text-gray-400.focus-within:text-gray-600
-         [:div.absolute.inset-y-0.flex.items-center.pointer-events-none.left-0
+         [:div.absolute.inset-y-0.flex.items-center.pointer-events-none {:style {:left 6}}
           [:svg.h-5.w-5
            {:view-box "0 0 20 20", :fill "currentColor"}
            [:path
             {:d
-             "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z",
-             :clip-rule "evenodd",
+             "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
+             :clip-rule "evenodd"
              :fill-rule "evenodd"}]]]
-         [:input#search_field.block.w-full.h-full.pr-3.py-2.rounded-md.focus:outline-none.placeholder-gray-500.focus:placeholder-gray-400.sm:text-sm.sm:bg-transparent
-
+         [:input#search-field.block.w-full.h-full.pr-3.py-2.rounded-md.focus:outline-none.placeholder-gray-500.focus:placeholder-gray-400.sm:text-sm.sm:bg-transparent
           {:style {:padding-left "2rem"}
            :placeholder (t :search)
            :auto-complete (if (util/chrome?) "chrome-off" "off") ; off not working here
            :default-value ""
            :on-change (fn [e]
+                        (when @search-timeout
+                          (js/clearTimeout @search-timeout))
                         (let [value (util/evalue e)]
                           (if (string/blank? value)
                             (search-handler/clear-search!)
                             (do
                               (state/set-q! value)
-                              (search-handler/search value)))))}]
+                              (reset! search-timeout
+                                      (js/setTimeout
+                                       #(search-handler/search value)
+                                       timeout))))))}]
          (when-not (string/blank? search-q)
            (ui/css-transition
             {:class-names "fade"

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

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

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

@@ -1,18 +1,22 @@
 (ns frontend.components.settings
   (:require [rum.core :as rum]
             [frontend.ui :as ui]
+            [frontend.components.svg :as svg]
             [frontend.handler.notification :as notification]
             [frontend.handler.user :as user-handler]
             [frontend.handler.ui :as ui-handler]
             [frontend.handler.repo :as repo-handler]
             [frontend.handler.config :as config-handler]
+            [frontend.handler.page :as page-handler]
             [frontend.state :as state]
+            [frontend.version :refer [version]]
             [frontend.util :as util]
             [frontend.config :as config]
             [frontend.dicts :as dicts]
             [clojure.string :as string]
             [goog.object :as gobj]
-            [frontend.context.i18n :as i18n]))
+            [frontend.context.i18n :as i18n]
+            [reitit.frontend.easy :as rfe]))
 
 (rum/defcs set-email < (rum/local "" ::email)
   [state]
@@ -23,8 +27,8 @@
        [:div
         [:h1.title.mb-1
          "Your email address:"]
-        [:div.mt-2.mb-4.relative.rounded-md.shadow-sm.max-w-xs
-         [:input#.form-input.block.w-full.pl-2.sm:text-sm.sm:leading-5
+        [:div.mt-2.mb-4.relative.rounded-md.max-w-xs
+         [:input#.form-input.is-small
           {:autoFocus true
            :on-change (fn [e]
                         (reset! email (util/evalue e)))}]]]]
@@ -47,8 +51,8 @@
        [:div
         [:h1.title.mb-1
          "Your cors address:"]
-        [:div.mt-2.mb-4.relative.rounded-md.shadow-sm.max-w-xs
-         [:input#.form-input.block.w-full.pl-2.sm:text-sm.sm:leading-5
+        [:div.mt-2.mb-4.relative.rounded-md.max-w-xs
+         [:input#.form-input.is-small
           {:autoFocus true
            :on-change (fn [e]
                         (reset! cors (util/evalue e)))}]]]]
@@ -62,179 +66,316 @@
 
       [: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
   []
-  (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])
         enable-timetracking? (state/enable-timetracking?)
+        current-repo (state/get-current-repo)
+        enable-journals? (state/enable-journals? current-repo)
+        enable-encryption? (state/enable-encryption? current-repo)
+        enable-git-auto-push? (state/enable-git-auto-push? current-repo)
         enable-block-time? (state/enable-block-time?)
         show-brackets? (state/show-brackets?)
         github-token (state/sub [:me :access-token])
         cors-proxy (state/sub [:me :cors_proxy])
         logged? (state/logged?)
-        current-repo (state/get-current-repo)
         developer-mode? (state/sub [:ui/developer-mode?])
         theme (state/sub :ui/theme)
         dark? (= "dark" theme)
         switch-theme (if dark? "white" "dark")]
     (rum/with-context [[t] i18n/*tongue-context*]
-      [:div#settings
+      [:div#settings.cp__settings-main
        [:h1.title (t :settings)]
 
-       [:div.mb-1.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start.sm:pt-5.pl-1
-        [:label.block.text-sm.font-medium.leading-5.sm:mt-px.sm:pt-2.opacity-70
-         {:for "toggle_theme"}
-         (t :right-side-bar/switch-theme (string/capitalize switch-theme))]
-        [:div.flex.flex-row.mt-1.sm:mt-0.sm:col-span-2.pt-2
-         [:div.max-w-lg.rounded-md.sm:max-w-xs
-          (ui/toggle dark?
-                     (fn []
-                       (state/set-theme! switch-theme)))]
-         [:span.ml-4.opacity-50 "t t"]]]
-
-       [:div.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start.sm:pt-5.pl-1
-        [:label.block.text-sm.font-medium.leading-5.opacity-70
-         {:for "show_brackets"}
-         (t :settings-page/show-brackets)]
-        [:div.flex.flex-row.mt-1.sm:mt-0.sm:col-span-2
-         [:div.max-w-lg.rounded-md.sm:max-w-xs
-          (ui/toggle show-brackets?
-                     config-handler/toggle-ui-show-brackets!)]
-         [:span.ml-4.opacity-50 "Ctrl-c Ctrl-b"]]]
-
-       [:div.mb-6.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start.sm:pt-5.pl-1
-        [:label.block.text-sm.font-medium.leading-5.sm:mt-px.sm:pt-2.opacity-70
-         {:for "preferred_language"}
-         (t :language)]
-        [:div.mt-1.sm:mt-0.sm:col-span-2
-         [:div.max-w-lg.rounded-md.shadow-sm.sm:max-w-xs
-          [:select.mt-1.form-select.block.w-full.pl-3.pr-10.py-2.text-base.leading-6.border-gray-300.focus:outline-none.focus:shadow-outline-blue.focus:border-blue-300.sm:text-sm.sm:leading-5
-           {:on-change (fn [e]
-                         (let [lang (util/evalue e)
-                               lang-val (filter (fn [el] (if (= (:label el) lang) true nil)) dicts/languages)
-                               lang-val (name (:value (first lang-val)))]
-                           (state/set-preferred-language! lang-val)
-                           (ui-handler/re-render-root!)))}
-           (for [language dicts/languages]
-             [:option (cond->
-                       {:key (:value language)}
-                        (= (name (:value language)) preferred-language)
-                        (assoc :selected "selected"))
-              (:label language)])]]]]
-
-       [:div.pl-1
-        ;; config.edn
+       [:div.panel-wrap
+        [:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start
+         [:label.block.text-sm.font-medium.leading-5.opacity-70
+          {:for "toggle_theme"}
+          (t :right-side-bar/switch-theme (string/capitalize switch-theme))]
+         [:div.flex.flex-row.mt-1.sm:mt-0.sm:col-span-2
+          [:div.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
-          [: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")]
                                    (if (= "Enter" k)
                                      (when-let [server (util/evalue event)]
                                        (user-handler/set-cors! server)
                                        (notification/show! "Custom CORS proxy updated successfully!" :success)))))}]]]]
+           (ui/admonition
+            :important
+            [:p (t :settings-page/dont-use-other-peoples-proxy-servers)
+             [:a {:href   "https://github.com/isomorphic-git/cors-proxy"
+                  :target "_blank"}
+              "https://github.com/isomorphic-git/cors-proxy"]])])
 
-            [:hr]])
-
-         [:div.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start.sm:pt-5
-          [:label.block.text-sm.font-medium.leading-5.sm:mt-px.sm:pt-2.opacity-70
-           {:for "developer_mode"}
-           (t :settings-page/developer-mode)]
-          [:div.mt-1.sm:mt-0.sm:col-span-2
-           [:div.max-w-lg.rounded-md.shadow-sm.sm:max-w-xs
-            (ui/button (if developer-mode? (t :settings-page/disable-developer-mode) (t :settings-page/enable-developer-mode))
-                       :on-click #(state/set-developer-mode! (not developer-mode?)))]]]
-
-         [:br]
-         (t :settings-page/developer-mode-desc)]]])))
+        (when logged?
+          [:div
+           [:hr]
+           [:div.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-center.sm:pt-5
+            [:label.block.text-sm.font-medium.leading-5.opacity-70.text-red-600.dark:text-red-400
+             {:for "delete account"}
+             (t :user/delete-account)]
+            [:div.mt-1.sm:mt-0.sm:col-span-2
+             [:div.max-w-lg.rounded-md.sm:max-w-xs
+              (ui/button (t :user/delete-your-account)
+                         :on-click (fn []
+                                     (ui-handler/toggle-settings-modal!)
+                                     (js/setTimeout #(state/set-modal! delete-account-confirm))))]]]])]])))

+ 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.util :as util]
             [frontend.state :as state]
+            [frontend.handler.ui :as ui-handler]
             [frontend.handler.user :as user-handler]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.route :as route-handler]
@@ -28,7 +29,8 @@
             [goog.object :as gobj]
             [frontend.context.i18n :as i18n]
             [reitit.frontend.easy :as rfe]
-            [goog.dom :as gdom]))
+            [goog.dom :as gdom]
+            [frontend.handler.web.nfs :as nfs-handler]))
 
 (defn nav-item
   [title href svg-d active? close-modal-fn]
@@ -54,16 +56,16 @@
         left-sidebar? (state/sub :ui/left-sidebar-open?)]
     (when left-sidebar?
       [:nav.flex-1.left-sidebar-inner
-       (nav-item "Journals" "/"
+       (nav-item "Journals" "#/"
                  "M3 12l9-9 9 9M5 10v10a1 1 0 001 1h3a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1h3a1 1 0 001-1V10M9 21h6"
                  (active? :home)
                  close-modal-fn)
-       (nav-item "All Pages" "/all-pages"
+       (nav-item "All Pages" "#/all-pages"
                  "M6 2h9a1 1 0 0 1 .7.3l4 4a1 1 0 0 1 .3.7v13a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4c0-1.1.9-2 2-2zm9 2.41V7h2.59L15 4.41zM18 9h-3a2 2 0 0 1-2-2V4H6v16h12V9zm-2 7a1 1 0 0 1-1 1H9a1 1 0 0 1 0-2h6a1 1 0 0 1 1 1zm0-4a1 1 0 0 1-1 1H9a1 1 0 0 1 0-2h6a1 1 0 0 1 1 1zm-5-4a1 1 0 0 1-1 1H9a1 1 0 1 1 0-2h1a1 1 0 0 1 1 1z"
                  (active? :all-pages)
                  close-modal-fn)
        (when-not config/publishing?
-         (nav-item "All Files" "/all-files"
+         (nav-item "All Files" "#/all-files"
                    "M3 7V17C3 18.1046 3.89543 19 5 19H19C20.1046 19 21 18.1046 21 17V9C21 7.89543 20.1046 7 19 7H13L11 5H5C3.89543 5 3 5.89543 3 7Z"
                    (active? :all-files)
                    close-modal-fn))
@@ -76,7 +78,7 @@
 (rum/defc sidebar-mobile-sidebar < rum/reactive
   [{:keys [open? close-fn route-match]}]
   [: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?
               "opacity-75 pointer-events-auto"
               "opacity-0 pointer-events-none")
@@ -85,20 +87,19 @@
     {:class (if @open?
               "translate-x-0"
               "-translate-x-full")
-     :style {:background-color "#002b36"
-             :max-width "15rem"}}
+     :style {:max-width "86vw"}}
     (if @open?
       [: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}
-        [:svg.h-6.w-6.text-white
+        [:svg.h-6.w-6
          {:viewBox "0 0 24 24", :fill "none", :stroke "currentColor"}
          [:path
           {:d "M6 18L18 6M6 6l12 12"
            :stroke-width "2"
            :stroke-linejoin "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)]
     [:div.flex-1.h-0.overflow-y-auto
      (sidebar-nav route-match close-fn)]]])
@@ -143,6 +144,7 @@
 
 (defonce sidebar-inited? (atom false))
 ;; TODO: simplify logic
+
 (rum/defc main-content < rum/reactive db-mixins/query
   {:init (fn [state]
            (when-not @sidebar-inited?
@@ -205,7 +207,7 @@
          (journal/journals latest-journals)
 
          (and logged? (empty? (:repos me)))
-         (widgets/add-repo)
+         (widgets/add-graph)
 
          ;; FIXME: why will this happen?
          :else
@@ -242,6 +244,21 @@
                     (state/sidebar-add-block! (state/get-current-repo) "help" :help nil))}
        "?"])))
 
+(rum/defc settings-modal
+  [settings-open?]
+  (rum/use-effect!
+   (fn []
+     (if settings-open?
+       (state/set-modal!
+        (fn [close-fn]
+          (gobj/set close-fn "user-close" #(ui-handler/toggle-settings-modal!))
+          [:div.settings-modal (settings/settings)]))
+       (state/set-modal! nil))
+
+     (util/lock-global-scroll settings-open?)
+     #())
+   [settings-open?]) nil)
+
 (rum/defcs sidebar <
   (mixins/modal :modal/show?)
   rum/reactive
@@ -257,6 +274,7 @@
                         (editor-handler/clear-selection! e)
                         (state/set-selection-start-block! nil))))
 
+     ;; TODO: move to keyboards
      (mixins/on-key-down
       state
       {;; esc
@@ -274,22 +292,10 @@
        ;; ?
        191 (fn [state e]
              (when-not (util/input? (gobj/get e "target"))
-               (state/sidebar-add-block! (state/get-current-repo) "help" :help nil)))
-       ;; c
-       67 (fn [state e]
-            (when (and (not (util/input? (gobj/get e "target")))
-                       (not (gobj/get e "shiftKey"))
-                       (not (gobj/get e "ctrlKey"))
-                       (not (gobj/get e "altKey"))
-                       (not (gobj/get e "metaKey")))
-              (when-let [repo-url (state/get-current-repo)]
-                (when-not (state/get-edit-input-id)
-                  (util/stop e)
-                  (state/set-modal! commit/add-commit-message)))))})))
+               (state/sidebar-add-block! (state/get-current-repo) "help" :help nil)))})))
   {:did-mount (fn [state]
                 (keyboards/bind-shortcuts!)
                 state)}
-  (mixins/keyboards-mixin keyboards/keyboards)
   [state route-match main-content]
   (let [{:keys [open? close-fn open-fn]} state
         close-fn (fn []
@@ -297,9 +303,11 @@
                    (state/set-left-sidebar-open! false))
         me (state/sub :me)
         current-repo (state/sub :git/current-repo)
+        granted? (state/sub [:nfs/user-granted? (state/get-current-repo)])
         theme (state/sub :ui/theme)
         white? (= "white" (state/sub :ui/theme))
-        sidebar-open? (state/sub :ui/sidebar-open?)
+        settings-open? (state/sub :ui/settings-open?)
+        sidebar-open?  (state/sub :ui/sidebar-open?)
         route-name (get-in route-match [:data :name])
         global-graph-pages? (= :graph route-name)
         logged? (:name me)
@@ -310,8 +318,11 @@
         default-home (get-default-home-if-valid)]
     (rum/with-context [[t] i18n/*tongue-context*]
       (theme/container
-       {:theme theme
-        :on-click editor-handler/unhighlight-block!}
+       {:theme         theme
+        :route         route-match
+        :nfs-granted?  granted?
+        :db-restoring? db-restoring?
+        :on-click      editor-handler/unhighlight-block!}
 
        [:div.theme-inner
         (sidebar-mobile-sidebar
@@ -319,7 +330,8 @@
           :close-fn    close-fn
           :route-match route-match})
         [:div.#app-container.cp__sidebar-layout
-         {:class (if sidebar-open? "is-right-sidebar-open")}
+         {:class (if sidebar-open? "is-right-sidebar-open")
+          :style {:padding-bottom (if global-graph-pages? 0 30)}}
          (header/header {:open-fn        open-fn
                          :white?         white?
                          :current-repo   current-repo
@@ -342,6 +354,7 @@
 
         (ui/notification)
         (ui/modal)
+        (settings-modal settings-open?)
         (custom-context-menu)
         [:a#download.hidden]
         (when

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

@@ -33,20 +33,42 @@
 }
 
 #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 {
     padding-right: 15px;
   }
 
-  a {
+  nav > a {
     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 {
   display: flex;
   flex-direction: column;
   min-height: 100vh;
-  padding-bottom: 30px;
 }
 
 .cp__sidebar-main-layout {
@@ -57,7 +79,7 @@
 
 .cp__sidebar-layout.is-right-sidebar-open {
   .cp__sidebar-main-layout {
-    margin-right: 40%;
+    margin-right: var(--ls-right-sidebar-width);
   }
 }
 
@@ -71,7 +93,7 @@
 }
 
 .cp__sidebar-main-content {
-  padding: 6rem 1.5rem;
+  padding: 5rem 1.5rem;
   max-width: var(--ls-main-content-max-width);
   min-height: 100vh;
   flex: 1;
@@ -129,12 +151,26 @@
 
   &-inner {
     padding: 15px;
+    padding-top: 0;
+    position: relative;
+    min-height: 100%;
+
+    .resizer {
+      position: absolute;
+      top: 0;
+      bottom: 0;
+      left: 0;
+      width: 4px;
+      user-select: none;
+      cursor: col-resize !important;
+    }
   }
 
   &-settings {
     @apply flex flex-row mb-2;
     margin: -15px;
     margin-bottom: 0;
+    margin-top: 0;
     overflow: auto;
 
     &-btn {
@@ -144,9 +180,13 @@
     }
   }
 
+  .close-arrow svg {
+    transform: scale(0.8);
+  }
+
   &.is-open {
     display: block;
-    width: 40%;
+    width: var(--ls-right-sidebar-width);
     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"
      :fill-rule "evenodd"}]])
 
-(rum/defc arrow-right
+(rum/defc arrow-right-2
   []
   [:svg
    {:aria-hidden "true"
@@ -29,6 +29,26 @@
     {:d "M7.5 8l-5 5L1 11.5 4.75 8 1 4.5 2.5 3l5 5z"
      :fill-rule "evenodd"}]])
 
+(rum/defc arrow-left
+  []
+  [:svg.w-6.h-6
+   {:viewBox "0 0 24 24", :stroke "currentColor", :fill "none"}
+   [:path
+    {:d "M15 19l-7-7 7-7",
+     :stroke-width "2",
+     :stroke-linejoin "round",
+     :stroke-linecap "round"}]])
+
+(rum/defc arrow-right
+  []
+  [:svg.w-6.h-6
+   {:viewBox "0 0 24 24", :stroke "currentColor", :fill "none"}
+   [:path
+    {:d "M9 5l7 7-7 7",
+     :stroke-width "2",
+     :stroke-linejoin "round",
+     :stroke-linecap "round"}]])
+
 (rum/defc big-arrow-right
   []
   [:svg
@@ -83,6 +103,22 @@
    [:path.opacity-75 {:fill "currentColor"
                       :d "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"}]])
 
+(defonce minus
+  [:svg.w-6.h-6
+   {:viewBox "0 0 24 24", :stroke "currentColor", :fill "none"}
+   [:path
+    {:d               "M20 12H4"
+     :stroke-width    "2"
+     :stroke-linejoin "round"
+     :stroke-linecap  "round"}]])
+
+(defonce rectangle
+  [:svg.w-6.h-6
+   {:viewBox "0 0 24 24", :stroke "currentColor", :fill "none"}
+   [:path
+    {:d               "M3.16580358,18.5038125 L20.5529464,18.5038125 C22.6525178,18.5038125 23.7072321,17.4593839 23.7072321,15.3902411 L23.7072321,3.12495537 C23.7072321,1.0558125 22.6525178,0.0113839219 20.5529464,0.0113839219 L3.16580358,0.0113839219 C1.07651787,0.0118125 0.0115178672,1.04638392 0.0115178672,3.12495537 L0.0115178672,15.3906696 C0.0115178672,17.4696696 1.07651787,18.5042411 3.16580358,18.5042411 L3.16580358,18.5038125 Z M3.19580358,16.8868125 C2.19123216,16.8868125 1.62894642,16.3545268 1.62894642,15.3096696 L1.62894642,3.20638392 C1.62894642,2.16152679 2.19123213,1.62924108 3.19580358,1.62924108 L20.5229464,1.62924108 C21.5172321,1.62924108 22.0898036,2.16152679 22.0898036,3.20638392 L22.0898036,15.3092411 C22.0898036,16.3540982 21.5172322,16.8863839 20.5229464,16.8863839 L3.19580358,16.8868125 Z"
+     :stroke-width    "2"}]])
+
 (defn- hero-icon
   ([d]
    (hero-icon d {}))
@@ -148,6 +184,16 @@
      :stroke-linejoin "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-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"}
@@ -172,6 +218,9 @@
 (defn vertical-dots
   [options]
   (hero-icon "M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z" options))
+(defn horizontal-dots
+  [options]
+  (hero-icon "M5 12h.01M12 12h.01M19 12h.01M6 12a1 1 0 11-2 0 1 1 0 012 0zm7 0a1 1 0 11-2 0 1 1 0 012 0zm7 0a1 1 0 11-2 0 1 1 0 012 0z" options))
 (def external-link
   [:svg {:fill "none", :view-box "0 0 24 24", :height "21", :width "21"
          :stroke "currentColor"}
@@ -424,6 +473,17 @@
      :stroke-linejoin "round"
      :stroke-linecap "round"}]])
 
+(def checkbox
+  [:svg.h-6.w-6
+   {:stroke "currentColor", :view-box "0 0 24 24", :fill "none"}
+   [:path
+    {:d "M11.167 16.167l-4.167-4.416 1.166-1.192 2.978 3.113 5.477-5.839 1.213 1.169-6.667 7.164zm9.167-12.5v16.667h-16.667v-16.667h16.667zm1.667-1.667h-20v20h20v-20z"
+     :fill-rule "evenodd"
+     :clip-rule "evenodd"
+     :stroke-width "2"
+     :stroke-linejoin "round"
+     :stroke-linecap "round"}]])
+
 (def page
   [:svg.h-5.w-4 {:viewBox "0 0 24 24", :fill "none", :xmlns "http://www.w3.org/2000/svg"}
    [:path {:d "M2 0.5H6.78272L13.5 7.69708V18C13.5 18.8284 12.8284 19.5 12 19.5H2C1.17157 19.5 0.5 18.8284 0.5 18V2C0.5 1.17157 1.17157 0.5 2 0.5Z", :fill "var(--ls-active-primary-color)"}]
@@ -443,3 +503,6 @@
     :class class }
    [:path
     {:d "M.53.53h15l-5 7v8h-5v-8z" :stroke-width "1.06" :stroke-linejoin "round"}]])
+
+(def collapse-right
+  (hero-icon "M4 6h16M4 12h16m-7 6h7"))

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

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

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

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

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

@@ -3,21 +3,16 @@
             [frontend.util :as util]
             [frontend.handler.user :as user-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.web.nfs :as nfs]
+            [frontend.handler.page :as page-handler]
             [frontend.state :as state]
-            [frontend.config :as config]
             [clojure.string :as string]
             [frontend.ui :as ui]
-            [frontend.db :as db]
-            [frontend.version :as version]
-            [frontend.components.commit :as commit]
             [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*]
     [:div
@@ -37,54 +32,100 @@
        :on-click
        #(user-handler/set-preferred-format! :org))]]))
 
-(rum/defcs add-repo <
+(rum/defcs add-github-repo <
   (rum/local "" ::repo)
+  (rum/local "" ::branch)
   [state]
-  (let [repo (get state ::repo)]
+  (let [repo (get state ::repo)
+        branch (get state ::branch)]
     (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!
   []
   (rfe/start!
-   (rf/router routes/routes {})
+   (rf/router routes/routes nil)
    route/set-route-match!
    ;; set to false to enable HistoryAPI
-   {:use-fragment false}))
+   {:use-fragment true}))
 
 (defn display-welcome-message
   []

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

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

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

@@ -10,6 +10,7 @@
             [frontend.state :as state]
             [promesa.core :as p]
             [frontend.db-schema :as db-schema]
+            [frontend.db.default :as default-db]
             [clojure.core.async :as async]
             [frontend.idb :as idb]))
 
@@ -37,7 +38,7 @@
  [frontend.db.model
   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
-  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-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
@@ -50,10 +51,11 @@
   get-page-properties-content get-page-referenced-blocks get-page-referenced-pages get-page-unlinked-references
   get-pages get-pages-relation get-pages-that-mentioned-page get-public-pages get-tag-pages
   journal-page? local-native-fs? mark-repo-as-cloned! page-alias-set page-blocks-transform pull-block
-  set-file-last-modified-at! transact-files-db! with-block-refs-count get-modified-pages]
+  set-file-last-modified-at! transact-files-db! with-block-refs-count get-modified-pages page-empty? get-alias-source-page
+  set-file-content!]
 
  [frontend.db.react
-  get-current-marker get-current-page get-current-priority get-handler-keys set-file-content! set-key-value
+  get-current-marker get-current-page get-current-priority get-handler-keys set-key-value
   transact-react! remove-key! remove-q! remove-query-component! add-q! add-query-component! clear-query-state!
   clear-query-state-without-refs-and-embeds! get-block-blocks-cache-atom get-page-blocks-cache-atom kv q
   query-state query-components query-entity-in-component remove-custom-query! set-new-result! sub-key-value]
@@ -65,10 +67,12 @@
 (defn persist! [repo]
   (let [file-key (datascript-files-db repo)
         non-file-key (datascript-db repo)
-        file-db (d/db (get-files-conn repo))
-        non-file-db (d/db (get-conn repo false))
-        file-db-str (db->string file-db)
-        non-file-db-str (db->string non-file-db)]
+        files-conn (get-files-conn repo)
+        file-db (when files-conn (d/db files-conn))
+        non-file-conn (get-conn repo false)
+        non-file-db (d/db non-file-conn)
+        file-db-str (if file-db (db->string file-db) "")
+        non-file-db-str (if non-file-db (db->string non-file-db) "")]
     (p/let [_ (idb/set-batch! [{:key file-key :value file-db-str}
                                {:key non-file-key :value non-file-db-str}])]
       (state/set-last-persist-transact-id! repo true (get-max-tx-id file-db))
@@ -146,7 +150,8 @@
          (p/let [stored (idb/get-item db-name)
                  _ (when stored
                      (let [stored-db (string->db stored)
-                           attached-db (d/db-with stored-db [(me-tx stored-db me)])]
+                           attached-db (d/db-with stored-db
+                                                  [(me-tx stored-db me)])]
                        (conn/reset-conn! db-conn attached-db)))
                  db-name (datascript-db repo)
                  db-conn (d/create-conn db-schema/schema)
@@ -155,7 +160,9 @@
                  stored (idb/get-item db-name)
                  _ (if stored
                      (let [stored-db (string->db stored)
-                           attached-db (d/db-with stored-db [(me-tx stored-db me)])]
+                           attached-db (d/db-with stored-db (concat
+                                                             [(me-tx stored-db me)]
+                                                             default-db/built-in-pages))]
                        (conn/reset-conn! db-conn attached-db))
                      (when logged?
                        (d/transact! db-conn [(me-tx (d/db db-conn) me)])))]

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

@@ -2,6 +2,7 @@
   "Contains db connections."
   (:require [clojure.string :as string]
             [frontend.db-schema :as db-schema]
+            [frontend.db.default :as default-db]
             [frontend.util :as util]
             [frontend.state :as state]
             [frontend.config :as config]
@@ -85,6 +86,8 @@
      (when 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)))))
 
 (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]
             [frontend.utf8 :as utf8]
             [frontend.config :as config]
-            [frontend.format.block :as block]
             [cljs.reader :as reader]
             [cljs-time.core :as t]
             [cljs-time.coerce :as tc]
@@ -22,6 +21,31 @@
 ;; TODO: extract to specific models and move data transform logic to the
 ;; correponding handlers.
 
+(def rules
+  '[[(parent ?p ?c)
+     [?p :block/children ?c]]
+    [(parent ?p ?c)
+     [?t :block/children ?c]
+     (parent ?p ?t)]
+
+    ;; from https://stackoverflow.com/questions/43784258/find-entities-whose-ref-to-many-attribute-contains-all-elements-of-input
+    ;; Quote:
+    ;; You're tackling the general problem of 'dynamic conjunction' in Datomic's Datalog.
+    ;; Write a dynamic Datalog query which uses 2 negations and 1 disjunction or a recursive rule
+    ;; Datalog has no direct way of expressing dynamic conjunction (logical AND / 'for all ...' / set intersection).
+    ;; However, you can achieve it in pure Datalog by combining one disjunction
+    ;; (logical OR / 'exists ...' / set union) and two negations, i.e
+    ;; (For all ?g in ?Gs p(?e,?g)) <=> NOT(Exists ?g in ?Gs, such that NOT(p(?e, ?g)))
+
+    ;; [(matches-all ?e ?a ?vs)
+    ;;  [(first ?vs) ?v0]
+    ;;  [?e ?a ?v0]
+    ;;  (not-join [?e ?vs]
+    ;;            [(identity ?vs) [?v ...]]
+    ;;            (not-join [?e ?v]
+    ;;                      [?e ?a ?v]))]
+])
+
 (defn transact-files-db!
   ([tx-data]
    (db-utils/transact! (state/get-current-repo) tx-data))
@@ -48,60 +72,46 @@
        react
        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
   [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
   [repo]
   (d/q '[:find ?page-name ?tag
          :where
          [?page :page/tags ?e]
-         [?e :tag/name ?tag]
-         [_ :page/name ?tag]
+         [?e :page/name ?tag]
          [?page :page/name ?page-name]]
        (conn/get-conn repo)))
 
 (defn get-pages
   [repo]
   (->> (d/q
-        '[:find ?page-name
+        '[:find ?page-original-name
           :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))
        (map first)))
 
 (defn get-modified-pages
   [repo]
-  (d/q
-   '[:find ?page-name ?modified-at
-     :where
-     [?page :page/original-name ?page-name]
-     [(get-else $ ?page :page/last-modified-at 0) ?modified-at]]
-   (conn/get-conn repo)))
+  (-> (d/q
+       '[:find ?page-name
+         :where
+         [?page :page/original-name ?page-name]]
+       (conn/get-conn repo))
+      (db-utils/seq-flatten)))
 
 (defn get-page-alias
   [repo page-name]
@@ -141,13 +151,15 @@
   [repo]
   (when-let [conn (conn/get-conn repo)]
     (->> (d/q
-          '[:find ?path ?modified-at
+          '[:find ?path
+             ;; ?modified-at
             :where
             [?file :file/path ?path]
-            [(get-else $ ?file :file/last-modified-at 0) ?modified-at]]
+            ;; [?file :file/last-modified-at ?modified-at]
+]
           conn)
          (seq)
-         (sort-by last)
+         ;; (sort-by last)
          (reverse))))
 
 (defn get-files-blocks
@@ -217,7 +229,7 @@
 (defn set-file-last-modified-at!
   [repo path last-modified-at]
   (when (and repo path last-modified-at)
-    (when-let [conn (conn/get-files-conn repo)]
+    (when-let [conn (conn/get-conn repo false)]
       (d/transact! conn
                    [{:file/path path
                      :file/last-modified-at last-modified-at}]))))
@@ -225,7 +237,7 @@
 (defn get-file-last-modified-at
   [repo path]
   (when (and repo path)
-    (when-let [conn (conn/get-files-conn repo)]
+    (when-let [conn (conn/get-conn repo false)]
       (-> (d/entity (d/db conn) [:file/path path])
           :file/last-modified-at))))
 
@@ -272,7 +284,8 @@
 
 (defn get-custom-css
   []
-  (get-file "logseq/custom.css"))
+  (when-let [repo (state/get-current-repo)]
+    (get-file (config/get-file-path repo "logseq/custom.css"))))
 
 (defn get-file-no-sub
   ([path]
@@ -280,20 +293,15 @@
   ([repo path]
    (when (and repo path)
      (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
-  [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
   [page-name]
@@ -323,12 +331,29 @@
      (set)
      (set/union #{page-id}))))
 
+(defn get-page-names-by-ids
+  ([ids]
+   (get-page-names-by-ids (state/get-current-repo) ids))
+  ([repo ids]
+   (when repo
+     (->> (db-utils/pull-many repo '[:page/name] ids)
+          (map :page/name)))))
+
+(defn get-page-ids-by-names
+  ([names]
+   (get-page-ids-by-names (state/get-current-repo) names))
+  ([repo names]
+   (when repo
+     (let [lookup-refs (map (fn [name]
+                              [:page/name (string/lower-case name)]) names)]
+       (->> (db-utils/pull-many repo '[:db/id] lookup-refs)
+            (mapv :db/id))))))
+
 (defn get-page-alias-names
   [repo page-name]
   (let [alias-ids (page-alias-set repo page-name)]
     (when (seq alias-ids)
-      (->> (db-utils/pull-many repo '[:page/name] alias-ids)
-           (map :page/name)
+      (->> (get-page-names-by-ids repo alias-ids)
            distinct
            (remove #(= (string/lower-case %) (string/lower-case page-name)))))))
 
@@ -361,7 +386,7 @@
 (defn sort-blocks
   [blocks]
   (let [pages-ids (map (comp :db/id :block/page) blocks)
-        pages (db-utils/pull-many '[:db/id :page/last-modified-at :page/name :page/original-name] pages-ids)
+        pages (db-utils/pull-many '[:db/id :page/name :page/original-name :page/journal-day] pages-ids)
         pages-map (reduce (fn [acc p] (assoc acc (:db/id p) p)) {} pages)
         blocks (map
                 (fn [block]
@@ -484,7 +509,8 @@
 (defn get-block-parent
   [repo block-id]
   (when-let [conn (conn/get-conn repo)]
-    (d/entity conn [:block/children [:block/uuid block-id]])))
+    (when-let [block (d/entity conn [:block/uuid block-id])]
+      (d/entity conn [:block/children [:block/uuid block-id]]))))
 
 ;; non recursive query
 (defn get-block-parents
@@ -548,8 +574,15 @@
              (string/join "\n"))
 
         :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))))
 
@@ -571,17 +604,12 @@
   (when-let [conn (conn/get-conn repo)]
     (let [eid (:db/id (db-utils/entity repo [:block/uuid block-uuid]))]
       (->> (d/q
-            '[:find ?e1
-              :in $ ?e2 %
-              :where (parent ?e2 ?e1)]
+            '[:find ?c
+              :in $ ?p %
+              :where (parent ?p ?c)]
             conn
             eid
-             ;; recursive rules
-            '[[(parent ?e2 ?e1)
-               [?e2 :block/children ?e1]]
-              [(parent ?e2 ?e1)
-               [?t :block/children ?e1]
-               (parent ?e2 ?t)]])
+            rules)
            (apply concat)))))
 
 (defn get-block-immediate-children
@@ -704,30 +732,40 @@
     (db-utils/entity [:block/uuid (uuid page-name)])
     (db-utils/entity [:page/name page-name])))
 
+(defn- heading-block?
+  [block]
+  (and
+   (vector? block)
+   (= "Heading" (first block))))
+
 (defn get-page-name
   [file ast]
   ;; headline
   (let [ast (map first ast)]
-    (if (util/starts-with? file "pages/contents.")
+    (if (string/includes? file "pages/contents.")
       "Contents"
-      (let [first-block (last (first (filter block/heading-block? ast)))
+      (let [first-block (last (first (filter heading-block? ast)))
             property-name (when (and (= "Properties" (ffirst ast))
                                      (not (string/blank? (:title (last (first ast))))))
                             (:title (last (first ast))))
-            first-block-name (and first-block
-                                  ;; FIXME:
-                                  (str (last (first (:title first-block)))))
+            first-block-name (let [title (last (first (:title first-block)))]
+                               (and first-block
+                                    (string? title)
+                                    title))
             file-name (when-let [file-name (last (string/split file #"/"))]
-                        (when-let [file-name (first (util/split-last "." file-name))]
-                          (-> file-name
-                              (string/replace "-" " ")
-                              (string/replace "_" " ")
-                              (util/capitalize-all))))]
+                        (first (util/split-last "." file-name)))]
         (or property-name
             (if (= (state/page-name-order) "file")
               (or file-name first-block-name)
               (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
   [utf8-content block]
   (let [meta (:block/meta block)]
@@ -812,6 +850,10 @@
      (db-utils/seq-flatten)
      (distinct))))
 
+(defn page-empty?
+  [repo page]
+  (nil? (:page/file (db-utils/entity repo [:page/name (string/lower-case page)]))))
+
 (defn get-pages-relation
   [repo with-journal?]
   (when-let [conn (conn/get-conn repo)]
@@ -854,6 +896,15 @@
                                db-utils/seq-flatten)]
       (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
   ([page]
    (get-page-referenced-blocks (state/get-current-repo) page))
@@ -884,14 +935,20 @@
                                        :where
                                        [?block :block/ref-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
   [journal-title]
@@ -909,9 +966,7 @@
              react
              db-utils/seq-flatten
              sort-blocks
-             db-utils/group-by-page
-             (remove (fn [[page _blocks]]
-                       (= journal-title (:page/original-name page)))))))))
+             db-utils/group-by-page)))))
 
 (defn get-files-that-referenced-page
   [page-id]
@@ -953,7 +1008,9 @@
                                 ref-pages
                                 pages))))))
              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
   [block-uuid]
@@ -1074,9 +1131,10 @@
   ([cache?]
    (if (and cache? @blocks-count-cache)
      @blocks-count-cache
-     (let [n (count (d/datoms (conn/get-conn) :avet :block/uuid))]
-       (reset! blocks-count-cache n)
-       n))))
+     (when-let [conn (conn/get-conn)]
+       (let [n (count (d/datoms conn :avet :block/uuid))]
+        (reset! blocks-count-cache n)
+        n)))))
 
 ;; block/uuid and block/content
 (defn get-all-block-contents
@@ -1103,10 +1161,9 @@
 
 (defn filter-only-public-pages-and-blocks
   [db]
-  (let [public-pages (get-public-pages db)
-        contents-id (:db/id (db-utils/entity [:page/name "contents"]))]
+  (let [public-pages (get-public-pages db)]
     (when (seq public-pages)
-      (let [public-pages (set (conj public-pages contents-id))
+      (let [public-pages (set public-pages)
             page-or-block? #(contains? #{"page" "block" "me" "recent" "file"} %)
             filtered-db (d/filter db
                                   (fn [db datom]
@@ -1159,3 +1216,53 @@
                    (remove nil?))]
     (when (seq 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]
             [lambdaisland.glogi :as log]
             [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
   [input]
@@ -34,42 +36,65 @@
           days (util/parse-int (subs input 0 (dec (count input))))]
       (date->int (t/plus (t/today) (t/days days))))
 
+    (and (string? input) (text/page-ref? input))
+    (-> (text/page-ref-un-brackets! input)
+        (string/lower-case))
+
     :else
     input))
 
 (defn custom-query-result-transform
   [query-result remove-blocks q]
-  (let [repo (state/get-current-repo)
-        result (db-utils/seq-flatten query-result)
-        block? (:block/uuid (first result))]
-    (let [result (if block?
-                   (let [result (if (seq remove-blocks)
-                                  (let [remove-blocks (set remove-blocks)]
-                                    (remove (fn [h]
-                                              (contains? remove-blocks (:block/uuid h)))
-                                            result))
-                                  result)]
-                     (some->> result
-                              (db-utils/with-repo repo)
-                              (model/with-block-refs-count repo)
-                              (model/sort-blocks)))
-                   result)]
-      (if-let [result-transform (:result-transform q)]
-        (if-let [f (sci/eval-string (pr-str result-transform))]
-          (try
-            (sci/call-fn f result)
-            (catch js/Error e
-              (log/error :sci/call-error e)
-              result))
-          result)
-        (if block?
-          (db-utils/group-by-page result)
-          result)))))
+  (try
+    (let [repo (state/get-current-repo)
+         result (db-utils/seq-flatten query-result)
+         block? (:block/uuid (first result))]
+     (let [result (if block?
+                    (let [result (if (seq remove-blocks)
+                                   (let [remove-blocks (set remove-blocks)]
+                                     (remove (fn [h]
+                                               (contains? remove-blocks (:block/uuid h)))
+                                             result))
+                                   result)]
+                      (some->> result
+                               (db-utils/with-repo repo)
+                               (model/with-block-refs-count repo)
+                               (model/sort-blocks)))
+                    result)]
+       (if-let [result-transform (:result-transform q)]
+         (if-let [f (sci/eval-string (pr-str result-transform))]
+           (try
+             (sci/call-fn f result)
+             (catch js/Error e
+               (log/error :sci/call-error e)
+               result))
+           result)
+         (if block?
+           (db-utils/group-by-page result)
+           result))))
+    (catch js/Error e
+      (log/error :query/failed e))))
+
+(defn- resolve-query
+  [query]
+  (let [page-ref? #(and (string? %) (text/page-ref? %))]
+    (walk/postwalk
+     (fn [f]
+       (if (and (list? f)
+                (= (first f) '=)
+                (= 3 (count f))
+                (some page-ref? (rest f)))
+         (let [[x y] (rest f)
+               [page-ref sym] (if (page-ref? x) [x y] [y x])
+               page-ref (string/lower-case page-ref)]
+           (list 'contains? sym (text/page-ref-un-brackets! page-ref)))
+         f)) query)))
 
 (defn react-query
   [repo {:keys [query inputs] :as query'} query-opts]
   (try
-    (let [inputs (map resolve-input inputs)
+    (let [query (resolve-query query)
+          inputs (map resolve-input inputs)
           repo (or repo (state/get-current-repo))
           k [:custom query']]
       (apply react/q repo k query-opts query inputs))

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

@@ -13,7 +13,9 @@
             [frontend.util :as util]
             [medley.core :as medley]
             [clojure.walk :as walk]
-            [clojure.core]))
+            [clojure.core]
+            [clojure.set :as set]
+            [frontend.template :as template]))
 
 ;; Query fields:
 
@@ -28,6 +30,7 @@
 ;; property (block)
 ;; todo (block)
 ;; priority (block)
+;; page
 ;; page-property (page)
 ;; page-tags (page)
 ;; all-page-tags
@@ -119,12 +122,12 @@
                  t/days)]
         (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
   ([repo e env]
@@ -135,17 +138,16 @@
          page-ref? (text/page-ref? e)]
      (when (or (and page-ref?
                     (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))
      (cond
        (nil? e)
        nil
 
        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)
        (let [clauses (->> (map (fn [form]
@@ -209,8 +211,8 @@
              (when (and start end)
                (let [[start end] (sort [start end])
                      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 end)]])))))
 
@@ -218,9 +220,11 @@
             (= 3 (count e)))
        (let [v (some-> (name (nth e 2))
                        (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
            'or
            [(list '= sym v)]
@@ -228,8 +232,8 @@
 
        (and (= 'property fe)
             (= 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)
        (let [markers (if (coll? (first (rest e)))
@@ -267,6 +271,11 @@
                                                   comp))))
              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)
        (let [[k v] (rest e)]
          (if v
@@ -283,18 +292,21 @@
             [(list 'get '?prop (keyword (nth e 1)))]]))
 
        (= '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)
-       [['?t :tag/name '?tag]
-        ['?p :page/name '?tag]]
+       [['?e :page/tags '?p]]
 
        :else
        nil))))
@@ -315,6 +327,26 @@
                                                  (string/join " ")
                                                  (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
   [repo s]
   (when (and (string? s)
@@ -332,18 +364,13 @@
               result (when (seq result)
                        (let [key (if (coll? (first 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
            :sort-by @sort-by
            :blocks? (boolean @blocks?)})
@@ -352,8 +379,9 @@
 
 (defn query
   [repo query-string]
-  (when query-string
-    (let [{:keys [query sort-by blocks?]} (parse repo query-string)]
+  (when (string? query-string)
+    (let [query-string (template/resolve-dynamic-template! query-string)
+          {:keys [query sort-by blocks?]} (parse repo query-string)]
       (when query
         (let [query (query-wrapper query blocks?)]
           (query-custom/react-query repo

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

@@ -165,7 +165,6 @@
   []
   (let [match (:route-match @state/state)
         route-name (get-in match [:data :name])
-        tag? (= route-name :tag)
         page (case route-name
                :page
                (get-in match [:path-params :name])
@@ -173,15 +172,10 @@
                :file
                (get-in match [:path-params :path])
 
-               :tag
-               (get-in match [:path-params :name])
-
                (date/journal-name))]
     (when 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
   []
@@ -297,7 +291,7 @@
                               (conn/get-files-conn repo-url)
                               (conn/get-conn repo-url false)))]
         (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)
                 handler-keys (get-handler-keys handler-opts)]
             (doseq [handler-key handler-keys]
@@ -345,18 +339,3 @@
      (-> (q repo-url [:kv key] {} key key)
          react
          key))))
-
-(defn set-file-content!
-  [repo path content]
-  (when (and repo path)
-    (let [tx-data {:file/path path
-                   :file/content content
-                   :file/last-modified-at (util/time-ms)}
-          tx-data (if (config/local-db? repo)
-                    (dissoc tx-data :file/last-modified-at)
-                    tx-data)]
-      (transact-react!
-       repo
-       [tx-data]
-       {:key [:file/content path]
-        :files-db? true}))))

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

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

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

@@ -5,7 +5,6 @@
 (def files-db-schema
   {:file/path {:db/unique :db.unique/identity}
    :file/content {}
-   :file/last-modified-at {}
    :file/size {}
    :file/handle {}})
 
@@ -16,7 +15,8 @@
   {:schema/version  {}
    :db/type         {}
    :db/ident        {:db/unique :db.unique/identity}
-
+   :db/encrypted?    {}
+   :db/encryption-keys {}
    ;; user
    :me/name  {}
    :me/email {}
@@ -50,8 +50,6 @@
                      :db/cardinality :db.cardinality/many}
    :page/journal?   {}
    :page/journal-day {}
-   :page/created-at {}
-   :page/last-modified-at {}
 
    ;; block
    :block/uuid   {:db/unique      :db.unique/identity}
@@ -64,6 +62,16 @@
    ;; referenced pages
    :block/ref-pages {:db/valueType   :db.type/ref
                      :db/cardinality :db.cardinality/many}
+   ;; referenced pages inherited from the parents
+   :block/path-ref-pages {:db/valueType   :db.type/ref
+                          :db/cardinality :db.cardinality/many}
+
+   ;; Referenced pages
+   ;; Notice: it's only for org mode, :tag1:tag2:
+   ;; Markdown tags will be only stored in :block/ref-pages
+   :block/tags {:db/valueType   :db.type/ref
+                :db/cardinality :db.cardinality/many}
+
    ;; referenced blocks
    :block/ref-blocks {:db/valueType   :db.type/ref
                       :db/cardinality :db.cardinality/many}
@@ -76,9 +84,6 @@
    :block/marker {}
    :block/priority {}
    :block/level {}
-   :block/tags {:db/valueType   :db.type/ref
-                :db/cardinality :db.cardinality/many
-                :db/isComponent true}
    ;; :start-pos :end-pos
    :block/meta {}
    :block/properties {}
@@ -92,7 +97,4 @@
    :block/scheduled-ast {}
    :block/deadline {}
    :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
 :END:
 ## Logseq is a _privacy-first_, _open-source_ platform for _knowledge_ sharing and management.
-## This is a 3 minutes tutorial on how to use Logseq. Let's get started!
+## This is a 3 minute tutorial on how to use Logseq. Let's get started!
 ## Here are some tips might be useful.
 #+BEGIN_TIP
 Click to edit any block.
@@ -115,6 +115,7 @@ title: How to take dummy notes?
         :on-boarding/sci-desc " - Small Clojure Interpreter"
         :on-boarding/isomorphic-git-desc " - A pure JavaScript implementation of git for node and browsers!"
         :help/about "About Logseq"
+        :help/roadmap "Roadmap"
         :help/bug "Bug report"
         :help/feature "Feature request"
         :help/changelog "Changelog"
@@ -138,10 +139,11 @@ title: How to take dummy notes?
         :help/move-block-down "Move Block Down"
         :help/create-new-block "Create New 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"
         :redo "Redo"
-        :help/zoom-in "Zoom In"
-        :help/zoom-out "Zoom out"
+        :help/zoom-in "Zoom In/Forward"
+        :help/zoom-out "Zoom out/Back"
         :help/follow-link-under-cursor "Follow link under cursor"
         :help/open-link-in-sidebar "Open link in Sidebar"
         :expand "Expand"
@@ -156,8 +158,10 @@ title: How to take dummy notes?
         :help/context-menu "Context Menu"
         :help/fold-unfold "Fold/Unfold blocks (when not in edit mode)"
         :help/toggle-doc-mode "Toggle document mode"
+        :help/toggle-contents "Toggle Contents"
         :help/toggle-theme "Toggle between dark/light theme"
         :help/toggle-right-sidebar "Toggle right sidebar"
+        :help/toggle-settings "Toggle settings"
         :help/toggle-insert-new-block "Toggle Enter/Alt+Enter for inserting new block"
         :help/jump-to-journals "Jump to Journals"
         :formatting "Formatting"
@@ -215,16 +219,18 @@ title: How to take dummy notes?
         :page/presentation-mode "Presentation mode (Powered by Reveal.js)"
         :page/edit-properties-placeholder "Click here to edit this page's properties"
         :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/priority "Priority \"{1}\""
         :page/re-index "Re-index this page"
         :page/copy-to-json "Copy the whole page as JSON"
         :page/rename "Rename page"
+        :page/open-in-finder "Open in directory"
+        :page/open-with-default-app "Open with default app"
         :page/action-publish "Publish"
         :page/make-public "Publish it when exporting to an html file"
         :page/make-private "Make it private"
-        :page/delete "Delete page (will delete the file too)"
+        :page/delete "Delete page"
         :page/publish "Publish this page on Logseq"
         :page/cancel-publishing "Cancel publishing 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/more-options "More options"
         :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/cut "Cut"
         :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-workflow "Preferred workflow"
         :settings-page/enable-timetracking "Enable timetracking"
+        :settings-page/enable-journals "Enable journals"
+        :settings-page/enable-encryption "Enable encryption feature"
+        :settings-page/home-default-page "Set the default home page"
         :settings-page/enable-block-time "Enable block timestamps"
         :settings-page/dont-use-other-peoples-proxy-servers "Don't use other people's proxy servers. It's very dangerous, which could make your token and notes stolen. Logseq will not be responsible for this loss if you use other people's proxy servers. You can deploy it yourself, check "
         :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/disable-developer-mode "Disable developer mode"
         :settings-page/developer-mode-desc "Developer mode helps contributors and extension developers test their integration with Logseq more efficient."
+        :settings-page/current-version "Current version"
         :logseq "Logseq"
         :dot-mode "Dot mode"
         :on "ON"
         :more-options "More options"
         :to "to"
         :yes "Yes"
+        :no "No"
         :submit "Submit"
         :cancel "Cancel"
+        :close "Close"
         :re-index "Re-index"
         :export-json "Export as JSON"
         :unlink "unlink"
@@ -302,9 +317,9 @@ title: How to take dummy notes?
         :new-page "New page"
         :new-file "New file"
         :graph "Graph"
+        :graph-view "View Graph"
         :publishing "Publishing"
         :export "Export public pages"
-        :all-repos "All repos"
         :all-graphs "All graphs"
         :all-pages "All pages"
         :all-files "All files"
@@ -322,6 +337,8 @@ title: How to take dummy notes?
         :parsing-files "Parsing files"
         :loading-files "Loading files"
         :login-github "Login with Github"
+        :login-google "Login with Google"
+        :login "Login"
         :go-to "Go to "
         :or "or"
         :download "Download"
@@ -331,7 +348,239 @@ title: How to take dummy notes?
         :dark "Dark"
         :remove-background "Remove background"
         :open "Open"
-        :open-a-directory "Open a local directory"}
+        :open-a-directory "Open a local directory"
+        :user/delete-account "Delete account"
+        :user/delete-your-account "Delete your account"
+        :user/delete-account-notice "All your published pages on logseq.com will be deleted."}
+
+   :de {:help/about "Über Logseq"
+        :help/bug "Fehlerbericht"
+        :help/feature "Feature-Anfrage"
+        :help/changelog "Änderungsprotokoll"
+        :help/blog "Logseq Blog"
+        :help/docs "Dokumentation"
+        :help/privacy "Datenschutzrichtlinie"
+        :help/terms "Bedingungen"
+        :help/community "Discord-Community"
+        :help/shortcuts "Tastaturkürzel"
+        :help/shortcuts-triggers "Auslöser"
+        :help/shortcut "Tastaturkürzel"
+        :help/slash-autocomplete "/-Autovervollständigung"
+        :help/block-content-autocomplete "Blockinhalt (Quelltext, Zitate, Abfragen, etc.) Autovervollständigung"
+        :help/reference-autocomplete "Seitenverweis Autovervollständigung"
+        :help/block-reference "Blockverweis"
+        :help/key-commands "Tastenbefehle"
+        :help/working-with-lists " (mit Listen arbeiten)"
+        :help/indent-block-tab "Block einrücken"
+        :help/unindent-block "Block ausrücken"
+        :help/move-block-up "Block nach oben verschieben"
+        :help/move-block-down "Block nach unten verschieben"
+        :help/create-new-block "Neuen Block erstellen"
+        :help/new-line-in-block "Neue Zeile innerhalb des Blocks erstellen"
+        :help/select-nfs-browser "Bitte einen anderen Browser verwenden (z. B. den neuesten Chrome), der NFS-Funktionen unterstützt, um lokale Verzeichnisse zu öffnen."
+        :undo "Rückgängig machen"
+        :redo "Wiederholen"
+        :help/zoom-in "Heranzoomen"
+        :help/zoom-out "Herauszoomen"
+        :help/follow-link-under-cursor "Link unter dem Cursor folgen"
+        :help/open-link-in-sidebar "Link in Seitenleiste öffnen"
+        :expand "Erweitern"
+        :collapse "Zusammenklappen"
+        :select-block-above "Block oberhalb auswählen"
+        :select-block-below "Block unterhalb auswählen"
+        :select-all-blocks "Alle Blöcke auswählen"
+        :general "Allgemein"
+        :help/toggle "Hilfe aktivieren"
+        :help/git-commit-message "Git Commit-Nachricht"
+        :help/full-text-search "Volltextsuche"
+        :help/context-menu "Kontextmenü"
+        :help/fold-unfold "Blöcke ein-/ausklappen (wenn nicht im Bearbeitungsmodus)"
+        :help/toggle-doc-mode "Dokumentenmodus umschalten"
+        :help/toggle-theme "Umschalten zwischen dunklem/hellem Thema"
+        :help/toggle-right-sidebar "Rechte Seitenleiste umschalten"
+        :help/toggle-insert-new-block "Umschalten von Enter/Alt+Enter zum Einfügen eines neuen Blocks"
+        :help/jump-to-journals "Zu Journalen springen"
+        :formatting "Formatierung"
+        :help/markdown-syntax "Markdown-Syntax"
+        :help/org-mode-syntax "Org-Mode-Syntax"
+        :bold "Fett"
+        :italics "Kursiv"
+        :html-link "Html-Link"
+        :highlight "Hervorhebung"
+        :strikethrough "Durchstreichung"
+        :code "Quelltext"
+        :right-side-bar/help "Hilfe"
+        :right-side-bar/switch-theme "Zu {1} Thema wechseln"
+        :right-side-bar/theme "{1} Thema"
+        :right-side-bar/page "Seiten-Graph"
+        :right-side-bar/recent "Neueste"
+        :right-side-bar/contents "Inhalt"
+        :right-side-bar/graph-ref "Graph von "
+        :right-side-bar/block-ref "Blockreferenz"
+        :git/set-access-token "Persönliches Github-Zugangs-Token festlegen"
+        :git/token-is-encrypted "Das Token wird verschlüsselt und im lokalen Speicher des Browsers gespeichert"
+        :git/token-server "Auf dem Server wird es niemals gespeichert"
+        :git/create-personal-access-token "Wie erstellt man ein persönliches Github-Zugangs-Token?"
+        :git/push "Jetzt übertragen"
+        :git/push-failed "Übertragung fehlgeschlagen!"
+        :git/local-changes-synced "Alle lokalen Änderungen sind synchronisiert!"
+        :git/pull "Jetzt runterladen"
+        :git/last-pull "Zuletzt runtergeladen am"
+        :git/version "Version"
+        :git/import-notes "Notizen importieren"
+        :git/import-notes-helper "Notizen aus einem Repository auf Github importieren."
+        :git/add-another-repo "Ein weiteres Repository hinzufügen"
+        :git/re-index "Erneut runterladen und Datenbank neu indizieren"
+        :git/message "Ihre Commit-Nachricht"
+        :git/commit-and-push "Commit und Hochladen!"
+        :git/use-remote "Entfernte Version nutzen"
+        :git/keep-local "Lokale Version beibehalten"
+        :git/edit "Bearbeiten"
+        :git/title "Unterschiede"
+        :git/no-diffs "Keine Unterschiede"
+        :git/commit-message "Commit-Nachricht (optional)"
+        :git/pushing "Hochladen"
+        :git/force-push "Commit und Hochladen forcieren"
+        :git/a-force-push "Ein forciertes Hochladen"
+        :git/add-repo-prompt "Logseq im Repository installieren"
+        :git/add-repo-prompt-confirm "Hinzufügen und installieren"
+        :format/preferred-mode "Was ist Ihr bevorzugter Modus?"
+        :format/markdown "Markdown"
+        :format/org-mode "Org-Mode"
+        :reference/linked "Verknüpfte Referenz"
+        :reference/unlinked-ref "Unverknüpfte Referenz"
+        :project/setup "Einrichten eines öffentlichen Projekts auf Logseq"
+        :project/location "Alle veröffentlichten Seiten befinden sich unter"
+        :project/sync-settings "Projekteinstellungen synchronisieren"
+        :page/presentation-mode "Präsentationsmodus (angetrieben von Reveal.js)"
+        :page/edit-properties-placeholder "Klicken Sie hier, um die Eigenschaften dieser Seite zu bearbeiten"
+        :page/delete-success "Die Seite {1} wurde erfolgreich gelöscht!"
+        :page/delete-confirmation "Diese Seite und die zugehörige Datei löschen?"
+        :page/rename-to "\"{1}\" umbenennen nach:"
+        :page/priority "Priorität \"{1}\""
+        :page/re-index "Diese Seite neu indizieren"
+        :page/copy-to-json "Gesamte Seite als JSON kopieren"
+        :page/rename "Seite umbenennen"
+        :page/open-in-finder "Im Verzeichnis öffnen"
+        :page/open-with-default-app "Mit Standard-Anwendung öffnen"
+        :page/action-publish "Veröffentlichen"
+        :page/make-public "Beim Export in HTML veröffentlichen"
+        :page/make-private "Privat machen"
+        :page/delete "Seite löschen"
+        :page/publish "Diese Seite auf Logseq veröffentlichen"
+        :page/cancel-publishing "Veröffentlichung auf Logseq abbrechen"
+        :page/publish-as-slide "Diese Seite als Folie auf Logseq veröffentlichen"
+        :page/unpublish "Veröffentlichung dieser Seite auf Logseq rückgängig machen"
+        :page/add-to-contents "Zum Inhaltsverzeichnis hinzufügen"
+        :page/show-journals "Journal anzeigen"
+        :page/show-name "Seitennamen anzeigen"
+        :page/hide-name "Seitennamen verbergen"
+        :page/name "Seitenname"
+        :page/last-modified "Zuletzt geändert am"
+        :page/new-title "Wie lautet der neue Seitenname?"
+        :publishing/pages "Seiten"
+        :publishing/page-name "Seitenname"
+        :publishing/current-project "Aktuelles Projekt"
+        :publishing/delete-from-logseq "Vom Logseq Server löschen"
+        :publishing/edit "Bearbeiten"
+        :publishing/save "Speichern"
+        :publishing/cancel "Abbrechen"
+        :publishing/delete "Löschen"
+        :journal/multiple-files-with-different-formats "Es scheint, dass Sie mehrere Journaldateien (mit unterschiedlichen Formaten) für denselben Monat haben, bitte führen Sie nur eine Journaldatei für jeden Monat."
+        :journal/go-to "Zu Dateien gehen"
+        :file/name "Dateinamen"
+        :file/file "Datei: "
+        :file/last-modified-at "Zuletzt geändert am"
+        :file/no-data "Keine Daten"
+        :file/format-not-supported "Format .{1} wird nicht unterstützt."
+        :editor/block-search "Nach einem Block suchen"
+        :editor/image-uploading "Hochladen"
+        :draw/invalid-file "Diese ungültige Excalidraw-Datei konnte nicht geladen werden"
+        :draw/specify-title "Bitte zuerst einen Titel angeben!"
+        :draw/rename-success "Die Datei wurde erfolgreich umbenannt!"
+        :draw/rename-failure "Umbennenung der Datei fehlgeschlagen, Grund: "
+        :draw/title-placeholder "Unbenannt"
+        :draw/save "Speichern"
+        :draw/save-changes "Änderungen speichern"
+        :draw/new-file "Neue Datei"
+        :draw/list-files "Dateien auflisten"
+        :draw/delete "Löschen"
+        :draw/more-options "Weitere Optionen"
+        :draw/back-to-logseq "Zurück zu Logseq"
+        :text/image "Bild"
+        :asset/confirm-delete "{1} wirklich löschen?"
+        :asset/physical-delete "Datei ebenfalls entfernen (Achtung: die Datei kann nicht wiederhergestellt werden)"
+        :content/copy "Kopieren"
+        :content/cut "Ausschneiden"
+        :content/make-todos "{1}s erstellen"
+        :content/copy-block-ref "Blockreferenz kopieren"
+        :content/focus-on-block "Auf Block fokussieren"
+        :content/open-in-sidebar "In Seitenleiste öffnen"
+        :content/copy-as-json "Als JSON kopieren"
+        :content/click-to-edit "Klicken zum bearbeiten"
+        :settings-page/edit-config-edn "config.edn bearbeiten (im aktuellen Repository)"
+        :settings-page/show-brackets "Klammern anzeigen"
+        :settings-page/preferred-file-format "Bevorzugtes Datei-Format"
+        :settings-page/preferred-workflow "Bevorzugter Workflow"
+        :settings-page/enable-timetracking "Zeiterfassung einschalten"
+        :settings-page/enable-journals "Journale einschalten"
+        :settings-page/home-default-page "Standard-Homepage einrichten"
+        :settings-page/enable-block-time "Zeitstempel für Blöcke aktivieren"
+        :settings-page/dont-use-other-peoples-proxy-servers "Verwenden Sie keine fremden Proxyserver. Hierbei können das Token und die Notizen gelesen werden. Logseq ist für kann die Sicherheit nicht garantieren, wenn Sie die Proxy-Server anderer Leute verwenden. Sie können selber einen Proxy-Server einrichten, mehr dazu unter "
+        :settings-page/custom-cors-proxy-server "Benutzerdefinierter CORS-Proxy-Server"
+        :settings-page/developer-mode "Entwicklermodus"
+        :settings-page/enable-developer-mode "Entwicklermodus aktivieren"
+        :settings-page/disable-developer-mode "Entwicklermodus deaktivieren"
+        :settings-page/developer-mode-desc "Der Entwicklermodus hilft Mitwirkenden und Erweiterungsentwicklern, ihre Integration mit Logseq effizienter zu testen."
+        :settings-page/current-version "Aktuelle Version"
+        :logseq "Logseq"
+        :dot-mode "Punktmodus"
+        :on "AN"
+        :more-options "Weitere Optionen"
+        :to "zu"
+        :yes "Ja"
+        :submit "Senden"
+        :cancel "Abbrechen"
+        :re-index "Neu-Indizieren"
+        :export-json "Als JSON exportieren"
+        :unlink "Verknüpfung aufheben"
+        :search (if config/publishing?
+              "Suchen"
+              "Suchen oder Seite erstellen")
+        :new-page "Neue Seite"
+        :new-file "Neue Datei"
+        :graph "Graph"
+        :publishing "Veröffentlichung"
+        :export "Öffentliche Seiten exportieren"
+        :all-graphs "Alle Graphen"
+        :all-pages "Alle Seiten"
+        :all-files "Alle Dateien"
+        :all-journals "Alle Journale"
+        :my-publishing "Meine Veröffentlichungen"
+        :settings "Einstellungen"
+        :import "Importieren"
+        :join-community "Community"
+        :sponsor-us "Sponsern"
+        :discord-title "Unsere Discord-Gruppe!"
+        :sign-out "Abmelden"
+        :help-shortcut-title "Hier klicken, um Tastenkombinationen und weitere Tipps zu sehen"
+        :loading "Laden"
+        :cloning "Klonen"
+        :parsing-files "Dateien analysieren"
+        :loading-files "Dateien laden"
+        :login-github "Einloggen mit Github"
+        :login-google "Einloggen mit Google"
+        :login "Einloggen"
+        :go-to "Gehe zu "
+        :or "oder"
+        :download "Herunterladen"
+        :repo/download-zip "Alle Dateien als Zip herunterladen"
+        :language "Sprache"
+        :white "Hell"
+        :dark "Dunkel"
+        :remove-background "Hintergrund entfernen"
+        :open "Öffnen"
+        :open-a-directory "Öffne ein lokales Verzeichnis"}
 
    :fr {:help/about "A propos de Logseq"
         :help/bug "Signaler une anomalie"
@@ -357,6 +606,7 @@ title: How to take dummy notes?
         :help/move-block-down "Déplacer un bloc en dessous"
         :help/create-new-block "Créer un nouveau 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"
         :redo "Redo"
         :help/zoom-in "Zoomer"
@@ -518,7 +768,6 @@ title: How to take dummy notes?
         :new-file "Nouveau fichier"
         :graph "Graphe"
         :publishing "Publication"
-        :all-repos "Tous les répertoires"
         :all-pages "Toutes les pages"
         :all-files "Tous les fichiers"
         :all-journals "Tous les journaux"
@@ -534,6 +783,7 @@ title: How to take dummy notes?
         :parsing-files "Analyse des fichiers"
         :loading-files "Chargement des fichiers"
         :login-github "S'authentifier avec Github"
+        :login-google "S'authentifier avec Google"
         :go-to "Aller à "
         :or "ou"
         :download "Télécharger"
@@ -613,10 +863,14 @@ title: How to take dummy notes?
            :help/move-block-down "向下移动块"
            :help/create-new-block "创建块"
            :help/new-line-in-block "块中新建行"
+           :help/select-nfs-browser "请选择支持nfs的浏览来使用logseq本地文件夹功能, 如最新的Chrome浏览器."
+           :text/image "图片"
+           :asset/confirm-delete "确定要删除{1}吗?"
+           :asset/physical-delete "同时删除本地文件(目前不可撤销)"
            :undo "撤销"
            :redo "重做"
            :help/zoom-in "聚焦"
-           :help/zoom-out "推出聚焦"
+           :help/zoom-out "退出聚焦"
            :help/follow-link-under-cursor "跟随光标下的链接"
            :help/open-link-in-sidebar "在侧边栏打开"
            :expand "展开"
@@ -631,8 +885,10 @@ title: How to take dummy notes?
            :help/context-menu "右键菜单"
            :help/fold-unfold "折叠/展开方块(不在编辑模式中)"
            :help/toggle-doc-mode "切换文档模式"
+           :help/toggle-contents "打开/关闭目录"
            :help/toggle-theme "“在暗色/亮色主题之间切换”"
            :help/toggle-right-sidebar "启用/关闭右侧栏"
+           :help/toggle-settings "显示/关闭设置"
            :help/toggle-insert-new-block "切换 Enter/Alt+Enter 以插入新块"
            :help/jump-to-journals "跳转到日记"
            :formatting "格式化"
@@ -690,16 +946,18 @@ title: How to take dummy notes?
            :page/edit-properties-placeholder "点击这里编辑当前页面的属性 (标签,别名等)"
            :page/presentation-mode "演讲模式 (由 Reveal.js 驱动)"
            :page/delete-success "页面 {1} 删除成功!"
-           :page/delete-confirmation "您确定要删除此页面吗?"
+           :page/delete-confirmation "您确定要删除此页面和文件吗?"
            :page/rename-to "重命名 \"{1}\" 至:"
            :page/priority "优先级 \"{1}\""
            :page/re-index "对此页面重新建立索引"
            :page/copy-to-json "将整页以 JSON 格式复制"
            :page/rename "重命名本页"
+           :page/open-in-finder "打开文件对应目录"
+           :page/open-with-default-app "用默认应用打开文件"
            :page/action-publish "发布"
            :page/make-public "导出 HTML 时发布本页面"
            :page/make-private "导出 HTML 时取消发布本页面"
-           :page/delete "删除本页(并删除文件)"
+           :page/delete "删除本页"
            :page/publish "将本页发布至 Logseq"
            :page/cancel-publishing "撤回本页在 Logseq 上的发布"
            :page/publish-as-slide "将本页作为幻灯片发布至 Logseq"
@@ -753,6 +1011,9 @@ title: How to take dummy notes?
            :settings-page/preferred-file-format "首选文件格式"
            :settings-page/preferred-workflow "首选工作流"
            :settings-page/enable-timetracking "开启 timetracking"
+           :settings-page/enable-journals "开启日记"
+           :settings-page/enable-encryption "激活加密功能"
+           :settings-page/home-default-page "设置首页默认页面"
            :settings-page/enable-block-time "记录 block 创建/修改时间"
            :settings-page/dont-use-other-peoples-proxy-servers "不要使用其他人的代理服务器。这非常危险,可能会使您的令牌和笔记被盗。 如果您使用其他人的代理服务器,Logseq 将不会对此损失负责。您可以自己部署它,请查阅 "
            :settings-page/custom-cors-proxy-server "自定义 CORS 代理服务器"
@@ -760,6 +1021,7 @@ title: How to take dummy notes?
            :settings-page/enable-developer-mode "启用开发者模式"
            :settings-page/disable-developer-mode "禁用开发者模式"
            :settings-page/developer-mode-desc "开发者模式帮助贡献者和扩展开发者更有效地测试他们与 Logseq 的集成。"
+           :settings-page/current-version "当前版本"
            :logseq "Logseq"
            :dot-mode "点模式"
            :on "已打开"
@@ -777,9 +1039,9 @@ title: How to take dummy notes?
            :new-page "新页面"
            :new-file "新文件"
            :graph "图谱"
+           :graph-view "全局图谱"
            :publishing "发布"
            :export "导出公开页面"
-           :all-repos "所有库"
            :all-graphs "所有库"
            :all-pages "所有页面"
            :all-files "所有文件"
@@ -790,12 +1052,14 @@ title: How to take dummy notes?
            :sponsor-us "赞助我们!"
            :discord-title "我们的 Discord 社群!"
            :sign-out "登出"
-           :help-shortcut-title "点此查看快捷方式和更多游泳帮助"
+           :help-shortcut-title "点此查看快捷方式和更多有用帮助"
            :loading "加载中"
            :cloning "Clone 中"
            :parsing-files "正在解析文件"
            :loading-files "正在加载文件"
            :login-github "用 Github 登录"
+           :login-google "用 Google 登录"
+           :login "登录"
            :go-to "转到"
            :or "或"
            :download "下载"
@@ -805,7 +1069,10 @@ title: How to take dummy notes?
            :dark "暗黑"
            :remove-background "去除背景"
            :open "打开"
-           :open-a-directory "打开本地文件夹"}
+           :open-a-directory "打开本地文件夹"
+           :user/delete-account "删除帐号"
+           :user/delete-your-account "删除你的帐号"
+           :user/delete-account-notice "你在 logseq.com 发布的页面(假如有的话)也会被删除。"}
 
    :zh-Hant {:on-boarding/title "你好,歡迎使用 Logseq!"
              :on-boarding/sharing "分享"
@@ -877,6 +1144,7 @@ title: How to take dummy notes?
              :help/move-block-down "向下移動塊"
              :help/create-new-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 "撤銷"
              :redo "重做"
              :help/zoom-in "聚焦"
@@ -1035,7 +1303,6 @@ title: How to take dummy notes?
              :new-page "新頁面"
              :graph "圖譜"
              :publishing "發布/下載 HTML 文件"
-             :all-repos "所有庫"
              :all-pages "所有頁面"
              :all-files "所有文件"
              :my-publishing "My publishing"
@@ -1044,12 +1311,13 @@ title: How to take dummy notes?
              :join-community "加入社區"
              :discord-title "我們的 Discord 社群!"
              :sign-out "登出"
-             :help-shortcut-title "點此查看快捷方式和更多游泳幫助"
+             :help-shortcut-title "點此查看快捷方式和更多有用幫助"
              :loading "加載中"
              :cloning "Clone 中"
              :parsing-files "正在解析文件"
              :loading-files "正在加載文件"
              :login-github "用 Github 登錄"
+             :login-google "用 Google 登錄"
              :go-to "轉到"
              :or "或"
              :download "下載"
@@ -1128,6 +1396,7 @@ title: How to take dummy notes?
         :help/move-block-down "Skuif Blok Ondertoe"
         :help/create-new-block "Skep 'n nuwe 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"
         :redo "Herdoen"
         :help/zoom-in "Zoem in"
@@ -1281,7 +1550,6 @@ title: How to take dummy notes?
         :search "Soek"
         :new-page "Nuwe bladsy"
         :graph "Grafiek"
-        :all-repos "Alle stoorplekke"
         :all-pages "Alle blaaie"
         :all-files "Alle lêers"
         :settings "Verstellings"
@@ -1295,6 +1563,7 @@ title: How to take dummy notes?
         :parsing-files "Lêer ontleding"
         :loading-files "Laai lêers"
         :login-github "Aantekening deur Github"
+        :login-google "Aantekening deur Google"
         :go-to "Gaan na "
         :or "of"
         :download "Laai af"
@@ -1306,6 +1575,7 @@ title: How to take dummy notes?
 
 (def languages [{:label "English" :value :en}
                 {:label "Français" :value :fr}
+                {:label "Deutsch" :value :de}
                 {:label "简体中文" :value :zh-CN}
                 {:label "繁體中文" :value :zh-Hant}
                 {:label "Afrikaans" :value :af}])

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

@@ -1,15 +1,34 @@
 (ns frontend.diff
   (:require [clojure.string :as string]
             ["diff" :as jsdiff]
+            ["diff-match-patch" :as diff-match-patch]
             [goog.object :as gobj]
             [lambdaisland.glogi :as log]
             [cljs-bean.core :as bean]))
 
+;; TODO: replace with diff-match-patch
 (defn diff
   [s1 s2]
   (-> ((gobj/get jsdiff "diffLines") s1 s2)
       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")
 (defn find-position
   [markup text]
@@ -32,6 +51,6 @@
 
           :else
           (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/closebrackets"]
             ["codemirror/mode/clojure/clojure"]
+            ["codemirror/mode/powershell/powershell"]
             ["codemirror/mode/javascript/javascript"]
             ["codemirror/mode/clike/clike"]
             ["codemirror/mode/vue/vue"]

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

@@ -11,7 +11,8 @@
   > .CodeMirror {
     z-index: 0;
     height: auto;
-    margin: 6px 0 0 0;
+    padding: 6px 0 0 0;
     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
                {:embedded true
                 :controls true
-                :history true
+                :history false
                 :center true
                 :transition "slide"}))]
     (.initialize deck)))
 
 (defn slide-content
   [loading? style sections]
-  [:div.reveal {:style style}
-   (when loading?
-     [:div.ls-center (ui/loading "")])
-   [:div.slides
-    (for [[idx sections] (medley/indexed sections)]
-      (if (> (count sections) 1)       ; nested
-        [:section {:key (str "slide-section-" idx)}
-         (for [[idx2 [block block-cp]] (medley/indexed sections)]
-           [:section (-> {:key (str "slide-section-" idx "-" idx2)}
+  [:div
+   [:p.text-sm
+    [:span.opacity-70 "Tip: press "]
+    [:code "F"]
+    [:span.opacity-70 " to go fullscreen"]]
+   [:div.reveal {:style style}
+    (when loading?
+      [:div.ls-center (ui/loading "")])
+    [:div.slides
+     (for [[idx sections] (medley/indexed sections)]
+       (if (> (count sections) 1)       ; nested
+         [:section {:key (str "slide-section-" idx)}
+          (for [[idx2 [block block-cp]] (medley/indexed sections)]
+            [:section (-> {:key (str "slide-section-" idx "-" idx2)}
+                          (with-properties block))
+             block-cp])]
+         (let [[block block-cp] (first sections)]
+           [:section (-> {:key (str "slide-section-" idx)}
                          (with-properties block))
-            block-cp])]
-        (let [[block block-cp] (first sections)]
-          [:section (-> {:key (str "slide-section-" idx)}
-                        (with-properties block))
-           block-cp])))]])
+            block-cp])))]]])
 
 (rum/defc slide < rum/reactive
   {:did-mount (fn [state]

+ 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
   (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:
   ;; (toMldocAst [this content])
   ;; (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
-  (: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]
             [clojure.walk :as walk]
             [clojure.string :as string]
@@ -42,6 +43,15 @@
                                              (util/format "{{%s %s}}" name arg))
                                            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!
   [data]
   (let [full-text (atom "")]
@@ -65,7 +75,8 @@
       (string/replace "{{[[TODO]]}}" "TODO")
       (string/replace "{{[[DONE]]}}" "DONE")
       (uid-transform)
-      (macro-transform)))
+      (macro-transform)
+      (fenced-code-transform)))
 
 (declare children->text)
 (defn child->text
@@ -119,11 +130,15 @@
                    (apply str))))
      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 []
   protocol/External
   (toMarkdownFiles [this content _config]
-    (let [data (bean/->clj (js/JSON.parse content))]
-      (->files data))))
+    (-> content json->edn ->files)))
 
 (comment
   (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]
             [frontend.date :as date]
             [frontend.text :as text]
-            [medley.core :as medley]))
+            [medley.core :as medley]
+            [frontend.state :as state]
+            [frontend.db :as db]))
 
 (defn heading-block?
   [block]
@@ -34,10 +36,12 @@
                    (= typ "Search")
                    ;; FIXME: alert error
                    (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)))
 
                   (and
@@ -141,22 +145,45 @@
       (update "created_at" util/safe-parse-int)
       (update "last_modified_at" util/safe-parse-int)))
 
+(defonce non-parsing-properties
+  (atom #{"background_color"}))
+
 (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]
-                                         (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))]
     {:properties properties
-     :start-pos start-pos
-     :end-pos end-pos}))
+     :page-refs page-refs}))
 
 (defn- paragraph-timestamp-block?
   [block]
@@ -192,10 +219,19 @@
                               (assoc :repeated? true))))))]
     (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
-  [{: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
      (fn [form]
        (when-let [page (get-page-reference form)]
@@ -254,11 +290,42 @@
          (block-keywordize (util/remove-nils block)))
        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
   [blocks last-pos encoded-content]
@@ -293,6 +360,7 @@
                                  (when (util/uuid-string? custom-id)
                                    (uuid custom-id))))
                              (d/squuid))
+                      ref-pages-in-properties (:page-refs properties)
                       block (second block)
                       level (:level block)
                       [children current-block-children]
@@ -313,16 +381,18 @@
                                        :uuid id
                                        :body (vec (reverse block-body))
                                        :properties (:properties properties)
+                                       :ref-pages ref-pages-in-properties
                                        :children (or current-block-children []))
                                 (assoc-in [:meta :start-pos] start_pos)
                                 (assoc-in [:meta :end-pos] last-pos))
                       block (if (seq timestamps)
                               (merge block (timestamps->scheduled-and-deadline timestamps))
                               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])]
                   (recur (conj headings block) [] (rest blocks) {} {} last-pos' (:level block) children))
 
@@ -332,35 +402,36 @@
             (-> (reverse headings)
                 safe-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
   [original-page-name]
   (when 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/original-name original-page-name
          :page/journal? true
@@ -379,10 +450,17 @@
            content-length (utf8/length encoded-content)
            blocks (extract-blocks ast content-length encoded-content)
            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
                    (map-indexed
                     (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
                                    {:block/meta meta
@@ -393,12 +471,10 @@
                                     :block/page page
                                     :block/content (utf8/substring encoded-content
                                                                    (:start-pos meta)
-                                                                   (:end-pos meta))}
+                                                                   (:end-pos meta))
+                                    :block/path-ref-pages path-ref-pages}
                                    ;; 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})
                                    (when (seq ref-pages)
                                      {:block/ref-pages
@@ -443,5 +519,4 @@
 
 (defn trim-break-lines!
   [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)))
       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
   [ast]
   (if (seq ast)
@@ -75,9 +86,10 @@
           properties (->> (take-while directive? ast)
                           (map (fn [[_ k v]]
                                  (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
-                                           (text/split-page-refs-without-brackets v))]
+                                           (text/split-page-refs-without-brackets v comma?))]
                                    [k v])))
                           (into {}))
           macro-properties (filter (fn [x] (= :macro (first x))) properties)
@@ -94,23 +106,33 @@
                    {})
           properties (->> (remove (fn [x] (= :macro (first x))) properties)
                           (into {}))
-          properties (if (:roam_alias properties)
-                       (assoc properties :alias (:roam_alias properties))
-                       properties)
           properties (if (seq properties)
                        (cond-> properties
                          (:roam_key properties)
                          (assoc :key (:roam_key properties)))
                        properties)
           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
                        (seq 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)]
       (if (seq properties)
         (cons [["Properties" properties] nil] other-ast)
@@ -161,6 +183,7 @@
   (let [ast (->> (->edn content
                         (default-config format))
                  (map first))
+        properties (collect-page-properties ast)
         properties (let [properties (and (seq ast)
                                          (= "Properties" (ffirst 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
   (:require [frontend.util :as util :refer-macros [profile]]
             [frontend.config :as config]
-            [frontend.state :as state]
             [clojure.string :as string]
-            [frontend.idb :as idb]
-            [frontend.db :as db]
             [promesa.core :as p]
-            [goog.object :as gobj]
-            [clojure.set :as set]
             [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?
   [dir]
   (and (string? dir)
        (config/local-db? (subs dir 1))))
 
-(defn mkdir
+(defn get-fs
   [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]
-  (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]
-  (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]
-  (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
   ([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]
   (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
-    (js/window.pfs.rename old-path new-path)))
+    (protocol/rename! (get-fs old-path) repo old-path new-path)))
 
 (defn stat
   [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
   [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
   ([repo dir path]
@@ -294,11 +142,11 @@
                 path
                 (str "/" path))]
      (->
-      (p/let [_ (stat dir path)]
+      (p/let [stat (stat dir path)]
         true)
       (p/catch
        (fn [_error]
-         (p/let [_ (write-file repo dir path initial-content)]
+         (p/let [_ (write-file! repo dir path initial-content nil)]
            false)))))))
 
 (defn file-exists?
@@ -307,10 +155,3 @@
    (stat dir path)
    (fn [_stat] true)
    (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)))))

部分文件因文件數量過多而無法顯示