Browse Source

Merge branch 'master' into mobile

Tienson Qin 4 years ago
parent
commit
0d13f9f081

+ 170 - 269
.github/workflows/build-desktop-release.yml

@@ -1,97 +1,138 @@
-# This is a basic workflow to help you get started with Actions
+# This is the main desktop application release workflow for both nightly and beta/stable releases.
 
 name: Build-Desktop-Release
 
 on:
   workflow_dispatch:
     inputs:
-      tag-version:
-        description: "Release Tag Version"
+      build-target:
+        description: 'Build Target ("nightly"/"beta")'
+        type: string
         required: true
+        default: "nightly"
       git-ref:
         description: "Release Git Ref"
         required: true
         default: "master"
       is-draft:
         description: 'Draft Release? '
+        type: boolean
         required: true
-        default: "true"
+        default: true
       is-pre-release:
         description: 'Pre Release?'
+        type: boolean
         required: true
-        default: "true"
+        default: true
+  # schedule: # Every workday at the noon (UTC) we run a scheduled nightly build
+  #   - cron: '0 12 * * MON-FRI'
+
+env:
+  CLOJURE_VERSION: '1.10.1.763'
+  NODE_VERSION: '16'
 
 jobs:
   compile-cljs:
     runs-on: ubuntu-18.04
     steps:
       - name: Check out Git repository
-        uses: actions/checkout@v1
+        uses: actions/checkout@v2
+        with:
+          ref: ${{ github.event.inputs.git-ref }}
 
       - name: Install Node.js, NPM and Yarn
         uses: actions/setup-node@v2
         with:
-          node-version: 16
+          node-version: ${{ env.NODE_VERSION }}
+
+      - name: Get yarn cache directory path
+        id: yarn-cache-dir-path
+        run: echo "::set-output name=dir::$(yarn cache dir)"
+
+      - name: Cache yarn cache directory
+        uses: actions/cache@v2
+        id: yarn-cache
+        with:
+          path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
+          key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
+          restore-keys: |
+            ${{ runner.os }}-yarn-
 
       - name: Setup Java JDK
         uses: actions/[email protected]
         with:
           java-version: 1.8
 
-      - name: Cache local Maven repository
+      - name: Cache clojure deps
         uses: actions/cache@v2
         with:
-          path: ~/.m2/repository
-          key: ${{ runner.os }}-maven
+          path: |
+            ~/.m2/repository
+            ~/.gitlibs
+          key: ${{ runner.os }}-clojure-lib-${{ hashFiles('**/deps.edn') }}
+
+      - name: Setup clojure
+        uses: DeLaGuardo/[email protected]
+        with:
+          cli: ${{ env.CLOJURE_VERSION }}
+
+      - name: Retrieve tag version
+        id: ref
+        run: |
+          pkgver=$(node ./scripts/get-pkg-version.js "${{ github.event.inputs.build-target }}")
+          echo ::set-output name=version::$pkgver
 
-      - name: Install clojure
+      - name: Update Nightly APP Version
+        if: ${{ github.event.inputs.build-target == 'nightly' || github.event_name == 'schedule' }}
         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
+          sed -i 's/defonce version ".*"/defonce version "${{ steps.ref.outputs.version }}"/g' src/main/frontend/version.cljs
 
       - name: Compile CLJS
         run: yarn install && gulp build && yarn cljs:release-electron
 
       - name: Update APP Version
         run: |
-          sed -i 's/"version": "0.0.1"/"version": "${{ github.event.inputs.tag-version }}"/g' ./package.json
+          sed -i 's/"version": "0.0.1"/"version": "${{ steps.ref.outputs.version }}"/g' ./package.json
         working-directory: ./static
 
       - name: Display Package.json
         run: cat ./package.json
         working-directory: ./static
 
+      - name: Save VERSION file
+        run: echo "${{ steps.ref.outputs.version }}" > ./VERSION
+        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
+        uses: actions/upload-artifact@v2
         with:
-          name: static.zip
-          path: static.zip
+          name: static
+          path: static
 
   build-linux:
     runs-on: ubuntu-18.04
     needs: [ compile-cljs ]
     steps:
       - name: Download The Static Asset
-        uses: actions/download-artifact@v1
+        uses: actions/download-artifact@v2
         with:
-          name: static.zip
-          path: ./
+          name: static
+          path: static
 
-      - name: Uncompress Static FIles
-        run: unzip static.zip
+      - name: Retrieve tag version
+        id: ref
+        run: |
+          pkgver=$(cat ./static/VERSION)
+          echo ::set-output name=version::$pkgver
 
       - name: Install Node.js, NPM and Yarn
         uses: actions/setup-node@v2
         with:
-          node-version: 16
+          node-version: ${{ env.NODE_VERSION }}
 
       # - name: Cache Node Modules
       #   uses: actions/cache@v2
@@ -101,44 +142,43 @@ jobs:
       #     key: ${{ runner.os }}-node-modules
 
       - name: Build/Release Electron App
-        run: yarn install  && yarn electron:make
+        run: yarn install && yarn electron:make
         working-directory: ./static
 
-      - name: Change Artifact Name For ZIP File
-        run: mv static/out/make/zip/linux/x64/*-linux-x64-*.zip  static/out/make/zip/linux/x64/Logseq-linux.zip
-
-      - name: Change Artifact Name For AppImage File
-        run: mv static/out/make/*-*.AppImage  static/out/make/Logseq-linux.AppImage
-
-      - name: Cache Artifact With ZIP format
-        uses: actions/upload-artifact@v1
-        with:
-          name: Logseq-linux.zip
-          path: static/out/make/zip/linux/x64/Logseq-linux.zip
+      - name: Save artifacts
+        run: |
+          mkdir -p builds
+          # NOTE: save VERSION file to builds directory
+          cp static/VERSION ./builds/VERSION
+          mv static/out/make/*-*.AppImage ./builds/Logseq-linux-x64-${{ steps.ref.outputs.version }}.AppImage
+          mv static/out/make/zip/linux/x64/*-linux-x64-*.zip ./builds/Logseq-linux-x64-${{ steps.ref.outputs.version }}.zip
 
-      - name: Cache Artifact With AppImage format
-        uses: actions/upload-artifact@v1
+      - name: Upload Artifact
+        uses: actions/upload-artifact@v2
         with:
-          name: Logseq-linux.AppImage
-          path: static/out/make/Logseq-linux.AppImage
+          name: logseq-linux-builds
+          path: builds
 
   build-windows:
     runs-on: windows-latest
     needs: [ compile-cljs ]
     steps:
       - name: Download The Static Asset
-        uses: actions/download-artifact@v1
+        uses: actions/download-artifact@v2
         with:
-          name: static.zip
-          path: ./
+          name: static
+          path: static
 
-      - name: Uncompress Static FIles
-        run: unzip static.zip
+      - name: Retrieve tag version
+        id: ref
+        run: |
+          $env:PkgVer=$(cat ./static/VERSION)
+          echo "::set-output name=version::$env:PkgVer"
 
       - name: Install Node.js, NPM and Yarn
         uses: actions/setup-node@v2
         with:
-          node-version: 16
+          node-version: ${{ env.NODE_VERSION }}
 
       # - name: Cache Node Modules
       #   uses: actions/cache@v2
@@ -162,18 +202,16 @@ jobs:
           CSC_LINK: ${{ secrets.CSC_LINK }}
           CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
 
-      - 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: Save Artifact
+        run: |
+          mkdir builds
+          mv static\out\make\squirrel.windows\x64\*.exe builds\Logseq-win-x64-${{ steps.ref.outputs.version }}.exe
 
-      - name: Cache Artifact
-        uses: actions/upload-artifact@v1
+      - name: Upload Artifact
+        uses: actions/upload-artifact@v2
         with:
-          name: Logseq-win64.exe
-          path: static/out/make/squirrel.windows/x64/Logseq-win64.exe
+          name: logseq-win64-builds
+          path: builds
 
   build-macos:
     needs: [ compile-cljs ]
@@ -181,13 +219,16 @@ jobs:
 
     steps:
       - name: Download The Static Asset
-        uses: actions/download-artifact@v1
+        uses: actions/download-artifact@v2
         with:
-          name: static.zip
-          path: ./
+          name: static
+          path: static
 
-      - name: Uncompress Static Files
-        run: unzip ./static.zip
+      - name: Retrieve tag version
+        id: ref
+        run: |
+          pkgver=$(cat ./static/VERSION)
+          echo ::set-output name=version::$pkgver
 
       - name: List Static Files
         run: ls -al ./static
@@ -195,7 +236,19 @@ jobs:
       - name: Install Node.js, NPM and Yarn
         uses: actions/setup-node@v2
         with:
-          node-version: 16
+          node-version: ${{ env.NODE_VERSION }}
+
+      - name: Get yarn cache directory path
+        id: yarn-cache-dir-path
+        run: echo "::set-output name=dir::$(yarn cache dir)"
+      - name: Cache yarn cache directory
+        uses: actions/cache@v2
+        id: yarn-cache
+        with:
+          path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
+          key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
+          restore-keys: |
+            ${{ runner.os }}-yarn-
 
       - name: Signing By Apple Developer ID
         uses: apple-actions/import-codesign-certs@v1
@@ -210,244 +263,92 @@ jobs:
       #       **/node_modules
       #     key: ${{ runner.os }}-node-modules
 
-      - name: Build/Release Electron App
+      - name: Build/Release Electron App for x64
         run: yarn install && yarn electron:make
         working-directory: ./static
         env:
           APPLE_ID: ${{ secrets.APPLE_ID_EMAIL }}
           APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
 
-      - name: Change DMG Name
-        run: mv static/out/make/*.dmg static/out/make/logseq-darwin-x64-${{ github.event.inputs.tag-version }}.dmg
-
-      - name: Cache Artifact DMG
-        uses: actions/upload-artifact@v1
-        with:
-          name: Logseq-x64.dmg
-          path: static/out/make/logseq-darwin-x64-${{ github.event.inputs.tag-version }}.dmg
-
-      - name: ls files
-        run: du -a static/out/
-
-      - name: Change zip Name
-        run: mv static/out/make/zip/darwin/x64/*.zip static/out/make/zip/darwin/x64/logseq-darwin-x64-${{ github.event.inputs.tag-version }}.zip
-
-      - name: Cache Artifact ZIP
-        uses: actions/upload-artifact@v1
-        with:
-          name: Logseq-x64.zip
-          path: static/out/make/zip/darwin/x64/logseq-darwin-x64-${{ github.event.inputs.tag-version }}.zip
-
-  build-macos-arm64:
-    needs: [ compile-cljs ]
-    runs-on: macos-latest
-
-    steps:
-      # this is only needed temporarily
-      # wait until macos-11 GA https://github.com/actions/virtual-environments/issues/2486
-      # or m1 hardware https://github.com/actions/virtual-environments/issues/2187
-      - name: hack osx sdk
+      - name: Save x64 artifacts
         run: |
-          if [ "$(sw_vers -productVersion | cut -d'.' -f1)" = 10 ]; then
-            pushd /Library/Developer/CommandLineTools/SDKs
-            sudo rm MacOSX.sdk
-            sudo ln -s MacOSX11.1.sdk MacOSX.sdk
-            sudo rm -rf MacOSX10.15.sdk
-            ls -l
-            popd
-          fi
-      - 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@v2
-        with:
-          node-version: 16
-
-      - 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
+          mkdir -p builds
+          mv static/out/make/Logseq.dmg ./builds/Logseq-darwin-x64-${{ steps.ref.outputs.version }}.dmg
+          mv static/out/make/zip/darwin/x64/*.zip ./builds/Logseq-darwin-x64-${{ steps.ref.outputs.version }}.zip
 
-      - name: Build/Release Electron App
+      - name: Build/Release Electron App for arm64
         run: yarn install && yarn electron:make-macos-arm64
         working-directory: ./static
         env:
           APPLE_ID: ${{ secrets.APPLE_ID_EMAIL }}
           APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
 
-      - name: Change DMG Name
-        run: mv static/out/make/*.dmg static/out/make/logseq-darwin-arm64-${{ github.event.inputs.tag-version }}.dmg
-
-      - name: Cache Artifact DMG
-        uses: actions/upload-artifact@v1
-        with:
-          name: Logseq-arm64.dmg
-          path: static/out/make/logseq-darwin-arm64-${{ github.event.inputs.tag-version }}.dmg
-
-      - name: ls files
-        run: du -a static/out/
-
-      - name: Change zip Name
-        run: mv static/out/make/zip/darwin/arm64/*.zip static/out/make/zip/darwin/arm64/logseq-darwin-arm64-${{ github.event.inputs.tag-version }}.zip
+      - name: Save arm64 artifacts
+        run: |
+          mv static/out/make/Logseq.dmg ./builds/Logseq-darwin-arm64-${{ steps.ref.outputs.version }}.dmg
+          mv static/out/make/zip/darwin/arm64/*.zip ./builds/Logseq-darwin-arm64-${{ steps.ref.outputs.version }}.zip
 
-      - name: Cache Artifact ZIP
-        uses: actions/upload-artifact@v1
+      - name: Upload Artifact
+        uses: actions/upload-artifact@v2
         with:
-          name: Logseq-arm64.zip
-          path: static/out/make/zip/darwin/arm64/logseq-darwin-arm64-${{ github.event.inputs.tag-version }}.zip
+          name: logseq-darwin-builds
+          path: builds
 
   release:
-    needs: [ build-macos, build-linux, build-windows, build-macos-arm64 ]
+    # NOTE: For now, we only have beta channel to be released on Github
+    if: ${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.build-target == 'beta') }}
+    needs: [ build-macos, build-linux, build-windows ]
     runs-on: ubuntu-18.04
-
     steps:
-      - name: Download The MacOS X64 DMG Artifact
-        uses: actions/download-artifact@v1
-        with:
-          name: Logseq-x64.dmg
-          path: ./
-
-      - name: Download The MacOS X64 ZIP Artifact
-        uses: actions/download-artifact@v1
-        with:
-          name: Logseq-x64.zip
-          path: ./
-
-      - name: Download The MacOS ARM64 DMG Artifact
-        uses: actions/download-artifact@v1
+      - name: Download MacOS Artifacts
+        uses: actions/download-artifact@v2
         with:
-          name: Logseq-arm64.dmg
+          name: logseq-darwin-builds
           path: ./
 
-      - name: Download The MacOS ARM64 ZIP Artifact
-        uses: actions/download-artifact@v1
+      - name: Download The Linux Artifacts
+        uses: actions/download-artifact@v2
         with:
-          name: Logseq-arm64.zip
-          path: ./
-
-      - name: Download The Linux Artifact In Zip format
-        uses: actions/download-artifact@v1
-        with:
-          name: Logseq-linux.zip
-          path: ./
-
-      - name: Download The Linux Artifact In AppImage format
-        uses: actions/download-artifact@v1
-        with:
-          name: Logseq-linux.AppImage
+          name: logseq-linux-builds
           path: ./
 
       - name: Download The Windows Artifact
-        uses: actions/download-artifact@v1
+        uses: actions/download-artifact@v2
         with:
-          name: Logseq-win64.exe
+          name: logseq-win64-builds
           path: ./
 
       - name: List files
         run: ls -rl
 
+      - name: Retrieve tag version
+        id: ref
+        run: |
+          pkgver=$(cat VERSION)
+          echo ::set-output name=version::$pkgver
+
+      - name: Generate SHA256 checksums
+        run: |
+          sha256sum *-darwin-* > SHA256SUMS.txt
+          sha256sum *-win-* >> SHA256SUMS.txt
+          sha256sum *-linux-* >> SHA256SUMS.txt
+          cat SHA256SUMS.txt
+
       - 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 }} (Beta Testing)
-          draft: ${{ github.event.inputs.is-draft }}
-          prerelease: ${{ github.event.inputs.is-pre-release }}
-
-      - name: Upload MacOS X64 ZIP Artifact
-        id: upload-macos-x64-zip-artifact
-        uses: actions/upload-release-asset@v1
-        env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-        with:
-          upload_url: ${{ steps.create_release.outputs.upload_url }}
-          asset_path: ./logseq-darwin-x64-${{ github.event.inputs.tag-version }}.zip
-          asset_name: logseq-darwin-x64-${{ github.event.inputs.tag-version }}.zip
-          asset_content_type: application/zip
-
-      - name: Upload MacOS X64 DMG Artifact
-        id: upload-macos-x64-dmg-artifact
-        uses: actions/upload-release-asset@v1
-        env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-        with:
-          upload_url: ${{ steps.create_release.outputs.upload_url }}
-          asset_path: ./logseq-darwin-x64-${{ github.event.inputs.tag-version }}.dmg
-          asset_name: logseq-darwin-x64-${{ github.event.inputs.tag-version }}.dmg
-          asset_content_type: application/x-apple-diskimage
-
-      - name: Upload MacOS ARM64 ZIP Artifact
-        id: upload-macos-arm64-zip-artifact
-        uses: actions/upload-release-asset@v1
+        uses: softprops/action-gh-release@v1
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
         with:
-          upload_url: ${{ steps.create_release.outputs.upload_url }}
-          asset_path: ./logseq-darwin-arm64-${{ github.event.inputs.tag-version }}.zip
-          asset_name: logseq-darwin-arm64-${{ github.event.inputs.tag-version }}.zip
-          asset_content_type: application/zip
-
-      - name: Upload MacOS ARM64 DMG Artifact
-        id: upload-macos-arm64-dmg-artifact
-        uses: actions/upload-release-asset@v1
-        env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-        with:
-          upload_url: ${{ steps.create_release.outputs.upload_url }}
-          asset_path: ./logseq-darwin-arm64-${{ github.event.inputs.tag-version }}.dmg
-          asset_name: logseq-darwin-arm64-${{ github.event.inputs.tag-version }}.dmg
-          asset_content_type: application/x-apple-diskimage
-
-      - name: Upload Linux Artifact With Zip format
-        id: upload-linux-artifact-with-zip-format
-        uses: actions/upload-release-asset@v1
-        env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-        with:
-          upload_url: ${{ steps.create_release.outputs.upload_url }}
-          asset_path: ./Logseq-linux.zip
-          asset_name: logseq-linux-x64-${{ github.event.inputs.tag-version }}.zip
-          asset_content_type: application/zip
-
-      - name: Upload Linux Artifact With AppImage format
-        id: upload-linux-artifact-with-appimage-format
-        uses: actions/upload-release-asset@v1
-        env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-        with:
-          upload_url: ${{ steps.create_release.outputs.upload_url }}
-          asset_path: ./Logseq-linux.AppImage
-          asset_name: logseq-linux-x64-${{ github.event.inputs.tag-version }}.AppImage
-          asset_content_type: application/octet-stream
-
-      - name: Upload Windows Artifact
-        id: upload-win-artifact
-        uses: actions/upload-release-asset@v1
-        env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-        with:
-          upload_url: ${{ steps.create_release.outputs.upload_url }}
-          asset_path: ./Logseq-win64.exe
-          asset_name: logseq-win-x64-${{ github.event.inputs.tag-version }}.exe
-          asset_content_type: application/octet-stream
+          tag_name: ${{ steps.ref.outputs.version }}
+          name: Desktop APP ${{ steps.ref.outputs.version }} (Beta Testing)
+          body: "TODO: Fill this changelog. Sorry for the inconvenience!"
+          draft: ${{ github.event.inputs.is-draft == true || github.event_name == 'schedule' }}
+          prerelease: ${{ github.event.inputs.is-pre-release == true || github.event_name == 'schedule' }}
+          files: |
+            ./VERSION
+            ./SHA256SUMS.txt
+            ./*.zip
+            ./*.dmg
+            ./*.exe
+            ./*.AppImage
+

+ 10 - 8
.github/workflows/build.yml

@@ -29,13 +29,15 @@ jobs:
           fetch-depth: 1
           submodules: 'true'
 
-      - name: Maven cache
-        uses: actions/cache@v1
-        id: maven-cache
+      - name: Clojure cache
+        uses: actions/cache@v2
+        id: clojure-deps
         with:
-          path: ~/.m2/repository
-          key: ${{ runner.os }}-maven-${{ hashFiles('deps.edn') }}
-          restore-keys: ${{ runner.os }}-maven-
+          path: |
+            ~/.m2/repository
+            ~/.gitlibs
+          key: ${{ runner.os }}-clojure-deps-${{ hashFiles('deps.edn') }}
+          restore-keys: ${{ runner.os }}-clojure-deps-
 
       - name: Prepare Java
         uses: actions/setup-java@v1
@@ -57,8 +59,8 @@ jobs:
         with:
           cli: ${{ env.CLOJURE_VERSION }}
 
-      - name: Fetch Maven deps
-        if: steps.maven-cache.outputs.cache-hit != 'true'
+      - name: Fetch Clojure deps
+        if: steps.clojure-deps.outputs.cache-hit != 'true'
         run: clojure -A:cljs -P
 
       - name: Get yarn cache directory path

+ 4 - 2
README.md

@@ -112,10 +112,12 @@ Run ClojureScript tests
 yarn test
 ```
 
-Run Cypress tests
+Run E2E tests
 
 ``` bash
-yarn e2e-test
+yarn electron-watch
+# in another shell
+yarn e2e-test # or npx playwright test
 ```
 
 ## Desktop app development

+ 13 - 69
e2e-tests/basic.spec.ts

@@ -1,79 +1,25 @@
-import { test, expect } from '@playwright/test'
-import { ElectronApplication, Page, BrowserContext, _electron as electron } from 'playwright'
+import { expect } from '@playwright/test'
+import { test } from './fixtures'
 import { randomString, createRandomPage, openSidebar, newBlock, lastBlock } from './utils'
 
-let electronApp: ElectronApplication
-let context: BrowserContext
-let page: Page
-
-test.beforeAll(async () => {
-  electronApp = await electron.launch({
-    cwd: "./static",
-    args: ["electron.js"],
-    // NOTE: video recording for Electron is not supported yet
-    // recordVideo: {
-    //   dir: "./videos",
-    // }
-  })
-
-  context = electronApp.context()
-  await context.tracing.start({ screenshots: true, snapshots: true });
-
-  // Evaluation expression in the Electron context.
-  const appPath = await electronApp.evaluate(async ({ app }) => {
-    // This runs in the main Electron process, parameter here is always
-    // the result of the require('electron') in the main app script.
-    return app.getAppPath()
-  })
-  console.log("Test start with AppPath:", appPath)
-})
-
-test.beforeEach(async () => {
-  // discard any dialog by ESC
-  if (page) {
-    await page.keyboard.press('Escape')
-    await page.keyboard.press('Escape')
-  } else {
-    page = await electronApp.firstWindow()
-  }
-})
 
-test.afterAll(async () => {
-  // await context.close();
-  await context.tracing.stop({ path: 'artifacts.zip' });
-  await electronApp.close()
-})
-
-test('render app', async () => {
+test('render app', async ({ page }) => {
   // Direct Electron console to Node terminal.
   // page.on('console', console.log)
 
-  // Wait for the app to load
-  await page.waitForLoadState('domcontentloaded')
-  await page.waitForFunction('window.document.title != "Loading"')
+  // NOTE: part of app startup tests is moved to `fixtures.ts`.
 
-  // Logseq: "A privacy-first platform for knowledge management and collaboration."
-  // or Logseq
   expect(await page.title()).toMatch(/^Logseq.*?/)
-
-  page.once('load', async () => {
-    console.log('Page loaded!')
-    await page.screenshot({ path: 'startup.png' })
-  })
 })
 
-test('first start', async () => {
-  await page.waitForSelector('text=This is a demo graph, changes will not be saved until you open a local folder')
-})
-
-test('open sidebar', async () => {
+test('open sidebar', async ({ page }) => {
   await openSidebar(page)
 
   await page.waitForSelector('#sidebar-nav-wrapper a:has-text("New page")', { state: 'visible' })
   await page.waitForSelector('#sidebar-nav-wrapper >> text=Journals', { state: 'visible' })
 })
 
-test('search', async () => {
+test('search', async ({ page }) => {
   await page.click('#search-button')
   await page.waitForSelector('[placeholder="Search or create page"]')
   await page.fill('[placeholder="Search or create page"]', 'welcome')
@@ -83,7 +29,7 @@ test('search', async () => {
   expect(results.length).toBeGreaterThanOrEqual(1)
 })
 
-test('create page and blocks', async () => {
+test('create page and blocks', async ({ page }) => {
   await createRandomPage(page)
 
   // do editing
@@ -126,7 +72,7 @@ test('create page and blocks', async () => {
   expect(await page.$$('.ls-block')).toHaveLength(5)
 })
 
-test('delete and backspace', async () => {
+test('delete and backspace', async ({ page }) => {
   await createRandomPage(page)
 
   await page.fill(':nth-match(textarea, 1)', 'test')
@@ -155,7 +101,7 @@ test('delete and backspace', async () => {
 })
 
 
-test('selection', async () => {
+test('selection', async ({ page }) => {
   await createRandomPage(page)
 
   await page.fill(':nth-match(textarea, 1)', 'line 1')
@@ -182,7 +128,7 @@ test('selection', async () => {
   expect(await page.$$('.ls-block')).toHaveLength(2)
 })
 
-test('template', async () => {
+test('template', async ({ page }) => {
   const randomTemplate = randomString(10)
 
   await createRandomPage(page)
@@ -220,7 +166,7 @@ test('template', async () => {
   expect(await page.$$('.ls-block')).toHaveLength(8)
 })
 
-test('auto completion square brackets', async () => {
+test('auto completion square brackets', async ({ page }) => {
   await createRandomPage(page)
 
   await page.fill(':nth-match(textarea, 1)', 'Auto-completion test')
@@ -257,7 +203,7 @@ test('auto completion square brackets', async () => {
   expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('This is a [[]]]')
 })
 
-test('auto completion and auto pair', async () => {
+test('auto completion and auto pair', async ({ page }) => {
   await createRandomPage(page)
 
   await page.fill(':nth-match(textarea, 1)', 'Auto-completion test')
@@ -265,8 +211,6 @@ test('auto completion and auto pair', async () => {
 
   // {}
   await page.type(':nth-match(textarea, 1)', 'type {{')
-  await page.press(':nth-match(textarea, 1)', 'Escape')
-
   // FIXME: keycode seq is wrong
   // expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('type {{}}')
 
@@ -290,7 +234,7 @@ test('auto completion and auto pair', async () => {
 
 
 // FIXME: Electron with filechooser is not working
-test.skip('open directory', async () => {
+test.skip('open directory', async ({ page }) => {
   await page.click('#sidebar-nav-wrapper >> text=Journals')
   await page.waitForSelector('h1:has-text("Open a local directory")')
   await page.click('h1:has-text("Open a local directory")')

+ 67 - 0
e2e-tests/fixtures.ts

@@ -0,0 +1,67 @@
+import { test as base } from '@playwright/test';
+import { ElectronApplication, Page, BrowserContext, _electron as electron } from 'playwright'
+
+let electronApp: ElectronApplication
+let context: BrowserContext
+let page: Page
+
+base.beforeAll(async () => {
+  if (electronApp) {
+    return ;
+  }
+
+  electronApp = await electron.launch({
+    cwd: "./static",
+    args: ["electron.js"],
+  })
+
+  context = electronApp.context()
+  await context.tracing.start({ screenshots: true, snapshots: true });
+
+  // NOTE: The following ensures App first start with the correct path.
+
+  const appPath = await electronApp.evaluate(async ({ app }) => {
+    return app.getAppPath()
+  })
+  console.log("Test start with AppPath:", appPath)
+
+  page = await electronApp.firstWindow()
+
+  await page.waitForLoadState('domcontentloaded')
+  await page.waitForFunction('window.document.title != "Loading"')
+  await page.waitForSelector('text=This is a demo graph, changes will not be saved until you open a local folder')
+
+  page.once('load', async () => {
+    console.log('Page loaded!')
+    await page.screenshot({ path: 'startup.png' })
+  })
+})
+
+base.beforeEach(async () => {
+  // discard any dialog by ESC
+  if (page) {
+    await page.keyboard.press('Escape')
+    await page.keyboard.press('Escape')
+  }
+})
+
+base.afterAll(async () => {
+  // if (electronApp) {
+  //  await electronApp.close()
+  //}
+})
+
+
+
+// hijack electron app into the test context
+export const test = base.extend<{ page: Page, context: BrowserContext, app: ElectronApplication }>({
+  page: async ({ }, use) => {
+    await use(page);
+  },
+  context: async ({ }, use) => {
+    await use(context);
+  },
+  app: async ({ }, use) => {
+    await use(electronApp);
+  }
+});

+ 55 - 0
e2e-tests/hotkey.spec.ts

@@ -0,0 +1,55 @@
+import { expect } from '@playwright/test'
+import { test } from './fixtures'
+import { createRandomPage, newBlock, lastBlock, appFirstLoaded, IsMac, IsLinux } from './utils'
+
+test('open search dialog', async ({ page }) => {
+  if (IsMac) {
+    await page.keyboard.press('Meta+k')
+  } else if (IsLinux) {
+    await page.keyboard.press('Control+k')
+  } else {
+    // TODO: test on Windows and other platforms
+    expect(false)
+  }
+
+  await page.waitForSelector('[placeholder="Search or create page"]')
+  await page.keyboard.press('Escape')
+  await page.waitForSelector('[placeholder="Search or create page"]', { state: 'hidden' })
+})
+
+// See-also: https://github.com/logseq/logseq/issues/3278
+test('insert link', async ({ page }) => {
+  await createRandomPage(page)
+
+  let hotKey = 'Control+l'
+  let selectAll = 'Control+a'
+  if (IsMac) {
+    hotKey = 'Meta+l'
+    selectAll = 'Meta+a'
+  }
+
+  // Case 1: empty link
+  await lastBlock(page)
+  await page.press(':nth-match(textarea, 1)', hotKey)
+  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('[]()')
+  await page.type(':nth-match(textarea, 1)', 'Logseq Website')
+  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('[Logseq Website]()')
+
+  // Case 2: link with label
+  await newBlock(page)
+  await page.type(':nth-match(textarea, 1)', 'Logseq')
+  await page.press(':nth-match(textarea, 1)', selectAll)
+  await page.press(':nth-match(textarea, 1)', hotKey)
+  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('[Logseq]()')
+  await page.type(':nth-match(textarea, 1)', 'https://logseq.com/')
+  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('[Logseq](https://logseq.com/)')
+
+  // Case 3: link with URL
+  await newBlock(page)
+  await page.type(':nth-match(textarea, 1)', 'https://logseq.com/')
+  await page.press(':nth-match(textarea, 1)', selectAll)
+  await page.press(':nth-match(textarea, 1)', hotKey)
+  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('[](https://logseq.com/)')
+  await page.type(':nth-match(textarea, 1)', 'Logseq')
+  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('[Logseq](https://logseq.com/)')
+})

+ 15 - 3
e2e-tests/utils.ts

@@ -1,6 +1,10 @@
-import { Page } from 'playwright'
+import { Page, Locator } from 'playwright'
 import { expect } from '@playwright/test'
+import process from 'process'
 
+export const IsMac = process.platform === 'darwin'
+export const IsLinux = process.platform === 'linux'
+export const IsWindows = process.platform === 'win32'
 
 export function randomString(length: number) {
     const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
@@ -14,6 +18,10 @@ export function randomString(length: number) {
     return result;
 }
 
+export async function appFirstLoaded(page: Page) {
+    await page.waitForSelector('text=This is a demo graph, changes will not be saved until you open a local folder')
+}
+
 export async function openSidebar(page: Page) {
     let sidebarVisible = await page.isVisible('#sidebar-nav-wrapper .left-sidebar-inner')
     if (!sidebarVisible) {
@@ -35,16 +43,20 @@ export async function createRandomPage(page: Page) {
     await page.waitForSelector(':nth-match(textarea, 1)', { state: 'visible' })
 }
 
-export async function lastBlock(page: Page) {
+export async function lastBlock(page: Page): Promise<Locator> {
     // discard any popups
     await page.keyboard.press('Escape')
     // click last block
     await page.click('.ls-block >> nth=-1')
     // wait for textarea
     await page.waitForSelector(':nth-match(textarea, 1)', { state: 'visible' })
+
+    return page.locator(':nth-match(textarea, 1)')
 }
 
-export async function newBlock(page: Page) {
+export async function newBlock(page: Page): Promise<Locator> {
     await lastBlock(page)
     await page.press(':nth-match(textarea, 1)', 'Enter')
+
+    return page.locator(':nth-match(textarea, 1)')
 }

+ 1 - 1
libs/src/LSPlugin.ts

@@ -226,7 +226,7 @@ export interface IAppProxy {
   replaceState: (k: string, params?: Record<string, any>, query?: Record<string, any>) => void
 
   // ui
-  queryElementById: (id: string) => string | boolean
+  queryElementById: (id: string) => Promise<string | boolean>
   showMsg: (content: string, status?: 'success' | 'warning' | 'error' | string) => void
   setZoomFactor: (factor: number) => void
   setFullScreen: (flag: boolean | 'toggle') => void

+ 1 - 1
package.json

@@ -91,7 +91,7 @@
         "ignore": "5.1.8",
         "is-svg": "4.2.2",
         "jszip": "3.5.0",
-        "mldoc": "1.2.3",
+        "mldoc": "1.2.4",
         "path": "0.12.7",
         "pixi-graph-fork": "0.1.6",
         "pixi.js": "6.2.0",

+ 1 - 0
playwright.config.ts

@@ -3,6 +3,7 @@ import { PlaywrightTestConfig } from '@playwright/test'
 const config: PlaywrightTestConfig = {
   testDir: './e2e-tests',
   maxFailures: 1,
+  workers: 1, // NOTE: must be 1 for now, otherwise tests will fail.
   use: {
     screenshot: 'only-on-failure',
   }

+ 28 - 0
scripts/get-pkg-version.js

@@ -0,0 +1,28 @@
+// This script file simply outputs the version in the package.json.
+// It is used as a helper by the continuous integration
+const path = require('path')
+const process = require('process')
+const fs = require('fs')
+
+const content = fs.readFileSync(
+  path.join(__dirname, '../src/main/frontend/version.cljs')
+)
+const pattern = /\(defonce version "(.*?)"\)/g
+
+const match = pattern.exec(content)
+let ver = '0.0.1'
+if (match) {
+  ver = match[1]
+} else {
+  console.error('Could not find version in version.cljs')
+  process.exit(1)
+}
+
+if (process.argv[2] === 'nightly' || process.argv[2] === '') {
+  const today = new Date()
+  console.log(
+    ver + '+nightly.' + today.toISOString().split('T')[0].replaceAll('-', '')
+  )
+} else {
+  console.log(ver)
+}

+ 16 - 6
src/main/frontend/components/block.cljs

@@ -89,6 +89,7 @@
 
 ;; TODO: dynamic
 (defonce max-blocks-per-page 200)
+(defonce max-depth-of-links 5)
 (defonce *blocks-container-id (atom 0))
 
 ;; TODO:
@@ -857,8 +858,12 @@
     (let [{:keys [url label title metadata full_text]} link]
       (match url
         ["Block_ref" id]
-        (let [label* (if (seq (mldoc/plain->text label)) label nil)]
-          (block-reference (assoc config :reference? true) id label*))
+        (let [label* (if (seq (mldoc/plain->text label)) label nil)
+              {:keys [link-depth]} config
+              link-depth (or link-depth 0)]
+          (if (> link-depth max-depth-of-links)
+            [:p.warning.text-sm "Block ref nesting is too deep"]
+            (block-reference (assoc config :reference? true :link-depth (inc link-depth)) id label*)))
 
         ["Page_ref" page]
         (let [format (get-in config [:block :block/format])]
@@ -1200,16 +1205,21 @@
               (ui/tweet-embed id))))
 
         (= name "embed")
-        (let [a (first arguments)]
+        (let [a (first arguments)
+              {:keys [link-depth]} config
+              link-depth (or link-depth 0)]
           (cond
             (nil? a) ; empty embed
             nil
 
+            (> link-depth max-depth-of-links)
+            [:p.warning.text-sm "Embed depth is too deep"]
+
             (and (string/starts-with? a "[[")
                  (string/ends-with? a "]]"))
             (let [page-name (text/get-page-name a)]
               (when-not (string/blank? page-name)
-                (page-embed config page-name)))
+                (page-embed (assoc config :link-depth (inc link-depth)) page-name)))
 
             (and (string/starts-with? a "((")
                  (string/ends-with? a "))"))
@@ -1220,7 +1230,7 @@
                                  (let [s (string/trim s)]
                                    (and (util/uuid-string? s)
                                         (uuid s))))]
-                (block-embed config id)))
+                (block-embed (assoc config :link-depth (inc link-depth)) id)))
 
             :else                       ;TODO: maybe collections?
             nil))
@@ -2589,7 +2599,7 @@
     (let [{:keys [lines language]} options
           attr (when language
                  {:data-lang language})
-          code (join-lines lines)]
+          code (apply str lines)]
       (cond
         html-export?
         (highlight/html-export attr code)

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

@@ -274,48 +274,49 @@
 }
 
 .ls-block h1,
-.editor-inner .h1 {
+.editor-inner .h1.uniline-block {
   font-size: 2em;
   min-height: 1.5em;
 }
 
 .ls-block h2,
-.editor-inner .h2 {
+.editor-inner .h2.uniline-block {
   font-size: 1.5em;
   min-height: 1.5em;
 }
 
 .ls-block h3,
-.editor-inner .h3 {
+.editor-inner .h3.uniline-block {
   font-size: 1.17em;
   min-height: 1.17em;
 }
 
 .ls-block h4,
-.editor-inner .h4 {
+.editor-inner .h4.uniline-block {
   font-size: 1.12em;
   min-height: 1.12em;
 }
 
 .ls-block h5,
-.editor-inner .h5 {
+.editor-inner .h5.uniline-block {
   font-size: 0.83em;
   min-height: 0.83em;
 }
 
 .ls-block h6,
-.editor-inner .h6 {
+.editor-inner .h6.uniline-block {
   font-size: 0.75em;
   min-height: 0.75em;
 }
 
 .ls-block :is(h1, h2, h3, h4, h5, h6),
-.editor-inner :is(.h1, .h2, .h3, .h4, .h5, .h6) {
+.editor-inner .uniline-block:is(.h1, .h2, .h3, .h4, .h5, .h6),
+.editor-inner .multiline-block:is(.h1, .h2, .h3, .h4, .h5, .h6)::first-line {
   font-weight: 600;
 }
 
 .ls-block :is(h1, h2),
-.editor-inner :is(.h1, .h2) {
+.editor-inner .uniline-block:is(.h1, .h2) {
   border-bottom: 1px solid var(--ls-quaternary-background-color);
   margin: 0.4em 0 0;
 }

+ 39 - 23
src/main/frontend/components/editor.cljs

@@ -19,6 +19,7 @@
             [frontend.ui :as ui]
             [frontend.util :as util]
             [frontend.util.cursor :as cursor]
+            [frontend.util.keycode :as keycode]
             [goog.dom :as gdom]
             [promesa.core :as p]
             [rum.core :as rum]))
@@ -455,23 +456,41 @@
 
 (def starts-with? clojure.string/starts-with?)
 
-(defn get-editor-heading-class [content]
+(defn get-editor-style-class
+  "Get textarea css class according to it's content"
+  [content format]
   (let [content (if content (str content) "")]
+    ;; as the function is binding to the editor content, optimization is welcome
     (str
-     (if (string/includes? content "\n") "multiline-block" "uniline-block")
+     (if (or (> (.-length content) 1000)
+             (string/includes? content "\n"))
+       "multiline-block"
+       "uniline-block")
      " "
-     (cond
-       (starts-with? content "# ") "h1"
-       (starts-with? content "## ") "h2"
-       (starts-with? content "### ") "h3"
-       (starts-with? content "#### ") "h4"
-       (starts-with? content "##### ") "h5"
-       (starts-with? content "###### ") "h6"
-       (starts-with? content "TODO ") "todo-block"
-       (starts-with? content "DOING ") "doing-block"
-       (starts-with? content "DONE ") "done-block"
-       (and (starts-with? content "---\n") (.endsWith content "\n---")) "page-properties"
-       :else "normal-block"))))
+     (case format
+       :markdown
+       (cond
+         (starts-with? content "# ") "h1"
+         (starts-with? content "## ") "h2"
+         (starts-with? content "### ") "h3"
+         (starts-with? content "#### ") "h4"
+         (starts-with? content "##### ") "h5"
+         (starts-with? content "###### ") "h6"
+         (and (starts-with? content "---\n") (.endsWith content "\n---")) "page-properties"
+         :else "normal-block")
+       ;; other formats
+       (cond
+         (and (starts-with? content "---\n") (.endsWith content "\n---")) "page-properties"
+         :else "normal-block")))))
+
+(defn editor-row-height-unchanged?
+  "Check if the row height of editor textarea is changed, which happens when font-size changed"
+  []
+  ;; FIXME: assuming enter key is the only trigger of the height changing (under markdown editing of headlines)
+  ;; FIXME: looking for an elegant & robust way to track the change of font-size, or wait for our own WYSIWYG text area
+  (let [last-key (state/get-last-key-code)]
+    (and (not= keycode/enter (:key-code last-key))
+         (not= keycode/enter-code (:code last-key)))))
 
 (rum/defc mock-textarea <
   rum/static
@@ -503,7 +522,7 @@
 
 (rum/defc mock-textarea-wrapper < rum/reactive
   []
-  (let [content (state/sub [:editor/content (state/get-edit-input-id)])]
+  (let [content (state/sub-edit-content)]
     (mock-textarea content)))
 
 (defn animated-modal
@@ -574,23 +593,20 @@
   lifecycle/lifecycle
   [state {:keys [on-hide node format block block-parent-id heading-level]
           :as   option} id config]
-  (let [content (state/get-edit-content)
-        heading-level (get state ::heading-level)]
-    [:div.editor-inner {:class (str
-                                (if block "block-editor" "non-block-editor")
-                                " "
-                                (get-editor-heading-class content))}
+  (let [content (state/sub-edit-content)
+        heading-class (get-editor-style-class content format)]
+    [:div.editor-inner {:class (if block "block-editor" "non-block-editor")}
      (when config/mobile? (mobile-bar state id))
      (ui/ls-textarea
       {:id                id
-       :cacheMeasurements true
+       :cacheMeasurements (editor-row-height-unchanged?) ;; check when content updated (as the content variable is binded)
        :default-value     (or content "")
        :minRows           (if (state/enable-grammarly?) 2 1)
        :on-click          (editor-handler/editor-on-click! id)
        :on-change         (editor-handler/editor-on-change! block id search-timeout)
        :on-paste          (editor-handler/editor-on-paste! id)
        :auto-focus        false
-       :class             (get-editor-heading-class content)})
+       :class             heading-class})
 
      (mock-textarea-wrapper)
      (modals id format)

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

@@ -152,7 +152,7 @@
     (when downloaded
       [:div.cp__header-tips
        [:p (t :updater/new-version-install)
-        [:a.ui__button.restart
+        [:a.restart.ml-2
          {:on-click #(handler/quit-and-install-new-version!)}
          (svg/reload 16) [:strong (t :updater/quit-and-install)]]]])))
 

+ 12 - 12
src/main/frontend/components/header.css

@@ -52,30 +52,29 @@
 
   &-tips {
     position: absolute;
+    padding: 6px 0;
+    text-align: center;
+    min-width: var(--ls-main-content-max-width);
     width: 100%;
-    padding: 0 0;
-    transform: translateY(100%);
-    display: flex;
-    justify-content: center;
+    font-weight: 500;
     align-items: center;
-    background-color: var(--ls-secondary-background-color);
-    box-sizing: border-box;
-    margin: 0;
+    background: var(--color-level-3);
+    margin-top: -16px;
     left: 0;
-    top: -2px;
-    color: var(--ls-secondary-text-color);
+    z-index: 1000;
 
     > p {
+      color: var(--ls-primary-text-color);
       margin: 0;
-      display: flex;
+      display: inline-flex;
       align-items: center;
       font-size: 14px;
     }
 
     a {
-      color: var(--ls-link-text-color) !important;
+      color: var(--ls-link-text-color, #045591) !important;
     }
-
+    
     a.restart {
       position: relative;
       cursor: pointer !important;
@@ -84,6 +83,7 @@
 
       svg {
         color: currentColor !important;
+        margin-right: 2px;
       }
 
       > strong {

+ 4 - 2
src/main/frontend/components/lazy_editor.cljs

@@ -2,7 +2,8 @@
   (:require [rum.core :as rum]
             [shadow.lazy :as lazy]
             [frontend.ui :as ui]
-            [frontend.state :as state]))
+            [frontend.state :as state]
+            [frontend.text :as text]))
 
 (def lazy-editor (lazy/loadable frontend.extensions.code/editor))
 
@@ -16,7 +17,8 @@
                  state)}
   [config id attr code options]
   (let [loaded? (rum/react loaded?)
-        theme (state/sub :ui/theme)]
+        theme (state/sub :ui/theme)
+        code (when code (text/remove-indentations code))]
     (if loaded?
       (@lazy-editor config id attr code theme options)
       (ui/loading "CodeMirror"))))

+ 2 - 3
src/main/frontend/components/page.cljs

@@ -443,7 +443,7 @@
                 ;;      (set-setting! :layout value))
                 ;;    "graph-layout")]
                 [:div.flex.items-center.justify-between.mb-2
-                 [:span "Journals"]
+                 [:span (t :right-side-bar/journals)]
                  ;; FIXME: why it's not aligned well?
                  [:div.mt-1
                   (ui/toggle journal?
@@ -748,8 +748,7 @@
 
         search-key (fn [key]
                      (when-let [key (and key (string/trim key))]
-                       (if (and (> (count key) 2)
-                                (not (string/blank? key))
+                       (if (and (not (string/blank? key))
                                 (seq @*results))
                          (reset! *search-key key)
                          (reset! *search-key nil))))

+ 146 - 146
src/main/frontend/components/settings.cljs

@@ -40,10 +40,10 @@
            :on-change (fn [e]
                         (reset! email (util/evalue e)))}]]]]
       (ui/button
-        "Submit"
-        :on-click
-        (fn []
-          (user-handler/set-email! @email)))
+       "Submit"
+       :on-click
+       (fn []
+         (user-handler/set-email! @email)))
 
       [:hr]
 
@@ -64,10 +64,10 @@
            :on-change (fn [e]
                         (reset! cors (util/evalue e)))}]]]]
       (ui/button
-        "Submit"
-        :on-click
-        (fn []
-          (user-handler/set-cors! @cors)))
+       "Submit"
+       :on-click
+       (fn []
+         (user-handler/set-cors! @cors)))
 
       [:hr]
 
@@ -79,12 +79,13 @@
    [:label.block.text-sm.font-medium.leading-5.opacity-70
     {:for label-for}
     name]
-   [:div.mt-1.sm:mt-0.sm:col-span-2
-    [:div.rounded-md
-     {:style {:display "flex" :gap "1rem" :align-items "center"}}
+   [:div.rounded-md.sm:max-w-tss.sm:col-span-2
+    [:div.rounded-md {:style {:display "flex" :gap "1rem" :align-items "center"}}
      (ui/toggle state on-toggle true)
      detail-text]]])
 
+
+
 (rum/defcs app-updater < rum/reactive
   [state version]
   (let [update-pending? (state/sub :electron/updater-pending?)
@@ -92,20 +93,23 @@
     [:span.cp__settings-app-updater
 
      [:div.ctls.flex.items-center
-      (ui/button
-        (if update-pending? "Checking ..." "Check for updates")
-        :class "text-sm p-1 mr-3"
-        :disabled update-pending?
-        :on-click #(js/window.apis.checkForUpdates false))
 
-      [:span version]]
+      [:div.mt-1.sm:mt-0.sm:col-span-2
+       {:style {:display "flex" :gap "0.5rem" :align-items "center"}}
+       [:div (ui/button
+              (if update-pending? "Checking ..." "Check for updates")
+              :class "text-sm p-1 mr-1"
+              :disabled update-pending?
+              :on-click #(js/window.apis.checkForUpdates false))]
+
+       [:div.text-sm.opacity-50 (str "Version " version)]]]
 
      (when-not (or update-pending?
                    (string/blank? type))
-       [:div.update-state
+       [:div.update-state.text-sm
         (case type
           "update-not-available"
-          [:p "😀 Your app is up-to-date!"]
+          [:p "Your app is up-to-date 🎉"]
 
           "update-available"
           (let [{:keys [name url]} payload]
@@ -159,12 +163,45 @@
            :width  500
            :height 500}]]])
 
+(defn row-with-button-action
+  [{:keys [left-label action button-label href on-click desc -for]}]
+  [:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start
+
+     ;; left column
+   [:label.block.text-sm.font-medium.leading-5.opacity-70
+    {:for -for}
+    left-label]
+
+     ;; right column
+   [:div.mt-1.sm:mt-0.sm:col-span-2
+    {:style {:display "flex" :gap "0.5rem" :align-items "center"}}
+    [:div (if action action (ui/button
+                             button-label
+                             :class    "text-sm p-1"
+                             :href     href
+                             :on-click on-click))]
+    (when-not (or (util/mobile?)
+                  (mobile-util/is-native-platform?))
+      [:div.text-sm desc])]])
+
+
 (defn edit-config-edn []
   (rum/with-context [[t] i18n/*tongue-context*]
-    [:div
-     [:a.text-xs {:href     (rfe/href :file {:path (config/get-config-path)})
-                  :on-click #(js/setTimeout (fn [] (ui-handler/toggle-settings-modal!)))}
-      (t :settings-page/edit-config-edn)]]))
+    (row-with-button-action
+     {:left-label   "Custom configuration"
+      :button-label (t :settings-page/edit-config-edn)
+      :href         (rfe/href :file {:path (config/get-config-path)})
+      :on-click     #(js/setTimeout (fn [] (ui-handler/toggle-settings-modal!)))
+      :-for         "config_edn"})))
+
+(defn edit-custom-css []
+  (rum/with-context [[t] i18n/*tongue-context*]
+    (row-with-button-action
+     {:left-label   "Custom theme"
+      :button-label (t :settings-page/edit-custom-css)
+      :href         (rfe/href :file {:path (config/get-custom-css-path)})
+      :on-click     #(js/setTimeout (fn [] (ui-handler/toggle-settings-modal!)))
+      :-for         "customize_css"})))
 
 (defn show-brackets-row [t show-brackets?]
   [:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start
@@ -204,7 +241,7 @@
   (let [enabled? (state/get-git-auto-commit-enabled?)]
     [:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start
      [:label.block.text-sm.font-medium.leading-5.opacity-70
-      "Enable Git auto commit"]
+      (t :settings-page/git-switcher-label)]
      [:div
       [:div.rounded-md.sm:max-w-xs
        (ui/toggle
@@ -219,7 +256,7 @@
   (let [secs (or (state/sub [:electron/user-cfgs :git/auto-commit-seconds]) 60)]
     [:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start
      [:label.block.text-sm.font-medium.leading-5.opacity-70
-      "Git auto commit seconds"]
+      (t :settings-page/git-commit-delay)]
      [:div.mt-1.sm:mt-0.sm:col-span-2
       [:div.max-w-lg.rounded-md.sm:max-w-xs
        [:input#home-default-page.form-input.is-small.transition.duration-150.ease-in-out
@@ -231,65 +268,42 @@
                               (state/set-state! [:electron/user-cfgs :git/auto-commit-seconds] value)
                               (ipc/ipc "userAppCfgs" :git/auto-commit-seconds value))))}]]]]))
 
-(rum/defc app-auto-update-row < rum/reactive
-  [t]
+(rum/defc app-auto-update-row < rum/reactive [t]
   (let [enabled? (state/sub [:electron/user-cfgs :auto-update])
         enabled? (if (nil? enabled?) true enabled?)]
-
-    [:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start
-     [:label.block.text-sm.font-medium.leading-5.opacity-70
-      (t :settings-page/auto-updater)]
-     [:div
-      [:div.rounded-md.sm:max-w-xs
-       (ui/toggle
-        enabled?
-        (fn []
-          (state/set-state! [:electron/user-cfgs :auto-update] (not enabled?))
-          (ipc/ipc "userAppCfgs" :auto-update (not enabled?)))
-        true)]]]))
-
-(rum/defcs graph-config
-  [state t]
-  (when-let [current-repo (state/sub :git/current-repo)]
-    (edit-config-edn)))
+    (toggle "usage-diagnostics"
+            (t :settings-page/auto-updater)
+            enabled?
+            #((state/set-state! [:electron/user-cfgs :auto-update] (not enabled?))
+              (ipc/ipc "userAppCfgs" :auto-update (not enabled?))))))
 
 (defn language-row [t preferred-language]
-  [:div.it.sm:grid.sm:grid-cols-5.sm:gap-4.sm:items-start
-   [:label.block.text-sm.font-medium.leading-5.opacity-70
-    {:for "preferred_language"}
-    (t :language)]
-   [:div.mt-1.sm:mt-0.sm:col-span-4
-    [:div.max-w-lg.rounded-md
-     [:select.form-select.is-small
-      {:value     preferred-language
-       :on-change (fn [e]
+  (let [on-change (fn [e]
                     (let [lang-code (util/evalue e)]
                       (state/set-preferred-language! lang-code)
-                      (ui-handler/re-render-root!)))}
-      (for [language dicts/languages]
-        (let [lang-code (name (:value language))
-              lang-label (:label language)]
-          [:option {:key lang-code :value lang-code} lang-label]))]]]])
+                      (ui-handler/re-render-root!)))
+        action [:select.form-select.is-small {:value     preferred-language
+                                              :on-change on-change}
+                (for [language dicts/languages]
+                  (let [lang-code (name (:value language))
+                        lang-label (:label language)]
+                    [:option {:key lang-code :value lang-code} lang-label]))]]
+    (row-with-button-action {:left-label (t :language)
+                             :-for       "preferred_language"
+                             :action     action})))
 
 (defn theme-modes-row [t switch-theme system-theme? dark?]
-  [:div.it.sm:grid.sm:grid-cols-5.sm:gap-4
-   [:label.block.text-sm.font-medium.leading-5.opacity-70
-    {:for "toggle_theme"}
-    (t :right-side-bar/switch-theme (string/capitalize switch-theme))]
-   [:div.flex.flex-row.mt-1.sm:mt-0.sm:col-span-4
-    [:div.rounded-md.sm:max-w-xs
-
-     [:ul.theme-modes-options
-      [:li {:on-click (partial state/use-theme-mode! "light")
-            :class    (classnames [{:active (and (not system-theme?) (not dark?))}])} [:i.mode-light] [:strong "light"]]
-      [:li {:on-click (partial state/use-theme-mode! "dark")
-            :class    (classnames [{:active (and (not system-theme?) dark?)}])} [:i.mode-dark] [:strong "dark"]]
-      [:li {:on-click (partial state/use-theme-mode! "system")
-            :class    (classnames [{:active system-theme?}])} [:i.mode-system] [:strong "system"]]]]
-
-    (when-not (mobile-util/is-native-platform?)
-     [:div.pl-16
-      (ui/render-keyboard-shortcut (shortcut-helper/gen-shortcut-seq :ui/toggle-theme))])]])
+  (let [pick-theme [:ul.theme-modes-options
+                    [:li {:on-click (partial state/use-theme-mode! "light")
+                          :class    (classnames [{:active (and (not system-theme?) (not dark?))}])} [:i.mode-light] [:strong "light"]]
+                    [:li {:on-click (partial state/use-theme-mode! "dark")
+                          :class    (classnames [{:active (and (not system-theme?) dark?)}])} [:i.mode-dark] [:strong "dark"]]
+                    [:li {:on-click (partial state/use-theme-mode! "system")
+                          :class    (classnames [{:active system-theme?}])} [:i.mode-system] [:strong "system"]]]]
+    (row-with-button-action {:left-label (t :right-side-bar/switch-theme (string/capitalize switch-theme))
+                             :-for       "toggle_theme"
+                             :action     pick-theme
+                             :desc       (ui/render-keyboard-shortcut (shortcut-helper/gen-shortcut-seq :ui/toggle-theme))})))
 
 (defn file-format-row [t preferred-format]
   [:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start
@@ -377,8 +391,7 @@
   (toggle "enable_timetracking"
           (t :settings-page/enable-timetracking)
           enable-timetracking?
-          (fn []
-            (let [value (not enable-timetracking?)]
+          #((let [value (not enable-timetracking?)]
               (config-handler/set-config! :feature/enable-timetracking? value)))))
 
 (defn update-home-page
@@ -440,28 +453,19 @@
 
 (defn encryption-row [t enable-encryption?]
   (toggle "enable_encryption"
-          (str (t :settings-page/enable-encryption) "\n(experimental!)")
+          (t :settings-page/enable-encryption)
           enable-encryption?
-          (fn []
-            (let [value (not enable-encryption?)]
-              (config-handler/set-config! :feature/enable-encryption? value)))))
-
-(rum/defc keyboard-shortcuts-row
-  [t]
-  [:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start
-   [:label.block.text-sm.font-medium.leading-5.opacity-70
-    {:for "customize_shortcuts"}
-    (t :settings-page/customize-shortcuts)]
-   (let [h (fn []
-             (state/close-settings!)
-             (route-handler/redirect! {:to :shortcut-setting}))]
-     [:div.mt-1.sm:mt-0.sm:col-span-2
-      [:div
-       (ui/button
-         (t :settings-page/shortcut-settings)
-         :class "text-sm p-1"
-         :style {:margin-top "0px"}
-         :on-click h)]])])
+          #((let [value (not enable-encryption?)]
+              (config-handler/set-config! :feature/enable-encryption? value)))
+          [:div.text-sm.opacity-50 "⚠️ This feature is experimental"]))
+
+(rum/defc keyboard-shortcuts-row [t]
+  (row-with-button-action
+   {:left-label   (t :settings-page/customize-shortcuts)
+    :button-label (t :settings-page/shortcut-settings)
+    :on-click      #((state/close-settings!)
+                     (route-handler/redirect! {:to :shortcut-setting}))
+    :-for         "customize_shortcuts"}))
 
 (defn zotero-settings-row [t]
   [:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start
@@ -471,13 +475,13 @@
    [:div.mt-1.sm:mt-0.sm:col-span-2
     [:div
      (ui/button
-       "Zotero settings"
-       :class "text-sm p-1"
-       :style {:margin-top "0px"}
-       :on-click
-       (fn []
-         (state/close-settings!)
-         (route-handler/redirect! {:to :zotero-setting})))]]])
+      "Zotero settings"
+      :class "text-sm p-1"
+      :style {:margin-top "0px"}
+      :on-click
+      (fn []
+        (state/close-settings!)
+        (route-handler/redirect! {:to :zotero-setting})))]]])
 
 (defn auto-push-row [t current-repo enable-git-auto-push?]
   (when (string/starts-with? current-repo "https://")
@@ -494,28 +498,18 @@
           (not instrument-disabled?)
           (fn [] (instrument/disable-instrument
                   (not instrument-disabled?)))
-          [:span.text-sm.text-justify.ml-2.5.opacity-50 "Logseq will never collect your local graph database or sell your data."]))
+          [:span.text-sm.opacity-50 "Logseq will never collect your local graph database or sell your data."]))
 
 (defn clear-cache-row [t]
-  [:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-center
-   [:label.block.text-sm.font-medium.leading-5.opacity-70
-    {:for "clear_cache"}
-    (t :settings-page/clear-cache)]
-   [:div.mt-1.sm:mt-0.sm:col-span-2
-    [:div.max-w-lg.rounded-md.sm:max-w-xs
-     (ui/button
-       (t :settings-page/clear)
-       :class "text-sm p-1"
-       :on-click handler/clear-cache!)]]])
+  (row-with-button-action {:left-label   (t :settings-page/clear-cache)
+                           :button-label (t :settings-page/clear)
+                           :on-click     handler/clear-cache!
+                           :-for         "clear_cache"}))
 
 (defn version-row [t version]
-  [:div.it.app-updater.sm:grid.sm:grid-cols-5.sm:gap-4.sm:items-center
-   [:label.block.text-sm.font-medium.leading-5.opacity-70
-    (t :settings-page/current-version)]
-   [:div.wrap.sm:mt-0.sm:col-span-4
-    (if (util/electron?)
-      (app-updater version)
-      [:span.ver version])]])
+  (row-with-button-action {:left-label (t :settings-page/current-version)
+                           :action     (app-updater version)
+                           :-for       "current-version"}))
 
 (defn developer-mode-row [t developer-mode?]
   (toggle "developer_mode"
@@ -578,16 +572,14 @@
 
         [:aside.md:w-64
          [:ul
-          (let [general [:general (t :settings-page/tab-general) (ui/icon "adjustments" {:style {:font-size 20}})]
-                editor [:editor (t :settings-page/tab-editor) (ui/icon "writing" {:style {:font-size 20}})]
-                shortcuts [:shortcuts (t :settings-page/tab-shortcuts) (ui/icon "command" {:style {:font-size 20}})]
-                git [:git (t :settings-page/tab-version-control) (ui/icon "history" {:style {:font-size 20}})]
-                advanced [:advanced (t :settings-page/tab-advanced) (ui/icon "bulb" {:style {:font-size 20}})]
-                labels&texts&icons (if (mobile-util/is-native-platform?)
-                                     (conj [] general editor shortcuts advanced)
-                                     (conj [] general editor shortcuts git advanced))]
-            (for [[label text icon] labels&texts&icons]
-
+          (for [[label text icon]
+                [[:general (t :settings-page/tab-general) (ui/icon "adjustments" {:style {:font-size 20}})]
+                 [:editor (t :settings-page/tab-editor) (ui/icon "writing" {:style {:font-size 20}})]
+                 (when-not (mobile-util/is-native-platform?)
+                   [:git (t :settings-page/tab-version-control) (ui/icon "history" {:style {:font-size 20}})])
+                 [:advanced (t :settings-page/tab-advanced) (ui/icon "bulb" {:style {:font-size 20}})]]]
+
+            (when label
               [:li
                {:class    (util/classnames [{:active (= label @*active)}])
                 :on-click #(reset! *active label)}
@@ -604,7 +596,11 @@
            [:div.panel-wrap.is-general
             (version-row t version)
             (language-row t preferred-language)
-            (theme-modes-row t switch-theme system-theme? dark?)]
+            (theme-modes-row t switch-theme system-theme? dark?)
+            (when-let [current-repo (state/sub :git/current-repo)]
+              [(edit-config-edn)
+               (edit-custom-css)])
+            (keyboard-shortcuts-row t)]
 
            :editor
            [:div.panel-wrap.is-editor
@@ -620,27 +616,29 @@
               (tooltip-row t enable-tooltip?))
             (timetracking-row t enable-timetracking?)
             (journal-row t enable-journals?)
-            (enable-all-pages-public-row t enable-all-pages-public?)
             (encryption-row t enable-encryption?)
+            (enable-all-pages-public-row t enable-all-pages-public?)
             (zotero-settings-row t)
             (auto-push-row t current-repo enable-git-auto-push?)]
 
-           :shortcuts
-           [:div.panel-wrap
-            (keyboard-shortcuts-row t)]
-
            :git
            [:div.panel-wrap
             [:div.text-sm.my-4
-             [:a {:href "https://git-scm.com/"
-                  :target "_blank"} "Git"]
-             " is used for pages version control, you can click the vertical three dots menu to check the page's history."]
+             [:span.text-sm.opacity-50.my-4
+              "You can view a page's edit history by clicking the three vertical dots "
+              "in the top-right corner and selecting \"Check page's history\". "
+              "Logseq uses "]
+             [:a {:href "https://git-scm.com/" :target "_blank"}
+              "Git"]
+             [:span.text-sm.opacity-50.my-4
+              " for version control."]]
+            [:br]
             (switch-git-auto-commit-row t)
             (git-auto-commit-seconds t)
 
             (ui/admonition
              :warning
-             [:p "You need to restart the app after updating the settings."])]
+             [:p (t :settings-page/git-confirm)])]
 
            :advanced
            [:div.panel-wrap.is-advanced
@@ -649,6 +647,10 @@
             (if-not (mobile-util/is-native-platform?) (developer-mode-row t developer-mode?))
             (clear-cache-row t)
 
+            (ui/admonition
+             :warning
+             [:p "Clearing the cache will discard open graphs. You will lose unsaved changes."])
+
             (when logged?
               [:div
                [:div.mt-6.sm:mt-5.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-center.sm:pt-5
@@ -676,8 +678,6 @@
                       :target "_blank"}
                   "https://github.com/isomorphic-git/cors-proxy"]])])
 
-            (graph-config t)
-
             (when logged?
               [:div
                [:hr]

+ 12 - 13
src/main/frontend/components/settings.css

@@ -14,13 +14,8 @@
   }
 
   &-inner {
-      > aside {
-        border-right: 0px solid var(--ls-quaternary-background-color);
-        border-bottom: 1px solid var(--ls-quaternary-background-color);
-        @screen md {
-            border-right: 1px solid var(--ls-quaternary-background-color);
-            border-bottom: 0px solid var(--ls-quaternary-background-color);
-        }
+    > aside {
+      border-right: 1px solid var(--ls-quaternary-background-color);
 
       ul {
         padding: 12px;
@@ -67,7 +62,7 @@
 
     > article {
       flex: 1;
-      padding: 0 12px 24px;
+      padding: 0 12px 12px;
       max-height: 70vh;
       overflow: auto;
     }
@@ -116,8 +111,8 @@
         }
 
         .form-select, .form-input {
-            width: 55%;
-            min-width: 200px;
+          width: 100%;
+          max-width: 200px;
           display: inline-block;
 
           &:hover {
@@ -213,7 +208,6 @@
 
     .ctls {
       position: relative;
-      top: -8px;
 
       &:disabled {
         cursor: progress;
@@ -221,10 +215,11 @@
     }
 
     .update-state {
-      padding: 15px;
+      padding: 6px 10px;
       background-color: var(--ls-quaternary-background-color);
       border-radius: 4px;
-      margin-bottom: -8px;
+      margin-top: 10px;
+      width: fit-content;
 
       > p {
         margin: 0;
@@ -250,6 +245,10 @@
   }
 }
 
+/* Styles for the category icon on the left of settings-modal */
+.cp__settings-inner > aside ul > li > a > i {
+  margin-right: 4px;
+}
 
 svg.git {
     margin-left: -4px;

+ 11 - 10
src/main/frontend/components/sidebar.cljs

@@ -194,12 +194,13 @@
                  200)
                 state)}
   [state]
-  (let [num (state/sub :srs/cards-due-count)]
-    [:a.item.group.flex.items-center.px-2.py-2.text-sm.font-medium.rounded-md {:on-click #(state/pub-event! [:modal/show-cards])}
-     (ui/icon "infinity mr-3" {:style {:font-size 20}})
-     [:span.flex-1 "Flashcards"]
-     (when (and num (not (zero? num)))
-       [:span.ml-3.inline-block.py-0.5.px-3.text-xs.font-medium.rounded-full.fade-in num])]))
+  (rum/with-context [[t] i18n/*tongue-context*]
+    (let [num (state/sub :srs/cards-due-count)]
+      [:a.item.group.flex.items-center.px-2.py-2.text-sm.font-medium.rounded-md {:on-click #(state/pub-event! [:modal/show-cards])}
+      (ui/icon "infinity mr-3" {:style {:font-size 20}})
+      [:span.flex-1 (t :right-side-bar/flashcards)]
+      (when (and num (not (zero? num)))
+        [:span.ml-3.inline-block.py-0.5.px-3.text-xs.font-medium.rounded-full.fade-in num])])))
 
 (defn get-default-home-if-valid
   []
@@ -254,7 +255,7 @@
                 :icon "home"})
               (sidebar-item
                {:class "journals-nav"
-                :title "Journals"
+                :title (t :right-side-bar/journals)
                 :on-click-handler route-handler/go-to-journals!
                 :icon "calendar"}))
 
@@ -263,13 +264,13 @@
 
             (sidebar-item
              {:class "graph-view-nav"
-              :title "Graph view"
+              :title (t :right-side-bar/graph-view)
               :href (rfe/href :graph)
               :icon "hierarchy"})
 
             (sidebar-item
              {:class "all-pages-nav"
-              :title "All pages"
+              :title (t :right-side-bar/all-pages)
               :href (rfe/href :all-pages)
               :icon "files"})]]
 
@@ -287,7 +288,7 @@
                            (state/toggle-left-sidebar!)
                            (state/pub-event! [:go/search]))}
               (ui/icon "circle-plus mr-3" {:style {:font-size 20}})
-              [:span.flex-1 "New page"]])]]]))))
+              [:span.flex-1 (t :right-side-bar/new-page)]])]]]))))
 
 (rum/defc sidebar-mobile-sidebar < rum/reactive
   [{:keys [left-sidebar-open? close-fn route-match]}]

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

@@ -87,7 +87,7 @@
   []
   (rum/with-context [[t] i18n/*tongue-context*]
     [:div.flex.flex-col
-     [:h1.title "Add a graph"]
+     [:h1.title (t :on-boarding/add-graph)]
      (let [nfs-supported? (or (nfs/supported?) (mobile/is-native-platform?))]
        (if (mobile-util/is-native-platform?)
          [:div.text-sm
@@ -123,17 +123,16 @@
            (when nfs-supported?
              {:on-click #(page-handler/ls-dir-files! shortcut/refresh!)})
            [:div
-            [:h1.title "Open a local directory"]
-            [:p "Logseq supports both Markdown and Org-mode. You can open an existing directory or create a new one on your device, a directory is also known simply as a folder. Your data will be stored only on this device."]
-            [:p "After you have opened your directory, it will create three folders in that directory:"]
+            [:h1.title (t :on-boarding/open-local-dir)]
+            [:p (t :on-boarding/new-graph-desc-1)]
+            [:p (t :on-boarding/new-graph-desc-2)]
             [:ul
-             [:li "/journals - store your journal pages"]
-             [:li "/pages - store the other pages"]
-             [:li "/logseq - store configuration, custom.css, and some metadata."]]
+             [:li (t :on-boarding/new-graph-desc-3)]
+             [:li (t :on-boarding/new-graph-desc-4)]
+             [:li (t :on-boarding/new-graph-desc-5)]]
             (when-not nfs-supported?
               (ui/admonition :warning
                              [:p "It seems that your browser doesn't support the "
-
                               [:a {:href   "https://web.dev/file-system-access/"
                                    :target "_blank"}
                                "new native filesystem API"]
@@ -167,9 +166,10 @@
   []
   (when (and (config/demo-graph?)
              (not config/publishing?))
-    (ui/admonition
-     :warning
-     [:p "This is a demo graph, changes will not be saved until you open a local folder."])))
+    (rum/with-context [[t] i18n/*tongue-context*]
+      (ui/admonition
+        :warning
+        [:p (t :on-boarding/demo-graph)]))))
 
 (rum/defc github-integration-soon-deprecated-alert
   []

+ 536 - 11
src/main/frontend/dicts.cljs

@@ -54,6 +54,14 @@
         :on-boarding/cuekeeper-desc " - Browser-based GTD (TODO list) system."
         :on-boarding/sci-desc " - Small Clojure Interpreter"
         :on-boarding/isomorphic-git-desc " - A pure JavaScript implementation of git for node and browsers!"
+        :on-boarding/demo-graph "This is a demo graph, changes will not be saved until you open a local folder."
+        :on-boarding/add-graph "Add a graph"
+        :on-boarding/open-local-dir "Open a local directory"
+        :on-boarding/new-graph-desc-1 "Logseq supports both Markdown and Org-mode. You can open an existing directory or create a new one on your device, a directory is also known simply as a folder. Your data will be stored only on this device."
+        :on-boarding/new-graph-desc-2 "After you have opened your directory, it will create three folders in that directory:"
+        :on-boarding/new-graph-desc-3 "/journals - store your journal pages"
+        :on-boarding/new-graph-desc-4 "/pages - store the other pages"
+        :on-boarding/new-graph-desc-5 "/logseq - store configuration, custom.css, and some metadata."
         :help/start "Getting started"
         :help/about "About Logseq"
         :help/roadmap "Roadmap"
@@ -102,6 +110,11 @@
         :right-side-bar/favorites "Favorites"
         :right-side-bar/page-graph "Page graph"
         :right-side-bar/block-ref "Block reference"
+        :right-side-bar/journals "Journals"
+        :right-side-bar/graph-view "Graph view"
+        :right-side-bar/all-pages "All pages"
+        :right-side-bar/flashcards "Flashcards"
+        :right-side-bar/new-page "New page"
         :left-side-bar/new-page "New page"
         :left-side-bar/nav-favorites "Favorites"
         :left-side-bar/nav-shortcuts "Shortcuts"
@@ -214,31 +227,36 @@
         :content/open-in-sidebar "Open in sidebar"
         :content/copy-as-json "Copy as JSON"
         :content/click-to-edit "Click to edit"
-        :settings-page/edit-config-edn "Edit config.edn for current graph"
+        :settings-page/git-desc "is used for pages version control, you can click the vertical three dots menu to check the page's history."
+        :settings-page/git-confirm "You need to restart the app after updating the Git settings."
+        :settings-page/git-switcher-label "Enable Git auto commit"
+        :settings-page/git-commit-delay "Git auto commit seconds"
+        :settings-page/edit-config-edn "Edit config.edn"
+        :settings-page/edit-custom-css "Edit custom.css"
         :settings-page/show-brackets "Show brackets"
         :settings-page/spell-checker "Spell checker"
         :settings-page/auto-updater "Auto updater"
         :settings-page/disable-sentry "Send usage data and diagnostics to Logseq"
-        :settings-page/preferred-outdenting "Enable logical outdenting"
+        :settings-page/preferred-outdenting "Logical outdenting"
         :settings-page/custom-date-format "Preferred date format"
         :settings-page/preferred-file-format "Preferred file format"
         :settings-page/preferred-workflow "Preferred workflow"
-        :settings-page/enable-timetracking "Enable timetracking"
-        :settings-page/enable-tooltip "Enable tooltips"
         :settings-page/enable-shortcut-tooltip "Enable shortcut tooltip"
-        :settings-page/enable-journals "Enable journals"
-        :settings-page/enable-all-pages-public "Enable all pages public when publishing"
-        :settings-page/enable-encryption "Enable encryption feature"
+        :settings-page/enable-timetracking "Timetracking"
+        :settings-page/enable-tooltip "Tooltips"
+        :settings-page/enable-journals "Journals"
+        :settings-page/enable-all-pages-public "All pages public when publishing"
+        :settings-page/enable-encryption "Encryption"
         :settings-page/customize-shortcuts "Keyboard shortcuts"
         :settings-page/shortcut-settings "Customize shortcuts"
         :settings-page/home-default-page "Set the default home page"
-        :settings-page/enable-block-time "Enable block timestamps"
+        :settings-page/enable-block-time "Block timestamps"
         :settings-page/dont-use-other-peoples-proxy-servers "Don't use other people's proxy servers. It's very dangerous, which could make your token and notes stolen. Logseq will not be responsible for this loss if you use other people's proxy servers. You can deploy it yourself, check "
         :settings-page/clear-cache "Clear cache"
         :settings-page/clear "Clear"
         :settings-page/custom-cors-proxy-server "Custom CORS proxy server"
         :settings-page/developer-mode "Developer mode"
-        :settings-page/enable-developer-mode "Enable developer mode"
+        :settings-page/enable-developer-mode "Developer mode"
         :settings-page/disable-developer-mode "Disable developer mode"
         :settings-page/developer-mode-desc "Developer mode helps contributors and extension developers test their integrations with Logseq more efficiently."
         :settings-page/current-version "Current version"
@@ -348,7 +366,7 @@
         :pdf/linked-ref "Linked references"
         :pdf/toggle-dashed "Dashed style for area highlight"
 
-        :updater/new-version-install "A new version has been downloaded. Restart the application to apply the updates."
+        :updater/new-version-install "A new version has been downloaded."
         :updater/quit-and-install "Restart to install"
 
         :paginates/pages "Total {1} pages"
@@ -360,6 +378,14 @@
         :command-palette/prompt "Type a command"}
 
    :de {:help/about "Über Logseq"
+        :on-boarding/demo-graph "This is a demo graph, changes will not be saved until you open a local folder."
+        :on-boarding/add-graph "Add a graph"
+        :on-boarding/open-local-dir "Open a local directory"
+        :on-boarding/new-graph-desc-1 "Logseq supports both Markdown and Org-mode. You can open an existing directory or create a new one on your device, a directory is also known simply as a folder. Your data will be stored only on this device."
+        :on-boarding/new-graph-desc-2 "After you have opened your directory, it will create three folders in that directory:"
+        :on-boarding/new-graph-desc-3 "/journals - store your journal pages"
+        :on-boarding/new-graph-desc-4 "/pages - store the other pages"
+        :on-boarding/new-graph-desc-5 "/logseq - store configuration, custom.css, and some metadata."
         :help/bug "Fehlerbericht"
         :help/feature "Feature-Anfrage"
         :help/changelog "Änderungsprotokoll"
@@ -398,6 +424,11 @@
         :right-side-bar/recent "Neueste"
         :right-side-bar/contents "Inhalt"
         :right-side-bar/block-ref "Blockreferenz"
+        :right-side-bar/journals "Journals"
+        :right-side-bar/graph-view "Graph view"
+        :right-side-bar/all-pages "All pages"
+        :right-side-bar/flashcards "Flashcards"
+        :right-side-bar/new-page "New page"
         :git/set-access-token "Persönliches GitHub-Zugangs-Token festlegen"
         :git/token-is-encrypted "Das Token wird verschlüsselt und im lokalen Speicher des Browsers gespeichert"
         :git/token-server "Auf dem Server wird es niemals gespeichert"
@@ -498,6 +529,10 @@
         :content/copy-as-json "Als JSON kopieren"
         :content/click-to-edit "Klicken zum bearbeiten"
         :settings-page/edit-config-edn "config.edn bearbeiten (im aktuellen Repository)"
+        :settings-page/git-desc "is used for pages version control, you can click the vertical three dots menu to check the page's history."
+        :settings-page/git-confirm "You need to restart the app after updating the Git settings."
+        :settings-page/git-switcher-label "Enable Git auto commit"
+        :settings-page/git-commit-delay "Git auto commit seconds"
         :settings-page/show-brackets "Klammern anzeigen"
         :settings-page/preferred-file-format "Bevorzugtes Datei-Format"
         :settings-page/preferred-workflow "Bevorzugter Workflow"
@@ -561,6 +596,14 @@
         :open-a-directory "Öffne ein lokales Verzeichnis"}
 
    :fr {:help/about "A propos de Logseq"
+        :on-boarding/demo-graph "This is a demo graph, changes will not be saved until you open a local folder."
+        :on-boarding/add-graph "Add a graph"
+        :on-boarding/open-local-dir "Open a local directory"
+        :on-boarding/new-graph-desc-1 "Logseq supports both Markdown and Org-mode. You can open an existing directory or create a new one on your device, a directory is also known simply as a folder. Your data will be stored only on this device."
+        :on-boarding/new-graph-desc-2 "After you have opened your directory, it will create three folders in that directory:"
+        :on-boarding/new-graph-desc-3 "/journals - store your journal pages"
+        :on-boarding/new-graph-desc-4 "/pages - store the other pages"
+        :on-boarding/new-graph-desc-5 "/logseq - store configuration, custom.css, and some metadata."
         :help/bug "Signaler une anomalie"
         :help/feature "Demander une fonctionnalité"
         :help/changelog "Journal des modifications"
@@ -599,6 +642,11 @@
         :right-side-bar/recent "Récents"
         :right-side-bar/contents "Contenus"
         :right-side-bar/block-ref "Référence des blocs"
+        :right-side-bar/journals "Journals"
+        :right-side-bar/graph-view "Graph view"
+        :right-side-bar/all-pages "All pages"
+        :right-side-bar/flashcards "Flashcards"
+        :right-side-bar/new-page "New page"
         :git/set-access-token "Définir un jeton d'accès personnel GitHub"
         :git/token-is-encrypted "Le jeton sera chiffré et gardé dans le stockage local du navigateur (local storage)"
         :git/token-server "Le serveur ne le stockera jamais"
@@ -692,6 +740,10 @@
         :content/copy-as-json "Copier au format JSON"
         :content/click-to-edit "Cliquer pour éditer"
         :settings-page/edit-config-edn "Editer config.edn (pour le repo actuel)"
+        :settings-page/git-desc "is used for pages version control, you can click the vertical three dots menu to check the page's history."
+        :settings-page/git-confirm "You need to restart the app after updating the Git settings."
+        :settings-page/git-switcher-label "Enable Git auto commit"
+        :settings-page/git-commit-delay "Git auto commit seconds"
         :settings-page/preferred-file-format "Format de fichier préféré"
         :settings-page/preferred-workflow "Workflow préféré"
         :settings-page/enable-timetracking "Activer le suivi de temps des tâches"
@@ -789,6 +841,14 @@
            :on-boarding/cuekeeper-desc " - 基于浏览器的 GTD (待办清单) 系统。"
            :on-boarding/sci-desc " - 小型 Clojure 解析器"
            :on-boarding/isomorphic-git-desc " - 用于 Node 和浏览器的纯 JavaScript Git 实现!"
+           :on-boarding/demo-graph "This is a demo graph, changes will not be saved until you open a local folder."
+           :on-boarding/add-graph "Add a graph"
+           :on-boarding/open-local-dir "Open a local directory"
+           :on-boarding/new-graph-desc-1 "Logseq supports both Markdown and Org-mode. You can open an existing directory or create a new one on your device, a directory is also known simply as a folder. Your data will be stored only on this device."
+           :on-boarding/new-graph-desc-2 "After you have opened your directory, it will create three folders in that directory:"
+           :on-boarding/new-graph-desc-3 "/journals - store your journal pages"
+           :on-boarding/new-graph-desc-4 "/pages - store the other pages"
+           :on-boarding/new-graph-desc-5 "/logseq - store configuration, custom.css, and some metadata."
            :help/start "入门"
            :help/about "关于 Logseq"
            :help/bug "Bug 反馈"
@@ -839,6 +899,11 @@
            :right-side-bar/favorites "收藏"
            :right-side-bar/page-graph "页面图谱:"
            :right-side-bar/block-ref "块引用"
+           :right-side-bar/journals "Journals"
+           :right-side-bar/graph-view "Graph view"
+           :right-side-bar/all-pages "All pages"
+           :right-side-bar/flashcards "Flashcards"
+           :right-side-bar/new-page "New page"
            :left-side-bar/new-page "新页面"
            :left-side-bar/nav-favorites "收藏页面"
            :left-side-bar/nav-shortcuts "快捷导航"
@@ -948,6 +1013,10 @@
            :content/copy-as-json "复制为 JSON"
            :content/click-to-edit "点击以编辑"
            :settings-page/edit-config-edn "编辑 config.edn (当前库)"
+           :settings-page/git-desc "is used for pages version control, you can click the vertical three dots menu to check the page's history."
+           :settings-page/git-confirm "You need to restart the app after updating the Git settings."
+           :settings-page/git-switcher-label "Enable Git auto commit"
+           :settings-page/git-commit-delay "Git auto commit seconds"
            :settings-page/show-brackets "显示括号 [[]]"
            :settings-page/spell-checker "单词拼写检查"
            :settings-page/auto-updater "自动更新"
@@ -1124,6 +1193,14 @@
              :on-boarding/cuekeeper-desc " - 基於瀏覽器的 GTD (待辦清單) 系統。"
              :on-boarding/sci-desc " - 小型 Clojure 解析器"
              :on-boarding/isomorphic-git-desc " - 用於 Node 和瀏覽器的純 JavaScript Git 實現!"
+             :on-boarding/demo-graph "This is a demo graph, changes will not be saved until you open a local folder."
+             :on-boarding/add-graph "Add a graph"
+             :on-boarding/open-local-dir "Open a local directory"
+             :on-boarding/new-graph-desc-1 "Logseq supports both Markdown and Org-mode. You can open an existing directory or create a new one on your device, a directory is also known simply as a folder. Your data will be stored only on this device."
+             :on-boarding/new-graph-desc-2 "After you have opened your directory, it will create three folders in that directory:"
+             :on-boarding/new-graph-desc-3 "/journals - store your journal pages"
+             :on-boarding/new-graph-desc-4 "/pages - store the other pages"
+             :on-boarding/new-graph-desc-5 "/logseq - store configuration, custom.css, and some metadata."
              :help/about "關於 Logseq"
              :help/bug "Bug 反饋"
              :help/feature "功能建議"
@@ -1164,6 +1241,11 @@
              :right-side-bar/recent "最近"
              :right-side-bar/contents "目錄"
              :right-side-bar/block-ref "塊引用"
+             :right-side-bar/journals "Journals"
+             :right-side-bar/graph-view "Graph view"
+             :right-side-bar/all-pages "All pages"
+             :right-side-bar/flashcards "Flashcards"
+             :right-side-bar/new-page "New page"
              :git/set-access-token "設定 GitHub 個人訪問令牌"
              :git/token-is-encrypted "令牌將被加密並存儲在瀏覽器本地存儲"
              :git/token-server "服務器將永遠不會存儲它"
@@ -1258,6 +1340,10 @@
              :content/copy-as-json "復制為 JSON"
              :content/click-to-edit "點擊以編輯"
              :settings-page/edit-config-edn "編輯 config.edn (當前庫)"
+             :settings-page/git-desc "is used for pages version control, you can click the vertical three dots menu to check the page's history."
+             :settings-page/git-confirm "You need to restart the app after updating the Git settings."
+             :settings-page/git-switcher-label "Enable Git auto commit"
+             :settings-page/git-commit-delay "Git auto commit seconds"
              :settings-page/preferred-file-format "首選文件格式"
              :settings-page/preferred-workflow "首選工作流"
              :settings-page/dont-use-other-peoples-proxy-servers "不要使用其他人的代理服務器。這非常危險,可能會使您的令牌和筆記被盜。 如果您使用其他人的代理服務器,Logseq 將不會對此損失負責。您可以自己部署它,請查閱 "
@@ -1362,6 +1448,14 @@
         :on-boarding/cuekeeper-desc " - Webblaaier gebaseerde GTD (TODO lys) stelsel."
         :on-boarding/sci-desc " - 'n Klein Clojure interpreteerder"
         :on-boarding/isomorphic-git-desc "- 'n Suiwer JavaScript implementasie van git vir node en webblaaiers!"
+        :on-boarding/demo-graph "This is a demo graph, changes will not be saved until you open a local folder."
+        :on-boarding/add-graph "Add a graph"
+        :on-boarding/open-local-dir "Open a local directory"
+        :on-boarding/new-graph-desc-1 "Logseq supports both Markdown and Org-mode. You can open an existing directory or create a new one on your device, a directory is also known simply as a folder. Your data will be stored only on this device."
+        :on-boarding/new-graph-desc-2 "After you have opened your directory, it will create three folders in that directory:"
+        :on-boarding/new-graph-desc-3 "/journals - store your journal pages"
+        :on-boarding/new-graph-desc-4 "/pages - store the other pages"
+        :on-boarding/new-graph-desc-5 "/logseq - store configuration, custom.css, and some metadata."
         :help/about "Oor Logseq"
         :help/bug "Fout verslag"
         :help/feature "Funksie versoek"
@@ -1400,6 +1494,11 @@
         :right-side-bar/recent "Onlangs"
         :right-side-bar/contents "Inhoud"
         :right-side-bar/block-ref "Blok verwysing"
+        :right-side-bar/journals "Journals"
+        :right-side-bar/graph-view "Graph view"
+        :right-side-bar/all-pages "All pages"
+        :right-side-bar/flashcards "Flashcards"
+        :right-side-bar/new-page "New page"
         :git/set-access-token "Set GitHub personal access token"
         :git/token-is-encrypted "The token will be encrypted and stored in the browser local storage"
         :git/token-server "The server will never store it"
@@ -1490,6 +1589,10 @@
         :content/copy-as-json "Kopieer na JSON"
         :content/click-to-edit "Kliek om te wysig"
         :settings-page/edit-config-edn "Wysig config.edn (vir huidige stoor)"
+        :settings-page/git-desc "is used for pages version control, you can click the vertical three dots menu to check the page's history."
+        :settings-page/git-confirm "You need to restart the app after updating the Git settings."
+        :settings-page/git-switcher-label "Enable Git auto commit"
+        :settings-page/git-commit-delay "Git auto commit seconds"
         :settings-page/preferred-file-format "Voorkeur lêer formaat"
         :settings-page/preferred-workflow "Voorkeur werkstroom"
         :settings-page/dont-use-other-peoples-proxy-servers "Moenie ander mense se instaanbedieners gebruik nie. Dis gevaarlik, en kan veroorsaak dat jou toegang teken en notas gesteel word. Logseq sal nie verantwoording neem vir verlies indien jy ander se instaanbedieners gebruik nie. Jy kan self self een ontplooi "
@@ -1576,6 +1679,14 @@
         :on-boarding/cuekeeper-desc " - Sistema de GTD (lista de tareas) basado en el navegador"
         :on-boarding/sci-desc " - Intérprete compacto de Clojure"
         :on-boarding/isomorphic-git-desc " - ¡Una implementación de git en JavaScript puro para node y navegadores!"
+        :on-boarding/demo-graph "This is a demo graph, changes will not be saved until you open a local folder."
+        :on-boarding/add-graph "Add a graph"
+        :on-boarding/open-local-dir "Open a local directory"
+        :on-boarding/new-graph-desc-1 "Logseq supports both Markdown and Org-mode. You can open an existing directory or create a new one on your device, a directory is also known simply as a folder. Your data will be stored only on this device."
+        :on-boarding/new-graph-desc-2 "After you have opened your directory, it will create three folders in that directory:"
+        :on-boarding/new-graph-desc-3 "/journals - store your journal pages"
+        :on-boarding/new-graph-desc-4 "/pages - store the other pages"
+        :on-boarding/new-graph-desc-5 "/logseq - store configuration, custom.css, and some metadata."
         :help/about "Acerca de Logseq"
         :help/roadmap "Hoja de ruta"
         :help/bug "Reportar un error"
@@ -1620,6 +1731,11 @@
         :right-side-bar/favorites "Favoritos"
         :right-side-bar/page-graph "Vista gráfica"
         :right-side-bar/block-ref "Referencia de bloque"
+        :right-side-bar/journals "Journals"
+        :right-side-bar/graph-view "Graph view"
+        :right-side-bar/all-pages "All pages"
+        :right-side-bar/flashcards "Flashcards"
+        :right-side-bar/new-page "New page"
         :git/set-access-token "Establece el token de acceso personal de GitHub"
         :git/token-is-encrypted "El token será encriptado y guardado en el almacenamiento local del navegador"
         :git/token-server "El servidor nunca lo guardará"
@@ -1723,6 +1839,10 @@
         :content/copy-as-json "Copiar como JSON"
         :content/click-to-edit "Clic para editar"
         :settings-page/edit-config-edn "Editar config.edn (para este repositorio)"
+        :settings-page/git-desc "is used for pages version control, you can click the vertical three dots menu to check the page's history."
+        :settings-page/git-confirm "You need to restart the app after updating the Git settings."
+        :settings-page/git-switcher-label "Enable Git auto commit"
+        :settings-page/git-commit-delay "Git auto commit seconds"
         :settings-page/show-brackets "Mostrar corchetes"
         :settings-page/disable-sentry "Enviar datos de uso y diagnósticos a Logseq"
         :settings-page/preferred-outdenting "Disminución lógica de sangría"
@@ -1860,6 +1980,14 @@
            :on-boarding/cuekeeper-desc " - Nettleser-basert GTD (gjøremålsliste) system."
            :on-boarding/sci-desc " - Liten Clojure Tolk"
            :on-boarding/isomorphic-git-desc " - En ren JavaScript impelemtering av git for node og nettlesere!"
+           :on-boarding/demo-graph "This is a demo graph, changes will not be saved until you open a local folder."
+           :on-boarding/add-graph "Add a graph"
+           :on-boarding/open-local-dir "Open a local directory"
+           :on-boarding/new-graph-desc-1 "Logseq supports both Markdown and Org-mode. You can open an existing directory or create a new one on your device, a directory is also known simply as a folder. Your data will be stored only on this device."
+           :on-boarding/new-graph-desc-2 "After you have opened your directory, it will create three folders in that directory:"
+           :on-boarding/new-graph-desc-3 "/journals - store your journal pages"
+           :on-boarding/new-graph-desc-4 "/pages - store the other pages"
+           :on-boarding/new-graph-desc-5 "/logseq - store configuration, custom.css, and some metadata."
            :help/start "Kom i gang"
            :help/about "Om Logseq"
            :help/roadmap "Veikart"
@@ -1907,6 +2035,11 @@
            :right-side-bar/favorites "Favoritter"
            :right-side-bar/page-graph "Sidegraf"
            :right-side-bar/block-ref "Blokkreferanse"
+           :right-side-bar/journals "Journals"
+           :right-side-bar/graph-view "Graph view"
+           :right-side-bar/all-pages "All pages"
+           :right-side-bar/flashcards "Flashcards"
+           :right-side-bar/new-page "New page"
            :left-side-bar/new-page "Ny side"
            :left-side-bar/nav-favorites "Favoritter"
            :left-side-bar/nav-shortcuts "Snarveier"
@@ -2018,6 +2151,10 @@
            :content/copy-as-json "Kopier som JSON"
            :content/click-to-edit "Klikk for å redigere"
            :settings-page/edit-config-edn "Rediger config.edn for nåværende repo"
+           :settings-page/git-desc "is used for pages version control, you can click the vertical three dots menu to check the page's history."
+           :settings-page/git-confirm "You need to restart the app after updating the Git settings."
+           :settings-page/git-switcher-label "Enable Git auto commit"
+           :settings-page/git-commit-delay "Git auto commit seconds"
            :settings-page/show-brackets "Vis klammer"
            :settings-page/spell-checker "Stavekontroll"
            :settings-page/auto-updater "Automatisk oppdatering"
@@ -2207,6 +2344,14 @@
     :on-boarding/cuekeeper-desc " - Sistema de GTD (lista de tarefas) baseado no navegador."
     :on-boarding/sci-desc " - Interpretador Compacto de Clojure"
     :on-boarding/isomorphic-git-desc " - Uma implementação de git em JavaScript puro para node e navegadores!"
+    :on-boarding/demo-graph "This is a demo graph, changes will not be saved until you open a local folder."
+    :on-boarding/add-graph "Add a graph"
+    :on-boarding/open-local-dir "Open a local directory"
+    :on-boarding/new-graph-desc-1 "Logseq supports both Markdown and Org-mode. You can open an existing directory or create a new one on your device, a directory is also known simply as a folder. Your data will be stored only on this device."
+    :on-boarding/new-graph-desc-2 "After you have opened your directory, it will create three folders in that directory:"
+    :on-boarding/new-graph-desc-3 "/journals - store your journal pages"
+    :on-boarding/new-graph-desc-4 "/pages - store the other pages"
+    :on-boarding/new-graph-desc-5 "/logseq - store configuration, custom.css, and some metadata."
     :help/start "Começar a usar"
     :help/about "Sobre o Logseq"
     :help/roadmap "Plano de implementação"
@@ -2254,6 +2399,11 @@
     :right-side-bar/favorites "Favoritos"
     :right-side-bar/page-graph "Grafo da página"
     :right-side-bar/block-ref "Referência de bloco"
+    :right-side-bar/journals "Journals"
+    :right-side-bar/graph-view "Graph view"
+    :right-side-bar/all-pages "All pages"
+    :right-side-bar/flashcards "Flashcards"
+    :right-side-bar/new-page "New page"
     :git/set-access-token "Definir token de acesso pessoal do GitHub"
     :git/token-is-encrypted "O token será encriptado e guardado no armazenamento local do navegador"
     :git/token-server "O servidor nunca o irá guardar"
@@ -2361,6 +2511,10 @@
     :content/copy-as-json "Copiar como JSON"
     :content/click-to-edit "Clicar para editar"
     :settings-page/edit-config-edn "Editar config.edn para o repositório atual"
+    :settings-page/git-desc "is used for pages version control, you can click the vertical three dots menu to check the page's history."
+    :settings-page/git-confirm "You need to restart the app after updating the Git settings."
+    :settings-page/git-switcher-label "Enable Git auto commit"
+    :settings-page/git-commit-delay "Git auto commit seconds"
     :settings-page/show-brackets "Mostrar parênteses rectos"
     :settings-page/spell-checker "Verificador ortográfico"
     :settings-page/disable-sentry "Enviar dados de utilização e diagnósticos para Logseq"
@@ -2469,6 +2623,376 @@
     :pdf/linked-ref "Referências ligadas"
     :command-palette/prompt "Introduza um comando"}
 
+    :ru {:on-boarding/title "Привет, добро пожаловать в Logseq!"
+        :on-boarding/sharing "распространения"
+        :on-boarding/is-a " "
+        :on-boarding/vision "Безопасная, открытая платформа для уравления базой знаний и совместной работы."
+        :on-boarding/local-first "локальный"
+        :on-boarding/non-linear "нелинейный"
+        :on-boarding/outliner "планировщик"
+        :on-boarding/notebook-for-organizing-and " блокнот для организации и "
+        :on-boarding/your-personal-knowledge-base " вашей персональной базы знаний."
+        :on-boarding/notice "Учтите, что этот проект находится в стадии бета-тестирования и активной разработки, не забывайте делать ежедневный бэкап."
+        :on-boarding/features-desc "Используйте для организации вашего списка дел, ведения дневника или для записи событий вашей уникальной жизни."
+        :on-boarding/privacy "Данный сервер никогда не хранит и не анализирует ваши персональный заметки. Ваши даныне это простые текстовые файлы. На данный момент мы поддерживаем Markdown и режим Emacs Org. Даже если сайт будет недоступен или остановится его поддержка, ваши данные всегда останутся с вами"
+        :on-boarding/inspired-by " сильно вдохновлен "
+        :on-boarding/where-are-my-notes-saved "Где сохраняются мои заметки?"
+        :on-boarding/storage "Ваши заметки будут сохранены в локальном хранилище браузера "
+        :on-boarding/how-do-i-use-it "Как мне это использовать?"
+        :on-boarding/use-1 "1. Синхронизация между несколькими устройствами"
+        :on-boarding/use-1-desc "В настоящее время мы поддерживаем синхронизацию только через GitHub, больше вариантов (собственный сервер GIT, WebDAV, Google Drive, и т.д.) будут добавлены в скором будущем."
+        :on-boarding/use-1-video "Посмотрите это потрясающее видео от "
+        :on-boarding/use-2 "2. Пользуйся локально (не нужно логиниться)"
+        :on-boarding/features "Особенности"
+        :on-boarding/features-backlinks "Обратные ссылки между [[Страницами]]"
+        :on-boarding/features-block-embed "Встроенные блоки"
+        :on-boarding/features-page-embed "Встроенные страницы"
+        :on-boarding/features-graph-vis "Визуализация графов"
+        :on-boarding/features-heading-properties "Свойства заголовка"
+        :on-boarding/features-datalog "Datalog запросы, база данных заметок, работающая на "
+        :on-boarding/features-custom-view-component "Настраиваемый вид компонентов"
+        :on-boarding/integration " интеграция"
+        :on-boarding/slide-support " поддержка слайдов"
+        :on-boarding/built-in-supports "Встроенная поддержка документов:"
+        :on-boarding/supports-code-highlights "Подсветка кода"
+        :on-boarding/supports-katex-latex "Katex latex"
+        :on-boarding/raw "Исходный "
+        :on-boarding/raw-html "Исходный HTML"
+        :on-boarding/learn-more "Узнать больше"
+        :on-boarding/discord-desc " где сообщество задает вопросы и делится советами"
+        :on-boarding/github-desc " где любой может сообщить о проблеме!"
+        :on-boarding/our-blog "Наш блог: "
+        :on-boarding/credits-to "Благодарности"
+        :on-boarding/clojure-desc " - Динамический функциональный универсальный язый программирования"
+        :on-boarding/datascript-desc " - Иммутабельная база данных и Datalog механизм запросов для Clojure, ClojureScript и JS"
+        :on-boarding/angstrom-desc-1 ", синтаксический анализатор "
+        :on-boarding/angstrom-desc-2 "документов"
+        :on-boarding/angstrom-desc-3 " встроенный в Angstrom."
+        :on-boarding/cuekeeper-desc " - Построенная на браузере GTD (список дел) система."
+        :on-boarding/sci-desc " - Small Clojure Interpreter"
+        :on-boarding/isomorphic-git-desc " - Реализация GIT на чистом JavaScript для Node и браузеров!"
+        :on-boarding/demo-graph "Это демонстрационный граф, изменения не будут сохранены, пока вы не откроете локальный файл."
+        :on-boarding/add-graph "Добавить новый граф"
+        :on-boarding/open-local-dir "Открыть локальный каталог"
+        :on-boarding/new-graph-desc-1 "Logseq поддерживает Markdown and Org-mode. Вы можете открыть существующий каталог или создать новый на вашем устройства, каталог также можно назвать просто папкой. Ваши данные будут храниться только на вашем устройстве."
+        :on-boarding/new-graph-desc-2 "После того, как вы укажете каталог, в нём будут созданы три папки:"
+        :on-boarding/new-graph-desc-3 "/journals - хранит страницы ваших дневников"
+        :on-boarding/new-graph-desc-4 "/pages - хранит остальные страницы"
+        :on-boarding/new-graph-desc-5 "/logseq - хранит конфигурации, custom.css, и другие метаданные."
+        :help/start "Начало работы"
+        :help/about "О Logseq"
+        :help/roadmap "Дорожная карта"
+        :help/bug "Сообщить об ошибке"
+        :help/feature "Запрос функционала"
+        :help/changelog "Журнал изменений"
+        :help/blog "Logseq блог"
+        :help/docs "Документация"
+        :help/privacy "Политика конфендициальности"
+        :help/terms "Условия"
+        :help/community "Discord сообщество"
+        :help/awesome-logseq "Потрясающий Logseq"
+        :help/shortcuts "Горчие клавиши"
+        :help/shortcuts-triggers "Действия"
+        :help/shortcut "Горчие клавиши"
+        :help/slash-autocomplete "Слэш - автодополнение"
+        :help/block-content-autocomplete "Блок контента - автодополнение"
+        :help/reference-autocomplete "Ссылка на страницу - автодополнение"
+        :help/block-reference "Ссылка на блок"
+        :help/key-commands "Key commands"
+        :help/working-with-lists " (работа со списками)"
+        :help/select-nfs-browser "Пожалуйста, используйте другой браузер (например, последний Chrome), который поддерживает NFS функционал, для работы с локальными файлами."
+        :undo "Отменить"
+        :redo "Вернуть"
+        :general "Общий"
+        :more "Больше"
+        :search/result-for "Искать результат для "
+        :search/items "элементы"
+        :search/page-names "Искать имена страниц"
+        :help/context-menu "Контекстное меню блока"
+        :help/fold-unfold "Свернуть/развернуть блоки (когда не в режиме редактирования)"
+        :help/markdown-syntax "Markdown синтаксис"
+        :help/org-mode-syntax "Org mode синтаксис"
+        :bold "Жирный"
+        :italics "Курсив"
+        :html-link "HTML ссылка"
+        :highlight "Выделение"
+        :strikethrough "Перечёркнутый"
+        :code "Код"
+        :right-side-bar/help "Справка"
+        :right-side-bar/switch-theme "Тема"
+        :right-side-bar/theme "{1} тема"
+        :right-side-bar/page "Граф страницы"
+        :right-side-bar/recent "Недавние"
+        :right-side-bar/contents "Содержание"
+        :right-side-bar/favorites "Избранные"
+        :right-side-bar/page-graph "Граф страницы"
+        :right-side-bar/block-ref "Ссылка на блок"
+        :left-side-bar/new-page "Новая страница"
+        :left-side-bar/nav-favorites "Избранное"
+        :left-side-bar/nav-shortcuts "Горячие клавиши"
+        :left-side-bar/nav-recent-pages "Недавнее"
+        :right-side-bar/journals "Дневники"
+        :right-side-bar/graph-view "Визуальный граф"
+        :right-side-bar/all-pages "Все страницы"
+        :right-side-bar/flashcards "Слайды"
+        :right-side-bar/new-page "Новая страница"
+        :git/set-access-token "Установить GitHub токен личного доступа"
+        :git/token-is-encrypted "Токен будет зашифрован и сохранен в локальном хранилище браузера"
+        :git/token-server "Сервер никогда не хранит это"
+        :git/create-personal-access-token "Как создать GitHub токен личного доступа?"
+        :git/push "Отправить сейчас"
+        :git/push-failed "Отправка не удалась!"
+        :git/local-changes-synced "Все локальные изменения синхронизированны!"
+        :git/pull "Получить сейчас"
+        :git/last-pull "Последнее получение "
+        :git/version "Версия"
+        :git/import-notes "Импортировать ваши заметки"
+        :git/import-notes-helper "Вы можете импортировать свои заметки из репозитория на GitHub."
+        :git/add-another-repo "Добавить другой репозиторий"
+        :git/re-index "Клонировать заново и переиндексировать базу данных"
+        :git/message "Ваше сообщение коммита"
+        :git/commit-and-push "Закоммитить и отправить!"
+        :git/use-remote "Использовать удаленный"
+        :git/keep-local "Оставить локальный"
+        :git/edit "Редактировать"
+        :git/title "Изменения"
+        :git/no-diffs "Нет изменений"
+        :git/commit-message "Сообщение коммита (опционально)"
+        :git/pushing "Отправка"
+        :git/force-push "Закоммитить и принудительно отправить"
+        :git/a-force-push "Принудительная отправка"
+        :git/add-repo-prompt "Установить Logseq в свой репозиторий"
+        :git/add-repo-prompt-confirm "Добавить и установить"
+        :format/preferred-mode "Какой режим вы предпочитаете?"
+        :format/markdown "Markdown"
+        :format/org-mode "Org mode"
+        :reference/linked "Связанные ссылки"
+        :reference/unlinked-ref "Несвязанные ссылки"
+        :project/setup "Настроить публичный проект в Logseq"
+        :project/location "Все публичные страницы будут расположены ниже"
+        :project/sync-settings "Синхронизировать настройки проекта"
+        :page/presentation-mode "Презентация"
+        :page/edit-properties-placeholder "Свойства"
+        :page/delete-success "Страница {1} была успешно удалена!"
+        :page/delete-confirmation "Вы уверены, что хотите удалить эту страницу и ее файл?"
+        :page/rename-to "Переименовать \"{1}\" в:"
+        :page/priority "Приоритет \"{1}\""
+        :page/copy-to-json "Скопировать всю страницу как JSON"
+        :page/rename "Переименовать страницу"
+        :page/open-in-finder "Открыть в каталоге"
+        :page/open-with-default-app "Открыть через приложение по умолчанию"
+        :page/action-publish "Опубликовать"
+        :page/make-public "Сделать доступным для публикации"
+        :page/version-history "Проверить историю страницы"
+        :page/make-private "Сделать приватным"
+        :page/delete "Удалить страницу"
+        :page/publish "Опубликовать эту страницу на Logseq"
+        :page/cancel-publishing "Отменить публикацию на Logseq"
+        :page/publish-as-slide "Опубликовать эту страницу как слайд на Logseq"
+        :page/unpublish "Отменить публикацию этой страницы на Logseq"
+        :page/add-to-favorites "Добавить в Избранное"
+        :page/unfavorite "Удалить страницу из Избранного"
+        :page/show-journals "Показать Дневники"
+        :page/show-name "Показать имя страницы"
+        :page/hide-name "Скрыть имя страницы"
+        :block/name "Имя страницы"
+        :page/last-modified "Последнее изменение"
+        :page/new-title "Какой заголовок у вашей новой страницы?"
+        :page/earlier "Ранее"
+        :page/no-more-journals "Дневников больше нет"
+        :publishing/pages "Страницы"
+        :publishing/page-name "Имя страницы"
+        :publishing/current-project "Текущий проект"
+        :publishing/delete-from-logseq "Удалить с сервера Logseq"
+        :publishing/edit "Редактировать"
+        :publishing/save "Сохранить"
+        :publishing/cancel "Отмена"
+        :publishing/delete "Удалить"
+        :journal/multiple-files-with-different-formats "Похоже, что у вас несколько файлов Дневника (с разными форматами) для одного месяца. Пожалуйста, используйста только один файл Дневника для каждого месяца."
+        :journal/go-to "Перейти к файлам"
+        :file/name "Имя файла"
+        :file/file "Файл: "
+        :file/last-modified-at "Последнее изменение"
+        :file/no-data "Нет данных"
+        :file/format-not-supported "Расширение .{1} не поддерживается."
+        :page/created-at "Создан"
+        :page/updated-at "Обновлен"
+        :page/backlinks "Обратная ссылка"
+        :editor/block-search "Искать блок"
+        :editor/image-uploading "Загрузка"
+        :draw/invalid-file "Не удалось загрузить недопустимый excalidraw файл"
+        :draw/specify-title "Пожалуйста, сначала укажите заголовок!"
+        :draw/rename-success "Файл был успешно переименован!"
+        :draw/rename-failure "Переименовать файл не удлось, причина: "
+        :draw/title-placeholder "Без названия"
+        :draw/save "Сохранить"
+        :draw/save-changes "Сохранить изменения"
+        :draw/new-file "Новый файл"
+        :draw/list-files "Список файлов"
+        :draw/delete "Удалить"
+        :draw/more-options "Больше опций"
+        :draw/back-to-logseq "Вернуться к logseq"
+        :text/image "Изображение"
+        :asset/confirm-delete "Вы уверены, что хотите удалить {1}?"
+        :asset/physical-delete "Также удалить файл (обратите внимание, восстановить его будет невозможно)"
+        :content/copy "Копировать"
+        :content/cut "Вырезать"
+        :content/make-todos "Make {1}s"
+        :content/copy-block-ref "Копировать ссылку блока"
+        :content/copy-block-emebed "Копировать встроенный блок"
+        :content/focus-on-block "Фокус на блоке"
+        :content/open-in-sidebar "Открыть в боковой панели"
+        :content/copy-as-json "Копировать как JSON"
+        :content/click-to-edit "Нажмите для редактирования"
+        :settings-page/edit-config-edn "Редактировать config.edn для текущего графа"
+        :settings-page/git-desc " используется для контроля версий страниы. Вы можете кликнуть на меню 'три точки' чтобы проверить историю страницы."
+        :settings-page/git-confirm "Необходимо перезапустить приложение после изменения настроек Git."
+        :settings-page/git-switcher-label "Автокоммит в Git"
+        :settings-page/git-commit-delay "Git автокоммит после сек."
+        :settings-page/show-brackets "Показывать скобки"
+        :settings-page/spell-checker "Проверка орфограции"
+        :settings-page/auto-updater "Автообновление"
+        :settings-page/disable-sentry "Отправлять данные использования и диагностику в Logseq"
+        :settings-page/preferred-outdenting "Логические отступы"
+        :settings-page/custom-date-format "Формат дат"
+        :settings-page/preferred-file-format "Формат файлов"
+        :settings-page/preferred-workflow "Рабочий процесс"
+        :settings-page/enable-timetracking "Отслеживание времени"
+        :settings-page/enable-tooltip "Всплывающие подсказки"
+        :settings-page/enable-shortcut-tooltip "Всплывающие подсказки горячих клавиш"
+        :settings-page/enable-journals "Включить Дневники"
+        :settings-page/enable-all-pages-public "Все страницы общедоступны при публикации"
+        :settings-page/enable-encryption "Функции шифрования"
+        :settings-page/customize-shortcuts "Горячие клавиши"
+        :settings-page/shortcut-settings "Настроить горячие клавиши"
+        :settings-page/home-default-page "Установить домашнюю страницу по умолчанию"
+        :settings-page/enable-block-time "Временные метки блока"
+        :settings-page/dont-use-other-peoples-proxy-servers "Не используйте прокси-сервера других людей. Это может быть очень опасно, ваши токен и заметки могут быть украдены. Logseq не несет ответственности за эту потерю, если вы будете использовать сторонние прокси-сервера. Но вы может развернуть собственный "
+        :settings-page/clear-cache "Отчистить кэш"
+        :settings-page/clear "Отчистить"
+        :settings-page/custom-cors-proxy-server "Настраевыемый CORS прокси сервер"
+        :settings-page/developer-mode "Режим разработчика"
+        :settings-page/enable-developer-mode "Включить режим разработчика"
+        :settings-page/disable-developer-mode "Выключить режим разработчика"
+        :settings-page/developer-mode-desc "Режим разработчика помогает людям, участвующим в разработке приложения и расширений, тестировать их интеграции с Logseq более эффективно."
+        :settings-page/current-version "Версия"
+        :settings-page/current-graph "Текущий граф"
+        :settings-page/tab-general "Общие"
+        :settings-page/tab-editor "Редактор"
+        :settings-page/tab-shortcuts "Горячие клавиши"
+        :settings-page/tab-version-control "Контроль версий"
+        :settings-page/tab-advanced "Расширенные"
+        :logseq "Logseq"
+        :on "ON"
+        :more-options "Больше опций"
+        :to "to"
+        :yes "Да"
+        :no "Нет"
+        :submit "Подтвердить"
+        :cancel "Отмена"
+        :close "Закрыть"
+        :delete "Удалить"
+        :re-index "Переиндексировать (перестроить граф)"
+        :sync-from-local-files "Обновить (импортировать изменния из локальных файлов)"
+        :unlink "отвязать"
+        :search (if config/publishing?
+                  "Искать"
+                  "Искать или создать страницу")
+        :page-search "Искать на текущей странице"
+        :graph-search "Искать граф"
+        :new-page "Новая страница"
+        :new-file "Новый файл"
+        :new-graph "Добавить новый граф"
+        :graph "Граф"
+        :graph-view "Смотреть граф"
+        :cards-view "Смотреть карточки"
+        :publishing "Публикация"
+        :export "Экспорт"
+        :export-graph "Экспортировать граф"
+        :export-page "Экспортировать страницу"
+        :export-markdown "Экспортировать как стандартный Markdown (без свойств блока)"
+        :export-opml "Экспортировать как OPML"
+        :export-public-pages "Экспорт публичных страниц"
+        :export-json "Экспортировать как JSON"
+        :export-roam-json "Экспортировать как Roam JSON"
+        :export-edn "Экспортировать как EDN"
+        :export-datascript-edn "Экспорт данных EDN"
+        :convert-markdown "Преобразовать Markdown заголовки в обычный список (# -> -)"
+        :all-graphs "Все графы"
+        :all-pages "Все страницы"
+        :all-files "Все файлы"
+        :remove-orphaned-pages "Удалить страницы без родителя"
+        :all-journals "Все журналы"
+        :my-publishing "Мои публикации"
+        :settings "Настройки"
+        :plugins "Расширения"
+        :themes "Темы"
+        :developer-mode-alert "Необходимо перезагрузить приложение для активации системы плагинов. Хотите сделать это сейчас?"
+        :relaunch-confirm-to-work "Необходимо перезапустить приложение. Сделать это сейчас?"
+        :import "Импорт"
+        :join-community "Присоединиться к сообществу"
+        :sponsor-us "Стать спонсором"
+        :discord-title "Наша группа в Дискорде!"
+        :sign-out "Выйти"
+        :help-shortcut-title "Нажмите для просмотра горячих клавиш и других полезностей"
+        :loading "Загрузка"
+        :cloning "Клонирование"
+        :parsing-files "Парсинг файлов"
+        :loading-files "Загрузка файлов"
+        :login-github "Логин через GitHub"
+        ;; :login-google "Login with Google"
+        :login "Логин"
+        :go-to "Go to "
+        :or "или"
+        :download "Скачать"
+        :repo/download-zip "Скачать все файлы как архив"
+        :language "Язык"
+        :white "Светлый"
+        :dark "Темный"
+        :remove-background "Удалить фон"
+        :open "Открыть"
+        :open-a-directory "Открыть локальную папку"
+        :user/delete-account "Удалить аккаунт"
+        :user/delete-your-account "Удалить ваш аккаунт"
+        :user/delete-account-notice "Все ваши опубликованные страницы на logseq.com будут удалены."
+
+        :help/shortcut-page-title "Горячие клавиши"
+
+        :plugin/installed "Установленно"
+        :plugin/installing "Установка"
+        :plugin/install "Установить"
+        :plugin/reload "Перезагрузить"
+        :plugin/update "Обновить"
+        :plugin/updating "Обновление"
+        :plugin/uninstall "Удаление"
+        :plugin/marketplace "Маркетплейс"
+        :plugin/open-settings "Открыть настройки"
+        :plugin/open-package "Открыть пакет"
+        :plugin/load-unpacked "Загрузить распакованные расширения"
+        :plugin/open-preferences "Открыть файл настроек плагина"
+        :plugin/restart "Перезапустить приложение"
+        :plugin/unpacked-tips "Выбрать папку для расширений"
+        :plugin/contribute "✨ Написать и подтвердить новое расширение"
+        :plugin/marketplace-tips "Если расширение работает некорректно после установки, попробуйте перезапустить Logseq."
+        :plugin/up-to-date "Обновлено"
+        :plugin/custom-js-alert "Найден файл custom.js, выполнить его? (Если вы не понимаете содержимое этого файла, рекомендуем не разрешать выполнение, т.к. это несет риски безопасности.)"
+
+        :pdf/copy-ref "Копировать ссылку"
+        :pdf/copy-text "Копировать текст"
+        :pdf/linked-ref "Связанные ссылки"
+        :pdf/toggle-dashed "Пунктир для выделения области"
+
+        :updater/new-version-install "Новая версия загружена. Перезапустите приложения для завершения обновления."
+        :updater/quit-and-install "Перезапустить для установки"
+
+        :paginates/pages "Всего {1} стр."
+        :paginates/prev "Предыдушая"
+        :paginates/next "Следующая"
+
+        :tips/all-done "Все сделано"
+
+        :command-palette/prompt "Набери команду"}
+
    :tongue/fallback :en})
 
 
@@ -2480,7 +3004,8 @@
                 {:label "Afrikaans" :value :af}
                 {:label "Español" :value :es}
                 {:label "Norsk (bokmål)" :value :nb-NO}
-                {:label "Português (Europeu)" :value :pt-PT}])
+                {:label "Português (Europeu)" :value :pt-PT}
+                {:label "Русский" :value :ru}])
 
 (defn translate [dicts]
   (tongue/build-translate dicts))

+ 75 - 56
src/main/frontend/extensions/code.cljs

@@ -46,6 +46,7 @@
             [frontend.handler.file :as file-handler]
             [frontend.state :as state]
             [frontend.util :as util]
+            [frontend.text :as text]
             [goog.dom :as gdom]
             [goog.object :as gobj]
             [rum.core :as rum]))
@@ -57,6 +58,7 @@
 (def textarea-ref-name "textarea")
 (def codemirror-ref-name "codemirror-instance")
 
+
 (defn- save-file-or-block-when-blur-or-esc!
   [editor textarea config state]
   (.save editor)
@@ -68,16 +70,27 @@
         (let [block (db/pull [:block/uuid (:block/uuid config)])
               format (:block/format block)
               content (:block/content block)
-              full-content (:full_content (last (:rum/args state)))]
-          (when (and full-content (string/includes? content full-content))
+              {:keys [lines]} (last (:rum/args state))
+              full-content (:full_content (last (:rum/args state)))
+              value (text/remove-indentations value)]
+          (when full-content
             (let [lines (string/split-lines full-content)
                   fl (first lines)
                   ll (last lines)]
               (when (and fl ll)
-                (let [value' (str (string/trim fl) "\n" value "\n" (string/trim ll))
-                      ;; FIXME: What if there're multiple code blocks with the same value?
-                      content' (string/replace-first content full-content value')]
-                  (editor-handler/save-block-if-changed! block content'))))))
+                (let [src (->> (subvec (vec lines) 1 (dec (count lines)))
+                               (string/join "\n"))
+                      src (text/remove-indentations src)
+                      full-content (str (string/trim fl)
+                                        (if (seq src)
+                                          (str "\n" src "\n")
+                                          "\n")
+                                        (string/trim ll))]
+                  (when (string/includes? content full-content)
+                    (let [value' (str (string/trim fl) "\n" value "\n" (string/trim ll))
+                          ;; FIXME: What if there're multiple code blocks with the same value?
+                          content' (string/replace-first content full-content value')]
+                      (editor-handler/save-block-if-changed! block content'))))))))
 
         (:file-path config)
         (let [path (:file-path config)
@@ -117,56 +130,62 @@
 (defn render!
   [state]
   (let [esc-pressed? (atom nil)
-        dark? (state/dark?)]
-    (let [[config id attr code theme] (:rum/args state)
-          original-mode (get attr :data-lang)
-          mode original-mode
-          clojure? (contains? #{"clojure" "clj" "text/x-clojure" "cljs" "cljc"} mode)
-          mode (if clojure? "clojure" (text->cm-mode mode))
-          lisp? (or clojure?
-                    (contains? #{"scheme" "racket" "lisp"} mode))
-          textarea (gdom/getElement id)
-          editor (when textarea
-                   (from-textarea textarea
-                                  #js {:mode mode
-                                       :theme (str "solarized " theme)
-                                       :matchBrackets lisp?
-                                       :autoCloseBrackets true
-                                       :lineNumbers true
-                                       :styleActiveLine true
-                                       :extraKeys #js {"Esc" (fn [cm]
-                                                               (save-file-or-block-when-blur-or-esc! cm textarea config state)
-                                                               (when-let [block-id (:block/uuid config)]
-                                                                 (let [block (db/pull [:block/uuid block-id])
-                                                                       value (.getValue cm)
-                                                                       textarea-value (gobj/get textarea "value")]
-                                                                   (editor-handler/edit-block! block :max block-id)))
-                                                               ;; TODO: return "handled" or false doesn't always prevent event bubbles
-                                                               (reset! esc-pressed? true)
-                                                               (js/setTimeout #(reset! esc-pressed? false) 10))}}))]
-      (when editor
-        (let [textarea-ref (rum/ref-node state textarea-ref-name)]
-          (gobj/set textarea-ref codemirror-ref-name editor))
-        (let [element (.getWrapperElement editor)]
-          (when (= mode "calc")
-            (.on editor "change" (fn [_cm e]
-                                   (let [new-code (.getValue editor)]
-                                     (reset! (:calc-atom state) (calc/eval-lines new-code))))))
-          (.on editor "blur" (fn [_cm e]
-                               (when e (util/stop e))
-                               (state/set-block-component-editing-mode! false)
-                               (when-not @esc-pressed?
-                                 (save-file-or-block-when-blur-or-esc! editor textarea config state))))
-          (.addEventListener element "mousedown"
-                             (fn [e]
-                               (state/clear-selection!)
-                               (when-let [block (and (:block/uuid config) (into {} (db/get-block-by-uuid (:block/uuid config))))]
-                                 (state/set-editing! id (.getValue editor) block nil false))
-                               (util/stop e)
-                               (state/set-block-component-editing-mode! true)))
-          (.save editor)
-          (.refresh editor)))
-      editor)))
+        dark? (state/dark?)
+        [config id attr code theme] (:rum/args state)
+        default-open? (and (:editor/code-mode? @state/state)
+                           (= (:block/uuid (state/get-edit-block))
+                              (get-in config [:block :block/uuid])))
+        _ (state/set-state! :editor/code-mode? false)
+        original-mode (get attr :data-lang)
+        mode original-mode
+        clojure? (contains? #{"clojure" "clj" "text/x-clojure" "cljs" "cljc"} mode)
+        mode (if clojure? "clojure" (text->cm-mode mode))
+        lisp? (or clojure?
+                  (contains? #{"scheme" "racket" "lisp"} mode))
+        textarea (gdom/getElement id)
+        editor (when textarea
+                 (from-textarea textarea
+                                #js {:mode mode
+                                     :theme (str "solarized " theme)
+                                     :matchBrackets lisp?
+                                     :autoCloseBrackets true
+                                     :lineNumbers true
+                                     :styleActiveLine true
+                                     :extraKeys #js {"Esc" (fn [cm]
+                                                             (save-file-or-block-when-blur-or-esc! cm textarea config state)
+                                                             (when-let [block-id (:block/uuid config)]
+                                                               (let [block (db/pull [:block/uuid block-id])
+                                                                     value (.getValue cm)
+                                                                     textarea-value (gobj/get textarea "value")]
+                                                                 (editor-handler/edit-block! block :max block-id)))
+                                                             ;; TODO: return "handled" or false doesn't always prevent event bubbles
+                                                             (reset! esc-pressed? true)
+                                                             (js/setTimeout #(reset! esc-pressed? false) 10))}}))]
+    (when editor
+      (let [textarea-ref (rum/ref-node state textarea-ref-name)]
+        (gobj/set textarea-ref codemirror-ref-name editor))
+      (let [element (.getWrapperElement editor)]
+        (when (= mode "calc")
+          (.on editor "change" (fn [_cm e]
+                                 (let [new-code (.getValue editor)]
+                                   (reset! (:calc-atom state) (calc/eval-lines new-code))))))
+        (.on editor "blur" (fn [_cm e]
+                             (when e (util/stop e))
+                             (state/set-block-component-editing-mode! false)
+                             (when-not @esc-pressed?
+                               (save-file-or-block-when-blur-or-esc! editor textarea config state))))
+        (.addEventListener element "mousedown"
+                           (fn [e]
+                             (state/clear-selection!)
+                             (when-let [block (and (:block/uuid config) (into {} (db/get-block-by-uuid (:block/uuid config))))]
+                               (state/set-editing! id (.getValue editor) block nil false))
+                             (util/stop e)
+                             (state/set-block-component-editing-mode! true)))
+        (.save editor)
+        (.refresh editor)))
+    (when default-open?
+      (.focus editor))
+    editor))
 
 (defn- load-and-render!
   [state]

+ 10 - 3
src/main/frontend/handler/editor.cljs

@@ -2421,7 +2421,8 @@
                   (when (thingatpt/get-setting :properties?)
                     (thingatpt/properties-at-point input))
                   (when (thingatpt/get-setting :list?)
-                    (thingatpt/list-item-at-point input)))]
+                    (and (cursor/end-of-line? input) ;; only apply DWIM when cursor at EOL 
+                         (thingatpt/list-item-at-point input))))]
           (cond
             thing-at-point
             (case (:type thing-at-point)
@@ -2431,7 +2432,12 @@
                           (+ (string/index-of content right-bound pos)
                              (count right-bound))))
               "admonition-block" (keydown-new-line)
-              "source-block" (keydown-new-line)
+              "source-block" (do
+                               (keydown-new-line)
+                               (case (:action thing-at-point)
+                                 :into-code-editor
+                                 (state/into-code-editor-mode!)
+                                 nil))
               "block-ref" (open-block-in-sidebar! (:link thing-at-point))
               "page-ref" (when-not (string/blank? (:link thing-at-point))
                            (insert-first-page-block-if-not-exists! (:link thing-at-point))
@@ -2765,7 +2771,8 @@
           shift? (.-shiftKey e)
           code (gobj/getValueByKeys e "event_" "code")]
       (cond
-        (or is-composing? (= key-code 229))
+        (and (or is-composing? (= key-code 229))
+             (not (state/get-editor-show-page-search-hashtag?)))
         nil
 
         (or ctrlKey metaKey)

+ 3 - 1
src/main/frontend/modules/outliner/file.cljs

@@ -27,7 +27,9 @@
                    (string/blank? (:block/content (first blocks)))
                    (nil? (:block/file page-block)))
       (let [tree (tree/blocks->vec-tree blocks (:block/name page-block))]
-        (file/save-tree page-block tree)))))
+        (if page-block
+          (file/save-tree page-block tree)
+          (js/console.error (str "can't find page id: " page-db-id)))))))
 
 (defn write-files!
   [page-db-ids]

+ 1 - 1
src/main/frontend/modules/shortcut/config.cljs

@@ -255,7 +255,7 @@
 
    :editor/insert-link             {:desc    "HTML Link"
                                     :binding "mod+l"
-                                    :fn      editor-handler/html-link-format!}
+                                    :fn      #(editor-handler/html-link-format!)}
 
    :editor/select-all-blocks       {:desc    "Select all blocks"
                                     :binding "mod+shift+a"

+ 66 - 0
src/main/frontend/modules/shortcut/dict.cljs

@@ -239,6 +239,72 @@
      :command.editor/open-edit               "Editar bloque seleccionado"
      :command.editor/delete-selection        "Eliminar bloques seleccionados"
      :command.editor/toggle-open-blocks      "Alternar bloques abieros, (colapsar o expandir todos)"}
+    :ru
+    {:shortcut.category/formatting            "Форматирование"
+     :shortcut.category/basics                "Базовые"
+     :shortcut.category/navigating            "Навигация"
+     :shortcut.category/block-editing         "Общее редактирование блока"
+     :shortcut.category/block-command-editing "Команды редактирования блока"
+     :shortcut.category/block-selection       "Выделение блоков (нажмите Esc для снятия выделения)"
+     :shortcut.category/toggle                "Переключатели"
+     :shortcut.category/others                "Разное"
+     :command.editor/indent                   "Увеличить отступ"
+     :command.editor/outdent                  "Уменьшить отступ"
+     :command.editor/move-block-up            "Передвинуть блок выше"
+     :command.editor/move-block-down          "Передвинуть блок ниже"
+     :command.editor/new-block                "Создать новый блок"
+     :command.editor/new-line                 "Новая строка в блоке"
+     :command.editor/zoom-in                  "Увеличить / Вперед"
+     :command.editor/zoom-out                 "Уменьшить / Назад"
+     :command.editor/follow-link              "Перейти по ссылке под курсором"
+     :command.editor/open-link-in-sidebar     "Открыть ссылку в боковой панели"
+     :command.editor/expand-block-children    "Раскрыть"
+     :command.editor/collapse-block-children  "Свернуть"
+     :command.editor/select-block-up          "Выбрать блок выше"
+     :command.editor/select-block-down        "Выбрать блок ниже"
+     :command.editor/select-all-blocks        "Выбрать все блоки"
+     :command.ui/toggle-help                  "Переключить помощь"
+     :command.git/commit                      "Подтвердить"
+     :command.go/search                       "Искать на графе"
+     :command.go/search-in-page               "Искать на текущей странице"
+     :command.ui/toggle-document-mode         "Переключить режи документа"
+     :command.ui/toggle-contents              "Переключить контент на боковой панели"
+     :command.ui/toggle-theme                 "Переключение между светлой / темной темой"
+     :command.ui/toggle-right-sidebar         "Переключить боковую панель"
+     :command.ui/toggle-settings              "Переключить параметры"
+     :command.ui/toggle-new-block             "Переключение Enter/Shift+Enter для перехода на новую строку"
+     :command.go/journals                     "Перейти в Дневники"
+     :command.ui/toggle-wide-mode             "Переключить широкоформатный режим"
+     :command.ui/toggle-brackets              "Переключить скобки"
+     :command.search/re-index                 "Восстановить индекс поиска"
+     :command.graph/re-index                  "Переиндексировать весь граф"
+     :command.editor/bold                     "Жирный"
+     :command.editor/italics                  "Курсив"
+     :command.editor/insert-link              "HTML ссылка"
+     :command.editor/highlight                "Выделение"
+     :command.editor/undo                     "Отменить"
+     :command.editor/redo                     "Вернуть"
+     :command.editor/copy                     "Копировать"
+     :command.editor/cut                      "Вырезать"
+     :command.editor/up                       "Переместить курсор вверх / Выбрать вверх"
+     :command.editor/down                     "Переместить курсор вниз / Выбрать вниз"
+     :command.editor/left                     "Переместить курсор влево / Открыть выбранный блок в начале"
+     :command.editor/right                    "Переместить курсор вправо / Открыть выбранный блок в конце"
+     :command.editor/backspace                "Удалить перед курсором"
+     :command.editor/delete                   "Удалить после курсора"
+     :command.editor/cycle-todo               "Переключить состояние элемента TODO"
+     :command.editor/clear-block              "Удалить содержимое блока"
+     :command.editor/kill-line-before         "Удалить строку до курсора"
+     :command.editor/kill-line-after          "Удалить строку после курсора"
+     :command.editor/beginning-of-block       "Переместите курсор в начало блока"
+     :command.editor/end-of-block             "Переместите курсор в конец блока"
+     :command.editor/forward-word             "Переместить курсор на одно слово вперед"
+     :command.editor/backward-word            "Переместить курсор на одно слово назад"
+     :command.editor/forward-kill-word        "Удалить следующее слово"
+     :command.editor/backward-kill-word       "Удалить предыдущее слово"
+     :command.editor/open-edit                "Редактировать выбранный блок"
+     :command.editor/delete-selection         "Удалить выбранные блоки"
+     :command.editor/toggle-open-blocks       "Переключить открытые блоки (свернуть или развернуть все)"}
     :nb-NO
     {:shortcut.category/formatting            "Formatering"
      :shortcut.category/basics                "Basis"

+ 10 - 1
src/main/frontend/state.cljs

@@ -540,6 +540,10 @@
   []
   (get (:editor/content @state) (get-edit-input-id)))
 
+(defn sub-edit-content
+  []
+  (sub [:editor/content (get-edit-input-id)]))
+
 (defn append-current-edit-content!
   [append-text]
   (when-not (string/blank? append-text)
@@ -692,7 +696,6 @@
 
 (defn drop-last-selection-block!
   []
-  (def blocks (:selection/blocks @state))
   (let [last-block (peek (vec (:selection/blocks @state)))]
     (swap! state assoc
            :selection/mode true
@@ -880,6 +883,12 @@
                       :editor/block nil
                       :cursor-range nil}))
 
+(defn into-code-editor-mode!
+  []
+  (swap! state merge {:editor/editing? nil
+                      :cursor-range nil
+                      :editor/code-mode? true}))
+
 (defn set-last-pos!
   [new-pos]
   (set-state! :editor/last-saved-cursor new-pos))

+ 11 - 0
src/main/frontend/text.cljs

@@ -346,3 +346,14 @@
       (if (not= (first parts) "0")
         (string/join "/" parts)
         (last parts)))))
+
+(defn remove-indentations
+  [text]
+  (when (string? text)
+    (let [lines (string/split-lines text)
+          spaces (re-find #"^[\s\t]+" (first lines))
+          spaces-count (count spaces)]
+      (string/join "\n" (map (fn [line]
+                               (let [spaces (re-find #"^[\s\t]+" line)
+                                     spaces-count (min (count spaces) spaces-count)]
+                                 (util/safe-subs line spaces-count))) lines)))))

+ 3 - 1
src/main/frontend/util/cursor.cljs

@@ -91,7 +91,9 @@
   [input]
   (let [[content pos] (get-input-content&pos input)]
     (if (zero? pos) 0
-        (inc (string/last-index-of content \newline (dec pos))))))
+        (let [last-newline-pos (string/last-index-of content \newline (dec pos))]
+          (if (= nil last-newline-pos) 0 ;; no newline found (first line)
+              (inc last-newline-pos))))))
 
 (defn line-end-pos
   [input]

+ 2 - 0
src/main/frontend/util/keycode.cljs

@@ -2,6 +2,8 @@
 
 (def left-square-bracket 219)
 (def left-paren 57)
+(def enter 13)
 
 (def left-square-bracket-code "BracketLeft")
 (def left-paren-code "Digit9")
+(def enter-code "Enter")

+ 11 - 6
src/main/frontend/util/thingatpt.cljs

@@ -147,12 +147,17 @@
                        string/split-lines
                        first
                        (string/replace "```" "")
-                       string/trim)]
-      (when-not (string/blank? language)
-            (assoc markdown-src
-                   :type "source-block"
-                   :language language
-                   :headers nil)))))
+                       string/trim)
+          raw-content (:raw-content markdown-src)
+          blank-raw-content? (string/blank? raw-content)
+          action (if (or blank-raw-content? (= (string/trim raw-content) language))
+                   :into-code-editor
+                   :none)]
+      (assoc markdown-src
+             :type "source-block"
+             :language language
+             :action action
+             :headers nil))))
 
 (defn admonition&src-at-point [& [input]]
   (or (org-admonition&src-at-point input)