Przeglądaj źródła

Merge remote-tracking branch 'upstream/master' into refacotor/add-cljs-env

defclass 5 lat temu
rodzic
commit
8d868bf842
100 zmienionych plików z 3734 dodań i 1445 usunięć
  1. 280 0
      .github/workflows/build-desktop-release.yml
  2. 3 0
      .gitignore
  3. 41 2
      README.md
  4. 10 8
      deps.edn
  5. 66 0
      docs/assets/jetbrains.svg
  6. 2 0
      externs.js
  7. 46 1
      gulpfile.js
  8. 21 7
      package.json
  9. 0 0
      public/index.html
  10. 7 7
      resources/css/common.css
  11. 39 39
      resources/css/inter.css
  12. 2 2
      resources/css/style.css
  13. 25 0
      resources/dev.html
  14. 108 0
      resources/electron-dev.html
  15. 107 0
      resources/electron.html
  16. 12 0
      resources/entitlements.plist
  17. 51 0
      resources/forge.config.js
  18. BIN
      resources/icons/logseq.icns
  19. BIN
      resources/icons/logseq.ico
  20. BIN
      resources/icons/logseq.png
  21. BIN
      resources/icons/logseq_big_sur.icns
  22. BIN
      resources/icons/logseq_big_sur.png
  23. BIN
      resources/img/dmg-bg.png
  24. 176 0
      resources/js/isomorphic-git/1.7.4/http-web-index.umd.js
  25. 0 0
      resources/js/isomorphic-git/1.7.4/index.umd.min.js
  26. 124 0
      resources/js/preload.js
  27. 4 4
      resources/js/worker.js
  28. 33 0
      resources/package.json
  29. 9 2
      shadow-cljs.edn
  30. 5 0
      src/dev-cljs/shadow/user.clj
  31. 129 0
      src/electron/electron/core.cljs
  32. 145 0
      src/electron/electron/handler.cljs
  33. 119 0
      src/electron/electron/updater.cljs
  34. 11 0
      src/electron/electron/utils.cljs
  35. 9 0
      src/main/electron/ipc.cljs
  36. 7 1
      src/main/frontend/commands.cljs
  37. 166 98
      src/main/frontend/components/block.cljs
  38. 56 0
      src/main/frontend/components/block.css
  39. 1 1
      src/main/frontend/components/diff.cljs
  40. 104 90
      src/main/frontend/components/editor.cljs
  41. 3 3
      src/main/frontend/components/file.cljs
  42. 116 111
      src/main/frontend/components/header.cljs
  43. 5 0
      src/main/frontend/components/header.css
  44. 5 4
      src/main/frontend/components/journal.cljs
  45. 16 15
      src/main/frontend/components/page.cljs
  46. 11 5
      src/main/frontend/components/repo.cljs
  47. 47 52
      src/main/frontend/components/right_sidebar.cljs
  48. 94 50
      src/main/frontend/components/settings.cljs
  49. 44 0
      src/main/frontend/components/settings.css
  50. 2 1
      src/main/frontend/components/sidebar.cljs
  51. 7 2
      src/main/frontend/components/sidebar.css
  52. 43 1
      src/main/frontend/components/svg.cljs
  53. 13 10
      src/main/frontend/components/theme.cljs
  54. 116 2
      src/main/frontend/components/theme.css
  55. 19 19
      src/main/frontend/components/widgets.cljs
  56. 64 0
      src/main/frontend/config.cljs
  57. 1 1
      src/main/frontend/core.cljs
  58. 1 15
      src/main/frontend/date.cljs
  59. 3 2
      src/main/frontend/db.cljs
  60. 48 19
      src/main/frontend/db/model.cljs
  61. 0 15
      src/main/frontend/db/react.cljs
  62. 1 2
      src/main/frontend/db/utils.cljs
  63. 0 3
      src/main/frontend/db_schema.cljs
  64. 14 2
      src/main/frontend/dicts.cljs
  65. 1 0
      src/main/frontend/extensions/code.cljs
  66. 20 15
      src/main/frontend/extensions/slide.cljs
  67. 0 0
      src/main/frontend/external.cljc
  68. 2 2
      src/main/frontend/external/protocol.cljc
  69. 9 4
      src/main/frontend/external/roam.cljc
  70. 86 266
      src/main/frontend/fs.cljs
  71. 40 0
      src/main/frontend/fs/bfs.cljs
  72. 197 0
      src/main/frontend/fs/nfs.cljs
  73. 49 0
      src/main/frontend/fs/node.cljs
  74. 14 0
      src/main/frontend/fs/protocol.cljs
  75. 68 0
      src/main/frontend/fs/watcher_handler.cljs
  76. 19 18
      src/main/frontend/git.cljs
  77. 6 2
      src/main/frontend/handler.cljs
  78. 30 40
      src/main/frontend/handler/common.cljs
  79. 1 1
      src/main/frontend/handler/config.cljs
  80. 4 101
      src/main/frontend/handler/dnd.cljs
  81. 14 17
      src/main/frontend/handler/draw.cljs
  82. 123 83
      src/main/frontend/handler/editor.cljs
  83. 2 4
      src/main/frontend/handler/extract.cljs
  84. 46 20
      src/main/frontend/handler/file.cljs
  85. 3 2
      src/main/frontend/handler/git.cljs
  86. 1 1
      src/main/frontend/handler/graph.cljs
  87. 38 37
      src/main/frontend/handler/image.cljs
  88. 23 22
      src/main/frontend/handler/page.cljs
  89. 29 30
      src/main/frontend/handler/repo.cljs
  90. 4 1
      src/main/frontend/handler/route.cljs
  91. 149 120
      src/main/frontend/handler/web/nfs.cljs
  92. 1 0
      src/main/frontend/page.cljs
  93. 3 2
      src/main/frontend/publishing.cljs
  94. 2 2
      src/main/frontend/publishing/html.cljs
  95. 0 0
      src/main/frontend/regex.cljc
  96. 0 12
      src/main/frontend/security.cljs
  97. 20 5
      src/main/frontend/state.cljs
  98. 15 7
      src/main/frontend/text.cljs
  99. 90 35
      src/main/frontend/ui.cljs
  100. 34 0
      src/main/frontend/ui.css

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

@@ -0,0 +1,280 @@
+# 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 Artifact Name
+        run: mv static/out/make/Logseq.dmg static/out/make/Logseq-x64.dmg
+
+      - name: Cache Artifact
+        uses: actions/upload-artifact@v1
+        with:
+          name: Logseq-x64.dmg
+          path: static/out/make/Logseq-x64.dmg
+
+  release:
+    needs: [ build-macos, build-linux, build-windows ]
+    runs-on: ubuntu-latest
+
+    steps:
+      - name: Download The MacOS X64 Artifact
+        uses: actions/download-artifact@v1
+        with:
+          name: Logseq-x64.dmg
+          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 Artifact
+        id: upload-macos-x64-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-x64.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

+ 3 - 0
.gitignore

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

+ 41 - 2
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 a freedom-respecting (and open-source too) 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 not be open-sourced for security reasons and other potential risks.
 
 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).
 
@@ -68,7 +69,7 @@ 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
 
@@ -88,6 +89,40 @@ Open <http://localhost:3001>.
 yarn release 
 ```
 
+### 5. Run tests
+
+Run ClojureScript tests
+```bash
+yarn
+yarn cljs:test
+node static/tests.js
+```
+
+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 debug app
+
+``` bash
+yarn debug-electron
+```
+
+### 3. Build a release
+
+``` bash
+yarn release-electron
+```
+
 ## Alternative: Docker based development environment
 
 ### 1. Fetch sources
@@ -116,3 +151,7 @@ cd logseq
 yarn
 yarn watch
 ```
+
+## Thanks
+
+[![JetBrains](docs/assets/jetbrains.svg)](https://www.jetbrains.com/?from=logseq)

+ 10 - 8
deps.edn

@@ -1,6 +1,7 @@
 {:paths ["src/main" "resources"]
  :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"}
@@ -28,14 +29,13 @@
   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/"]
+ :aliases {:cljs {:extra-paths ["src/dev-cljs/" "src/test/" "src/electron/"]
                   :extra-deps  {org.clojure/clojurescript   {:mvn/version "1.10.764"}
-                                thheller/shadow-cljs        {:mvn/version "2.8.81"}
-                                binaryage/devtools          {:mvn/version "1.0.2"}
                                 org.clojure/tools.namespace {:mvn/version "0.2.11"}
                                 cider/cider-nrepl           {:mvn/version "0.25.5"}
                                 aero/aero                   {:mvn/version "1.1.6"}
@@ -48,9 +48,11 @@
                           aero/aero                   {:mvn/version "1.1.6"}
                           mhuebert/shadow-env         {:mvn/version "0.1.6"}}
             :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"]}}}

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

+ 2 - 0
externs.js

@@ -17,7 +17,9 @@ dummy.getRangeAt = function() {};
 dummy.getElementsByClassName = function() {};
 dummy.containsNode = function() {};
 dummy.select = function() {};
+dummy.closest = function () {};
 dummy.setAttribute = function() {};
+dummy.getAttribute = function() {};
 dummy.font = function() {};
 dummy.measureText = function() {};
 dummy.fillStyle = function() {};

+ 46 - 1
gulpfile.js

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

+ 21 - 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",
@@ -25,20 +26,26 @@
     },
     "scripts": {
         "watch": "run-p gulp:build gulp:watch cljs:watch",
+        "electron-watch": "run-p gulp:build gulp:watch cljs:electron-watch",
         "release": "ENV=prod run-s gulp:build cljs:release",
         "release-staging": "ENV=staging 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",
+        "release-app": "ENV=prod run-s gulp:build cljs:release-app",
+        "release-publishing": "ENV=prod run-s gulp:build cljs:release-publishing",
+        "dev-release-app": "ENV=prod run-s gulp:build cljs:dev-release-app",
+        "dev-electron-app": "gulp electron",
+        "release-electron": "gulp build && ENV=prod 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",
@@ -46,22 +53,29 @@
         "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": {
+        "chokidar": "^3.5.1",
         "codemirror": "^5.58.1",
         "diff": "5.0.0",
         "diff-match-patch": "^1.0.5",
+        "electron": "^11.2.0",
+        "fs": "^0.0.1-security",
         "fuzzysort": "^1.1.4",
         "gulp-cached": "^1.1.1",
         "ignore": "^5.1.8",
         "jszip": "^3.5.0",
-        "mldoc": "^0.3.0",
+        "mldoc": "^0.3.7",
         "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"
     }
 }

Plik diff jest za duży
+ 0 - 0
public/index.html


+ 7 - 7
resources/css/common.css

@@ -61,8 +61,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);
@@ -578,11 +578,6 @@ h1.title {
   padding: -1px;
 }
 
-.content img {
-  margin-top: 0.5rem;
-  margin-bottom: 0.5rem;
-}
-
 span.timestamp {
   margin: 0 0.25rem;
 }
@@ -711,3 +706,8 @@ hr {
   margin: 2rem 0;
   border-color: var(--ls-border-color, #ccc);
 }
+
+.resize {
+    resize: both;
+    overflow: hidden;
+}

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

+ 108 - 0
resources/electron-dev.html

@@ -0,0 +1,108 @@
+<!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 src="./js/main.js"></script>
+<script src="./js/highlight.min.js"></script>
+<script src="./js/code-editor.js"></script>
+</body>
+</html>

+ 107 - 0
resources/electron.html

@@ -0,0 +1,107 @@
+<!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">
+</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 src="./js/main.js"></script>
+<script src="./js/highlight.min.js"></script>
+<script 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>

+ 51 - 0
resources/forge.config.js

@@ -0,0 +1,51 @@
+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'
+      }
+    },
+    {
+      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
+      }
+    }
+  ]
+}

BIN
resources/icons/logseq.icns


BIN
resources/icons/logseq.ico


BIN
resources/icons/logseq.png


BIN
resources/icons/logseq_big_sur.icns


BIN
resources/icons/logseq_big_sur.png


BIN
resources/img/dmg-bg.png


+ 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 });
+
+})));

Plik diff jest za duży
+ 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"
+  }
+}

+ 9 - 2
shadow-cljs.edn

@@ -15,7 +15,7 @@
    :asset-path "/static/js"
    :release {:asset-path "https://asset.logseq.com/static/js"}
    :compiler-options {:infer-externs :auto
-                      :output-feature-set :es6
+                      :output-feature-set :es-next
                       :source-map true
                       :externs ["datascript/externs.js"
                                 "externs.js"]
@@ -34,6 +34,13 @@
     :preloads     [devtools.preload]}
    :build-hooks [(shadow-env.core/hook)]}
 
+  :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"
@@ -56,7 +63,7 @@
                      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"]
                       :warnings {:fn-deprecated false}}

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

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

@@ -0,0 +1,129 @@
+(ns electron.core
+  (:require [electron.handler :as handler]
+            [electron.updater :refer [init-updater]]
+            [electron.utils :refer [mac? win32? prod? dev? log 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?
+                  :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))]
+    (when win32? (.removeMenu win))
+    (.loadURL win url)
+    (when dev? (.. win -webContents (openDevTools)))
+    win))
+
+(defn setup-updater! [^js win]
+  ;; manual updater
+  (init-updater {:repo   "logseq/logseq"
+                 :logger log
+                 :win    win}))
+
+(defn setup-interceptor! []
+  (.registerFileProtocol
+   protocol "assets"
+   (fn [^js request callback]
+     (let [url (.-url request)
+           path (string/replace url "assets://" "")]
+       (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]
+           (.. log (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)]
+
+           (.. log (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)))

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

@@ -0,0 +1,145 @@
+(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]]
+  (fs/writeFileSync path content))
+
+(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 [window _messages]
+  (let [result (.showOpenDialogSync dialog (bean/->js
+                                            {:properties ["openDirectory"]}))
+        path (first result)]
+    (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]
+  (let [watcher (.watch watcher dir
+                        (clj->js
+                         {:ignored (fn [path]
+                                     (some #(string/starts-with? path (str dir "/" %))
+                                           ["." "assets" "node_modules"]))
+                          ;; :ignoreInitial true
+                          :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)))

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

@@ -0,0 +1,119 @@
+(ns electron.updater
+  (:require [electron.utils :refer [mac? win32? prod? open fetch]]
+            [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))
+
+;Event: 'error'
+;Event: 'checking-for-update'
+;Event: 'update-available'
+;Event: 'update-not-available'
+;Event: 'download-progress'
+;Event: 'update-downloaded'
+;Event: 'completed'
+
+(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 "/" (if mac? "darwin" "win32") "-x64/" version)]
+    (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 logger ^js win]
+    [auto-download] :args}]
+  (let [debug (partial (.-warn logger) "[updater]")
+        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 log (js/require "electron-log"))
+
+(defonce open (js/require "open"))
+(defonce fetch (js/require "node-fetch"))

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

+ 7 - 1
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,7 +124,12 @@
                   [: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" [[:editor/click-hidden-file-input :id]]]
+
+       (state/logged?)
        ["Upload an image" [[:editor/click-hidden-file-input :id]]])
      ["Embed Youtube Video" [[:editor/input "{{youtube }}" {:last-pattern slash
                                                             :backward-pos 2}]]]

+ 166 - 98
src/main/frontend/components/block.cljs

@@ -49,15 +49,20 @@
             [lambdaisland.glogi :as log]
             [frontend.context.i18n :as i18n]))
 
+;; 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
@@ -156,36 +161,94 @@
                 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 @size
+                         (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 href label]
-  (let [title (second (first label))
-        src (::src state)
+  [state config title href label metadata full_text]
+  (let [src (::src state)
         granted? (state/sub [:nfs/user-granted? (state/get-current-repo)])]
 
-    (when granted?
+    (when (or granted? (util/electron?))
       (p/then (editor-handler/make-asset-url href) #(reset! src %)))
 
     (when @src
-      [:img
-       {:loading "lazy"
-        :src     @src
-        :title   title}])))
+      (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]
-  (if (or (util/starts-with? href "/assets")
-          (util/starts-with? href "../assets"))
-    (asset-link 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 (config/local-asset? href)
+      (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]]
@@ -315,6 +378,12 @@
            label
            original-page-name))])))
 
+(rum/defc asset-reference
+  [title path]
+  (let [repo-path (config/get-repo-dir (state/get-current-repo))
+        full-path (str repo-path (string/replace 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?)
@@ -401,36 +470,35 @@
     (util/format "{{{%s}}}" name)))
 
 (declare block-content)
-(defn block-reference
+(rum/defc block-reference < rum/reactive
   [config id]
-  (rum/with-context [[t] i18n/*tongue-context*]
-    (when-not (string/blank? id)
-      (let [block (and (util/uuid-string? id)
-                       (db/pull-block (uuid id)))]
-        (if block
-          [:span
-           [:div.block-ref-wrap
-            {:on-click (fn [e]
-                         (util/stop e)
-                         (if (gobj/get e "shiftKey")
-                           (state/sidebar-add-block!
-                            (state/get-current-repo)
-                            (:db/id block)
-                            :block-ref
-                            {:block block})
-                           (route-handler/redirect! {:to          :page
-                                                     :path-params {:name id}})))}
-
-            (let [title (:block/title block)]
-              (if (empty? title)
-                ;; display the content
-                [:div.block-ref
-                 (block-content config block nil (:block/uuid block) (:slide? config))]
-                (->elem
-                 :span.block-ref
-                 (map-inline config title))))]]
-          [:span.warning.mr-1 {:title "Block ref invalid"}
-           (util/format "((%s))" id)])))))
+  (when-not (string/blank? id)
+    (let [block (and (util/uuid-string? id)
+                     (db/pull-block (uuid id)))]
+      (if block
+        [:span
+         [:div.block-ref-wrap
+          {:on-click (fn [e]
+                       (util/stop e)
+                       (if (gobj/get e "shiftKey")
+                         (state/sidebar-add-block!
+                          (state/get-current-repo)
+                          (:db/id block)
+                          :block-ref
+                          {:block block})
+                         (route-handler/redirect! {:to          :page
+                                                   :path-params {:name id}})))}
+
+          (let [title (:block/title block)]
+            (if (empty? title)
+              ;; display the content
+              [:div.block-ref
+               (block-content config block nil (:block/uuid block) (:slide? config))]
+              (->elem
+               :span.block-ref
+               (map-inline config title))))]]
+        [:span.warning.mr-1 {:title "Block ref invalid"}
+         (util/format "((%s))" id)]))))
 
 (defn inline-text
   [format v]
@@ -494,17 +562,17 @@
     (->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 {: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)])
@@ -550,17 +618,18 @@
     (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
           ;; 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))
+
           ;; FIXME: same headline, see more https://orgmode.org/manual/Internal-Links.html
           (and (= \* (first s))
                (not= \* (last s)))
@@ -570,6 +639,9 @@
           (->elem :a {:href s}
                   (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))
 
@@ -589,7 +661,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:" ""))}
@@ -612,7 +684,7 @@
 
             ;; 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
@@ -701,27 +773,23 @@
 
         (= 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 [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}]))))
+          (let [YouTube-regex #"^((?:https?:)?//)?((?:www|m).)?((?:youtube.com|youtu.be))(/(?:[\w-]+\?v=|embed/|v/)?)([\w-]+)(\S+)?$"]
+            (when-let [youtube-id (cond
+                                    (== 11 (count url)) url
+                                    :else
+                                    (nth (re-find YouTube-regex url) 5))]
+              (when-not (string/blank? youtube-id)
+                (let [width (min (- (util/get-width) 96)
+                                 560)
+                      height (int (* width (/ 315 560)))]
+                  [:iframe
+                   {:allow-full-screen "allowfullscreen"
+                    :allow
+                    "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
+                    :frame-border "0"
+                    :src (str "https://www.youtube.com/embed/" youtube-id)
+                    :height height
+                    :width width}])))))
 
         (= name "embed")
         (let [a (first arguments)]
@@ -837,13 +905,13 @@
      [: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)))})
+                           block)
+                          (util/stop e)))})
       [:span.bullet-container.cursor
        {:id (str "dot-" uuid)
         :draggable true
@@ -1087,8 +1155,7 @@
                       [:span (t :page/edit-properties-placeholder)]
                       (markup-elements-cp (assoc config :block/format format) ast))]]
       (if slide?
-        [:div [:h1 (:page-name config)]
-         block-cp]
+        [:div [:h1 (:page-name config)]]
         block-cp))))
 
 (rum/defc properties-cp
@@ -1386,7 +1453,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

+ 56 - 0
src/main/frontend/components/block.css

@@ -25,6 +25,53 @@
       width: 9px;
     }
   }
+
+  .asset-container {
+    display: inline-block;
+    position: relative;
+    margin-top: .5rem;
+    margin-bottom: .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;
+      }
+    }
+  }
 }
 
 .open-block-ref-link {
@@ -46,6 +93,15 @@
 .block-children {
   border-left: 2px solid;
   border-left-color: var(--ls-guideline-color, #ddd);
+
+  padding-top: 2px;
+  padding-bottom: 3px;
+
+  > .ls-block {
+    &:last-child {
+      margin-bottom: -5px;
+    }
+  }
 }
 
 .block-control,

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

@@ -57,7 +57,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]

+ 104 - 90
src/main/frontend/components/editor.cljs

@@ -58,7 +58,7 @@
                        (editor-handler/insert-command! id command-steps
                                                        format
                                                        {:restore? restore-slash?})))
-        :class "black"}))))
+        :class     "black"}))))
 
 (rum/defc block-commands < rum/reactive
   [id format]
@@ -71,7 +71,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)}
@@ -108,7 +108,7 @@
                                                                    page-ref-text
                                                                    format
                                                                    {:last-pattern (str "[[" (if @editor-handler/*selected-text "" q))
-                                                                    :postfix-fn (fn [s] (util/replace-first "]]" s ""))}))))
+                                                                    :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))
@@ -128,9 +128,9 @@
           (ui/auto-complete
            matched-pages
            {:on-chosen chosen-handler
-            :on-enter non-exist-page-handler
+            :on-enter  non-exist-page-handler
             :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)}
@@ -141,12 +141,13 @@
       (when input
         (let [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))
+                               (editor-handler/get-matched-blocks q (:block/uuid edit-block)))
               chosen-handler (fn [chosen _click?]
                                (state/set-editor-show-block-search! false)
                                (let [uuid-string (str (:block/uuid chosen))]
@@ -156,7 +157,7 @@
                                                                  (util/format "((%s))" uuid-string)
                                                                  format
                                                                  {:last-pattern (str "((" (if @editor-handler/*selected-text "" q))
-                                                                  :postfix-fn (fn [s] (util/replace-first "))" s ""))})
+                                                                  :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)
@@ -170,12 +171,12 @@
                                         (util/cursor-move-forward input 2))]
           (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)}
@@ -223,12 +224,12 @@
                                   (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   chosen-handler
+            :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]
@@ -252,17 +253,17 @@
     {:on-click #(commands/simple-insert!
                  parent-id "[[]]"
                  {:backward-pos 2
-                  :check-fn (fn [_ _ new-pos]
-                              (reset! commands/*slash-caret-pos new-pos)
-                              (commands/handle-step [:editor/search-page]))})}
+                  :check-fn     (fn [_ _ new-pos]
+                                  (reset! commands/*slash-caret-pos new-pos)
+                                  (commands/handle-step [:editor/search-page]))})}
     "[[]]"]
    [:button.font-extrabold.bottom-action.-mt-1
     {:on-click #(commands/simple-insert!
                  parent-id "(())"
                  {:backward-pos 2
-                  :check-fn (fn [_ _ new-pos]
-                              (reset! commands/*slash-caret-pos new-pos)
-                              (commands/handle-step [:editor/search-block]))})}
+                  :check-fn     (fn [_ _ new-pos]
+                                  (reset! commands/*slash-caret-pos new-pos)
+                                  (commands/handle-step [:editor/search-block]))})}
     "(())"]])
 
 (rum/defcs input < rum/reactive
@@ -276,7 +277,7 @@
             (let [input-value (get state ::input-value)
                   input-option (get @state/state :editor/show-input)]
               (when (seq @input-value)
-                ;; no new line input
+                                   ;; no new line input
                 (util/stop e)
                 (let [[_id on-submit] (:rum/args state)
                       {:keys [pos]} @*slash-caret-pos
@@ -306,11 +307,11 @@
               [:input.form-input.block.w-full.pl-2.sm:text-sm.sm:leading-5
                (merge
                 (cond->
-                 {:key (str "modal-input-" (name id))
-                  :id (str "modal-input-" (name id))
-                  :type (or type "text")
-                  :on-change (fn [e]
-                               (swap! input-value assoc id (util/evalue e)))
+                 {:key           (str "modal-input-" (name id))
+                  :id            (str "modal-input-" (name id))
+                  :type          (or type "text")
+                  :on-change     (fn [e]
+                                   (swap! input-value assoc id (util/evalue e)))
                   :auto-complete (if (util/chrome?) "chrome-off" "off")}
                   placeholder
                   (assoc :placeholder placeholder))
@@ -357,31 +358,31 @@
     (when-let [pos (rum/react pos)]
       (ui/css-transition
        {:class-names "fade"
-        :timeout {:enter 500
-                  :exit 300}}
+        :timeout     {:enter 500
+                      :exit  300}}
        (absolute-modal cp set-default-width? pos)))))
 
 (rum/defc image-uploader < rum/reactive
   {:did-mount    (fn [state]
                    (let [[id format] (:rum/args state)]
-                     (add-watch editor-handler/*image-pending-file ::pending-image
+                     (add-watch editor-handler/*asset-pending-file ::pending-asset
                                 (fn [_ _ _ f]
                                   (reset! *slash-caret-pos (util/get-caret-pos (gdom/getElement id)))
-                                  (editor-handler/upload-image id #js[f] format editor-handler/*image-uploading? true))))
+                                  (editor-handler/upload-asset id #js[f] format editor-handler/*asset-uploading? true))))
                    state)
    :will-unmount (fn [state]
-                   (remove-watch editor-handler/*image-pending-file ::pending-image))}
+                   (remove-watch editor-handler/*asset-pending-file ::pending-asset))}
   [id format]
   [:div.image-uploader
    [:input
-    {:id "upload-file"
-     :type "file"
+    {:id        "upload-file"
+     :type      "file"
      :on-change (fn [e]
                   (let [files (.-files (.-target e))]
-                    (editor-handler/upload-image id files format editor-handler/*image-uploading? false)))
-     :hidden true}]
-   (when-let [uploading? (util/react editor-handler/*image-uploading?)]
-     (let [processing (util/react editor-handler/*image-uploading-process)]
+                    (editor-handler/upload-asset id files format editor-handler/*asset-uploading? false)))
+     :hidden    true}]
+   (when-let [uploading? (util/react editor-handler/*asset-uploading?)]
+     (let [processing (util/react editor-handler/*asset-uploading-process)]
        (transition-cp
         [:div.flex.flex-row.align-center.rounded-md.shadow-sm.bg-base-2.px-1.py-1
          (ui/loading
@@ -434,21 +435,21 @@
                             (profile
                              "Insert block"
                              (editor-handler/insert-new-block! state))))))))))
-         ;; up
+                          ;; 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
+                          ;; 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
+                          ;; backspace
          8  (fn [state e]
               (let [node (gdom/getElement input-id)
                     current-pos (:pos (util/get-caret-pos node))
@@ -464,7 +465,7 @@
                   nil
 
                   (and (zero? current-pos)
-                       ;; not the top block in a block page
+                                        ;; not the top block in a block page
                        (not (and page
                                  (util/uuid-string? page)
                                  (= (medley/uuid page) block-id))))
@@ -482,7 +483,7 @@
                     (reset! *angle-bracket-caret-pos nil)
                     (reset! *show-block-commands false))
 
-                  ;; pair
+                                   ;; pair
                   (and
                    deleted
                    (contains?
@@ -505,13 +506,13 @@
                       :else
                       nil))
 
-                  ;; deleting hashtag
+                                   ;; deleting hashtag
                   (and (= deleted "#") (state/get-editor-show-page-search-hashtag?))
                   (state/set-editor-show-page-search-hashtag! false)
 
                   :else
                   nil)))
-         ;; tab
+                          ;; tab
          9  (fn [state e]
               (let [input-id (state/get-edit-input-id)
                     input (and input-id (gdom/getElement id))
@@ -615,18 +616,18 @@
           (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 /
+              (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 <
+              (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
+                      (= key-code 9)       ;tab
                       (when @*show-block-commands
                         (util/stop e)
                         (editor-handler/insert-command! input-id
@@ -638,41 +639,41 @@
                       (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)
+  {: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-asset id files format editor-handler/*asset-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)))
+                                      ;; (.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)]
@@ -698,8 +699,8 @@
                          (editor-handler/save-block! (get-state state) value))))
                    state)}
   [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))
@@ -741,20 +742,33 @@
                             (when-let [handled
                                        (let [pick-one-allowed-item
                                              (fn [items]
-                                               (when (and items (.-length items))
-                                                 (let [files (. (js/Array.from items) (filter #(= (.-kind %) "file")))
-                                                       it (gobj/get files 0) ;;; TODO: support multiple files
-                                                       mime (and it (.-type it))]
-                                                   (cond
-                                                     (contains? #{"image/jpeg" "image/png" "image/jpg" "image/gif"} mime) [:image (. it getAsFile)]))))
+                                               (if (util/electron?)
+                                                 (let [existed-file-path (js/window.apis.getFilePathFromClipboard)
+                                                       existed-file-path (if (and
+                                                                              (string? existed-file-path)
+                                                                              (not util/mac?)
+                                                                              (not util/win32?)) ; FIXME: linuxcx
+                                                                           (when (re-find #"^(/[^/ ]*)+/?$" existed-file-path)
+                                                                             existed-file-path)
+                                                                           existed-file-path)
+                                                       has-file-path? (not (string/blank? existed-file-path))
+                                                       has-image? (js/window.apis.isClipboardHasImage)]
+                                                   (if (or has-image? has-file-path?)
+                                                     [:asset (js/File. #js[] (if has-file-path? existed-file-path "image.png"))]))
+
+                                                 (when (and items (.-length items))
+                                                   (let [files (. (js/Array.from items) (filter #(= (.-kind %) "file")))
+                                                         it (gobj/get files 0) ;;; TODO: support multiple files
+                                                         mime (and it (.-type it))]
+                                                     (cond
+                                                       (contains? #{"image/jpeg" "image/png" "image/jpg" "image/gif"} mime) [:asset (. it getAsFile)])))))
                                              clipboard-data (gobj/get e "clipboardData")
                                              items (or (.-items clipboard-data)
                                                        (.-files clipboard-data))
                                              picked (pick-one-allowed-item items)]
-                                         (when (and picked (get picked 1))
+                                         (if (get picked 1)
                                            (match picked
-                                             [:image file] (editor-handler/set-image-pending-file file))
-                                           true))]
+                                             [:asset file] (editor-handler/set-asset-pending-file file))))]
                               (util/stop e)))
        :auto-focus        false})
 

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

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

+ 116 - 111
src/main/frontend/components/header.cljs

@@ -22,7 +22,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 +31,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 {:on-click toggle-fn}
+          [:span.ml-1.text-sm (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 +68,82 @@
 
 (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)])
+     (->>
+      [{:title (t :help/toggle-right-sidebar)
+        :options {:on-click state/toggle-sidebar-open?!}}
+
+       (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}))
+
+       (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 nil)])
+          :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,44 +153,39 @@
                    (remove #(= (:url %) config/local-repo)))]
     (rum/with-context [[t] i18n/*tongue-context*]
       [:div.cp__header#head
+       {:on-double-click #(when (util/electron?) (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-50.hover:opacity-100.it
+          {:style {:margin-left -10}
+           :title "Go Back" :on-click #(js/window.history.back)} (svg/arrow-left)])
+
+       (when (util/electron?)
+         [:a.opacity-50.hover:opacity-100.it
+          {:style {:margin-right 15}
+           :title "Go Forward" :on-click #(js/window.history.forward)} (svg/arrow-right)])
+
        (if current-repo
          (search/search)
          [:div.flex-1])
 
        (new-block-mode)
 
-       (when (and (not logged?)
-                  (not config/publishing?))
-
-         (ui/dropdown-with-links
-          (fn [{:keys [toggle-fn]}]
-            [:a {:on-click toggle-fn}
-             [:span.ml-1.text-sm (t :login)]])
-          (let [list [{:title (t :login-google)
-                       :url "/login/google"}
-                      {:title (t :login-github)
-                       :url "/login/github"}]]
-            (mapv
-             (fn [{:keys [title url]}]
-               {:title title
-                :options
-                {:on-click
-                 (fn [_] (set! (.-href js/window.location) url))}})
-             list))
-          nil))
+       (when-not (util/electron?)
+         (login logged?))
 
        (repo/sync-status current-repo)
 
        [:div.repos.hidden.md:block
         (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))}
@@ -190,14 +197,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]])))

+ 5 - 0
src/main/frontend/components/header.css

@@ -10,6 +10,11 @@
   width: 100%;
   top: 0;
   left: 0;
+
+  user-select: none;
+  .it svg {
+      transform: scale(0.8);
+  }
 }
 
 .cp__header-left-menu {

+ 5 - 4
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]
@@ -77,17 +78,17 @@
     [:div.flex-1.journal.page {:class (if intro? "intro" "")}
      (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)])]
                         (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)]]
 

+ 16 - 15
src/main/frontend/components/page.cljs

@@ -202,7 +202,7 @@
          [:ul.mt-2
           (for [[original-name name] pages]
             [:li {:key (str "tagged-page-" name)}
-             [:a {:href (str "/page/" (util/encode-str name))}
+             [:a {:href (rfe/href :page {:name name})}
               original-name]])])]])))
 
 (defonce last-route (atom :home))
@@ -283,6 +283,11 @@
                              :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))}}
+                            (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)}}])
                             {:title (t :page/delete)
                              :options {:on-click #(state/set-modal! (delete-page-dialog page-name))}}
                             {:title   (t :page/action-publish)
@@ -304,8 +309,8 @@
                                                                              (page-handler/publish-page! page-name project/add-project))}})
                                                     (when-not published?
                                                       {:title   (t :page/publish-as-slide)
-                                                      :options {:on-click (fn []
-                                                                            (page-handler/publish-page-as-slide! page-name project/add-project))}})
+                                                       :options {:on-click (fn []
+                                                                             (page-handler/publish-page-as-slide! page-name project/add-project))}})
                                                     {:title   (t (if public? :page/make-private :page/make-public))
                                                      :options {:background (if public? "gray" "indigo")
                                                                :on-click (fn []
@@ -326,6 +331,7 @@
                                                                     :on-click #(.writeText js/navigator.clipboard page-data))]
                                                         :success
                                                         false)))}})]
+                           (flatten)
                            (remove nil?))]
                 (when (seq links)
                   (ui/dropdown-with-links
@@ -373,12 +379,11 @@
                  {:key "page-file"}
                  [:span.opacity-50 {:style {:margin-top 2}} (t :file/file)]
                  [:a.bg-base-2.px-1.ml-1.mr-3 {:style {:border-radius 4
-                                                       :word-break "break-word"}
-                                               :href (str "/file/" (util/url-encode file-path))}
+                                                       :word-break    "break-word"}
+                                               :href  (rfe/href :file {:path file-path})}
                   file-path]
 
-                 (when (and (not config/mobile?)
-                            (not journal?))
+                 (when (not config/mobile?)
                    (presentation repo page))])]
 
              (when (and repo (not block?))
@@ -387,7 +392,7 @@
                    [: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?))
@@ -434,7 +439,7 @@
            {:width (if (and (> width 1280) sidebar-open?)
                      (- width 24 600)
                      (- width 24))
-            :height (- height 120)
+            :height height
             :ref (fn [v] (reset! graph-ref v))
             :ref-atom graph-ref}))
          [:div.ls-center.mt-20
@@ -473,11 +478,10 @@
               [:th (t :page/name)]
               [:th (t :file/last-modified-at)]]]
             [:tbody
-             (for [[page modified-at] pages]
+             (for [page pages]
                (let [encoded-page (util/encode-str page)]
                  [:tr {:key encoded-page}
                   [:td [:a {:on-click (fn [e]
-                                        (.preventDefault e)
                                         (let [repo (state/get-current-repo)
                                               page (db/pull repo '[*] [:page/name (string/lower-case page)])]
                                           (when (gobj/get e "shiftKey")
@@ -489,10 +493,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)

+ 11 - 5
src/main/frontend/components/repo.cljs

@@ -45,7 +45,7 @@
               (ui/button
                (t :open-a-directory)
                :on-click nfs-handler/ls-dir-files)])
-           (when (state/logged?)
+           (when (and (state/logged?) (not (util/electron?)))
              (ui/button
               "Add another git repo"
               :href (rfe/href :repo-add nil {:graph-types "github"})))]
@@ -65,7 +65,7 @@
                              :on-click (fn []
                                          (if local?
                                            (nfs-handler/rebuild-index! url
-                                                                 repo-handler/create-today-journal!)
+                                                                       repo-handler/create-today-journal!)
                                            (repo-handler/rebuild-index! url))
                                          (js/setTimeout
                                           (fn []
@@ -97,7 +97,7 @@
              [: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])
@@ -199,7 +199,11 @@
           (ui/dropdown-with-links
            (fn [{:keys [toggle-fn]}]
              [:a#repo-switch {:on-click toggle-fn}
-              [:span (get-repo-name current-repo)]
+              (let [repo-name (get-repo-name current-repo)
+                    repo-name (if (util/electron?)
+                                (last (string/split repo-name #"/"))
+                                repo-name)]
+                [:span repo-name])
               [:span.dropdown-caret.ml-1 {:style {:border-top-color "#6b7280"}}]])
            (mapv
             (fn [{:keys [id url]}]
@@ -221,7 +225,9 @@
           (and current-repo (not local-repo?))
           (let [repo-name (get-repo-name current-repo)]
             (if (config/local-db? current-repo)
-              repo-name
+              (if (util/electron?)
+                (last (string/split repo-name #"/"))
+                repo-name)
               [:a
                {:href current-repo
                 :target "_blank"}

+ 47 - 52
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]
@@ -60,33 +61,18 @@
      (if (seq pages)
        (for [page pages]
          [:a.mb-1 {:key (str "recent-page-" page)
-                   :href (rfe/href :page {:name 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
@@ -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
@@ -137,8 +123,11 @@
 
     :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)]])
 
@@ -154,8 +143,8 @@
                                                        :slide? true
                                                        :sidebar? true
                                                        :page-name page-name})]
-      [[:a {:href (str "/page/" (util/url-encode page-name))}
-        (util/capitalize-all page-name)]
+      [[:a {:href {:href (rfe/href :page {:name page-name})}}
+        (db-model/get-page-original-name page-name)]
        [:div.ml-2.slide.mt-2
         (slide/slide sections)]])
 
@@ -220,6 +209,9 @@
 (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,32 +222,35 @@
        {:class (if sidebar-open? "is-open")}
        (if sidebar-open?
          [:div.cp__right-sidebar-inner
-          [:div.cp__right-sidebar-settings.hide-scrollbar {:key "right-sidebar-settings"}
-           [:div.ml-4.text-sm
-            [:a.cp__right-sidebar-settings-btn {:on-click (fn [e]
-                                                            (state/sidebar-add-block! repo "contents" :contents nil))}
-             (t :right-side-bar/contents)]]
+          [:div.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)]]
 
-           [: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)]]]
+            [: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

+ 94 - 50
src/main/frontend/components/settings.cljs

@@ -1,6 +1,7 @@
 (ns frontend.components.settings
   (:require [rum.core :as rum]
             [frontend.ui :as ui]
+            [frontend.components.svg :as svg]
             [frontend.handler.notification :as notification]
             [frontend.handler.user :as user-handler]
             [frontend.handler.ui :as ui-handler]
@@ -8,12 +9,14 @@
             [frontend.handler.config :as config-handler]
             [frontend.handler.page :as page-handler]
             [frontend.state :as state]
+            [frontend.version :refer [version]]
             [frontend.util :as util]
             [frontend.config :as config]
             [frontend.dicts :as dicts]
             [clojure.string :as string]
             [goog.object :as gobj]
-            [frontend.context.i18n :as i18n]))
+            [frontend.context.i18n :as i18n]
+            [reitit.frontend.easy :as rfe]))
 
 (rum/defcs set-email < (rum/local "" ::email)
   [state]
@@ -73,6 +76,41 @@
     [:div.max-w-lg.rounded-md.sm:max-w-xs
      (ui/toggle state on-toggle)]]])
 
+(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
+     [:button.ui__button_base.is-logseq.check-update
+      {:disabled update-pending?
+       :on-click #(js/window.apis.checkForUpdates false)}
+      (if update-pending? "Checking ..." "Check for updates")]
+     (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/defcs settings < rum/reactive
   []
   (let [preferred-format (state/get-preferred-format)
@@ -137,9 +175,9 @@
               (:label language)])]]]]
 
        [:div.pl-1
-        ;; config.edn
+                        ;; config.edn
         (when current-repo
-          [:a {:href (str "/file/" (util/url-encode (str config/app-name "/" config/config-file)))}
+          [:a {:href (rfe/href :file {:path (config/get-config-path)})}
            (t :settings-page/edit-config-edn)])
 
         [:hr]
@@ -194,12 +232,12 @@
                    (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_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)
@@ -210,48 +248,45 @@
 
          (when (not enable-journals?)
            [:div.mt-6.sm:mt-5.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start.sm:pt-5
-             [:label.block.text-sm.font-medium.leading-5.sm:mt-px.sm:pt-2.opacity-70
-              {:for "default page"}
-              (t :settings-page/home-default-page)]
-             [:div.mt-1.sm:mt-0.sm:col-span-2
-              [:div.max-w-lg.rounded-md.shadow-sm.sm:max-w-xs
-               [:input#home-default-page.form-input.block.w-full.transition.duration-150.ease-in-out.sm:text-sm.sm:leading-5
-                {:default-value (state/sub-default-home-page)
-                 :on-blur (fn [event]
-                            (let [value (util/evalue event)]
-                              (cond
-                                (string/blank? value)
-                                (let [home (get (state/get-config) :default-home {})
-                                      new-home (dissoc home :page)]
-                                  (config-handler/set-config! :default-home new-home)
-                                  (notification/show! "Home default page updated successfully!" :success))
+            [:label.block.text-sm.font-medium.leading-5.sm:mt-px.sm:pt-2.opacity-70
+             {:for "default page"}
+             (t :settings-page/home-default-page)]
+            [:div.mt-1.sm:mt-0.sm:col-span-2
+             [:div.max-w-lg.rounded-md.shadow-sm.sm:max-w-xs
+              [:input#home-default-page.form-input.block.w-full.transition.duration-150.ease-in-out.sm:text-sm.sm:leading-5
+               {:default-value (state/sub-default-home-page)
+                :on-blur       (fn [event]
+                                 (let [value (util/evalue event)]
+                                   (cond
+                                     (string/blank? value)
+                                     (let [home (get (state/get-config) :default-home {})
+                                           new-home (dissoc home :page)]
+                                       (config-handler/set-config! :default-home new-home)
+                                       (notification/show! "Home default page updated successfully!" :success))
 
-                                (page-handler/page-exists? (string/lower-case value))
-                                (let [home (get (state/get-config) :default-home {})
-                                      new-home (assoc home :page value)]
-                                  (config-handler/set-config! :default-home new-home)
-                                  (notification/show! "Home default page updated successfully!" :success))
+                                     (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))))}]]]])
+                                     :else
+                                     (notification/show! "Please make sure the page exists!" :warning))))}]]]])
 
          (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]
+                   "Enable Git auto push"
+                   enable-git-auto-push?
+                   (fn []
+                     (let [value (not enable-git-auto-push?)]
+                       (config-handler/set-config! :git-auto-push value))))) [:hr]
 
          (when logged?
            [:div
             (ui/admonition
              :important
              [:p (t :settings-page/dont-use-other-peoples-proxy-servers)
-              [:a {:href "https://github.com/isomorphic-git/cors-proxy"
+              [: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
@@ -262,19 +297,28 @@
               [: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]
-                                 (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)))))}]]]]
+                 :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)))))}]]]]
 
             [: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
+           (t :settings-page/current-version)]
+          [:div.mt-1.sm:mt-0.sm:col-span-2
+           [:p version]
+           (if (util/electron?) (app-updater))]]
+
+         [: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"}

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

@@ -0,0 +1,44 @@
+.cp__settings {
+  &-app-updater {
+    min-height: 20px;
+    position: relative;
+    margin-bottom: -10px;
+
+    button.check-update {
+      position: absolute;
+      right: 0;
+      top: -45px;
+
+      &: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;
+        }
+      }
+    }
+  }
+}

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

@@ -320,7 +320,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

+ 7 - 2
src/main/frontend/components/sidebar.css

@@ -56,7 +56,6 @@
   display: flex;
   flex-direction: column;
   min-height: 100vh;
-  padding-bottom: 30px;
 }
 
 .cp__sidebar-main-layout {
@@ -81,7 +80,7 @@
 }
 
 .cp__sidebar-main-content {
-  padding: 6rem 1.5rem;
+  padding: 5rem 1.5rem;
   max-width: var(--ls-main-content-max-width);
   min-height: 100vh;
   flex: 1;
@@ -139,12 +138,14 @@
 
   &-inner {
     padding: 15px;
+    padding-top: 0;
   }
 
   &-settings {
     @apply flex flex-row mb-2;
     margin: -15px;
     margin-bottom: 0;
+    margin-top: 0;
     overflow: auto;
 
     &-btn {
@@ -154,6 +155,10 @@
     }
   }
 
+  .close-arrow svg {
+      transform: scale(0.8);
+  }
+
   &.is-open {
     display: block;
     width: 40%;

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

@@ -15,7 +15,7 @@
     {:d "M5 11L0 6l1.5-1.5L5 8.25 8.5 4.5 10 6l-5 5z"
      :fill-rule "evenodd"}]])
 
-(rum/defc arrow-right
+(rum/defc arrow-right-2
   []
   [:svg
    {:aria-hidden "true"
@@ -29,6 +29,26 @@
     {:d "M7.5 8l-5 5L1 11.5 4.75 8 1 4.5 2.5 3l5 5z"
      :fill-rule "evenodd"}]])
 
+(rum/defc arrow-left
+  []
+  [:svg.w-6.h-6
+   {:viewBox "0 0 24 24", :stroke "currentColor", :fill "none"}
+   [:path
+    {:d "M15 19l-7-7 7-7",
+     :stroke-width "2",
+     :stroke-linejoin "round",
+     :stroke-linecap "round"}]])
+
+(rum/defc arrow-right
+  []
+  [:svg.w-6.h-6
+   {:viewBox "0 0 24 24", :stroke "currentColor", :fill "none"}
+   [:path
+    {:d "M9 5l7 7-7 7",
+     :stroke-width "2",
+     :stroke-linejoin "round",
+     :stroke-linecap "round"}]])
+
 (rum/defc big-arrow-right
   []
   [:svg
@@ -83,6 +103,22 @@
    [:path.opacity-75 {:fill "currentColor"
                       :d "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"}]])
 
+(defonce minus
+  [:svg.w-6.h-6
+   {:viewBox "0 0 24 24", :stroke "currentColor", :fill "none"}
+   [:path
+    {:d               "M20 12H4"
+     :stroke-width    "2"
+     :stroke-linejoin "round"
+     :stroke-linecap  "round"}]])
+
+(defonce rectangle
+  [:svg.w-6.h-6
+   {:viewBox "0 0 24 24", :stroke "currentColor", :fill "none"}
+   [:path
+    {:d               "M3.16580358,18.5038125 L20.5529464,18.5038125 C22.6525178,18.5038125 23.7072321,17.4593839 23.7072321,15.3902411 L23.7072321,3.12495537 C23.7072321,1.0558125 22.6525178,0.0113839219 20.5529464,0.0113839219 L3.16580358,0.0113839219 C1.07651787,0.0118125 0.0115178672,1.04638392 0.0115178672,3.12495537 L0.0115178672,15.3906696 C0.0115178672,17.4696696 1.07651787,18.5042411 3.16580358,18.5042411 L3.16580358,18.5038125 Z M3.19580358,16.8868125 C2.19123216,16.8868125 1.62894642,16.3545268 1.62894642,15.3096696 L1.62894642,3.20638392 C1.62894642,2.16152679 2.19123213,1.62924108 3.19580358,1.62924108 L20.5229464,1.62924108 C21.5172321,1.62924108 22.0898036,2.16152679 22.0898036,3.20638392 L22.0898036,15.3092411 C22.0898036,16.3540982 21.5172322,16.8863839 20.5229464,16.8863839 L3.19580358,16.8868125 Z"
+     :stroke-width    "2"}]])
+
 (defn- hero-icon
   ([d]
    (hero-icon d {}))
@@ -182,6 +218,9 @@
 (defn vertical-dots
   [options]
   (hero-icon "M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z" options))
+(defn horizontal-dots
+  [options]
+  (hero-icon "M5 12h.01M12 12h.01M19 12h.01M6 12a1 1 0 11-2 0 1 1 0 012 0zm7 0a1 1 0 11-2 0 1 1 0 012 0zm7 0a1 1 0 11-2 0 1 1 0 012 0z" options))
 (def external-link
   [:svg {:fill "none", :view-box "0 0 24 24", :height "21", :width "21"
          :stroke "currentColor"}
@@ -441,3 +480,6 @@
 
 (def online
   (hero-icon "M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0"))
+
+(def collapse-right
+  (hero-icon "M4 6h16M4 12h16m-7 6h7"))

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

@@ -1,17 +1,20 @@
 (ns frontend.components.theme
-  (:require [rum.core :as rum]))
+  (:require [rum.core :as rum]
+            [frontend.util :as util]
+            [frontend.version :refer [version]]
+            [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")))
-
-  [theme]
+  (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])
   [:div
-   {:class (str theme "-theme")
+   {:class    (str theme "-theme")
     :on-click on-click}
    child])

+ 116 - 2
src/main/frontend/components/theme.css

@@ -10,7 +10,7 @@
   --ls-z-index-level-5: 99999;
 }
 
-html:not(.is-mac) {
+html {
   ::-webkit-scrollbar-thumb {
     background-color: var(--ls-scrollbar-foreground-color);
   }
@@ -26,7 +26,6 @@ html:not(.is-mac) {
   ::-webkit-scrollbar {
     width: 8px;
     height: 8px;
-    -webkit-border-radius: 100px;
   }
 
   ::-webkit-scrollbar-thumb {
@@ -91,3 +90,118 @@ 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;
+        }
+      }
+    }
+  }
+}
+
+

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

@@ -59,24 +59,24 @@
                           (reset! branch (util/evalue e)))}]]]]
 
         (ui/button
-          (t :git/add-repo-prompt-confirm)
-          :on-click
-          (fn []
-            (let [branch (string/trim @branch)]
-              (if (string/blank? branch)
-                (notification/show!
-                 [:p.text-gray-700.dark:text-gray-300 "Please input a branch, make sure it's matched with your setting on Github."]
-                 :error
-                 false)
-                (let [repo (util/lowercase-first @repo)]
-                  (if (util/starts-with? repo "https://github.com/")
-                    (let [repo (string/replace repo ".git" "")]
-                      (repo-handler/create-repo! repo branch))
+         (t :git/add-repo-prompt-confirm)
+         :on-click
+         (fn []
+           (let [branch (string/trim @branch)]
+             (if (string/blank? branch)
+               (notification/show!
+                [:p.text-gray-700.dark:text-gray-300 "Please input a branch, make sure it's matched with your setting on Github."]
+                :error
+                false)
+               (let [repo (util/lowercase-first @repo)]
+                 (if (util/starts-with? repo "https://github.com/")
+                   (let [repo (string/replace repo ".git" "")]
+                     (repo-handler/create-repo! repo branch))
 
-                    (notification/show!
-                     [:p.text-gray-700.dark:text-gray-300 "Please input a valid repo url, e.g. https://github.com/username/repo"]
-                     :error
-                     false)))))))]])))
+                   (notification/show!
+                    [:p.text-gray-700.dark:text-gray-300 "Please input a valid repo url, e.g. https://github.com/username/repo"]
+                    :error
+                    false)))))))]])))
 
 (rum/defcs add-local-directory
   []
@@ -109,9 +109,9 @@
         generate-f (fn [x]
                      (case x
                        :github
-                       (when github-authed?
+                       (when (and github-authed? (not (util/electron?)))
                          (rum/with-key (add-github-repo)
-                                       "add-github-repo"))
+                           "add-github-repo"))
 
                        :local
                        (rum/with-key (add-local-directory)

+ 64 - 0
src/main/frontend/config.cljs

@@ -19,14 +19,23 @@
 (def prod? (= :prod (env/get-static :runtime)))
 
 (def app-name "logseq")
+
+
 (def website (env/get-static :website))
 (def api (env/get-static :api))
 (def asset-domain (env/get-static :asset-domain))
 (def github-app-name (env/get-static :github-app-name))
 
+;; change if you want to use your own domain instead of default asset.logseq.com
+(def asset-domain (util/format "https://asset.%s.com"
+                               app-name))
+
 (defn asset-uri
   [path]
   (cond
+    (util/file-protocol?)
+    (string/replace path "/static/" "./")
+
     dev? path
 
     staging?
@@ -268,6 +277,7 @@
   (util/starts-with? path default-draw-directory))
 
 (defonce local-repo "local")
+(defonce local-assets-dir "assets")
 (def config-file "config.edn")
 (def custom-css-file "custom.css")
 (def metadata-file "metadata.edn")
@@ -289,6 +299,60 @@
   [s]
   (string/starts-with? s local-db-prefix))
 
+(defn local-asset?
+  [s]
+  (or (string/starts-with? s (str "/" local-assets-dir))
+      (string/starts-with? s (str "../" local-assets-dir))))
+
 (defn get-local-dir
   [s]
   (string/replace s local-db-prefix ""))
+
+(defn get-local-repo
+  [dir]
+  (str local-db-prefix dir))
+
+(defn get-repo-dir
+  [repo-url]
+  (if (and (util/electron?) (local-db? repo-url))
+    (get-local-dir repo-url)
+    (str "/"
+         (->> (take-last 2 (string/split repo-url #"/"))
+              (string/join "_")))))
+
+(defn get-repo-path
+  [repo-url path]
+  (if (and (util/electron?) (local-db? repo-url))
+    path
+    (str (get-repo-dir repo-url) "/" path)))
+
+(defn get-file-path
+  [repo-url relative-path]
+  (when (and repo-url relative-path)
+    (cond
+      (and (util/electron?) (local-db? repo-url))
+      (let [dir (get-repo-dir repo-url)]
+        (if (string/starts-with? relative-path dir)
+          relative-path
+          (str dir "/"
+               (string/replace relative-path #"^/" ""))))
+      (= "/" (first relative-path))
+      (subs relative-path 1)
+
+      :else
+      relative-path)))
+
+(defn get-config-path
+  ([]
+   (get-config-path (state/get-current-repo)))
+  ([repo]
+   (when repo
+     (get-file-path repo (str app-name "/" config-file)))))
+
+(defn get-custom-css-path
+  ([]
+   (get-custom-css-path (state/get-current-repo)))
+  ([repo]
+   (when repo
+     (get-file-path repo
+                    (str app-name "/" custom-css-file)))))

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

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

+ 1 - 15
src/main/frontend/date.cljs

@@ -127,21 +127,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]

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

@@ -50,10 +50,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 page-empty? get-alias-source-page]
+  set-file-last-modified-at! transact-files-db! with-block-refs-count get-modified-pages page-empty? get-alias-source-page
+  set-file-content!]
 
  [frontend.db.react
-  get-current-marker get-current-page get-current-priority get-handler-keys set-file-content! set-key-value
+  get-current-marker get-current-page get-current-priority get-handler-keys set-key-value
   transact-react! remove-key! remove-q! remove-query-component! add-q! add-query-component! clear-query-state!
   clear-query-state-without-refs-and-embeds! get-block-blocks-cache-atom get-page-blocks-cache-atom kv q
   query-state query-components query-entity-in-component remove-custom-query! set-new-result! sub-key-value]

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

@@ -82,12 +82,12 @@
 
 (defn get-modified-pages
   [repo]
-  (d/q
-   '[:find ?page-name ?modified-at
-     :where
-     [?page :page/original-name ?page-name]
-     [(get-else $ ?page :page/last-modified-at 0) ?modified-at]]
-   (conn/get-conn repo)))
+  (-> (d/q
+       '[:find ?page-name
+         :where
+         [?page :page/original-name ?page-name]]
+       (conn/get-conn repo))
+      (db-utils/seq-flatten)))
 
 (defn get-page-alias
   [repo page-name]
@@ -127,13 +127,15 @@
   [repo]
   (when-let [conn (conn/get-conn repo)]
     (->> (d/q
-          '[:find ?path ?modified-at
+           '[:find ?path
+             ;; ?modified-at
             :where
             [?file :file/path ?path]
-            [(get-else $ ?file :file/last-modified-at 0) ?modified-at]]
+            ;; [?file :file/last-modified-at ?modified-at]
+]
           conn)
          (seq)
-         (sort-by last)
+         ;; (sort-by last)
          (reverse))))
 
 (defn get-files-blocks
@@ -203,7 +205,7 @@
 (defn set-file-last-modified-at!
   [repo path last-modified-at]
   (when (and repo path last-modified-at)
-    (when-let [conn (conn/get-files-conn repo)]
+    (when-let [conn (conn/get-conn repo false)]
       (d/transact! conn
                    [{:file/path path
                      :file/last-modified-at last-modified-at}]))))
@@ -211,7 +213,7 @@
 (defn get-file-last-modified-at
   [repo path]
   (when (and repo path)
-    (when-let [conn (conn/get-files-conn repo)]
+    (when-let [conn (conn/get-conn repo false)]
       (-> (d/entity (d/db conn) [:file/path path])
           :file/last-modified-at))))
 
@@ -258,7 +260,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]
@@ -269,8 +272,12 @@
        (: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]
@@ -338,7 +345,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] pages-ids)
         pages-map (reduce (fn [acc p] (assoc acc (:db/id p) p)) {} pages)
         blocks (map
                 (fn [block]
@@ -692,15 +699,16 @@
   [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)))
             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
@@ -712,6 +720,13 @@
               (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)]
@@ -1177,3 +1192,17 @@
         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}
+          tx-data (if (config/local-db? repo)
+                    (dissoc tx-data :file/last-modified-at)
+                    tx-data)]
+      (react/transact-react!
+       repo
+       [tx-data]
+       {:key [:file/content path]
+        :files-db? true}))))

+ 0 - 15
src/main/frontend/db/react.cljs

@@ -339,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}))))

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

@@ -35,8 +35,7 @@
 (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 get-tx-id [tx-report]
   (get-in tx-report [:tempids :db/current-tx]))

+ 0 - 3
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 {}})
 
@@ -50,8 +49,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}

+ 14 - 2
src/main/frontend/dicts.cljs

@@ -222,6 +222,8 @@ title: How to take dummy notes?
         :page/re-index "Re-index this page"
         :page/copy-to-json "Copy the whole page as JSON"
         :page/rename "Rename page"
+        :page/open-in-finder "Open in directory"
+        :page/open-with-default-app "Open with default app"
         :page/action-publish "Publish"
         :page/make-public "Publish it when exporting to an html file"
         :page/make-private "Make it private"
@@ -266,6 +268,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"
@@ -288,6 +293,7 @@ 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"
@@ -619,6 +625,9 @@ title: How to take dummy notes?
            :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 "聚焦"
@@ -702,6 +711,8 @@ title: How to take dummy notes?
            :page/re-index "对此页面重新建立索引"
            :page/copy-to-json "将整页以 JSON 格式复制"
            :page/rename "重命名本页"
+           :page/open-in-finder "打开文件对应目录"
+           :page/open-with-default-app "用默认应用打开文件"
            :page/action-publish "发布"
            :page/make-public "导出 HTML 时发布本页面"
            :page/make-private "导出 HTML 时取消发布本页面"
@@ -768,6 +779,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 "已打开"
@@ -797,7 +809,7 @@ title: How to take dummy notes?
            :sponsor-us "赞助我们!"
            :discord-title "我们的 Discord 社群!"
            :sign-out "登出"
-           :help-shortcut-title "点此查看快捷方式和更多游泳帮助"
+           :help-shortcut-title "点此查看快捷方式和更多有用帮助"
            :loading "加载中"
            :cloning "Clone 中"
            :parsing-files "正在解析文件"
@@ -1053,7 +1065,7 @@ title: How to take dummy notes?
              :join-community "加入社區"
              :discord-title "我們的 Discord 社群!"
              :sign-out "登出"
-             :help-shortcut-title "點此查看快捷方式和更多游泳幫助"
+             :help-shortcut-title "點此查看快捷方式和更多有用幫助"
              :loading "加載中"
              :cloning "Clone 中"
              :parsing-files "正在解析文件"

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

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

+ 9 - 4
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]
@@ -119,11 +120,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"))

+ 86 - 266
src/main/frontend/fs.cljs

@@ -1,298 +1,125 @@
 (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]
-            [frontend.handler.common :as common-handler]
             [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]))
 
-;; 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 (common-handler/verify-permission nil 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]
-                  (throw error)
-                  (js/console.debug "mkdir error: " error ", dir: " dir)))))
+  (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})))))
-
-    :else
-    (p/let [stat (js/window.pfs.stat path)]
-      (if (= (.-type stat) "file")
-        (js/window.pfs.unlink path opts)
-        (p/rejected "Unlinking a directory is not allowed")))))
+  (protocol/unlink! (get-fs path) 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 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 [_ (common-handler/verify-permission repo 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 [_ (common-handler/verify-permission repo 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
+  [dir path]
+  (protocol/read-file (get-fs dir) dir path))
+
+(defn write-file!
+  [repo dir path content opts]
+  (->
+   (do
+     (protocol/write-file! (get-fs dir) repo dir path content opts)
+     (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 rename!
   [repo old-path new-path]
   (cond
     ; See https://github.com/isomorphic-git/lightning-fs/issues/41
     (= old-path new-path)
     (p/resolved nil)
 
-    (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))
-
     :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]
@@ -302,11 +129,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?
@@ -315,10 +142,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
-        (common-handler/verify-permission repo handle true)))))

+ 40 - 0
src/main/frontend/fs/bfs.cljs

@@ -0,0 +1,40 @@
+(ns frontend.fs.bfs
+  (:require [frontend.fs.protocol :as protocol]
+            [frontend.util :as util]
+            [clojure.string :as string]
+            [promesa.core :as p]))
+
+(defrecord Bfs []
+  protocol/Fs
+  (mkdir! [this dir]
+    (when (and js/window.pfs (not (util/electron?)))
+      (->
+       (js/window.pfs.mkdir dir)
+       (p/catch (fn [error] (println "Mkdir error: " error))))))
+  (readdir [this dir]
+    (when js/window.pfs
+      (js/window.pfs.readdir dir)))
+  (unlink! [this path opts]
+    (when js/window.pfs
+      (p/let [stat (js/window.pfs.stat path)]
+        (if (= (.-type stat) "file")
+          (js/window.pfs.unlink path opts)
+          (p/rejected "Unlinking a directory is not allowed")))))
+  (rmdir! [this dir]
+    (js/window.workerThread.rimraf dir))
+  (read-file [this dir path]
+    (let [option (clj->js {:encoding "utf8"})]
+      (js/window.pfs.readFile (str dir "/" path) option)))
+  (write-file! [this repo dir path content opts]
+    (when-not (util/electron?)
+      (js/window.pfs.writeFile (str dir "/" path) content)))
+  (rename! [this repo old-path new-path]
+    (js/window.pfs.rename old-path new-path))
+  (stat [this dir path]
+    (js/window.pfs.stat (str dir path)))
+  (open-dir [this ok-handler]
+    nil)
+  (get-files [this path-or-handle ok-handler]
+    nil)
+  (watch-dir! [this dir]
+    nil))

+ 197 - 0
src/main/frontend/fs/nfs.cljs

@@ -0,0 +1,197 @@
+(ns frontend.fs.nfs
+  (:require [frontend.fs.protocol :as protocol]
+            [frontend.util :as util]
+            [clojure.string :as string]
+            [frontend.idb :as idb]
+            [promesa.core :as p]
+            [lambdaisland.glogi :as log]
+            [goog.object :as gobj]
+            [frontend.db :as db]
+            [frontend.config :as config]
+            [frontend.state :as state]
+            ["/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))
+
+(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)]
+      ;; Bad code
+      (db/set-file-last-modified-at! repo path last-modified))))
+
+(defn verify-permission
+  [repo handle read-write?]
+  (let [repo (or repo (state/get-current-repo))]
+    (p/then
+     (utils/verifyPermission handle read-write?)
+     (fn []
+       (state/set-state! [:nfs/user-granted? repo] true)
+       true))))
+
+(defn check-directory-permission!
+  [repo]
+  (when (config/local-db? repo)
+    (p/let [handle (idb/get-item (str "handle/" repo))]
+      (when handle
+        (verify-permission repo handle true)))))
+
+(defrecord Nfs []
+  protocol/Fs
+  (mkdir! [this dir]
+    (let [[root new-dir] (rest (string/split dir "/"))
+          root-handle (str "handle/" root)]
+      (->
+       (p/let [handle (idb/get-item root-handle)
+               _ (when handle (verify-permission nil 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]
+                  (js/console.debug "mkdir error: " error ", dir: " dir)
+                  (throw error))))))
+
+  (readdir [this 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 "")))))))
+
+  (unlink! [this path opts]
+    (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}))))))
+
+  (rmdir! [this dir]
+    nil)
+
+  (read-file [this dir path]
+    (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)))))
+
+  (write-file! [this repo dir path content opts]
+    (let [{:keys [old-content last-modified-at]} opts
+          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)]
+            (if (and local-content old-content new?
+                     (or
+                      (> pending-writes 0)
+                      not-changed?
+                      new-created?))
+              (do
+                (p/let [_ (verify-permission repo 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 [_ (verify-permission repo 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))))))))
+
+  (rename! [this repo old-path new-path]
+    (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)
+            _ (protocol/write-file! this repo dir new-path content nil)]
+      (protocol/unlink! this old-path nil)))
+  (stat [this dir path]
+    (if-let [file (get-nfs-file-handle (str "handle/"
+                                            (string/replace-first dir "/" "")
+                                            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")))
+  (open-dir [this ok-handler]
+    (utils/openDirectory #js {:recursive true}
+                         ok-handler))
+  (get-files [this path-or-handle ok-handler]
+    (utils/getFiles path-or-handle true ok-handler))
+
+  ;; TODO:
+  (watch-dir! [this dir]
+    nil))

+ 49 - 0
src/main/frontend/fs/node.cljs

@@ -0,0 +1,49 @@
+(ns frontend.fs.node
+  (:require [frontend.fs.protocol :as protocol]
+            [frontend.util :as util]
+            [clojure.string :as string]
+            [promesa.core :as p]
+            [electron.ipc :as ipc]
+            [cljs-bean.core :as bean]))
+
+(defn concat-path
+  [dir path]
+  (cond
+    (nil? path)
+    dir
+
+    (string/starts-with? path dir)
+    path
+
+    :else
+    (str (string/replace dir #"/$" "")
+         (when path
+           (str "/" (string/replace path #"^/" ""))))))
+
+(defrecord Node []
+  protocol/Fs
+  (mkdir! [this dir]
+    (ipc/ipc "mkdir" dir))
+  (readdir [this dir]                   ; recursive
+    (ipc/ipc "readdir" dir))
+  (unlink! [this path _opts]
+    (ipc/ipc "unlink" path))
+  (rmdir! [this dir]
+    nil)
+  (read-file [this dir path]
+    (let [path (concat-path dir path)]
+      (ipc/ipc "readFile" path)))
+  (write-file! [this repo dir path content _opts]
+    (let [path (concat-path dir path)]
+      (ipc/ipc "writeFile" path content)))
+  (rename! [this repo old-path new-path]
+    (ipc/ipc "rename" old-path new-path))
+  (stat [this dir path]
+    (let [path (concat-path dir path)]
+      (ipc/ipc "stat" path)))
+  (open-dir [this ok-handler]
+    (ipc/ipc "openDir" {}))
+  (get-files [this path-or-handle ok-handler]
+    (ipc/ipc "getFiles" path-or-handle))
+  (watch-dir! [this dir]
+    (ipc/ipc "addDirWatcher" dir)))

+ 14 - 0
src/main/frontend/fs/protocol.cljs

@@ -0,0 +1,14 @@
+(ns frontend.fs.protocol)
+
+(defprotocol Fs
+  (mkdir! [this dir])
+  (readdir [this dir])
+  (unlink! [this path opts])
+  (rmdir! [this dir])
+  (read-file [this dir path])
+  (write-file! [this repo dir path content opts])
+  (rename! [this repo old-path new-path])
+  (stat [this dir path])
+  (open-dir [this ok-handler])
+  (get-files [this path-or-handle ok-handler])
+  (watch-dir! [this dir]))

+ 68 - 0
src/main/frontend/fs/watcher_handler.cljs

@@ -0,0 +1,68 @@
+(ns frontend.fs.watcher-handler
+  (:require [clojure.core.async :as async]
+            [lambdaisland.glogi :as log]
+            [frontend.handler.file :as file-handler]
+            [frontend.handler.page :as page-handler]
+            [frontend.handler.notification :as notification]
+            [frontend.handler.route :as route-handler]
+            [cljs-time.coerce :as tc]
+            [frontend.config :as config]
+            [cljs-bean.core :as bean]
+            [frontend.db :as db]
+            [frontend.state :as state]
+            [clojure.string :as string]))
+
+(defn handle-changed!
+  [type {:keys [dir path content stat] :as payload}]
+  (when dir
+    (let [repo (config/get-local-repo dir)
+          {:keys [mtime]} stat]
+      (cond
+        (= "add" type)
+        (let [db-content (db/get-file path)]
+          (when (and (not= content db-content)
+                     ;; Avoid file overwrites
+                     ;; 1. create a new page which writes a new file
+                     ;; 2. add some new content
+                     ;; 3. file watcher notified it with the old content
+                     ;; 4. old content will overwrites the new content in step 2
+                     (not (and db-content
+                               (string/starts-with? db-content content))))
+            (file-handler/alter-file repo path content {:re-render-root? true})))
+
+        (and (= "change" type)
+             (nil? (db/get-file path)))
+        (println "Can't get file in the db: " path)
+
+        (and (= "change" type)
+             (not= content (db/get-file path))
+             (when-let [last-modified-at (db/get-file-last-modified-at repo path)]
+               (> mtime last-modified-at)))
+
+        (file-handler/alter-file repo path content {:re-render-root? true})
+
+        (= "unlink" type)
+        (when-let [page-name (db/get-file-page path)]
+          (page-handler/delete!
+           page-name
+           (fn []
+             (notification/show! (str "Page " page-name " was deleted on disk.")
+                                 :success)
+             (when (= (state/get-current-page) page-name)
+               ;; redirect to home
+               (route-handler/redirect-to-home!)))))
+
+        (contains? #{"add" "change" "unlink"} type)
+        nil
+
+        :else
+        (log/error :fs/watcher-no-handler {:type type
+                                           :payload payload})))))
+
+(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)]
+                         (handle-changed! type payload)))))

+ 19 - 18
src/main/frontend/git.cljs

@@ -2,6 +2,7 @@
   (:refer-clojure :exclude [clone merge])
   (:require [promesa.core :as p]
             [frontend.util :as util]
+            [frontend.config :as config]
             [clojure.string :as string]
             [clojure.set :as set]
             [frontend.state :as state]
@@ -30,7 +31,7 @@
 
 (defn clone
   [repo-url token]
-  (js/window.workerThread.clone (util/get-repo-dir repo-url)
+  (js/window.workerThread.clone (config/get-repo-dir repo-url)
                                 repo-url
                                 (get-cors-proxy repo-url)
                                 1
@@ -40,12 +41,12 @@
 
 (defn list-files
   [repo-url]
-  (js/window.workerThread.listFiles (util/get-repo-dir repo-url)
+  (js/window.workerThread.listFiles (config/get-repo-dir repo-url)
                                     (state/get-default-branch repo-url)))
 
 (defn fetch
   [repo-url token]
-  (js/window.workerThread.fetch (util/get-repo-dir repo-url)
+  (js/window.workerThread.fetch (config/get-repo-dir repo-url)
                                 repo-url
                                 (get-cors-proxy repo-url)
                                 100
@@ -55,23 +56,23 @@
 
 (defn merge
   [repo-url]
-  (js/window.workerThread.merge (util/get-repo-dir repo-url)
+  (js/window.workerThread.merge (config/get-repo-dir repo-url)
                                 (state/get-default-branch repo-url)))
 
 (defn checkout
   [repo-url]
-  (js/window.workerThread.checkout (util/get-repo-dir repo-url)
+  (js/window.workerThread.checkout (config/get-repo-dir repo-url)
                                    (state/get-default-branch repo-url)))
 
 (defn log
   [repo-url depth]
-  (js/window.workerThread.log (util/get-repo-dir repo-url)
+  (js/window.workerThread.log (config/get-repo-dir repo-url)
                               (state/get-default-branch repo-url)
                               depth))
 
 (defn pull
   [repo-url token]
-  (js/window.workerThread.pull (util/get-repo-dir repo-url)
+  (js/window.workerThread.pull (config/get-repo-dir repo-url)
                                (get-cors-proxy repo-url)
                                (state/get-default-branch repo-url)
                                (get-username)
@@ -80,12 +81,12 @@
 (defn add
   [repo-url file]
   (when js/window.git
-    (js/window.workerThread.add (util/get-repo-dir repo-url)
+    (js/window.workerThread.add (config/get-repo-dir repo-url)
                                 file)))
 
 (defn remove-file
   [repo-url file]
-  (js/window.workerThread.remove (util/get-repo-dir repo-url)
+  (js/window.workerThread.remove (config/get-repo-dir repo-url)
                                  file))
 
 (defn rename
@@ -100,7 +101,7 @@
    (commit repo-url message nil))
   ([repo-url message parent]
    (let [{:keys [name email]} (:me @state/state)]
-     (js/window.workerThread.commit (util/get-repo-dir repo-url)
+     (js/window.workerThread.commit (config/get-repo-dir repo-url)
                                     message
                                     name
                                     email
@@ -109,7 +110,7 @@
 (defn add-all
   "Equivalent to `git add --all`. Returns changed files."
   [repo-url]
-  (p/let [repo-dir (util/get-repo-dir repo-url)
+  (p/let [repo-dir (config/get-repo-dir repo-url)
 
           ; statusMatrix will return `[]` rather than raising an error if the repo directory does
           ; not exist. So checks whether repo-dir exists before proceeding.
@@ -141,14 +142,14 @@
 
 (defn read-commit
   [repo-url oid]
-  (js/window.workerThread.readCommit (util/get-repo-dir repo-url)
+  (js/window.workerThread.readCommit (config/get-repo-dir repo-url)
                                      oid))
 
 
 ;; FIXME: not working
 ;; (defn descendent?
 ;;   [repo-url oid ancestor]
-;;   (js/window.workerThread.isDescendent (util/get-repo-dir repo-url)
+;;   (js/window.workerThread.isDescendent (config/get-repo-dir repo-url)
 ;;                                        oid
 ;;                                        ancestor))
 
@@ -166,7 +167,7 @@
   ([repo-url token]
    (push repo-url token false))
   ([repo-url token force?]
-   (js/window.workerThread.push (util/get-repo-dir repo-url)
+   (js/window.workerThread.push (config/get-repo-dir repo-url)
                                 (get-cors-proxy repo-url)
                                 (state/get-default-branch repo-url)
                                 force?
@@ -188,7 +189,7 @@
 (defn get-diffs
   [repo-url hash-1 hash-2]
   (and js/window.git
-       (let [dir (util/get-repo-dir repo-url)]
+       (let [dir (config/get-repo-dir repo-url)]
          (p/let [diffs (js/window.workerThread.getFileStateChanges hash-1 hash-2 dir)
                  diffs (cljs-bean.core/->clj diffs)
                  diffs (remove #(= (:type %) "equal") diffs)
@@ -217,16 +218,16 @@
 
 (defn read-blob
   [repo-url oid path]
-  (js/window.workerThread.readBlob (util/get-repo-dir repo-url)
+  (js/window.workerThread.readBlob (config/get-repo-dir repo-url)
                                    oid
                                    path))
 ;; (resolve-ref (state/get-current-repo) "refs/remotes/origin/master")
 (defn resolve-ref
   [repo-url ref]
-  (js/window.workerThread.resolveRef (util/get-repo-dir repo-url) ref))
+  (js/window.workerThread.resolveRef (config/get-repo-dir repo-url) ref))
 
 (defn write-ref!
   [repo-url oid]
-  (js/window.workerThread.writeRef (util/get-repo-dir repo-url)
+  (js/window.workerThread.writeRef (config/get-repo-dir repo-url)
                                    (state/get-default-branch repo-url)
                                    oid))

+ 6 - 2
src/main/frontend/handler.cljs

@@ -19,6 +19,7 @@
             [frontend.handler.ui :as ui-handler]
             [frontend.handler.export :as export-handler]
             [frontend.handler.web.nfs :as nfs]
+            [frontend.fs.watcher-handler :as fs-watcher-handler]
             [frontend.ui :as ui]
             [goog.object :as gobj]
             [frontend.idb :as idb]
@@ -105,7 +106,8 @@
                                (fn []
                                  (js/console.error "Failed to request GitHub app tokens."))))
 
-                            (watch-for-date!)))
+                            (watch-for-date!)
+                            (file-handler/watch-for-local-dirs!)))
                          (p/catch (fn [error]
                                     (log/error :db/restore-failed error))))))]
     ;; clear this interval
@@ -156,4 +158,6 @@
     (reset! db/*sync-search-indice-f search/sync-search-indice!)
     (db/run-batch-txs!)
     (file-handler/run-writes-chan!)
-    (editor-handler/periodically-save!)))
+    (editor-handler/periodically-save!)
+    (when (util/electron?)
+      (fs-watcher-handler/run-dirs-watcher!))))

+ 30 - 40
src/main/frontend/handler/common.cljs

@@ -35,7 +35,7 @@
           (gobj/get js/window "workerThread")
           (gobj/get js/window.workerThread "getChangedFiles"))
      (->
-      (p/let [files (js/window.workerThread.getChangedFiles (util/get-repo-dir repo))
+      (p/let [files (js/window.workerThread.getChangedFiles (config/get-repo-dir repo))
               files (bean/->clj files)]
         (->
          (p/let [remote-latest-commit (get-remote-ref repo)
@@ -68,12 +68,11 @@
             diffs (git/get-diffs repo local-oid remote-oid)]
       (println {:local-oid local-oid
                 :remote-oid remote-oid
-                :diffs diffs})))
-  )
+                :diffs diffs}))))
 
 (defn get-config
   [repo-url]
-  (db/get-file repo-url (str config/app-name "/" config/config-file)))
+  (db/get-file repo-url (config/get-config-path)))
 
 (defn reset-config!
   [repo-url content]
@@ -91,20 +90,20 @@
   [ok-handler error-handler]
   (let [repos (state/get-repos)
         installation-ids (->> (map :installation_id repos)
-                           (remove nil?)
-                           (distinct))]
+                              (remove nil?)
+                              (distinct))]
     (when (or (seq repos)
-            (seq installation-ids))
+              (seq installation-ids))
       (util/post (str config/api "refresh_github_token")
-        {:installation-ids installation-ids
-         :repos repos}
-        (fn [result]
-          (state/set-github-installation-tokens! result)
-          (when ok-handler (ok-handler)))
-        (fn [error]
-          (log/error :token/http-request-failed error)
-          (js/console.dir error)
-          (when error-handler (error-handler)))))))
+                 {:installation-ids installation-ids
+                  :repos repos}
+                 (fn [result]
+                   (state/set-github-installation-tokens! result)
+                   (when ok-handler (ok-handler)))
+                 (fn [error]
+                   (log/error :token/http-request-failed error)
+                   (js/console.dir error)
+                   (when error-handler (error-handler)))))))
 
 (defn- get-github-token*
   [repo]
@@ -114,7 +113,7 @@
           (state/get-github-token repo)]
       (spec/validate :repos/repo token-state)
       (if (and (map? token-state)
-            (string? expires_at))
+               (string? expires_at))
         (let [expires-at (tf/parse (tf/formatters :date-time-no-ms) expires_at)
               now (t/now)
               expired? (t/after? now expires-at)]
@@ -129,26 +128,17 @@
   ([repo]
    (when-not (config/local-db? repo)
      (js/Promise.
-       (fn [resolve reject]
-         (let [{:keys [expired? token exist?]} (get-github-token* repo)
-               valid-token? (and exist? (not expired?))]
-           (if valid-token?
-             (resolve token)
-             (request-app-tokens!
-               (fn []
-                 (let [{:keys [expired? token exist?] :as token-m} (get-github-token* repo)
-                       valid-token? (and exist? (not expired?))]
-                   (if valid-token?
-                     (resolve token)
-                     (do (log/error :token/failed-get-token token-m)
-                         (reject)))))
-               nil))))))))
-
-(defn verify-permission
-  [repo handle read-write?]
-  (let [repo (or repo (state/get-current-repo))]
-    (p/then
-      (utils/verifyPermission handle read-write?)
-      (fn []
-        (state/set-state! [:nfs/user-granted? repo] true)
-        true))))
+      (fn [resolve reject]
+        (let [{:keys [expired? token exist?]} (get-github-token* repo)
+              valid-token? (and exist? (not expired?))]
+          (if valid-token?
+            (resolve token)
+            (request-app-tokens!
+             (fn []
+               (let [{:keys [expired? token exist?] :as token-m} (get-github-token* repo)
+                     valid-token? (and exist? (not expired?))]
+                 (if valid-token?
+                   (resolve token)
+                   (do (log/error :token/failed-get-token token-m)
+                       (reject)))))
+             nil))))))))

+ 1 - 1
src/main/frontend/handler/config.cljs

@@ -9,7 +9,7 @@
 (defn set-config!
   [k v]
   (when-let [repo (state/get-current-repo)]
-    (let [path (str config/app-name "/" config/config-file)]
+    (let [path (config/get-config-path)]
       (when-let [config (db/get-file-no-sub path)]
         (let [config (try
                        (rewrite/parse-string config)

+ 4 - 101
src/main/frontend/handler/dnd.cljs

@@ -277,22 +277,13 @@
                              bottom-area))
           after-blocks (->> (compute-after-blocks-in-same-file repo target-block to-block direction top? nested? target-child? target-file original-top-block-start-pos block-changes)
                             (remove nil?))
-          path (:file/path (db/entity repo (:db/id (:block/file to-block))))
-          modified-time (let [modified-at (tc/to-long (t/now))]
-                          (->
-                           [[:db/add (:db/id (:block/page target-block)) :page/last-modified-at modified-at]
-                            [:db/add (:db/id (:block/page to-block)) :page/last-modified-at modified-at]
-                            [:db/add (:db/id (:block/file target-block)) :file/last-modified-at modified-at]
-                            [:db/add (:db/id (:block/file to-block)) :file/last-modified-at modified-at]]
-                           distinct
-                           vec))]
+          path (:file/path (db/entity repo (:db/id (:block/file to-block))))]
       (profile
        "Move block in the same file: "
        (repo-handler/transact-react-and-alter-file!
         repo
         (concat
-         after-blocks
-         modified-time)
+         after-blocks)
         {:key :block/change
          :data block-changes}
         [[path new-file-content]]))
@@ -327,14 +318,6 @@
                                 (utf8/substring to-file-content 0 separate-pos)
                                 target-content
                                 (utf8/substring to-file-content separate-pos))))
-        modified-time (let [modified-at (tc/to-long (t/now))]
-                        (->
-                         [[:db/add (:db/id (:block/page target-block)) :page/last-modified-at modified-at]
-                          [:db/add (:db/id (:block/page to-block)) :page/last-modified-at modified-at]
-                          [:db/add (:db/id (:block/file target-block)) :file/last-modified-at modified-at]
-                          [:db/add (:db/id (:block/file to-block)) :file/last-modified-at modified-at]]
-                         distinct
-                         vec))
         target-after-blocks (rebuild-dnd-blocks repo target-file target-child?
                                                 (get-start-pos target-block)
                                                 block-changes nil {:delete? true})
@@ -363,92 +346,12 @@
       repo
       (concat
        target-after-blocks
-       to-after-blocks
-       modified-time)
+       to-after-blocks)
       {:key :block/change
        :data (conj block-changes target-block)}
       [[target-file-path new-target-file-content]
        [to-file-path new-to-file-content]]))))
 
-(defn- move-block-in-different-repos
-  [target-block-repo to-block-repo target-block to-block top-block bottom-block nested? top? target-child? direction target-content target-file original-top-block-start-pos block-changes]
-  (let [target-file (db/entity target-block-repo (:db/id (:block/file target-block)))
-        target-file-path (:file/path target-file)
-        target-file-content (db/get-file target-block-repo target-file-path)
-        to-file (db/entity to-block-repo (:db/id (:block/file to-block)))
-        to-file-path (:file/path to-file)
-        target-block-end-pos (block-handler/get-block-end-pos-rec target-block-repo target-block)
-        to-block-start-pos (get-start-pos to-block)
-        to-block-end-pos (block-handler/get-block-end-pos-rec to-block-repo to-block)
-        new-target-file-content (utf8/delete! target-file-content
-                                              (get-start-pos target-block)
-                                              target-block-end-pos)
-        to-file-content (utf8/encode (db/get-file to-block-repo to-file-path))
-        new-to-file-content (let [separate-pos (cond nested?
-                                                     (get-end-pos to-block)
-                                                     top?
-                                                     to-block-start-pos
-                                                     :else
-                                                     to-block-end-pos)]
-                              (string/trim
-                               (util/join-newline
-                                (utf8/substring to-file-content 0 separate-pos)
-                                target-content
-                                (utf8/substring to-file-content separate-pos))))
-        target-delete-tx (map (fn [id]
-                                [:db.fn/retractEntity [:block/uuid id]])
-                              (block-handler/get-block-ids target-block))
-        [target-modified-time to-modified-time]
-        (let [modified-at (tc/to-long (t/now))]
-          [[[:db/add (:db/id (:block/page target-block)) :page/last-modified-at modified-at]
-            [:db/add (:db/id (:block/file target-block)) :file/last-modified-at modified-at]]
-           [[:db/add (:db/id (:block/page to-block)) :page/last-modified-at modified-at]
-            [:db/add (:db/id (:block/file to-block)) :file/last-modified-at modified-at]]])
-        target-after-blocks (rebuild-dnd-blocks target-block-repo target-file target-child?
-                                                (get-start-pos target-block)
-                                                block-changes nil {:delete? true})
-        to-after-blocks (cond
-                          top?
-                          (rebuild-dnd-blocks to-block-repo to-file target-child?
-                                              (get-start-pos to-block)
-                                              block-changes
-                                              nil
-                                              {:same-file? false})
-
-                          :else
-                          (let [offset-block-id (if nested?
-                                                  (:block/uuid to-block)
-                                                  (last (block-handler/get-block-ids to-block)))
-                                offset-end-pos (get-end-pos
-                                                (db/entity to-block-repo [:block/uuid offset-block-id]))]
-                            (rebuild-dnd-blocks to-block-repo to-file target-child?
-                                                offset-end-pos
-                                                block-changes
-                                                nil
-                                                {:same-file? false})))]
-    (profile
-     "[Target file] Move block between different files: "
-     (repo-handler/transact-react-and-alter-file!
-      target-block-repo
-      (concat
-       target-delete-tx
-       target-after-blocks
-       target-modified-time)
-      {:key :block/change
-       :data [(dissoc target-block :block/children)]}
-      [[target-file-path new-target-file-content]]))
-
-    (profile
-     "[Destination file] Move block between different files: "
-     (repo-handler/transact-react-and-alter-file!
-      to-block-repo
-      (concat
-       to-after-blocks
-       to-modified-time)
-      {:key :block/change
-       :data [block-changes]}
-      [[to-file-path new-to-file-content]]))))
-
 (defn move-block
   "There can be at least 3 possible situations:
   1. Move a block in the same file (either top-to-bottom or bottom-to-top).
@@ -510,7 +413,7 @@
 
           ;; different repos
           :else
-          (move-block-in-different-repos target-block-repo to-block-repo target-block to-block top-block bottom-block nested? top? target-child? direction target-content target-file original-top-block-start-pos block-changes))
+          nil)
 
         (when (state/get-git-auto-push?)
           (doseq [repo (->> #{target-block-repo to-block-repo}

+ 14 - 17
src/main/frontend/handler/draw.cljs

@@ -48,32 +48,29 @@
 (defn create-draws-directory!
   [repo]
   (when repo
-    (let [repo-dir (util/get-repo-dir repo)]
-     (util/p-handle
-      (fs/mkdir (str repo-dir (str "/" config/default-draw-directory)))
-      (fn [_result] nil)
-      (fn [_error] nil)))))
+    (let [repo-dir (config/get-repo-dir repo)]
+      (util/p-handle
+       (fs/mkdir! (str repo-dir (str "/" config/default-draw-directory)))
+       (fn [_result] nil)
+       (fn [_error] nil)))))
 
 (defn save-excalidraw!
   [file data ok-handler]
   (let [path (str config/default-draw-directory "/" file)
         repo (state/get-current-repo)]
     (when repo
-      (let [repo-dir (util/get-repo-dir repo)]
+      (let [repo-dir (config/get-repo-dir repo)]
         (->
          (p/do!
           (create-draws-directory! repo)
-          (fs/write-file repo repo-dir path data)
+          (fs/write-file! repo repo-dir path data nil)
           (git-handler/git-add repo path)
           (ok-handler file)
-          (let [modified-at (tc/to-long (t/now))]
-            (db/transact! repo
-                          [{:file/path path
-                            :file/last-modified-at modified-at}
-                           {:page/name file
-                            :page/file path
-                            :page/last-modified-at (tc/to-long (t/now))
-                            :page/journal? false}])))
+          (db/transact! repo
+            [{:file/path path}
+             {:page/name file
+              :page/file path
+              :page/journal? false}]))
          (p/catch (fn [error]
                     (prn "Write file failed, path: " path ", data: " data)
                     (js/console.dir error))))))))
@@ -82,7 +79,7 @@
   [ok-handler]
   (when-let [repo (state/get-current-repo)]
     (p/let [_ (create-draws-directory! repo)]
-      (let [dir (str (util/get-repo-dir repo)
+      (let [dir (str (config/get-repo-dir repo)
                      "/"
                      config/default-draw-directory)]
         (util/p-handle
@@ -121,7 +118,7 @@
 (defn create-draw-with-default-content
   [current-file ok-handler]
   (when-let [repo (state/get-current-repo)]
-    (p/let [exists? (fs/file-exists? (util/get-repo-dir repo)
+    (p/let [exists? (fs/file-exists? (config/get-repo-dir repo)
                                      (str config/default-draw-directory current-file))]
       (when-not exists?
         (save-excalidraw! current-file default-content

+ 123 - 83
src/main/frontend/handler/editor.cljs

@@ -1,6 +1,8 @@
 (ns frontend.handler.editor
   (:require [frontend.state :as state]
+            [lambdaisland.glogi :as log]
             [frontend.db.model :as db-model]
+            [frontend.db.utils :as db-utils]
             [frontend.handler.common :as common-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.git :as git-handler]
@@ -46,17 +48,11 @@
             [lambdaisland.glogi :as log]))
 
 ;; FIXME: should support multiple images concurrently uploading
-(defonce *image-pending-file (atom nil))
-(defonce *image-uploading? (atom false))
-(defonce *image-uploading-process (atom 0))
+(defonce *asset-pending-file (atom nil))
+(defonce *asset-uploading? (atom false))
+(defonce *asset-uploading-process (atom 0))
 (defonce *selected-text (atom nil))
 
-(defn modified-time-tx
-  [page file]
-  (let [modified-at (tc/to-long (t/now))]
-    [[:db/add (:db/id page) :page/last-modified-at modified-at]
-     [:db/add (:db/id file) :file/last-modified-at modified-at]]))
-
 (defn- get-selection-and-format
   []
   (when-let [block (state/get-edit-block)]
@@ -500,7 +496,7 @@
                              (util/page-name-sanity))) "."
                        (if (= format "markdown") "md" format))
                  file-path (str "/" path)
-                 dir (util/get-repo-dir repo)]
+                 dir (config/get-repo-dir repo)]
              (p/let [exists? (fs/file-exists? dir file-path)]
                (if exists?
                  (notification/show!
@@ -554,9 +550,6 @@
                  [after-blocks block-children-content new-end-pos] (rebuild-after-blocks-indent-outdent repo file block (:end-pos (:block/meta block)) end-pos indent-left?)
                  retract-refs (compute-retract-refs (:db/id e) (first blocks) ref-pages ref-blocks)
                  page-id (:db/id page)
-                 modified-time (let [modified-at (tc/to-long (t/now))]
-                                 [[:db/add page-id :page/last-modified-at modified-at]
-                                  [:db/add (:db/id file) :file/last-modified-at modified-at]])
                  page-properties (when pre-block?
                                    (if (seq new-properties)
                                      [[:db/retract page-id :page/properties]
@@ -595,8 +588,7 @@
                 page-properties
                 page-tags
                 page-alias
-                after-blocks
-                modified-time)
+                after-blocks)
                {:key :block/change
                 :data (map (fn [block] (assoc block :block/page page)) blocks)}
                (let [new-content (new-file-content-indent-outdent block file-content value block-children-content new-end-pos indent-left?)]
@@ -604,6 +596,14 @@
                (when chan {:chan chan
                            :chan-callback chan-callback})))
 
+             ;; fix editing template with multiple headings
+             (when (> (count blocks) 1)
+               (let [new-value (-> (text/remove-level-spaces (:block/content (first blocks)) (:block/format (first blocks)))
+                                   (string/trim-newline))
+                     edit-input-id (state/get-edit-input-id)]
+                 (when edit-input-id
+                   (state/set-edit-content! edit-input-id new-value))))
+
              (when (or (seq retract-refs) pre-block?)
                (ui-handler/re-render-root!))
 
@@ -771,7 +771,7 @@
                   "."
                   (if (= format "markdown") "md" format))
             file-path (str "/" path)
-            dir (util/get-repo-dir repo)]
+            dir (config/get-repo-dir repo)]
         (p/let [exists? (fs/file-exists? dir file-path)]
           (if exists?
             (do (notification/show!
@@ -1419,10 +1419,16 @@
                              opts))))
 
 (defn save-block!
-  [{:keys [format block id repo dummy?] :as state} value]
-  (when (or (:db/id (db/entity repo [:block/uuid (:block/uuid block)]))
-            dummy?)
-    (save-block-aux! block value format {})))
+  ([repo block-or-uuid content]
+   (let [block (if (or (uuid? block-or-uuid)
+                       (string? block-or-uuid))
+                 (db-model/query-block-by-uuid block-or-uuid) block-or-uuid)
+         format (:block/format block)]
+     (save-block! {:block block :repo repo :format format} content)))
+  ([{:keys [format block repo dummy?] :as state} value]
+   (when (or (:db/id (db/entity repo [:block/uuid (:block/uuid block)]))
+             dummy?)
+     (save-block-aux! block value format {}))))
 
 (defn save-current-block-when-idle!
   ([]
@@ -1512,10 +1518,10 @@
                                      true)]
       (commands/restore-state restore-slash-caret-pos?))))
 
-(defn- get-image-link
-  [format url file-name]
+(defn- get-asset-file-link
+  [format url file-name image?]
   (case (keyword format)
-    :markdown (util/format "![%s](%s)" file-name url)
+    :markdown (util/format (str (when image? "!") "[%s](%s)") file-name url)
     :org (util/format "[[%s][%s]]" url file-name)
     nil))
 
@@ -1525,7 +1531,7 @@
 
 (defn ensure-assets-dir!
   [repo]
-  (let [repo-dir (util/get-repo-dir repo)
+  (let [repo-dir (config/get-repo-dir repo)
         assets-dir "assets"]
     (p/then
      (fs/mkdir-if-not-exists (str repo-dir "/" assets-dir))
@@ -1536,41 +1542,65 @@
    (when-let [block-file (db-model/get-block-file block-id)]
      (p/let [[repo-dir assets-dir] (ensure-assets-dir! repo)]
        (let [prefix (:file/path block-file)
-             prefix (and prefix (string/replace prefix "/" "_"))
+             prefix (and prefix (string/replace
+                                 (if (util/electron?)
+                                   (string/replace prefix (str repo-dir "/") "")
+                                   prefix) "/" "_"))
              prefix (and prefix (subs prefix 0 (string/last-index-of prefix ".")))]
          (save-assets! repo repo-dir assets-dir files
                        (fn [index]
                          (str prefix "_" (.now js/Date) "_" index)))))))
   ([repo dir path files gen-filename]
    (p/all
-    (for [[index file] (map-indexed vector files)]
+    (for [[index ^js file] (map-indexed vector files)]
       (let [ext (.-name file)
             ext (if ext (subs ext (string/last-index-of ext ".")) "")
             filename (str (gen-filename index file) ext)
             filename (str path "/" filename)]
-        ;(js/console.debug "Write asset #" filename file)
-        (p/then (fs/write-file repo dir filename (.stream file))
-                #(p/resolved [filename file])))))))
+        ;(js/console.debug "Write asset #" dir filename file)
+        (if (util/electron?)
+          (let [from (.-path file)]
+            (p/then (js/window.apis.copyFileToAssets dir filename from)
+                    #(p/resolved [filename (if (string? %) (js/File. #js[] %) file)])))
+          (p/then (fs/write-file! repo dir filename (.stream file) nil)
+                  #(p/resolved [filename file]))))))))
 
-(def *assets-url-cache (atom {}))
+(defonce *assets-url-cache (atom {}))
 
 (defn make-asset-url
   [path]                                                    ;; path start with "/assets" or compatible for "../assets"
-  (let [repo-dir (util/get-repo-dir (state/get-current-repo))
-        path (string/replace path "../" "/")
-        handle-path (str "handle" repo-dir path)
-        cached-url (get @*assets-url-cache (keyword handle-path))]
-    (if cached-url
-      (p/resolved cached-url)
-      (p/let [handle (frontend.idb/get-item handle-path)
-              file (and handle (.getFile handle))]
-        (when file
-          (p/let [url (js/URL.createObjectURL file)]
-            (swap! *assets-url-cache assoc (keyword handle-path) url)
-            url))))))
-
-(defn upload-image
-  [id files format uploading? drop-or-paste?]
+  (let [repo-dir (config/get-repo-dir (state/get-current-repo))
+        path (string/replace path "../" "/")]
+    (if (util/electron?)
+      (str "assets://" repo-dir path)
+      (let [handle-path (str "handle" repo-dir path)
+            cached-url (get @*assets-url-cache (keyword handle-path))]
+        (if cached-url
+          (p/resolved cached-url)
+          (p/let [handle (frontend.idb/get-item handle-path)
+                  file (and handle (.getFile handle))]
+            (when file
+              (p/let [url (js/URL.createObjectURL file)]
+                (swap! *assets-url-cache assoc (keyword handle-path) url)
+                url))))))))
+
+(defn delete-asset-of-block!
+  [{:keys [repo href title full-text block-id local?] :as opts}]
+  (let [block (db-model/query-block-by-uuid block-id)
+        _ (or block (throw (str block-id " not exists")))
+        format (:block/format block)
+        text (:block/content block)
+        content (string/replace text full-text "")]
+    (save-block! repo block content)
+    (when local?
+      ;; FIXME: should be relative to current block page path
+      (fs/unlink! (config/get-repo-path
+                   repo (-> href
+                            (string/replace #"^../" "/")
+                            (string/replace #"^assets://" ""))) nil))))
+
+(defn upload-asset
+  [id ^js files format uploading? drop-or-paste?]
   (let [repo (state/get-current-repo)
         block (state/get-edit-block)]
     (if (config/local-db? repo)
@@ -1578,41 +1608,44 @@
           (p/then
            (fn [res]
              (when-let [[url file] (and (seq res) (first res))]
-               (insert-command!
-                id
-                (get-image-link format (get-asset-link url) (.-name file))
-                format
-                {:last-pattern (if drop-or-paste? "" commands/slash)
-                 :restore?     true}))))
+               (let [image? (util/ext-of-image? url)]
+                 (insert-command!
+                  id
+                  (get-asset-file-link format (get-asset-link url)
+                                       (if file (.-name file) (if image? "image" "asset"))
+                                       image?)
+                  format
+                  {:last-pattern (if drop-or-paste? "" commands/slash)
+                   :restore?     true})))))
           (p/finally
             (fn []
               (reset! uploading? false)
-              (reset! *image-uploading? false)
-              (reset! *image-uploading-process 0))))
+              (reset! *asset-uploading? false)
+              (reset! *asset-uploading-process 0))))
       (image/upload
-       files
-       (fn [file file-name file-type]
+        files
+        (fn [file file-name file-type]
          (image-handler/request-presigned-url
-          file file-name file-type
-          uploading?
-          (fn [signed-url]
+           file file-name file-type
+           uploading?
+           (fn [signed-url]
             (insert-command! id
-                             (get-image-link format signed-url file-name)
+                             (get-asset-file-link format signed-url file-name true)
                              format
                              {:last-pattern (if drop-or-paste? "" commands/slash)
                               :restore?     true})
 
-            (reset! *image-uploading? false)
-            (reset! *image-uploading-process 0))
-          (fn [e]
+            (reset! *asset-uploading? false)
+            (reset! *asset-uploading-process 0))
+           (fn [e]
             (let [process (* (/ (gobj/get e "loaded")
                                 (gobj/get e "total"))
                              100)]
-              (reset! *image-uploading? false)
-              (reset! *image-uploading-process process)))))))))
+              (reset! *asset-uploading? false)
+              (reset! *asset-uploading-process process)))))))))
 
-(defn set-image-pending-file [file]
-  (reset! *image-pending-file file))
+(defn set-asset-pending-file [file]
+  (reset! *asset-pending-file file))
 
 ;; Editor should track some useful information, like editor modes.
 ;; For example:
@@ -1708,13 +1741,17 @@
         pages))))
 
 (defn get-matched-blocks
-  [q]
+  [q block-id]
   ;; remove current block
-  (let [current-block (state/get-edit-block)]
+  (let [current-block (state/get-edit-block)
+        block-parents (set (->> (db/get-block-parents (state/get-current-repo)
+                                                      block-id
+                                                      99)
+                                (map (comp str :block/uuid))))
+        current-and-parents (set/union #{(str (:block/uuid current-block))} block-parents)]
     (remove
      (fn [h]
-       (= (:block/uuid current-block)
-          (:block/uuid h)))
+       (contains? current-and-parents (:block/uuid h)))
      (search/search q 10))))
 
 (defn get-matched-templates
@@ -1760,7 +1797,7 @@
   [input]
   (or @*show-commands
       @*show-block-commands
-      @*image-uploading?
+      @*asset-uploading?
       (state/get-editor-show-input)
       (state/get-editor-show-page-search?)
       (state/get-editor-show-block-search?)
@@ -1904,15 +1941,12 @@
                                                                 (concat hc2 hc1)])]
                   (when (and start-pos end-pos)
                     (let [new-file-content (utf8/insert! old-file-content start-pos end-pos new-content)
-                          modified-time (modified-time-tx page file)
                           blocks-meta (rebuild-blocks-meta start-pos blocks)]
                       (profile
                        (str "Move block " (if up? "up: " "down: "))
                        (repo-handler/transact-react-and-alter-file!
                         repo
-                        (concat
-                         blocks-meta
-                         modified-time)
+                        blocks-meta
                         {:key :block/change
                          :data (map (fn [block] (assoc block :block/page page)) blocks)}
                         [[file-path new-file-content]])))))))))))))
@@ -1989,16 +2023,14 @@
                 ;;         :last-start-pos @last-start-pos})
                 file-path (:file/path file)
                 file-content (db/get-file file-path)
-                new-content (utf8/insert! file-content start-pos old-end-pos (apply str (map :block/content blocks)))
-                modified-time (modified-time-tx page file)]
+                new-content (utf8/insert! file-content start-pos old-end-pos (apply str (map :block/content blocks)))]
             (profile
              "Indent/outdent: "
              (repo-handler/transact-react-and-alter-file!
               repo
               (concat
                blocks
-               after-blocks
-               modified-time)
+               after-blocks)
               {:key :block/change
                :data (map (fn [block] (assoc block :block/page page)) blocks)}
               [[file-path new-content]])))
@@ -2055,16 +2087,14 @@
               after-blocks (rebuild-after-blocks repo file old-end-pos @last-start-pos)
               file-path (:file/path file)
               file-content (db/get-file file-path)
-              new-content (utf8/insert! file-content start-pos old-end-pos (apply str (map :block/content blocks)))
-              modified-time (modified-time-tx page file)]
+              new-content (utf8/insert! file-content start-pos old-end-pos (apply str (map :block/content blocks)))]
           (profile
            "Indent/outdent: "
            (repo-handler/transact-react-and-alter-file!
             repo
             (concat
              blocks
-             after-blocks
-             modified-time)
+             after-blocks)
             {:key :block/change
              :data (map (fn [block] (assoc block :block/page page)) blocks)}
             [[file-path new-content]])))
@@ -2178,3 +2208,13 @@
     (save-current-block-when-idle! {:check-idle? false})
     (when (string/starts-with? repo "https://") ; git repo
       (repo-handler/auto-push!))))
+
+(defn resize-image!
+  [block-id metadata full_text size]
+  (let [new-meta (merge metadata size)
+        image-part (first (string/split full_text #"\{"))
+        new-full-text (str image-part (pr-str new-meta))
+        block (db/pull [:block/uuid block-id])
+        value (:block/content block)
+        new-value (string/replace value full_text new-full-text)]
+    (save-block-aux! block new-value (:block/format block) {})))

+ 2 - 4
src/main/frontend/handler/extract.cljs

@@ -70,9 +70,7 @@
                          :page/journal? journal?
                          :page/journal-day (if journal?
                                              (date/journal-title->int (string/capitalize page))
-                                             0)
-                         :page/created-at journal-date-long
-                         :page/last-modified-at journal-date-long})
+                                             0)})
                         (seq properties)
                         (assoc :page/properties properties)
 
@@ -129,7 +127,7 @@
   [repo-url file content utf8-content]
   (if (string/blank? content)
     []
-    (let [journal? (util/starts-with? file "journals/")
+    (let [journal? (util/journal? file)
           format (format/get-format file)
           ast (mldoc/->edn content
                            (mldoc/default-config format))

+ 46 - 20
src/main/frontend/handler/file.cljs

@@ -2,6 +2,7 @@
   (:refer-clojure :exclude [load-file])
   (:require [frontend.util :as util :refer-macros [profile]]
             [frontend.fs :as fs]
+            [frontend.fs.nfs :as nfs]
             [promesa.core :as p]
             [frontend.state :as state]
             [frontend.db :as db]
@@ -24,12 +25,15 @@
             [cljs-time.core :as t]
             [cljs-time.coerce :as tc]
             [frontend.utf8 :as utf8]
-            ["ignore" :as Ignore]))
+            ["ignore" :as Ignore]
+            ["/frontend/utils" :as utils]))
+
+;; TODO: extract all git ops using a channel
 
 (defn load-file
   [repo-url path]
   (->
-   (p/let [content (fs/read-file (util/get-repo-dir repo-url) path)]
+   (p/let [content (fs/read-file (config/get-repo-dir repo-url) path)]
      content)
    (p/catch
     (fn [e]
@@ -96,7 +100,8 @@
   (state/set-loading-files! true)
   (p/let [files (git/list-files repo-url)
           files (bean/->clj files)
-          config-content (load-file repo-url (str config/app-name "/" config/config-file))
+          config-content (load-file repo-url
+                                    (config/get-config-path repo-url))
           files (if config-content
                   (let [config (restore-config! repo-url config-content true)]
                     (if-let [patterns (seq (:hidden config))]
@@ -125,7 +130,23 @@
 
 (defn reset-file!
   [repo-url file content]
-  (let [new? (nil? (db/entity [:file/path file]))]
+  (let [electron-local-repo? (and (util/electron?)
+                                  (config/local-db? repo-url))
+        ;; FIXME: store relative path in db
+        file (cond
+               (and electron-local-repo?
+                    util/win32?
+                    (utils/win32 file))
+               file
+
+               (and electron-local-repo? (or
+                                          util/win32?
+                                          (not= "/" (first file))))
+               (str (config/get-repo-dir repo-url) "/" file)
+
+               :else
+               file)
+        new? (nil? (db/entity [:file/path file]))]
     (db/set-file-content! repo-url file content)
     (let [format (format/get-format file)
           utf8-content (utf8/encode content)
@@ -137,8 +158,7 @@
                file-content)
           tx (concat tx [(let [t (tc/to-long (t/now))]
                            (cond->
-                            {:file/path file
-                             :file/last-modified-at t}
+                            {:file/path file}
                              new?
                              (assoc :file/created-at t)))])]
       (db/transact! repo-url tx))))
@@ -161,13 +181,13 @@
         (reset-file! repo path content))
       (db/set-file-content! repo path content))
     (util/p-handle
-     (fs/write-file repo (util/get-repo-dir repo) path content {:old-content original-content
-                                                                :last-modified-at (db/get-file-last-modified-at repo path)})
+     (fs/write-file! repo (config/get-repo-dir repo) path content {:old-content original-content
+                                                                   :last-modified-at (db/get-file-last-modified-at repo path)})
      (fn [_]
        (git-handler/git-add repo path update-status?)
-       (when (= path (str config/app-name "/" config/config-file))
+       (when (= path (config/get-config-path repo))
          (restore-config! repo true))
-       (when (= path (str config/app-name "/" config/custom-css-file))
+       (when (= path (config/get-custom-css-path repo))
          (ui-handler/add-style-if-exists!))
        (when re-render-root? (ui-handler/re-render-root!))
        (when add-history?
@@ -207,8 +227,7 @@
           (db/set-file-content! repo path content))))
 
     (when-let [chan (state/get-file-write-chan)]
-      (let [chan-callback
-            (:chan-callback opts)]
+      (let [chan-callback (:chan-callback opts)]
         (async/put! chan [repo files opts file->content])
         (when chan-callback
           (chan-callback))))))
@@ -220,10 +239,10 @@
                     reset? false}} file->content]
   (let [write-file-f (fn [[path content]]
                        (let [original-content (get file->content path)]
-                         (-> (p/let [_ (fs/check-directory-permission! repo)]
-                               (fs/write-file repo (util/get-repo-dir repo) path content
-                                              {:old-content original-content
-                                               :last-modified-at (db/get-file-last-modified-at repo path)}))
+                         (-> (p/let [_ (nfs/check-directory-permission! repo)]
+                               (fs/write-file! repo (config/get-repo-dir repo) path content
+                                               {:old-content original-content
+                                                :last-modified-at (db/get-file-last-modified-at repo path)}))
                              (p/catch (fn [error]
                                         (log/error :write-file/failed {:path path
                                                                        :content content
@@ -263,10 +282,7 @@
   (when-not (string/blank? file)
     (->
      (p/let [_ (git/remove-file repo file)
-             result (fs/unlink (str (util/get-repo-dir repo)
-                                    "/"
-                                    file)
-                               nil)]
+             result (fs/unlink! (config/get-repo-path repo file) nil)]
        (when-let [file (db/entity repo [:file/path file])]
          (common-handler/check-changed-files-status)
          (let [file-id (:db/id file)
@@ -304,3 +320,13 @@
         (<p! (apply alter-files-handler! args)))
       (recur))
     chan))
+
+(defn watch-for-local-dirs!
+  []
+  (when (util/electron?)
+    (let [repos (->> (state/get-repos)
+                     (filter (fn [repo]
+                               (config/local-db? (:url repo)))))
+          directories (map (fn [repo] (config/get-repo-dir (:url repo))) repos)]
+      (doseq [dir directories]
+        (fs/watch-dir! dir)))))

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

@@ -30,7 +30,8 @@
   ([repo-url file]
    (git-add repo-url file true))
   ([repo-url file update-status?]
-   (when-not (config/local-db? repo-url)
+   (when (and (not (config/local-db? repo-url))
+              (not (util/electron?)))
      (-> (p/let [result (git/add repo-url file)]
            (when update-status?
              (common-handler/check-changed-files-status)))
@@ -57,6 +58,6 @@
   [repo-url {:keys [name email]}]
   (when (and name email)
     (git/set-username-email
-     (util/get-repo-dir repo-url)
+     (config/get-repo-dir repo-url)
      name
      email)))

+ 1 - 1
src/main/frontend/handler/graph.cljs

@@ -51,7 +51,7 @@
                        (map string/lower-case))
         names (db/pull-many '[:page/name :page/original-name] (mapv (fn [page] [:page/name page]) all-pages))
         names (zipmap (map :page/name names)
-                      (map (fn [x] (get x :page/original-name (util/capitalize-all (:page/name x)))) names))
+                      (map (fn [x] (get x :page/original-name (:page/name x))) names))
         nodes (mapv (fn [node] (assoc node :id (get names (:id node)))) nodes)
         links (mapv (fn [{:keys [source target]}]
                       {:source (get names source)

+ 38 - 37
src/main/frontend/handler/image.cljs

@@ -11,43 +11,44 @@
 
 (defn render-local-images!
   []
-  (try
-    (let [images (array-seq (gdom/getElementsByTagName "img"))
-          get-src (fn [image] (.getAttribute image "src"))
-          local-images (filter
-                        (fn [image]
-                          (let [src (get-src image)]
-                            (and src
-                                 (not (or (util/starts-with? src "http://")
-                                          (util/starts-with? src "https://")
-                                          (util/starts-with? src "blob:"))))))
-                        images)]
-      (doseq [img local-images]
-        (gobj/set img
-                  "onerror"
-                  (fn []
-                    (gobj/set (gobj/get img "style")
-                              "display" "none")))
-        (let [path (get-src img)
-              path (string/replace-first path "file:" "")
-              path (if (= (first path) \.)
-                     (subs path 1)
-                     path)]
-          (util/p-handle
-           (fs/read-file (util/get-repo-dir (state/get-current-repo))
-                           path
-                           {})
-           (fn [blob]
-             (let [blob (js/Blob. (array blob) (clj->js {:type "image"}))
-                   img-url (image/create-object-url blob)]
-               (gobj/set img "src" img-url)
-               (gobj/set (gobj/get img "style")
-                         "display" "initial")))
-           (fn [error]
-             (println "Can't read local image file: ")
-             (js/console.dir error))))))
-    (catch js/Error e
-      nil)))
+  (when-not (and (util/electron?)
+                 (config/local-db? (state/get-current-repo)))
+    (try
+      (let [images (array-seq (gdom/getElementsByTagName "img"))
+            get-src (fn [image] (.getAttribute image "src"))
+            local-images (filter
+                          (fn [image]
+                            (let [src (get-src image)]
+                              (and src
+                                   (not (or (util/starts-with? src "http://")
+                                            (util/starts-with? src "https://")
+                                            (util/starts-with? src "blob:"))))))
+                          images)]
+        (doseq [img local-images]
+          (gobj/set img
+                    "onerror"
+                    (fn []
+                      (gobj/set (gobj/get img "style")
+                                "display" "none")))
+          (let [path (get-src img)
+                path (string/replace-first path "file:" "")
+                path (if (= (first path) \.)
+                       (subs path 1)
+                       path)]
+            (util/p-handle
+             (fs/read-file (config/get-repo-dir (state/get-current-repo))
+                           path)
+             (fn [blob]
+               (let [blob (js/Blob. (array blob) (clj->js {:type "image"}))
+                     img-url (image/create-object-url blob)]
+                 (gobj/set img "src" img-url)
+                 (gobj/set (gobj/get img "style")
+                           "display" "initial")))
+             (fn [error]
+               (println "Can't read local image file: ")
+               (js/console.dir error))))))
+      (catch js/Error e
+        nil))))
 
 (defn request-presigned-url
   [file filename mime-type uploading? url-handler on-processing]

+ 23 - 22
src/main/frontend/handler/page.cljs

@@ -46,11 +46,11 @@
            :or {redirect? true}}]
    (let [title (and title (string/trim title))
          repo (state/get-current-repo)
-         dir (util/get-repo-dir repo)
+         dir (config/get-repo-dir repo)
          journal-page? (date/valid-journal-title? title)
          directory (get-directory journal-page?)]
      (when dir
-       (p/let [_ (-> (fs/mkdir (str dir "/" directory))
+       (p/let [_ (-> (fs/mkdir! (str dir "/" directory))
                      (p/catch (fn [_e])))]
          (let [format (name (state/get-preferred-format))
                page (string/lower-case title)
@@ -65,14 +65,20 @@
                 [:p.content
                  (util/format "File %s already exists!" file-path)]
                 :error)
-               ;; create the file
+               ;; Create the file
                (let [content (util/default-content-with-title format title)]
-                 (p/let [_ (fs/create-if-not-exists repo dir file-path content)
+                 ;; Write to the db first, then write to the filesystem,
+                 ;; otherwise, the main electron ipc will notify that there's
+                 ;; a new file created.
+                 ;; Question: what if the fs write failed?
+                 (p/let [_ (file-handler/reset-file! repo path content)
+                         _ (fs/create-if-not-exists repo dir file-path content)
                          _ (git-handler/git-add repo path)]
-                   (file-handler/reset-file! repo path content)
                    (when redirect?
                      (route-handler/redirect! {:to :page
                                                :path-params {:name page}})
+
+                     ;; Edit the first block
                      (let [blocks (db/get-page-blocks page)
                            last-block (last blocks)]
                        (when last-block
@@ -273,10 +279,7 @@
               ;; remove file
               (->
                (p/let [_ (git/remove-file repo file-path)
-                       _ (fs/unlink (str (util/get-repo-dir repo)
-                                         "/"
-                                         file-path)
-                                    nil)]
+                       _ (fs/unlink! (config/get-repo-path repo file-path) nil)]
                  (common-handler/check-changed-files-status)
                  (repo-handler/push-if-auto-enabled! repo))
                (p/catch (fn [err]
@@ -309,10 +312,15 @@
       (when-let [file (d/entity (d/db conn) [:file/path old-path])]
         (d/transact! conn [{:db/id (:db/id file)
                             :file/path new-path}])))
+
     (->
-     (p/let [_ (fs/rename repo
-                          (str (util/get-repo-dir repo) "/" old-path)
-                          (str (util/get-repo-dir repo) "/" new-path))
+     (p/let [_ (fs/rename! repo
+                           (if (util/electron?)
+                             old-path
+                             (str (config/get-repo-dir repo) "/" old-path))
+                           (if (util/electron?)
+                             new-path
+                             (str (config/get-repo-dir repo) "/" new-path)))
              _ (when-not (config/local-db? repo)
                  (git/rename repo old-path new-path))]
        (common-handler/check-changed-files-status)
@@ -488,18 +496,11 @@
         (let [templates (map string/lower-case templates)]
           (contains? (set templates) (string/lower-case title)))))))
 
+;; TODO: add use :file/last-modified-at
 (defn get-pages-with-modified-at
   [repo]
-  (let [now-long (tc/to-long (t/now))]
-    (->> (db/get-modified-pages repo)
-         (seq)
-         (sort-by (fn [[page modified-at]]
-                    [modified-at page]))
-         (reverse)
-         (remove (fn [[page modified-at]]
-                   (or (util/file-page? page)
-                       (and modified-at
-                            (> modified-at now-long))))))))
+  (->> (db/get-modified-pages repo)
+       (remove util/file-page?)))
 
 (defn page-exists?
   [page-name]

+ 29 - 30
src/main/frontend/handler/repo.cljs

@@ -2,6 +2,7 @@
   (:refer-clojure :exclude [clone])
   (:require [frontend.util :as util :refer-macros [profile]]
             [frontend.fs :as fs]
+            [frontend.fs.nfs :as nfs]
             [promesa.core :as p]
             [lambdaisland.glogi :as log]
             [frontend.state :as state]
@@ -51,7 +52,7 @@
 (defn create-config-file-if-not-exists
   [repo-url]
   (spec/validate :repos/url repo-url)
-  (let [repo-dir (util/get-repo-dir repo-url)
+  (let [repo-dir (config/get-repo-dir repo-url)
         app-dir config/app-name
         dir (str repo-dir "/" app-dir)]
     (p/let [_ (fs/mkdir-if-not-exists dir)]
@@ -69,7 +70,7 @@
 (defn create-contents-file
   [repo-url]
   (spec/validate :repos/url repo-url)
-  (let [repo-dir (util/get-repo-dir repo-url)
+  (let [repo-dir (config/get-repo-dir repo-url)
         format (state/get-preferred-format)
         path (str (state/get-pages-directory)
                   "/contents."
@@ -85,7 +86,7 @@
 (defn create-custom-theme
   [repo-url]
   (spec/validate :repos/url repo-url)
-  (let [repo-dir (util/get-repo-dir repo-url)
+  (let [repo-dir (config/get-repo-dir repo-url)
         path (str config/app-name "/" config/custom-css-file)
         file-path (str "/" path)
         default-content ""]
@@ -98,7 +99,7 @@
 (defn create-dummy-notes-page
   [repo-url content]
   (spec/validate :repos/url repo-url)
-  (let [repo-dir (util/get-repo-dir repo-url)
+  (let [repo-dir (config/get-repo-dir repo-url)
         path (str (config/get-pages-directory) "/how_to_make_dummy_notes.md")
         file-path (str "/" path)]
     (p/let [_ (fs/mkdir-if-not-exists (str repo-dir "/" (config/get-pages-directory)))
@@ -111,7 +112,7 @@
   ([repo-url content]
    (spec/validate :repos/url repo-url)
    (when (state/enable-journals? repo-url)
-     (let [repo-dir (util/get-repo-dir repo-url)
+     (let [repo-dir (config/get-repo-dir repo-url)
            format (state/get-preferred-format repo-url)
            title (date/today)
            file-name (date/journal-title->default title)
@@ -136,13 +137,14 @@
            empty-blocks? (empty? (db/get-page-blocks-no-cache repo-url (string/lower-case title)))]
        (when (or empty-blocks?
                  (not page-exists?))
-         (p/let [_ (fs/check-directory-permission! repo-url)
+         (p/let [_ (nfs/check-directory-permission! repo-url)
                  _ (fs/mkdir-if-not-exists (str repo-dir "/" config/default-journals-directory))
-                 file-exists? (fs/create-if-not-exists repo-url repo-dir file-path content)]
+                 file-exists? (fs/file-exists? repo-dir file-path)]
            (when-not file-exists?
              (file-handler/reset-file! repo-url path content)
-             (ui-handler/re-render-root!)
-             (git-handler/git-add repo-url path))))))))
+             (p/let [_ (fs/create-if-not-exists repo-url repo-dir file-path content)]
+               (ui-handler/re-render-root!)
+               (git-handler/git-add repo-url path)))))))))
 
 (defn create-today-journal!
   []
@@ -151,7 +153,8 @@
     (when (or (db/cloned? repo)
               (and (config/local-db? repo)
                    ;; config file exists
-                   (db/get-file (str config/app-name "/" config/config-file))))
+                   (let [path (config/get-config-path)]
+                     (db/get-file path))))
       (let [today-page (string/lower-case (date/today))]
         (when (empty? (db/get-page-blocks-no-cache repo today-page))
           (create-today-journal-if-not-exists repo))))))
@@ -167,7 +170,7 @@
 (defn- reset-contents-and-blocks!
   [repo-url files blocks-pages delete-files delete-blocks]
   (db/transact-files-db! repo-url files)
-  (let [files (map #(select-keys % [:file/path]) files)
+  (let [files (map #(select-keys % [:file/path :file/last-modified-at]) files)
         all-data (-> (concat delete-files delete-blocks files blocks-pages)
                      (util/remove-nils))]
     (db/transact! repo-url all-data)))
@@ -187,7 +190,7 @@
                        (extract-handler/extract-all-blocks-pages repo-url parsed-files)
                        [])]
     (reset-contents-and-blocks! repo-url files blocks-pages delete-files delete-blocks)
-    (let [config-file (str config/app-name "/" config/config-file)]
+    (let [config-file (config/get-config-path)]
       (if (contains? (set file-paths) config-file)
         (when-let [content (some #(when (= (:file/path %) config-file)
                                     (:file/content %)) files)]
@@ -319,9 +322,12 @@
                                      (util/get-block-idx-inside-container block-element))]
        (when (and idx container)
          (state/set-state! :editor/last-edit-block {:block edit-block
-                                               :idx idx
+                                                    :idx idx
                                                     :container (gobj/get container "id")})))
 
+     (when (seq files)
+       (file-handler/alter-files repo files opts))
+
      (db/transact-react!
       repo
       tx
@@ -329,10 +335,7 @@
      (when (seq pages)
        (let [children-tx (mapcat #(rebuild-page-blocks-children repo %) pages)]
          (when (seq children-tx)
-           (db/transact! repo children-tx))))
-     (when (seq files)
-       (file-handler/alter-files repo files opts))
-     )))
+           (db/transact! repo children-tx)))))))
 
 (declare push)
 
@@ -359,7 +362,7 @@
                 (nil? local-latest-commit)
                 (not descendent?)
                 force-pull?)
-        (p/let [files (js/window.workerThread.getChangedFiles (util/get-repo-dir repo-url))]
+        (p/let [files (js/window.workerThread.getChangedFiles (config/get-repo-dir repo-url))]
           (when (empty? files)
             (let [status (db/get-key-value repo-url :git/status)]
               (when (or
@@ -539,7 +542,7 @@
                       (db/remove-conn! url)
                       (db/remove-db! url)
                       (db/remove-files-db! url)
-                      (fs/rmdir (util/get-repo-dir url))
+                      (fs/rmdir! (config/get-repo-dir url))
                       (state/delete-repo! repo))]
     (if (config/local-db? url)
       (p/let [_ (idb/clear-local-db! url)] ; clear file handles
@@ -620,16 +623,12 @@
     (do
       (doseq [{:keys [id url]} (:repos me)]
         (let [repo url]
-          (p/let [config-exists? (fs/file-exists?
-                                  (util/get-repo-dir url)
-                                  ".git/config")]
-            (if (and config-exists?
-                     (db/cloned? repo))
-              (p/do!
-               (git-handler/git-set-username-email! repo me)
-               (pull repo nil))
-              (p/do!
-               (clone-and-load-db repo))))))
+          (if (db/cloned? repo)
+            (p/do!
+             (git-handler/git-set-username-email! repo me)
+             (pull repo nil))
+            (p/do!
+             (clone-and-load-db repo)))))
 
       (periodically-pull-current-repo)
       (periodically-push-current-repo))
@@ -645,7 +644,7 @@
     (db/clear-query-state!)
     (-> (p/do! (db/remove-db! url)
                (db/remove-files-db! url)
-               (fs/rmdir (util/get-repo-dir url))
+               (fs/rmdir! (config/get-repo-dir url))
                (clone-and-load-db url))
         (p/catch (fn [error]
                    (prn "Delete repo failed, error: " error))))))

+ 4 - 1
src/main/frontend/handler/route.cljs

@@ -60,7 +60,10 @@
               (str (subs content 0 48) "...")
               content))
           "Page no longer exists!!")
-        (util/capitalize-all (util/url-decode name))))
+        (let [page (util/url-decode name)
+              page (db/pull [:page/name (string/lower-case page)])]
+          (or (:page/original-name page)
+              (:page/name page)))))
     :tag
     (str "#" (util/url-decode (:name path-params)))
     :diff

+ 149 - 120
src/main/frontend/handler/web/nfs.cljs

@@ -16,6 +16,7 @@
             [clojure.set :as set]
             [frontend.ui :as ui]
             [frontend.fs :as fs]
+            [frontend.fs.nfs :as nfs]
             [frontend.db :as db]
             [frontend.db.model :as db-model]
             [frontend.config :as config]
@@ -24,13 +25,13 @@
 (defn remove-ignore-files
   [files]
   (let [files (remove (fn [f]
-                        (string/starts-with? (:file/path f) ".git/"))
+                        (or (string/starts-with? (:file/path f) ".git/")
+                            (string/includes? (:file/path f) ".git/")))
                       files)]
     (if-let [ignore-file (some #(when (= (:file/name %) ".gitignore")
                                   %) files)]
       (if-let [file (:file/file ignore-file)]
         (p/let [content (.text file)]
-
           (when content
             (let [paths (set (file-handler/ignore-files content (map :file/path files)))]
               (when (seq paths)
@@ -39,20 +40,28 @@
       (p/resolved files))))
 
 (defn- ->db-files
-  [dir-name result]
-  (let [result (flatten (bean/->clj result))]
-    (map (fn [file]
-           (let [handle (gobj/get file "handle")
-                 get-attr #(gobj/get file %)
-                 path (-> (get-attr "webkitRelativePath")
-                          (string/replace-first (str dir-name "/") ""))]
-             {:file/name             (get-attr "name")
-              :file/path             path
-              :file/last-modified-at (get-attr "lastModified")
-              :file/size             (get-attr "size")
-              :file/type             (get-attr "type")
-              :file/file             file
-              :file/handle           handle})) result)))
+  [electron? dir-name result]
+  (if electron?
+    (map (fn [{:keys [path stat content]}]
+           (let [{:keys [mtime size]} stat]
+             {:file/path             path
+              :file/last-modified-at mtime
+              :file/size             size
+              :file/content content}))
+         result)
+    (let [result (flatten (bean/->clj result))]
+      (map (fn [file]
+             (let [handle (gobj/get file "handle")
+                   get-attr #(gobj/get file %)
+                   path (-> (get-attr "webkitRelativePath")
+                            (string/replace-first (str dir-name "/") ""))]
+               {:file/name             (get-attr "name")
+                :file/path             path
+                :file/last-modified-at (get-attr "lastModified")
+                :file/size             (get-attr "size")
+                :file/type             (get-attr "type")
+                :file/file             file
+                :file/handle           handle})) result))))
 
 (defn- filter-markup-and-built-in-files
   [files]
@@ -86,45 +95,55 @@
                          [handle-path handle]))
                      handles)]
     (doseq [[path handle] handles]
-      (fs/add-nfs-file-handle! path handle))
+      (nfs/add-nfs-file-handle! path handle))
     (set-files-aux! handles)))
 
+;; TODO: extract code for `ls-dir-files` and `reload-dir!`
 (defn ls-dir-files
   []
-  (let [path-handles (atom {})]
+  (let [path-handles (atom {})
+        electron? (util/electron?)
+        nfs? (not electron?)]
     ;; TODO: add ext filter to avoid loading .git or other ignored file handlers
     (->
-     (p/let [result (utils/openDirectory #js {:recursive true}
-                                         (fn [path handle]
-                                           (swap! path-handles assoc path handle)))
+     (p/let [result (fs/open-dir (fn [path handle]
+                                   (when nfs?
+                                     (swap! path-handles assoc path handle))))
              _ (state/set-loading-files! true)
-             root-handle (nth result 0)
-             dir-name (gobj/get root-handle "name")
+             root-handle (first result)
+             dir-name (if nfs?
+                        (gobj/get root-handle "name")
+                        root-handle)
              repo (str config/local-db-prefix dir-name)
              root-handle-path (str config/local-handle-prefix dir-name)
-             _ (idb/set-item! root-handle-path root-handle)
-             _ (fs/add-nfs-file-handle! root-handle-path root-handle)
+             _ (when nfs?
+                 (idb/set-item! root-handle-path root-handle)
+                 (nfs/add-nfs-file-handle! root-handle-path root-handle))
              result (nth result 1)
-             files (-> (->db-files dir-name result)
+             files (-> (->db-files electron? dir-name result)
                        remove-ignore-files)
-             _ (let [file-paths (set (map :file/path files))]
-                 (swap! path-handles (fn [handles]
-                                       (->> handles
-                                            (filter (fn [[path _handle]]
-                                                      (or
-                                                       (contains? file-paths
-                                                                  (string/replace-first path (str dir-name "/") ""))
-                                                       (let [last-part (last (string/split path "/"))]
-                                                         (contains? #{config/app-name
-                                                                      config/default-draw-directory
-                                                                      config/default-journals-directory
-                                                                      config/default-pages-directory}
-                                                                    last-part)))))
-                                            (into {})))))
-             _ (set-files! @path-handles)
+             _ (when nfs?
+                 (let [file-paths (set (map :file/path files))]
+                   (swap! path-handles (fn [handles]
+                                         (->> handles
+                                              (filter (fn [[path _handle]]
+                                                        (or
+                                                         (contains? file-paths
+                                                                    (string/replace-first path (str dir-name "/") ""))
+                                                         (let [last-part (last (string/split path "/"))]
+                                                           (contains? #{config/app-name
+                                                                        config/default-draw-directory
+                                                                        config/default-journals-directory
+                                                                        config/default-pages-directory}
+                                                                      last-part)))))
+                                              (into {})))))
+
+                 (set-files! @path-handles))
              markup-files (filter-markup-and-built-in-files files)]
        (-> (p/all (map (fn [file]
-                         (p/let [content (.text (:file/file file))]
+                         (p/let [content (if nfs?
+                                           (.text (:file/file file))
+                                           (:file/content file))]
                            (assoc file :file/content content))) markup-files))
            (p/then (fn [result]
                      _ (state/set-loading-files! false)
@@ -134,20 +153,15 @@
                                                       {:first-clone? true
                                                        :nfs-files    files})
 
-                       (state/add-repo! {:url repo :nfs? true}))))
+                       (state/add-repo! {:url repo :nfs? true})
+                       (when (util/electron?)
+                         (fs/watch-dir! dir-name)))))
            (p/catch (fn [error]
                       (log/error :nfs/load-files-error error)))))
      (p/catch (fn [error]
                 (when (not= "AbortError" (gobj/get error "name"))
                   (log/error :nfs/open-dir-error error)))))))
 
-(defn open-file-picker
-  "Shows a file picker that lets a user select a single existing file, returning a handle for the selected file. "
-  ([]
-   (open-file-picker {}))
-  ([option]
-   (.showOpenFilePicker js/window (bean/->js option))))
-
 (defn get-local-repo
   []
   (when-let [repo (state/get-current-repo)]
@@ -156,16 +170,17 @@
 
 (defn ask-permission
   [repo]
-  (fn [close-fn]
-    [:div
-     [:p.text-gray-700
-      "Grant native filesystem permission for directory: "
-      [:b (config/get-local-dir repo)]]
-     (ui/button
-      "Grant"
-      :on-click (fn []
-                  (fs/check-directory-permission! repo)
-                  (close-fn)))]))
+  (when-not (util/electron?)
+    (fn [close-fn]
+      [:div
+       [:p.text-gray-700
+        "Grant native filesystem permission for directory: "
+        [:b (config/get-local-dir repo)]]
+       (ui/button
+        "Grant"
+        :on-click (fn []
+                    (nfs/check-directory-permission! repo)
+                    (close-fn)))])))
 
 (defn ask-permission-if-local? []
   (when-let [repo (get-local-repo)]
@@ -192,6 +207,58 @@
      :modified modified
      :deleted  deleted}))
 
+(defn- handle-diffs!
+  [repo nfs? old-files new-files handle-path path-handles re-index?]
+  (let [get-last-modified-at (fn [path] (some (fn [file]
+                                                (when (= path (:file/path file))
+                                                  (:file/last-modified-at file)))
+                                              new-files))
+        get-file-f (fn [path files] (some #(when (= (:file/path %) path) %) files))
+        {:keys [added modified deleted] :as diffs} (compute-diffs old-files new-files)
+        ;; Use the same labels as isomorphic-git
+        rename-f (fn [typ col] (mapv (fn [file] {:type typ :path file :last-modified-at (get-last-modified-at file)}) col))
+        _ (when (and nfs? (seq deleted))
+            (let [deleted (doall
+                           (-> (map (fn [path] (if (= "/" (first path))
+                                                 path
+                                                 (str "/" path))) deleted)
+                               (distinct)))]
+              (p/all (map (fn [path]
+                            (let [handle-path (str handle-path path)]
+                              (idb/remove-item! handle-path)
+                              (nfs/remove-nfs-file-handle! handle-path))) deleted))))
+        added-or-modified (set (concat added modified))
+        _ (when (and nfs? (seq added-or-modified))
+            (p/all (map (fn [path]
+                          (when-let [handle (get @path-handles path)]
+                            (idb/set-item! (str handle-path path) handle))) added-or-modified)))]
+    (-> (p/all (map (fn [path]
+                      (when-let [file (get-file-f path new-files)]
+                        (p/let [content (if nfs?
+                                          (.text (:file/file file))
+                                          (:file/content file))]
+                          (assoc file :file/content content)))) added-or-modified))
+        (p/then (fn [result]
+                  (let [files (map #(dissoc % :file/file :file/handle) result)
+                        non-modified? (fn [file]
+                                        (let [content (:file/content file)
+                                              old-content (:file/content (get-file-f (:file/path file) old-files))]
+                                          (= content old-content)))
+                        non-modified-files (->> (filter non-modified? files)
+                                                (map :file/path))
+                        [modified-files modified] (if re-index?
+                                                    [files (set modified)]
+                                                    [(remove non-modified? files) (set/difference (set modified) (set non-modified-files))])
+                        diffs (concat
+                               (rename-f "remove" deleted)
+                               (rename-f "add" added)
+                               (rename-f "modify" modified))]
+                    (when (or (and (seq diffs) (seq modified-files))
+                              (seq diffs))
+                      (repo-handler/load-repo-to-db! repo
+                                                     {:diffs     diffs
+                                                      :nfs-files modified-files}))))))))
+
 (defn- reload-dir!
   ([repo]
    (reload-dir! repo false))
@@ -200,69 +267,31 @@
      (let [old-files (db/get-files-full repo)
            dir-name (config/get-local-dir repo)
            handle-path (str config/local-handle-prefix dir-name)
-           path-handles (atom {})]
+           path-handles (atom {})
+           electron? (util/electron?)
+           nfs? (not electron?)]
        (state/set-graph-syncing? true)
        (->
         (p/let [handle (idb/get-item handle-path)]
-          (when handle
-            (p/let [_ (when handle (common-handler/verify-permission repo handle true))
-                    files-result (utils/getFiles handle true
-                                                 (fn [path handle]
-                                                   (swap! path-handles assoc path handle)))
-                    new-files (-> (->db-files dir-name files-result)
+          (when (or handle electron?)   ; electron doesn't store the file handle
+            (p/let [_ (when handle (nfs/verify-permission repo handle true))
+                    files-result (fs/get-files (if nfs? handle
+                                                   (config/get-local-dir repo))
+                                               (fn [path handle]
+                                                 (when nfs?
+                                                   (swap! path-handles assoc path handle))))
+                    new-files (-> (->db-files electron? dir-name files-result)
                                   remove-ignore-files)
-                    _ (let [file-paths (set (map :file/path new-files))]
-                        (swap! path-handles (fn [handles]
-                                              (->> handles
-                                                   (filter (fn [[path _handle]]
-                                                             (contains? file-paths
-                                                                        (string/replace-first path (str dir-name "/") ""))))
-                                                   (into {})))))
-                    _ (set-files! @path-handles)
-                    get-file-f (fn [path files] (some #(when (= (:file/path %) path) %) files))
-                    {:keys [added modified deleted] :as diffs} (compute-diffs old-files new-files)
-                    ;; Use the same labels as isomorphic-git
-                    rename-f (fn [typ col] (mapv (fn [file] {:type typ :path file}) col))
-                    _ (when (seq deleted)
-                        (let [deleted (doall
-                                       (-> (map (fn [path] (if (= "/" (first path))
-                                                             path
-                                                             (str "/" path))) deleted)
-                                           (distinct)))]
-                          (p/all (map (fn [path]
-                                        (let [handle-path (str handle-path path)]
-                                          (idb/remove-item! handle-path)
-                                          (fs/remove-nfs-file-handle! handle-path))) deleted))))
-                    added-or-modified (set (concat added modified))
-                    _ (when (seq added-or-modified)
-                        (p/all (map (fn [path]
-                                      (when-let [handle (get @path-handles path)]
-                                        (idb/set-item! (str handle-path path) handle))) added-or-modified)))]
-              (-> (p/all (map (fn [path]
-                                (when-let [file (get-file-f path new-files)]
-                                  (p/let [content (.text (:file/file file))]
-                                    (assoc file :file/content content)))) added-or-modified))
-                  (p/then (fn [result]
-                            (let [files (map #(dissoc % :file/file :file/handle) result)
-                                  non-modified? (fn [file]
-                                                  (let [content (:file/content file)
-                                                        old-content (:file/content (get-file-f (:file/path file) old-files))]
-                                                    (= content old-content)))
-                                  non-modified-files (->> (filter non-modified? files)
-                                                          (map :file/path))
-                                  [modified-files modified] (if re-index?
-                                                              [files (set modified)]
-                                                              [(remove non-modified? files) (set/difference (set modified) (set non-modified-files))])
-                                  diffs (concat
-                                         (rename-f "remove" deleted)
-                                         (rename-f "add" added)
-                                         (rename-f "modify" modified))]
-                              (when (or (and (seq diffs) (seq modified-files))
-                                        (seq diffs) ; delete
-)
-                                (repo-handler/load-repo-to-db! repo
-                                                               {:diffs     diffs
-                                                                :nfs-files modified-files})))))))))
+                    _ (when nfs?
+                        (let [file-paths (set (map :file/path new-files))]
+                          (swap! path-handles (fn [handles]
+                                                (->> handles
+                                                     (filter (fn [[path _handle]]
+                                                               (contains? file-paths
+                                                                          (string/replace-first path (str dir-name "/") ""))))
+                                                     (into {})))))
+                        (set-files! @path-handles))]
+              (handle-diffs! repo nfs? old-files new-files handle-path path-handles re-index?))))
         (p/catch (fn [error]
                    (log/error :nfs/load-files-error error)))
         (p/finally (fn [_]
@@ -289,4 +318,4 @@
 
 (defn supported?
   []
-  (utils/nfsSupported))
+  (or (utils/nfsSupported) (util/electron?)))

+ 1 - 0
src/main/frontend/page.cljs

@@ -12,6 +12,7 @@
 (rum/defc current-page < rum/reactive
   {:did-mount    (fn [state]
                    (state/set-root-component! (:rum/react-component state))
+                   (state/setup-electron-updater!)
                    (ui/inject-document-devices-envs!)
                    (ui/inject-dynamic-style-node!)
                    (let [teardown-fn (comp (ui/setup-patch-ios-fixed-bottom-position!))]

+ 3 - 2
src/main/frontend/publishing.cljs

@@ -6,6 +6,7 @@
             [rum.core :as rum]
             [frontend.handler.route :as route]
             [frontend.page :as page]
+            [frontend.util :as util]
             [frontend.routes :as routes]
             [reitit.frontend :as rf]
             [reitit.frontend.easy :as rfe]
@@ -30,7 +31,7 @@
   []
   (state/set-current-repo! "local")
   (when-let [data js/window.logseq_db]
-    (let [data (js/JSON.stringify data)
+    (let [data (util/unescape-html data)
           db-conn (d/create-conn db-schema/schema)
           _ (swap! db/conns assoc "logseq-db/local" db-conn)
           db (db/string->db data)]
@@ -48,7 +49,7 @@
    (rf/router routes/routes {})
    route/set-route-match!
    ;; set to false to enable HistoryAPI
-   {:use-fragment false}))
+   {:use-fragment true}))
 
 (defn start []
   (when-let [node (.getElementById js/document "root")]

+ 2 - 2
src/main/frontend/publishing/html.cljs

@@ -1,6 +1,7 @@
 (ns frontend.publishing.html
   (:require-macros [hiccups.core])
   (:require [frontend.state :as state]
+            [frontend.util :as util]
             [hiccups.runtime]))
 
 (defn publishing-html
@@ -50,9 +51,8 @@
             {:description description}]]
           [:body
            [:div#root]
-           [:script (str "window.logseq_db=" transit-db)]
+           [:script (util/format "window.logseq_db=%s" (js/JSON.stringify (util/escape-html transit-db)))]
            [:script (str "window.logseq_state=" (js/JSON.stringify app-state))]
-           [:script {:src "/static/js/mldoc.min.js"}]
            [:script {:type "text/javascript"}
             "// Single Page Apps for GitHub Pages
       // https://github.com/rafgraph/spa-github-pages

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


+ 0 - 12
src/main/frontend/security.cljs

@@ -30,15 +30,3 @@
 
 ;; Example 2:
 ;; <div style="padding: 20px; opacity: 0;height: 20px;" onmouseout="alert('Gotcha!')"></div>
-
-;; Copy from hiccup
-;; (defn escape-html
-;;   "Change special characters into HTML character entities."
-;;   [text]
-;;   (-> text
-;;       (string/replace "&"  "&amp;")
-;;       (string/replace "<"  "&lt;")
-;;       (string/replace ">"  "&gt;")
-;;       (string/replace "\"" "&quot;")
-;;       (string/replace "'" "&apos;")
-;;       (string/replace #"(?i)javascript:" "")))

+ 20 - 5
src/main/frontend/state.cljs

@@ -3,6 +3,7 @@
             [rum.core :as rum]
             [frontend.util :as util :refer-macros [profile]]
             [clojure.string :as string]
+            [cljs-bean.core :as bean]
             [medley.core :as medley]
             [goog.object :as gobj]
             [goog.dom :as gdom]
@@ -96,6 +97,10 @@
 
     :preferred-language (storage/get :preferred-language)
 
+    ;; electron
+    :electron/updater-pending? false
+    :electron/updater {}
+
     ;; all notification contents as k-v pairs
     :notification/contents {}
     :graph/syncing? false}))
@@ -194,8 +199,7 @@
   ;;         (get (sub-config) (get-current-repo))))
 
   ;; Disable block timestamps for now, because it doesn't work with undo/redo
-  false
-  )
+  false)
 
 ;; Enable by default
 (defn show-brackets?
@@ -714,6 +718,17 @@
   []
   (get @state :ui/root-component))
 
+(defn setup-electron-updater!
+  []
+  (when (util/electron?)
+    (js/window.apis.setUpdatesCallback
+     (fn [_ args]
+       (let [data (bean/->clj args)
+             pending? (not= (:type data) "completed")]
+         (set-state! :electron/updater-pending? pending?)
+         (when pending? (set-state! :electron/updater data))
+         nil)))))
+
 (defn set-file-component!
   [component]
   (set-state! :ui/file-component component))
@@ -767,7 +782,7 @@
   (or
    (when-let [repo (get-current-repo)]
      (get-in @state [:config repo :date-formatter]))
-   ;; TODO:
+    ;; TODO:
    (get-in @state [:me :settings :date-formatter])
    "MMM do, yyyy"))
 
@@ -1005,7 +1020,7 @@
      (when-let [last-time (get-in @state [:editor/last-input-time repo])]
        (let [now (util/time-ms)]
          (>= (- now last-time) 1000)))
-     ;; not in editing mode
+      ;; not in editing mode
      (not (get-edit-input-id)))))
 
 (defn set-last-persist-transact-id!
@@ -1027,7 +1042,7 @@
                                     (remove (fn [tx] (<= (:tx-id tx) last-persist-tx-id)) result)))
                        latest-txs)
           new-txs (update-in latest-txs [repo files?] (fn [result]
-                                                        (vec (conj result {:tx-id tx-id
+                                                        (vec (conj result {:tx-id   tx-id
                                                                            :tx-data tx-data}))))]
       (storage/set-transit! :db/latest-txs new-txs)
       (set-state! :db/latest-txs new-txs))))

+ 15 - 7
src/main/frontend/text.cljs

@@ -13,6 +13,8 @@
 
 (defonce page-ref-re #"\[\[(.*?)\]\]")
 
+(defonce page-ref-re-2 #"(\[\[.*?\]\])")
+
 (defonce between-re #"\(between ([^\)]+)\)")
 
 (defn page-ref-un-brackets!
@@ -34,15 +36,21 @@
 (defn split-page-refs-without-brackets
   [s]
   (if (and (string? s)
-           (or (re-find #"[\"|\,|,]+" s)
+           (or (re-find #"[\"|\,|,|#]+" s)
                (re-find page-ref-re s)))
-    (let [result (->> s
-                      (sep-by-comma-or-quote)
-                      (map page-ref-un-brackets!)
+    (let [result (->> (string/split s page-ref-re-2)
+                      (remove string/blank?)
+                      (mapcat (fn [s]
+                                (if (page-ref? s)
+                                  [(page-ref-un-brackets! s)]
+                                  (sep-by-comma-or-quote s))))
                       (distinct))]
-      (if (and (coll? result)
-               (> (count result) 1))
-        (set result)
+      (if (or (coll? result)
+              (and (string? result)
+                   (string/starts-with? result "#")))
+        (let [result (if coll? result [result])
+              result (map (fn [s] (string/replace s #"^#+" "")) result)]
+          (set result))
         (first result)))
     s))
 

+ 90 - 35
src/main/frontend/ui.cljs

@@ -3,6 +3,7 @@
             [frontend.rum :as r]
             ["react-transition-group" :refer [TransitionGroup CSSTransition]]
             ["react-textarea-autosize" :as TextareaAutosize]
+            ["react-resize-context" :as Resize]
             [frontend.util :as util]
             [frontend.mixins :as mixins]
             [frontend.handler.notification :as notification-handler]
@@ -12,11 +13,14 @@
             [goog.object :as gobj]
             [goog.dom :as gdom]
             [medley.core :as medley]
-            [frontend.ui.date-picker]))
+            [frontend.ui.date-picker]
+            [frontend.context.i18n :as i18n]))
 
 (defonce transition-group (r/adapt-class TransitionGroup))
 (defonce css-transition (r/adapt-class CSSTransition))
 (defonce textarea (r/adapt-class (gobj/get TextareaAutosize "default")))
+(def resize-provider (r/adapt-class (gobj/get Resize "ResizeProvider")))
+(def resize-consumer (r/adapt-class (gobj/get Resize "ResizeConsumer")))
 
 (rum/defc ls-textarea < rum/reactive
   [{:keys [on-change] :as props}]
@@ -59,7 +63,7 @@
        :or   {z-index 999}
        :as   opts}]]
   (let [{:keys [open? toggle-fn]} state
-        modal-content             (modal-content-fn state)]
+        modal-content (modal-content-fn state)]
     [:div.ml-1.relative {:style {:z-index z-index}}
      (content-fn state)
      (css-transition
@@ -75,7 +79,7 @@
    child])
 
 (rum/defc dropdown-with-links
-  [content-fn links {:keys [modal-class links-header z-index] :as opts}]
+  [content-fn links {:keys [modal-class links-header links-footer z-index] :as opts}]
   (dropdown
    content-fn
    (fn [{:keys [close-fn] :as state}]
@@ -91,28 +95,29 @@
               child [:div
                      {:style {:display "flex" :flex-direction "row"}}
                      [:div {:style {:margin-right "8px"}} title]
-                     ;; [:div {:style {:position "absolute" :right "8px"}}
-                     ;;  icon]
+                      ;; [:div {:style {:position "absolute" :right "8px"}}
+                      ;;  icon]
 ]]
           (rum/with-key
             (menu-link new-options child)
-            title)))])
+            title)))
+      (when links-footer links-footer)])
    opts))
 
 (defn button
   [text & {:keys [background on-click href]
-           :as option}]
+           :as   option}]
   (let [class "inline-flex.items-center.px-3.py-2.border.border-transparent.text-sm.leading-4.font-medium.rounded-md.text-white.bg-indigo-600.hover:bg-indigo-700.focus:outline-none.focus:border-indigo-700.focus:shadow-outline-indigo.active:bg-indigo-700.transition.ease-in-out.duration-150.mt-1"
         class (if background (string/replace class "indigo" background) class)]
     (if href
       [:a.button (merge
-                  {:type "button"
+                  {:type  "button"
                    :class (util/hiccup->class class)}
                   (dissoc option :background))
        text]
       [:button
        (merge
-        {:type "button"
+        {:type  "button"
          :class (util/hiccup->class class)}
         (dissoc option :background))
        text])))
@@ -127,19 +132,19 @@
              [:svg.h-6.w-6.text-green-400
               {:stroke "currentColor", :viewBox "0 0 24 24", :fill "none"}
               [:path
-               {:d "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
-                :stroke-width "2"
+               {:d               "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
+                :stroke-width    "2"
                 :stroke-linejoin "round"
-                :stroke-linecap "round"}]]]
+                :stroke-linecap  "round"}]]]
             :warning
             ["text-gray-900"
              [:svg.h-6.w-6.text-yellow-500
               {:stroke "currentColor", :viewBox "0 0 24 24", :fill "none"}
               [:path
-               {:d "M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
-                :stroke-width "2"
+               {:d               "M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
+                :stroke-width    "2"
                 :stroke-linejoin "round"
-                :stroke-linecap "round"}]]]
+                :stroke-linecap  "round"}]]]
 
             ["text-red-500"
              [:svg.h-6.w-6.text-red-500
@@ -154,7 +159,7 @@
                                  (= state "exited"))
                            -1
                            99)
-                :top "3.2em"}}
+                :top     "3.2em"}}
        [:div.max-w-sm.w-full.shadow-lg.rounded-lg.pointer-events-auto.notification-area
         {:class (case state
                   "entering" "transition ease-out duration-300 transform opacity-0 translate-y-2 sm:translate-x-0"
@@ -192,7 +197,7 @@
                          v (second el)]
                      (css-transition
                       {:timeout 100
-                       :key (name k)}
+                       :key     (name k)}
                       (fn [state]
                         (notification-content state (:content v) (:status v) k)))))
                  contents)))))
@@ -227,9 +232,13 @@
   []
   (let [cl (.-classList js/document.documentElement)]
     (if util/mac? (.add cl "is-mac"))
+    (if util/win32? (.add cl "is-win32"))
+    (if (util/electron?) (.add cl "is-electron"))
     (if (util/ios?) (.add cl "is-ios"))
     (if (util/mobile?) (.add cl "is-mobile"))
-    (if (util/safari?) (.add cl "is-safari"))))
+    (if (util/safari?) (.add cl "is-safari"))
+    (if (util/electron?)
+      (js/window.apis.on "full-screen" #(js-invoke cl (if (= % "enter") "add" "remove") "is-fullscreen")))))
 
 (defn inject-dynamic-style-node!
   []
@@ -297,7 +306,7 @@
   (mixins/event-mixin attach-listeners)
   "Render an infinite list."
   [state body {:keys [on-load on-top-reached]
-               :as opts}]
+               :as   opts}]
   body)
 
 (rum/defcs auto-complete <
@@ -324,7 +333,7 @@
                       element-top (gobj/get element "offsetTop")
                       scroll-top (- (gobj/get element "offsetTop") 360)]
                   (set! (.-scrollTop ac-inner) scroll-top)))))
-       ;; down
+         ;; down
        40 (fn [state e]
             (let [current-idx (get state ::current-idx)
                   matched (first (:rum/args state))]
@@ -339,7 +348,7 @@
                       scroll-top (- (gobj/get element "offsetTop") 360)]
                   (set! (.-scrollTop ac-inner) scroll-top)))))
 
-       ;; enter
+         ;; enter
        13 (fn [state e]
             (util/stop e)
             (let [[matched {:keys [on-chosen on-enter]}] (:rum/args state)]
@@ -365,7 +374,7 @@
              {:id       (str "ac-" idx)
               :class    (when (= @current-idx idx)
                           "chosen")
-                ;; :tab-index -1
+               ;; :tab-index -1
               :on-click (fn [e]
                           (.preventDefault e)
                           (if (and (gobj/get e "shiftKey") on-shift-chosen)
@@ -383,9 +392,9 @@
   [:a {:on-click on-click}
    [:span.relative.inline-block.flex-shrink-0.h-6.w-11.border-2.border-transparent.rounded-full.cursor-pointer.transition-colors.ease-in-out.duration-200.focus:outline-none.focus:shadow-outline
     {:aria-checked "false", :tab-index "0", :role "checkbox"
-     :class (if on? "bg-indigo-600" "bg-gray-200")}
+     :class        (if on? "bg-indigo-600" "bg-gray-200")}
     [:span.inline-block.h-5.w-5.rounded-full.bg-white.shadow.transform.transition.ease-in-out.duration-200
-     {:class (if on? "translate-x-5" "translate-x-0")
+     {:class       (if on? "translate-x-5" "translate-x-0")
       :aria-hidden "true"}]]])
 
 (defn tooltip
@@ -422,15 +431,15 @@
    [:div.absolute.top-0.right-0.pt-4.pr-4
     [:button.text-gray-400.hover:text-gray-500.focus:outline-none.focus:text-gray-500.transition.ease-in-out.duration-150
      {:aria-label "Close"
-      :type "button"
-      :on-click close-fn}
+      :type       "button"
+      :on-click   close-fn}
      [:svg.h-6.w-6
       {:stroke "currentColor", :view-box "0 0 24 24", :fill "none"}
       [:path
-       {:d "M6 18L18 6M6 6l12 12"
-        :stroke-width "2"
+       {:d               "M6 18L18 6M6 6l12 12"
+        :stroke-width    "2"
         :stroke-linejoin "round"
-        :stroke-linecap "round"}]]]]
+        :stroke-linecap  "round"}]]]]
 
    (panel-content close-fn)])
 
@@ -451,6 +460,52 @@
       (fn [state]
         (modal-panel modal-panel-content state close-fn)))]))
 
+(defn make-confirm-modal
+  [{:keys [tag title sub-title sub-checkbox? on-cancel on-confirm] :as opts}]
+  (fn [close-fn]
+    (rum/with-context [[t] i18n/*tongue-context*]
+      (let [*sub-checkbox-selected (and sub-checkbox? (atom []))]
+        [:div.ui__confirm-modal
+         {:class (str "is-" tag)}
+         [:div.sm:flex.sm:items-start
+          [:div.mx-auto.flex-shrink-0.flex.items-center.justify-center.h-12.w-12.rounded-full.bg-red-100.sm:mx-0.sm:h-10.sm:w-10
+           [:svg.h-6.w-6.text-red-600
+            {:stroke "currentColor", :view-box "0 0 24 24", :fill "none"}
+            [:path
+             {:d
+              "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
+              :stroke-width    "2"
+              :stroke-linejoin "round"
+              :stroke-linecap  "round"}]]]
+          [:div.mt-3.text-center.sm:mt-0.sm:ml-4.sm:text-left
+           [:h2.headline.text-lg.leading-6.font-medium.text-gray-900
+            (if (keyword? title) (t title) title)]
+           [:label.sublabel
+            (when sub-checkbox?
+              (checkbox
+               {:default-value false
+                :on-change     (fn [e]
+                                 (let [checked (.. e -target -checked)]
+                                   (reset! *sub-checkbox-selected [checked])))}))
+            [:h3.subline.text-gray-400
+             (if (keyword? sub-title)
+               (t sub-title)
+               sub-title)]]]]
+
+         [: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 #(and (fn? on-confirm)
+                             (on-confirm % {:close-fn close-fn
+                                            :sub-selected (and *sub-checkbox-selected @*sub-checkbox-selected)}))}
+            (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 (comp on-cancel close-fn)}
+            (t :cancel)]]]]))))
+
 (defn loading
   [content]
   [:div.flex.flex-row.items-center
@@ -471,12 +526,12 @@
     [:div.flex.flex-col
      [:div.content
       [:div.flex-1.flex-row.foldable-title {:on-mouse-over #(reset! control? true)
-                                            :on-mouse-out #(reset! control? false)}
+                                            :on-mouse-out  #(reset! control? false)}
        [:div.flex.flex-row.items-center
         [:a.block-control.opacity-50.hover:opacity-100.mr-2
-         {:style {:width 14
-                  :height 16
-                  :margin-left -24}
+         {:style    {:width       14
+                     :height      16
+                     :margin-left -24}
           :on-click (fn [e]
                       (util/stop e)
                       (swap! collapsed? not))}
@@ -534,13 +589,13 @@
 (rum/defc select
   [options on-change]
   [:select.mt-1.form-select.block.w-full.px-3.text-base.leading-6.border-gray-300.focus:outline-none.focus:shadow-outline-blue.focus:border-blue-300.sm:text-sm.sm:leading-5.ml-4
-   {:style {:padding "0 0 0 12px"}
+   {:style     {:padding "0 0 0 12px"}
     :on-change (fn [e]
                  (let [value (util/evalue e)]
                    (on-change value)))}
    (for [{:keys [label value selected]} options]
      [:option (cond->
-               {:key label
+               {:key   label
                 :value (or value label)}
                 selected
                 (assoc :selected selected))

+ 34 - 0
src/main/frontend/ui.css

@@ -30,6 +30,40 @@
   }
 }
 
+.ui__confirm-modal {
+  .sublabel {
+    display: flex;
+    padding: 2px 0;
+    align-items: center;
+    font-size: 14px;
+
+    input[type=checkbox] {
+      margin-right: 8px;
+    }
+  }
+}
+
+.ui__button_base {
+  @apply inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4
+  font-medium rounded-md text-white
+  bg-gray-500 hover:bg-gray-700 active:bg-gray-700
+  focus:border-gray-700 focus:shadow-outline-gray
+  focus:outline-none transition
+  ease-in-out duration-150 mt-1;
+
+  &.is-logseq {
+    @apply focus:border-gray-500;
+
+    color: var(--ls-primary-text-color);
+    background: var(--ls-secondary-background-color);
+  }
+
+  &.is-primary {
+    @apply bg-indigo-600 hover:bg-indigo-700 active:bg-indigo-700
+    focus:border-indigo-700 focus:shadow-outline-indigo;
+  }
+}
+
 .dropdown-wrapper {
   background-color: var(--ls-primary-background-color, #fff);
   min-width: 12rem;

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików