浏览代码

Merge remote-tracking branch 'upstream/master' into whiteboards

Peng Xiao 3 年之前
父节点
当前提交
26ad627661
共有 55 个文件被更改,包括 957 次插入377 次删除
  1. 23 3
      .github/workflows/build-desktop-release.yml
  2. 0 46
      android/README.md
  3. 2 2
      android/app/build.gradle
  4. 29 3
      android/app/src/main/java/com/logseq/app/FsWatcher.java
  5. 33 4
      bb.edn
  6. 1 1
      deps/db/src/logseq/db/default.cljs
  7. 8 3
      deps/graph-parser/src/logseq/graph_parser/block.cljs
  8. 1 1
      deps/graph-parser/src/logseq/graph_parser/config.cljs
  9. 1 1
      deps/graph-parser/src/logseq/graph_parser/test/docs_graph_helper.cljs
  10. 96 0
      docs/develop-logseq-on-mobile.md
  11. 1 0
      docs/develop-logseq.md
  12. 0 18
      docs/mobile.md
  13. 3 2
      e2e-tests/basic.spec.ts
  14. 4 4
      ios/App/App.xcodeproj/project.pbxproj
  15. 0 4
      ios/App/App/capacitor.config.json
  16. 0 33
      ios/README.md
  17. 1 1
      resources/css/common.css
  18. 1 1
      resources/package.json
  19. 3 26
      scripts/src/logseq/tasks/dev.clj
  20. 21 0
      scripts/src/logseq/tasks/dev/desktop.clj
  21. 73 0
      scripts/src/logseq/tasks/dev/mobile.clj
  22. 7 1
      scripts/src/logseq/tasks/util.clj
  23. 3 1
      shadow-cljs.edn
  24. 24 5
      src/electron/electron/url.cljs
  25. 3 1
      src/electron/electron/window.cljs
  26. 26 1
      src/main/electron/listener.cljs
  27. 45 28
      src/main/frontend/components/block.cljs
  28. 1 0
      src/main/frontend/components/block.css
  29. 2 0
      src/main/frontend/components/external.cljs
  30. 11 8
      src/main/frontend/components/onboarding.cljs
  31. 95 41
      src/main/frontend/components/onboarding/setups.cljs
  32. 1 0
      src/main/frontend/components/search.cljs
  33. 1 1
      src/main/frontend/components/settings.cljs
  34. 2 1
      src/main/frontend/components/sidebar.cljs
  35. 9 0
      src/main/frontend/config.cljs
  36. 14 6
      src/main/frontend/db.cljs
  37. 15 0
      src/main/frontend/db/model.cljs
  38. 41 3
      src/main/frontend/dicts.cljc
  39. 1 1
      src/main/frontend/extensions/code.css
  40. 4 19
      src/main/frontend/extensions/html_parser.cljs
  41. 0 6
      src/main/frontend/fs.cljs
  42. 55 23
      src/main/frontend/fs/capacitor_fs.cljs
  43. 0 1
      src/main/frontend/fs/protocol.cljs
  44. 22 10
      src/main/frontend/handler/block.cljs
  45. 33 15
      src/main/frontend/handler/editor.cljs
  46. 118 1
      src/main/frontend/handler/external.cljs
  47. 1 1
      src/main/frontend/handler/file_sync.cljs
  48. 2 5
      src/main/frontend/handler/graph.cljs
  49. 18 16
      src/main/frontend/handler/page.cljs
  50. 77 16
      src/main/frontend/modules/shortcut/dicts.cljc
  51. 1 2
      src/main/frontend/state.cljs
  52. 15 7
      src/main/frontend/util/marker.cljs
  53. 1 1
      src/main/frontend/util/text.cljs
  54. 1 1
      src/main/frontend/version.cljs
  55. 7 2
      templates/config.edn

+ 23 - 3
.github/workflows/build-desktop-release.yml

@@ -6,12 +6,16 @@ on:
   workflow_dispatch:
   workflow_dispatch:
     inputs:
     inputs:
       build-target:
       build-target:
-        description: 'Build Target ("nightly"/"beta"/"non-release")'
-        type: string
+        description: 'Build Target (Release Type)'
+        type: choice
         required: true
         required: true
+        options:
+          - beta
+          - nightly
+          - non-release
         default: "beta"
         default: "beta"
       git-ref:
       git-ref:
-        description: "Release Git Ref(master)"
+        description: "Release Git Ref (Which branch or tag to build?)"
         required: true
         required: true
         default: "master"
         default: "master"
       is-draft:
       is-draft:
@@ -24,6 +28,16 @@ on:
         type: boolean
         type: boolean
         required: true
         required: true
         default: true
         default: true
+      enable-file-sync:
+        description: 'Build with file sync support'
+        type: boolean
+        required: true
+        default: false
+      enable-plugins:
+        description: 'Build with plugin system support'
+        type: boolean
+        required: true
+        default: true
   schedule: # Every workday at the 2 P.M. (UTC) we run a scheduled nightly build
   schedule: # Every workday at the 2 P.M. (UTC) we run a scheduled nightly build
     - cron: '0 14 * * MON-FRI'
     - cron: '0 14 * * MON-FRI'
 
 
@@ -87,6 +101,12 @@ jobs:
         run: |
         run: |
           sed -i 's/defonce version ".*"/defonce version "${{ steps.ref.outputs.version }}"/g' src/main/frontend/version.cljs
           sed -i 's/defonce version ".*"/defonce version "${{ steps.ref.outputs.version }}"/g' src/main/frontend/version.cljs
 
 
+      - name: Set Build Environment Variables (only when workflow_dispath)
+        if: ${{ github.event_name == 'workflow_dispatch' }}
+        run: |
+          echo "ENABLE_FILE_SYNC=${{ github.event.inputs.enable-file-sync }}" >> $GITHUB_ENV
+          echo "ENABLE_PLUGINS=${{ github.event.inputs.enable-plugins }}" >> $GITHUB_ENV
+
       - name: Compile CLJS
       - name: Compile CLJS
         run: yarn install && gulp build && yarn cljs:release-electron
         run: yarn install && gulp build && yarn cljs:release-electron
 
 

+ 0 - 46
android/README.md

@@ -1,46 +0,0 @@
-## Set up development environment 
-* Install Android studio [^1] and SDK (newer than 30) tools
-   Note: for M1 MacBook users.
-   - Download version **Mac with Apple Chip** 
-   - unzip it and move **Android Studio.app** file to **Applications**, or you will get the following error later.
-     ```
-     [error] Unable to launch Android Studio. Is it installed?
-        Attempted to open Android Studio at: /Applications/Android Studio.app
-        You can configure this with the CAPACITOR_ANDROID_STUDIO_PATH environment variable.
-     ```
-* In Android Studio, open **Tools** -> **SDK Manager** to install other SDK tools [^2].
-  > In the SDK Tools tab, make sure to install at least the following:
-  >> - Android SDK Build-Tools
-  >> - Android SDK Command-line Tools
-  >> - Android Emulator
-  >> - Android SDK Platform-Tools
-
-## Build the development app in Android emulator
-* Replace `server url` with your local-ip-address:3001 (run ifconfig to check) in *capacitor.config.ts*.
-* Run `yarn && yarn app-watch` from the logseq project root directory in terminal.
-* Run `npx cap sync android` in another termimal (all-in-one cmd).
-* In Android Studio, open **Tools** -> **AVD Manager** to create Android Virtual Device (AVD), and lanuch it in the emulator.
-* In Android Studio, open **Run** -> **Run** to run Logseq.
-* After logseq startup in Android virtual device, repl should be able to connect
-* For browser console print and devtool remote debug, open chrome, type url chrome://inspect/#devices, you should see your device there, click inspect
-
-## Build a release and install it to your android device 
-* Comment in `server url` in *capacitor.config.ts*.
-* Connect your device to PC.
-* Run `yarn clean && yarn release-app && rm -rf ./public/static && rm -rf ./static/js/*.map && mv static ./public && npx cap sync android && npx cap run android`
-
-## Build a apk
-* Comment out `server url` in *capacitor.config.ts*.
-* Run `yarn clean && yarn release-app && rm -rf ./public/static && rm -rf ./static/js/*.map && mv static ./public && npx cap sync android`.
-* In Android Studio, open **Build** -> **Build Bundles / APKs** -> **Build APKs**.
-* Get your apk in `android/app/build/apk/debug`.
-
-[^1] https://developer.android.com/studio/index.html
-
-[^2] https://capacitorjs.com/docs/getting-started/environment-setup
-
-## Develop without opening Android Studio
-1. brew install gradle
-2. make sure java version using 11
-3. cd web/android && gradle wrapper
-4. install android sdk 30

+ 2 - 2
android/app/build.gradle

@@ -6,8 +6,8 @@ android {
         applicationId "com.logseq.app"
         applicationId "com.logseq.app"
         minSdkVersion rootProject.ext.minSdkVersion
         minSdkVersion rootProject.ext.minSdkVersion
         targetSdkVersion rootProject.ext.targetSdkVersion
         targetSdkVersion rootProject.ext.targetSdkVersion
-        versionCode 28
-        versionName "0.7.4"
+        versionCode 29
+        versionName "0.7.5"
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
         aaptOptions {
         aaptOptions {
              // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
              // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.

+ 29 - 3
android/app/src/main/java/com/logseq/app/FsWatcher.java

@@ -7,6 +7,7 @@ import android.system.Os;
 import android.system.StructStat;
 import android.system.StructStat;
 import android.util.Log;
 import android.util.Log;
 import android.os.FileObserver;
 import android.os.FileObserver;
+
 import android.net.Uri;
 import android.net.Uri;
 
 
 import java.io.*;
 import java.io.*;
@@ -133,7 +134,6 @@ public class FsWatcher extends Plugin {
         File f = new File(path);
         File f = new File(path);
         obj.put("path", Uri.fromFile(f));
         obj.put("path", Uri.fromFile(f));
         obj.put("dir", mPath);
         obj.put("dir", mPath);
-        Log.i("FsWatcher", "prepare event " + obj);
 
 
         switch (event) {
         switch (event) {
             case FileObserver.CLOSE_WRITE:
             case FileObserver.CLOSE_WRITE:
@@ -144,6 +144,7 @@ public class FsWatcher extends Plugin {
                 } catch (IOException | ErrnoException e) {
                 } catch (IOException | ErrnoException e) {
                     e.printStackTrace();
                     e.printStackTrace();
                 }
                 }
+                Log.i("FsWatcher", "prepare event " + obj);
                 obj.put("content", content);
                 obj.put("content", content);
                 break;
                 break;
             case FileObserver.MOVED_TO:
             case FileObserver.MOVED_TO:
@@ -155,13 +156,20 @@ public class FsWatcher extends Plugin {
                 } catch (IOException | ErrnoException e) {
                 } catch (IOException | ErrnoException e) {
                     e.printStackTrace();
                     e.printStackTrace();
                 }
                 }
+                Log.i("FsWatcher", "prepare event " + obj);
                 obj.put("content", content);
                 obj.put("content", content);
                 break;
                 break;
             case FileObserver.MOVE_SELF:
             case FileObserver.MOVE_SELF:
             case FileObserver.MOVED_FROM:
             case FileObserver.MOVED_FROM:
             case FileObserver.DELETE:
             case FileObserver.DELETE:
             case FileObserver.DELETE_SELF:
             case FileObserver.DELETE_SELF:
-                obj.put("event", "unlink");
+                if (f.exists()) {
+                    Log.i("FsWatcher", "abandon notification due to file exists");
+                    return;
+                } else {
+                    obj.put("event", "unlink");
+                }
+                Log.i("FsWatcher", "prepare event " + obj);
                 break;
                 break;
             default:
             default:
                 // unreachable?
                 // unreachable?
@@ -218,7 +226,25 @@ public class FsWatcher extends Plugin {
                 Log.d("FsWatcher", "got path=" + path + " event=" + event);
                 Log.d("FsWatcher", "got path=" + path + " event=" + event);
                 if (Pattern.matches("(?i)[^.].*?\\.(md|org|css|edn|js|markdown)$", path)) {
                 if (Pattern.matches("(?i)[^.].*?\\.(md|org|css|edn|js|markdown)$", path)) {
                     String fullPath = mPath + "/" + path;
                     String fullPath = mPath + "/" + path;
-                    FsWatcher.this.onObserverEvent(event, fullPath);
+                    if (event == FileObserver.MOVE_SELF || event == FileObserver.MOVED_FROM ||
+                        event == FileObserver.DELETE || event == FileObserver.DELETE_SELF) {
+                        Log.d("FsWatcher", "defer delete notification for " + path);
+                        Thread timer = new Thread() {
+                            @Override
+                            public void run() {
+                                try {
+                                    // delay 500ms then send, enough for most syncing net disks
+                                    Thread.sleep(500);
+                                    FsWatcher.this.onObserverEvent(event, fullPath);
+                                } catch (InterruptedException e) {
+                                    e.printStackTrace();
+                                }
+                            }
+                        };
+                        timer.start();
+                    } else {
+                        FsWatcher.this.onObserverEvent(event, fullPath);
+                    }
                 }
                 }
             }
             }
         }
         }

+ 33 - 4
bb.edn

@@ -13,20 +13,49 @@
  {clj-kondo/clj-kondo {:version "2022.02.09"}
  {clj-kondo/clj-kondo {:version "2022.02.09"}
   org.babashka/fswatcher {:version "0.0.3"}}
   org.babashka/fswatcher {:version "0.0.3"}}
  :tasks
  :tasks
- {dev:watch
-  logseq.tasks.dev/watch
+ {dev:desktop-watch
+  logseq.tasks.dev.desktop/watch
 
 
   dev:open-dev-electron-app
   dev:open-dev-electron-app
-  logseq.tasks.dev/open-dev-electron-app
+  logseq.tasks.dev.desktop/open-dev-electron-app
 
 
   -dev:electron-start
   -dev:electron-start
-  {:depends [dev:watch dev:open-dev-electron-app]}
+  {:depends [dev:desktop-watch dev:open-dev-electron-app]}
 
 
   dev:electron-start
   dev:electron-start
   {:doc "Start electron dev by watching assets and opening dev app"
   {:doc "Start electron dev by watching assets and opening dev app"
    ;; Parallel execution - https://book.babashka.org/#parallel
    ;; Parallel execution - https://book.babashka.org/#parallel
    :task (run '-dev:electron-start {:parallel true})}
    :task (run '-dev:electron-start {:parallel true})}
 
 
+  dev:app-watch
+  logseq.tasks.dev.mobile/app-watch
+
+  dev:npx-cap-run-ios
+  logseq.tasks.dev.mobile/npx-cap-run-ios
+
+  -dev:ios-app
+  {:depends [dev:app-watch dev:npx-cap-run-ios]}
+
+  dev:ios-app
+  {:doc "iOS development environment"
+   :task (run '-dev:ios-app {:parallel true})}
+
+  release:ios-app
+  logseq.tasks.dev.mobile/run-ios-release
+
+  dev:npx-cap-run-android
+  logseq.tasks.dev.mobile/npx-cap-run-android
+
+  -dev:android-app
+  {:depends [dev:app-watch dev:npx-cap-run-android]}
+
+  dev:android-app
+  {:doc "Android development environment"
+   :task (run '-dev:android-app {:parallel true})}
+
+  release:android-app
+  logseq.tasks.dev.mobile/run-android-release
+
   dev:validate-local-storage
   dev:validate-local-storage
   logseq.tasks.spec/validate-local-storage
   logseq.tasks.spec/validate-local-storage
 
 

+ 1 - 1
deps/db/src/logseq/db/default.cljs

@@ -2,7 +2,7 @@
   (:require [clojure.string :as string]))
   (:require [clojure.string :as string]))
 
 
 (defonce built-in-pages-names
 (defonce built-in-pages-names
-  #{"NOW" "LATER" "DOING" "DONE" "IN-PROGRESS" "TODO" "WAIT" "WAITING" "A" "B" "C" "Favorites" "Contents" "card"})
+  #{"NOW" "LATER" "DOING" "DONE" "CANCELED" "CANCELLED" "IN-PROGRESS" "TODO" "WAIT" "WAITING" "A" "B" "C" "Favorites" "Contents" "card"})
 
 
 (def built-in-pages
 (def built-in-pages
   (mapv (fn [p]
   (mapv (fn [p]

+ 8 - 3
deps/graph-parser/src/logseq/graph_parser/block.cljs

@@ -237,6 +237,10 @@
 (defn page-name->map
 (defn page-name->map
   "Create a page's map structure given a original page name (string).
   "Create a page's map structure given a original page name (string).
    map as input is supported for legacy compatibility.
    map as input is supported for legacy compatibility.
+   with-id?: if true, assign uuid to the map structure.
+    if the page entity already exists, no-op.
+    else, if with-id? is a uuid, the uuid is used.
+    otherwise, generate a uuid.
    with-timestamp?: assign timestampes to the map structure.
    with-timestamp?: assign timestampes to the map structure.
     Useful when creating new pages from references or namespaces,
     Useful when creating new pages from references or namespaces,
     as there's no chance to introduce timestamps via editing in page"
     as there's no chance to introduce timestamps via editing in page"
@@ -253,9 +257,10 @@
        {:block/name page-name
        {:block/name page-name
         :block/original-name original-page-name}
         :block/original-name original-page-name}
        (when with-id?
        (when with-id?
-         (if page-entity
-           {:block/uuid (:block/uuid page-entity)}
-           {:block/uuid (d/squuid)}))
+         (let [new-uuid (cond page-entity      (:block/uuid page-entity)
+                              (uuid? with-id?) with-id?
+                              :else            (d/squuid))]
+           {:block/uuid new-uuid}))
        (when namespace?
        (when namespace?
          (let [namespace (first (gp-util/split-last "/" original-page-name))]
          (let [namespace (first (gp-util/split-last "/" original-page-name))]
            (when-not (string/blank? namespace)
            (when-not (string/blank? namespace)

+ 1 - 1
deps/graph-parser/src/logseq/graph_parser/config.cljs

@@ -31,7 +31,7 @@
 (defn text-formats
 (defn text-formats
   []
   []
   #{:json :org :md :yml :dat :asciidoc :rst :txt :markdown :adoc :html :js :ts :edn :clj :ml :rb :ex :erl :java :php :c :css
   #{:json :org :md :yml :dat :asciidoc :rst :txt :markdown :adoc :html :js :ts :edn :clj :ml :rb :ex :erl :java :php :c :css
-    :excalidraw})
+    :excalidraw :sh})
 
 
 (defn img-formats
 (defn img-formats
   []
   []

+ 1 - 1
deps/graph-parser/src/logseq/graph_parser/test/docs_graph_helper.cljs

@@ -142,7 +142,7 @@
   ;; only increase over time as the docs graph rarely has deletions
   ;; only increase over time as the docs graph rarely has deletions
   (testing "Counts"
   (testing "Counts"
     (is (= 211 (count files)) "Correct file count")
     (is (= 211 (count files)) "Correct file count")
-    (is (= 40943 (count (d/datoms db :eavt))) "Correct datoms count")
+    (is (= 40945 (count (d/datoms db :eavt))) "Correct datoms count")
 
 
     (is (= 3600
     (is (= 3600
            (ffirst
            (ffirst

+ 96 - 0
docs/develop-logseq-on-mobile.md

@@ -0,0 +1,96 @@
+# iOS development
+
+## Installation
+- Install Xcode 13 from App Store.
+- Install [CocoaPods](https://cocoapods.org/)
+  ```shell
+  sudo gem install cocoapods
+  ```
+  Note: use the following commands from *ios/App* directory to fix **ffi_c.bundle** related issue for M1 MacBook [^1].
+  ```shell
+  sudo arch -x86_64 gem install ffi
+  arch -x86_64 pod install
+  ```
+ 
+## Set up development environment
+### Build the development app
+- comment in `server` section in **capacitor.config.ts**, and replace `process.env.LOGSEQ_APP_ASERVER_URL` with your `http://your-local-ip-address:3001` (run `ifconfig` to check).
+    ```typescript
+    server: {
+        url: "process.env.LOGSEQ_APP_ASERVER_URL",
+        cleartext: true
+        } 
+    ```
+- Run `yarn && yarn app-watch` from the logseq project root directory in terminal.
+- Run `npx cap sync ios` in another termimal to copy web assets from public to *ios/App/App/public*, and create *capacitor.config.json* in *ios/App/App*, and update iOS plugins.
+- Connect your iOS device to MacBook.
+- Run `npx cap open ios` to open Logseq project in Xcode, and build the app there.
+
+or, you can run `bb dev:ios-app` to do those steps with one command if you are on MacOS. To download bb, see https://github.com/babashka/babashka#installation. Also, in order to use mobile bb tasks on macOS, `gsed` needs to be installed in your system (run `brew install gnu-sed` to install).
+
+### Build the release app
+- Comment out `server` section above in **capacitor.config.ts**.
+- Connect your iOS device to MacBook.
+- Run `yarn run-ios-release` to install the release app to your iOS device.
+
+or, you can run `bb release:ios-app` to do those steps with one command.
+
+[^1] https://github.com/CocoaPods/CocoaPods/issues/10220#issuecomment-730963835
+
+
+# Android development  
+## Installation
+- Install Android studio [^1] and SDK (newer than 30) tools
+  Note: for M1 MacBook users.
+  - Download version **Mac with Apple Chip** 
+  - unzip it and move **Android Studio.app** file to **Applications**, or you will get the following error later.
+    ```
+     [error] Unable to launch Android Studio. Is it installed?
+        Attempted to open Android Studio at: /Applications/Android Studio.app
+        You can configure this with the CAPACITOR_ANDROID_STUDIO_PATH environment variable.
+     ```
+- In Android Studio, open **Tools** -> **SDK Manager** to install other SDK tools [^2].
+  > In the SDK Tools tab, make sure to install at least the following:
+  >> - Android SDK Build-Tools
+  >> - Android SDK Command-line Tools
+  >> - Android Emulator
+  >> - Android SDK Platform-Tools
+
+## Set up development environment
+### Build the development app
+- comment in `server` section in **capacitor.config.ts**, and replace `process.env.LOGSEQ_APP_ASERVER_URL` with your `http://your-local-ip-address:3001` (run `ifconfig` to check).
+    ```typescript
+    server: {
+        url: "process.env.LOGSEQ_APP_ASERVER_URL",
+        cleartext: true
+        } 
+    ```
+- Run `yarn && yarn app-watch` from the logseq project root directory in terminal.
+- Run `npx cap sync android` in another termimal.
+- Run `npx cap run android` to install app into your device.
+
+or, you can run `bb dev:android-app` to do those steps with one command if you are on macOS.
+
+Then,
+- In Android Studio, open **Tools** -> **AVD Manager** to create Android Virtual Device (AVD), and lanuch it in the emulator.
+- In Android Studio, open **Run** -> **Run** to run Logseq.
+- After logseq startup in Android virtual device, repl should be able to connect
+- For browser console print and devtool remote debug, open chrome, type url chrome://inspect/#devices, you should see your device there, click inspect
+
+
+### Build a release and install it to your android device 
+- Comment out `server` section above in **capacitor.config.ts**.
+- Connect your device to PC.
+- Run `yarn run-android-release`.
+
+or, you can run `bb release:android-app` to do those steps with one command.
+
+### Build an apk
+- Comment out `server` section above in **capacitor.config.ts**.
+- Run `yarn run-android-release`
+
+or, you can run `bb release:android-app` to do those steps with one command.
+
+Then,
+- In Android Studio, open **Build** -> **Build Bundles / APKs** -> **Build APKs**.
+- Get your apk in `android/app/build/apk/debug`.

+ 1 - 0
docs/develop-logseq.md

@@ -44,6 +44,7 @@ yarn install && cd static && yarn install && cd ..
 ```bash
 ```bash
 yarn watch
 yarn watch
 # Wait until watch is finished building and then in a different shell
 # Wait until watch is finished building and then in a different shell
+# If you have opened desktop logseq, you should close it. Otherwise, the following command will fail.
 yarn dev-electron-app
 yarn dev-electron-app
 ```
 ```
 
 

+ 0 - 18
docs/mobile.md

@@ -1,18 +0,0 @@
-## install list:
-* Android studio
-* SDK 30
-* other sdk tools in Android studio preference setting https://capacitorjs.com/docs/getting-started/environment-setup
-* change the server url in `capacitor.config.ts` with your local ip:3001 (run ifconfig to check)
-* run `yarn && yarn app-watch`
-* in another console, run `npx cap open android`
-* create Android virtual device in Android studio
-* click the run button in Android stutio to run the project
-* after logseq startup in Android virtual device, repl should be able to connect
-* for browser console print and devtool remote debug, open chrome, type url chrome://inspect/#devices, you should see your device there, click inspect
-
-
-## Develop without opening Android Studio
-1. brew install gradle
-2. make sure java version using 11
-3. cd web/android && gradle wrapper
-4. install android sdk 30

+ 3 - 2
e2e-tests/basic.spec.ts

@@ -120,7 +120,8 @@ test('template', async ({ page, block }) => {
 
 
   await createRandomPage(page)
   await createRandomPage(page)
 
 
-  await block.mustFill('template test\ntemplate:: ' + randomTemplate)
+  await block.mustFill('template test\ntemplate:: ')
+  await page.keyboard.type(randomTemplate, {delay: 100})
   await page.keyboard.press('Enter')
   await page.keyboard.press('Enter')
   await block.clickNext()
   await block.clickNext()
 
 
@@ -195,7 +196,7 @@ test('auto completion and auto pair', async ({ page, block }) => {
 
 
   // {{
   // {{
   await block.mustType('type {{', { toBe: 'type {{}}' })
   await block.mustType('type {{', { toBe: 'type {{}}' })
-
+  await page.waitForTimeout(100);
   // ((
   // ((
   await block.clickNext()
   await block.clickNext()
 
 

+ 4 - 4
ios/App/App.xcodeproj/project.pbxproj

@@ -542,7 +542,7 @@
 				INFOPLIST_FILE = App/Info.plist;
 				INFOPLIST_FILE = App/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
-				MARKETING_VERSION = 0.7.4;
+				MARKETING_VERSION = 0.7.5;
 				OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
 				OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PRODUCT_NAME = "$(TARGET_NAME)";
@@ -568,7 +568,7 @@
 				INFOPLIST_FILE = App/Info.plist;
 				INFOPLIST_FILE = App/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
-				MARKETING_VERSION = 0.7.4;
+				MARKETING_VERSION = 0.7.5;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
 				SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
@@ -593,7 +593,7 @@
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
-				MARKETING_VERSION = 0.7.4;
+				MARKETING_VERSION = 0.7.5;
 				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
 				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
 				MTL_FAST_MATH = YES;
 				MTL_FAST_MATH = YES;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
@@ -620,7 +620,7 @@
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
-				MARKETING_VERSION = 0.7.4;
+				MARKETING_VERSION = 0.7.5;
 				MTL_FAST_MATH = YES;
 				MTL_FAST_MATH = YES;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PRODUCT_NAME = "$(TARGET_NAME)";

+ 0 - 4
ios/App/App/capacitor.config.json

@@ -17,9 +17,5 @@
 	},
 	},
 	"ios": {
 	"ios": {
 		"scheme": "Logseq"
 		"scheme": "Logseq"
-	},
-	"server": {
-		"url": "http://192.168.1.59:3001",
-		"cleartext": true
 	}
 	}
 }
 }

+ 0 - 33
ios/README.md

@@ -1,33 +0,0 @@
-Installation:
-
-* Install Xcode 13 from App Store.
-* Install [CocoaPods](https://cocoapods.org/)
-  ```shell
-  sudo gem install cocoapods
-  ```
-  Note: use the following commands from *ios/App* directory to fix **ffi_c.bundle** related issue for M1 MacBook [^1].
-  ```shell
-  sudo arch -x86_64 gem install ffi
-  arch -x86_64 pod install
-  ```
-* Run `yarn && yarn app-watch` from the logseq project root directory in terminal.
-* Open Logseq project in Xcode by running the following command in termimal.
-  ```shell
-  npx cap open ios
-  ```
-  Note: for the first time after a fresh clone.
-  - Run `npx cap copy ios` to copy web assets from public to *ios/App/App/public*, and create *capacitor.config.json* in *ios/App/App*.
-  - Run `npx cap update ios` to update iOS plugins.
-  - Add the following code to *ios/App/App/capacitor.config.json*, and replace `server url` with your local-ip-address:3001 (run ifconfig to check)
-    ```json
-    "server": {
-        "url": "http://your-own-id-address:3001",
-        "cleartext": true} 
-    ```
-* Run logseq 
-  ```shell
-  npx cap run ios
-  ```
-  
-[^1] https://github.com/CocoaPods/CocoaPods/issues/10220#issuecomment-730963835
-  

+ 1 - 1
resources/css/common.css

@@ -878,7 +878,7 @@ h1.title {
 
 
 .block-highlight,
 .block-highlight,
 .content .selected {
 .content .selected {
-  transition: background-color 0.15s;
+  transition: background-color 0.2s cubic-bezier(0, 1, 0, 1);
   background-color: var(--ls-block-highlight-color);
   background-color: var(--ls-block-highlight-color);
   padding: -1px;
   padding: -1px;
 }
 }

+ 1 - 1
resources/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "Logseq",
   "name": "Logseq",
-  "version": "0.7.4",
+  "version": "0.7.5",
   "main": "electron.js",
   "main": "electron.js",
   "author": "Logseq",
   "author": "Logseq",
   "license": "AGPL-3.0",
   "license": "AGPL-3.0",

+ 3 - 26
scripts/src/logseq/tasks/dev.clj

@@ -1,30 +1,7 @@
 (ns logseq.tasks.dev
 (ns logseq.tasks.dev
-  "Tasks for development"
-  (:require [babashka.fs :as fs]
-            [babashka.tasks :refer [shell]]))
-
-(defn watch
-  "Watches environment to reload cljs, css and other assets"
-  []
-  (shell "yarn electron-watch"))
-
-(defn- file-modified-later-than?
-  [file comparison-instant]
-  (pos? (.compareTo (fs/file-time->instant (fs/last-modified-time file))
-                    comparison-instant)))
-
-;; Works whether yarn clean has been run before or not
-(defn open-dev-electron-app
-  "Opens dev-electron-app when watch process has built main.js"
-  []
-  (let [start-time (java.time.Instant/now)]
-    (dotimes [_n 1000]
-             (if (and (fs/exists? "static/js/main.js")
-                      (file-modified-later-than? "static/js/main.js" start-time))
-               (shell "yarn dev-electron-app")
-               (println "Waiting for app to build..."))
-             (Thread/sleep 1000))))
-
+  "Tasks for general development. For desktop or mobile development see their
+  namespaces"
+  (:require [babashka.tasks :refer [shell]]))
 
 
 (defn lint
 (defn lint
   "Run all lint tasks
   "Run all lint tasks

+ 21 - 0
scripts/src/logseq/tasks/dev/desktop.clj

@@ -0,0 +1,21 @@
+(ns logseq.tasks.dev.desktop
+  "Tasks for desktop (electron) development"
+  (:require [babashka.tasks :refer [shell]]
+            [babashka.fs :as fs]
+            [logseq.tasks.util :as task-util]))
+
+(defn watch
+  "Watches environment to reload cljs, css and other assets"
+  []
+  (shell "yarn electron-watch"))
+
+(defn open-dev-electron-app
+  "Opens dev-electron-app when watch process has built main.js"
+  []
+  (let [start-time (java.time.Instant/now)]
+    (dotimes [_n 1000]
+             (if (and (fs/exists? "static/js/main.js")
+                      (task-util/file-modified-later-than? "static/js/main.js" start-time))
+               (shell "yarn dev-electron-app")
+               (println "Waiting for app to build..."))
+             (Thread/sleep 1000))))

+ 73 - 0
scripts/src/logseq/tasks/dev/mobile.clj

@@ -0,0 +1,73 @@
+(ns logseq.tasks.dev.mobile
+  "Tasks for mobile development"
+  (:require [babashka.tasks :refer [shell]]
+            [babashka.fs :as fs]
+            [clojure.string :as string]
+            [logseq.tasks.util :as task-util]))
+
+(defn- open-dev-app
+  "Opens mobile app when watch process has built main.js"
+  [cmd]
+  (let [start-time (java.time.Instant/now)]
+    (loop [n 1000]
+      (if (and (fs/exists? "static/js/main.js")
+               (task-util/file-modified-later-than? "static/js/main.js" start-time))
+        (shell cmd)
+        (println "Waiting for app to build..."))
+      (Thread/sleep 1000)
+      (when-not (or (and (fs/exists? "ios/App/App/public/static/js/main.js")
+                      (task-util/file-modified-later-than? "ios/App/App/public/static/js/main.js" start-time))
+                    (and (fs/exists? "android/App/src/main/assets/public/static/js/main.js")
+                      (task-util/file-modified-later-than? "android/App/src/main/assets/public/static/js/main.js" start-time)))
+        (recur (dec n))))))
+
+(defn- set-system-env
+  "Updates capacitor.config.ts serve url with IP from ifconfig"
+  []
+  (let [ip (string/trim (:out (shell {:out :string} "ipconfig getifaddr en0")))
+        logseq-app-server-url (format "%s://%s:%s" "http" ip "3001")]
+    (println "Server URL:" logseq-app-server-url)
+    (shell "git checkout capacitor.config.ts")
+    (let [new-body (-> (slurp "capacitor.config.ts")
+                       (string/replace "// , server:" " , server:")
+                       (string/replace "//    url:" "    url:")
+                       (string/replace "process.env.LOGSEQ_APP_SERVER_URL"
+                                       (pr-str logseq-app-server-url))
+                       (string/replace "//    cleartext:" "    cleartext:")
+                       (string/replace "// }" " }"))]
+      (spit "capacitor.config.ts" new-body))))
+
+
+(defn app-watch
+  "Watches environment to reload cljs, css and other assets for mobile"
+  []
+  (println "set-system-env")
+  (set-system-env)
+  (doseq [cmd ["yarn clean"
+               "yarn app-watch"]]
+    (println cmd)
+    (shell cmd)))
+
+(defn npx-cap-run-ios
+  "Copy assets files to iOS build directory, and run app in Xcode"
+  []
+  (open-dev-app "npx cap sync ios")
+  (shell "npx cap open ios"))
+
+(defn npx-cap-run-android
+  "Copy assets files to Android build directory, and run app in Android Studio"
+  []
+  (open-dev-app "npx cap sync android")
+  (shell "npx cap open android"))
+
+(defn run-ios-release
+  "Build iOS app release"
+  []
+  (shell "git checkout capacitor.config.ts")
+  (shell "yarn run-ios-release"))
+
+(defn run-android-release
+  "Build Android app release"
+  []
+  (shell "git checkout capacitor.config.ts")
+  (shell "yarn run-android-release"))

+ 7 - 1
scripts/src/logseq/tasks/util.clj

@@ -1,6 +1,12 @@
 (ns logseq.tasks.util
 (ns logseq.tasks.util
   "Utils for tasks"
   "Utils for tasks"
-  (:require [clojure.pprint :as pprint]))
+  (:require [clojure.pprint :as pprint]
+            [babashka.fs :as fs]))
+
+(defn file-modified-later-than?
+  [file comparison-instant]
+  (pos? (.compareTo (fs/file-time->instant (fs/last-modified-time file))
+                    comparison-instant)))
 
 
 (defn print-usage [arg-str]
 (defn print-usage [arg-str]
   (println (format
   (println (format

+ 3 - 1
shadow-cljs.edn

@@ -37,7 +37,9 @@
                                                 "externs.js"]
                                                 "externs.js"]
                            :warnings           {:fn-deprecated false
                            :warnings           {:fn-deprecated false
                                                 :redef false}}
                                                 :redef false}}
-        :closure-defines  {goog.debug.LOGGING_ENABLED      true}
+        :closure-defines  {goog.debug.LOGGING_ENABLED       true
+                           frontend.config/ENABLE-FILE-SYNC #shadow/env ["ENABLE_FILE_SYNC" :as :bool :default false]
+                           frontend.config/ENABLE-PLUGINS   #shadow/env ["ENABLE_PLUGINS"   :as :bool :default true]}
 
 
         ;; NOTE: electron, browser/mobile-app use different asset-paths.
         ;; NOTE: electron, browser/mobile-app use different asset-paths.
         ;;   For browser/mobile-app devs, assets are located in /static/js(via HTTP root).
         ;;   For browser/mobile-app devs, assets are located in /static/js(via HTTP root).

+ 24 - 5
src/electron/electron/url.cljs

@@ -29,7 +29,7 @@
                                       :payload (str "Failed to open link. Missing graph identifier after `logseq://graph/`.")})))
                                       :payload (str "Failed to open link. Missing graph identifier after `logseq://graph/`.")})))
 
 
 (defn local-url-handler
 (defn local-url-handler
-  "Given a URL with `graph identifier` as path, `page` (optional) and `block-id` 
+  "Given a URL with `graph identifier` as path, `page` (optional) and `block-id`
    (optional) as parameters, open the local graphs accordingly.
    (optional) as parameters, open the local graphs accordingly.
    `graph identifier` is the name of the graph to open, e.g. `lambda`"
    `graph identifier` is the name of the graph to open, e.g. `lambda`"
   [^js win parsed-url force-new-window?]
   [^js win parsed-url force-new-window?]
@@ -45,10 +45,10 @@
           ;; TODO: allow open new window on specific page, without waiting for `graph ready` ipc then redirect to that page
           ;; TODO: allow open new window on specific page, without waiting for `graph ready` ipc then redirect to that page
         (when (or page-name block-id file)
         (when (or page-name block-id file)
           (let [redirect-f (fn [win' graph-name']
           (let [redirect-f (fn [win' graph-name']
-                         (when (= graph-name graph-name')
-                           (send-to-renderer win' "redirectWhenExists" {:page-name page-name
-                                                                        :block-id block-id
-                                                                        :file file})))]
+                             (when (= graph-name graph-name')
+                               (send-to-renderer win' "redirectWhenExists" {:page-name page-name
+                                                                            :block-id block-id
+                                                                            :file file})))]
             (if open-new-window?
             (if open-new-window?
               (state/set-state! :window/once-graph-ready redirect-f)
               (state/set-state! :window/once-graph-ready redirect-f)
               (do (win/switch-to-window! window-on-graph)
               (do (win/switch-to-window! window-on-graph)
@@ -57,6 +57,22 @@
           (send-to-renderer win "openNewWindowOfGraph" graph-name)))
           (send-to-renderer win "openNewWindowOfGraph" graph-name)))
       (graph-identifier-error-handler graph-identifier))))
       (graph-identifier-error-handler graph-identifier))))
 
 
+(defn- x-callback-url-handler
+  [^js parsed-url]
+  (let [action (.-pathname parsed-url)]
+    (cond
+      (= action "/quickCapture")
+      (let [[url title content] (get-URL-decoded-params parsed-url ["url" "title" "content"])]
+        (send-to-renderer "quickCapture" {:url url
+                                          :title title
+                                          :content content}))
+
+      :else
+      (send-to-renderer "notification" {:type "error"
+                                        :payload (str "Unimplemented x-callback-url action: `"
+                                                      action
+                                                      "`.")}))))
+
 (defn logseq-url-handler
 (defn logseq-url-handler
   [^js win parsed-url]
   [^js win parsed-url]
   (let [url-host (.-host parsed-url)] ;; return "" when no pathname provided
   (let [url-host (.-host parsed-url)] ;; return "" when no pathname provided
@@ -64,6 +80,9 @@
       (= "auth-callback" url-host)
       (= "auth-callback" url-host)
       (send-to-renderer win "loginCallback" (.get (.-searchParams parsed-url) "code"))
       (send-to-renderer win "loginCallback" (.get (.-searchParams parsed-url) "code"))
 
 
+      (= "x-callback-url" url-host)
+      (x-callback-url-handler parsed-url)
+
       ;; identifier of graph in local
       ;; identifier of graph in local
       (= "graph" url-host)
       (= "graph" url-host)
       (local-url-handler win parsed-url false)
       (local-url-handler win parsed-url false)

+ 3 - 1
src/electron/electron/window.cljs

@@ -103,7 +103,9 @@
 (defn graph-has-other-windows? [win dir]
 (defn graph-has-other-windows? [win dir]
   (let [windows (get-graph-all-windows dir)]
   (let [windows (get-graph-all-windows dir)]
         ;; windows (filter #(.isVisible %) windows) ;; for mac .hide windows. such windows should also included
         ;; windows (filter #(.isVisible %) windows) ;; for mac .hide windows. such windows should also included
-    (boolean (some (fn [^js window] (not= (.-id win) (.-id window))) windows))))
+    (boolean (some (fn [^js window] (and (not (.isDestroyed window))
+                                         (not= (.-id win) (.-id window))))
+                   windows))))
 
 
 (defn- open-default-app!
 (defn- open-default-app!
   [url default-open]
   [url default-open]

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

@@ -1,9 +1,12 @@
 (ns electron.listener
 (ns electron.listener
   (:require [frontend.state :as state]
   (:require [frontend.state :as state]
             [frontend.context.i18n :refer [t]]
             [frontend.context.i18n :refer [t]]
+            [frontend.date :as date]
             [frontend.handler.route :as route-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.ui :as ui-handler]
             [frontend.handler.ui :as ui-handler]
+            [frontend.config :as config]
+            [clojure.string :as string]
             [cljs-bean.core :as bean]
             [cljs-bean.core :as bean]
             [frontend.fs.watcher-handler :as watcher-handler]
             [frontend.fs.watcher-handler :as watcher-handler]
             [frontend.fs.sync :as sync]
             [frontend.fs.sync :as sync]
@@ -34,7 +37,7 @@
    (fn [_req]
    (fn [_req]
      (persist-dbs!))))
      (persist-dbs!))))
 
 
-(defn listen-to-electron!
+(defn ^:large-vars/cleanup-todo listen-to-electron!
   []
   []
   ;; TODO: move "file-watcher" to electron.ipc.channels
   ;; TODO: move "file-watcher" to electron.ipc.channels
   (js/window.apis.on "file-watcher"
   (js/window.apis.on "file-watcher"
@@ -125,6 +128,28 @@
                      (fn [code]
                      (fn [code]
                        (user/login-callback code)))
                        (user/login-callback code)))
 
 
+  (js/window.apis.on "quickCapture"
+                     (fn [args]
+                       (let [{:keys [url title content]} (bean/->clj args)
+                             page (or (state/get-current-page)
+                                      (string/lower-case (date/journal-name)))
+                             format (db/get-page-format page)
+                             time (date/get-current-time)
+                             text (or (and content (not-empty (string/trim content))) "")
+                             link (if (not-empty title) (config/link-format format title url) url)
+                             template (get-in (state/get-config)
+                                              [:quick-capture-templates :text]
+                                              "**{time}** [[quick capture]]: {text} {url}")
+                             content (-> template
+                                         (string/replace "{time}" time)
+                                         (string/replace "{url}" link)
+                                         (string/replace "{text}" text))]
+                         (if (and (state/get-edit-block) (state/editing?))
+                           (editor-handler/insert content)
+                           (editor-handler/api-insert-new-block! content {:page page
+                                                                          :edit-block? false
+                                                                          :replace-empty-target? true})))))
+
   (js/window.apis.on "openNewWindowOfGraph"
   (js/window.apis.on "openNewWindowOfGraph"
                      ;; Handle open new window in renderer, until the destination graph doesn't rely on setting local storage
                      ;; Handle open new window in renderer, until the destination graph doesn't rely on setting local storage
                      ;; No db cache persisting ensured. Should be handled by the caller
                      ;; No db cache persisting ensured. Should be handled by the caller

+ 45 - 28
src/main/frontend/components/block.cljs

@@ -33,6 +33,7 @@
             [frontend.extensions.zotero :as zotero]
             [frontend.extensions.zotero :as zotero]
             [frontend.format.block :as block]
             [frontend.format.block :as block]
             [frontend.format.mldoc :as mldoc]
             [frontend.format.mldoc :as mldoc]
+            [frontend.fs :as fs]
             [frontend.handler.block :as block-handler]
             [frontend.handler.block :as block-handler]
             [frontend.handler.common :as common-handler]
             [frontend.handler.common :as common-handler]
             [frontend.handler.dnd :as dnd]
             [frontend.handler.dnd :as dnd]
@@ -48,20 +49,20 @@
             [frontend.security :as security]
             [frontend.security :as security]
             [frontend.state :as state]
             [frontend.state :as state]
             [frontend.template :as template]
             [frontend.template :as template]
-            [logseq.graph-parser.text :as text]
             [frontend.ui :as ui]
             [frontend.ui :as ui]
             [frontend.util :as util]
             [frontend.util :as util]
             [frontend.util.clock :as clock]
             [frontend.util.clock :as clock]
             [frontend.util.drawer :as drawer]
             [frontend.util.drawer :as drawer]
-            [frontend.util.text :as text-util]
             [frontend.util.property :as property]
             [frontend.util.property :as property]
-            [logseq.graph-parser.config :as gp-config]
-            [logseq.graph-parser.util :as gp-util]
-            [logseq.graph-parser.mldoc :as gp-mldoc]
-            [logseq.graph-parser.block :as gp-block]
+            [frontend.util.text :as text-util]
             [goog.dom :as gdom]
             [goog.dom :as gdom]
             [goog.object :as gobj]
             [goog.object :as gobj]
             [lambdaisland.glogi :as log]
             [lambdaisland.glogi :as log]
+            [logseq.graph-parser.block :as gp-block]
+            [logseq.graph-parser.config :as gp-config]
+            [logseq.graph-parser.mldoc :as gp-mldoc]
+            [logseq.graph-parser.text :as text]
+            [logseq.graph-parser.util :as gp-util]
             [medley.core :as medley]
             [medley.core :as medley]
             [promesa.core :as p]
             [promesa.core :as p]
             [reitit.frontend.easy :as rfe]
             [reitit.frontend.easy :as rfe]
@@ -271,12 +272,15 @@
 
 
     (when @src
     (when @src
       (let [ext (keyword (util/get-file-ext @src))
       (let [ext (keyword (util/get-file-ext @src))
+            repo (state/get-current-repo)
+            repo-dir (config/get-repo-dir repo)
+            path (str repo-dir href)
             share-fn (fn [event]
             share-fn (fn [event]
                        (util/stop event)
                        (util/stop event)
                        (when (mobile-util/native-platform?)
                        (when (mobile-util/native-platform?)
-                         (p/let [url (str (config/get-repo-dir (state/get-current-repo)) href)]
-                           (.share Share #js {:url url
-                                              :title "Open file with your favorite app"}))))]
+                         (.share Share #js {:url path
+                                            :title "Open file with your favorite app"})))]
+
         (cond
         (cond
           (contains? config/audio-formats ext)
           (contains? config/audio-formats ext)
           (audio-cp @src)
           (audio-cp @src)
@@ -284,13 +288,20 @@
           (contains? (gp-config/img-formats) ext)
           (contains? (gp-config/img-formats) ext)
           (resizable-image config title @src metadata full_text true)
           (resizable-image config title @src metadata full_text true)
 
 
+          (contains? (gp-config/text-formats) ext)
+          [:a.asset-ref.is-plaintext {:href (rfe/href :file {:path path})
+                                      :on-click (fn [_event]
+                                                  (p/let [result (fs/read-file repo-dir path)]
+                                                    (db/set-file-content! repo path result )))}
+           title]
+
           (= ext :pdf)
           (= ext :pdf)
           [:a.asset-ref.is-pdf {:href @src
           [:a.asset-ref.is-pdf {:href @src
                                 :on-click share-fn}
                                 :on-click share-fn}
            title]
            title]
 
 
           :else
           :else
-          [:a.asset-ref.is-doc {:ref @src
+          [:a.asset-ref.is-doc {:href @src
                                 :on-click share-fn}
                                 :on-click share-fn}
            title])))))
            title])))))
 
 
@@ -1142,7 +1153,7 @@
       (when-not (string/blank? id)
       (when-not (string/blank? id)
         (let [width (min (- (util/get-width) 96)
         (let [width (min (- (util/get-width) 96)
                          560)
                          560)
-              height (int (* width (/ 315 560)))]
+              height (int (* width (/ 360 560)))]
           [:iframe
           [:iframe
            {:allowfullscreen true
            {:allowfullscreen true
             :framespacing "0"
             :framespacing "0"
@@ -1156,10 +1167,7 @@
 (defn- macro-video-cp
 (defn- macro-video-cp
   [_config arguments]
   [_config arguments]
   (when-let [url (first arguments)]
   (when-let [url (first arguments)]
-    (let [width (min (- (util/get-width) 96)
-                     560)
-          height (int (* width (/ 315 560)))
-          results (text-util/get-matched-video url)
+    (let [results (text-util/get-matched-video url)
           src (match results
           src (match results
                      [_ _ _ (:or "youtube.com" "youtu.be" "y2u.be") _ id _]
                      [_ _ _ (:or "youtube.com" "youtu.be" "y2u.be") _ id _]
                      (if (= (count id) 11) ["youtube-player" id] url)
                      (if (= (count id) 11) ["youtube-player" id] url)
@@ -1173,8 +1181,10 @@
                      [_ _ _ (_ :guard #(string/ends-with? % "vimeo.com")) _ id _]
                      [_ _ _ (_ :guard #(string/ends-with? % "vimeo.com")) _ id _]
                      (str "https://player.vimeo.com/video/" id)
                      (str "https://player.vimeo.com/video/" id)
 
 
-                     [_ _ _ "bilibili.com" _ id _]
-                     (str "https://player.bilibili.com/player.html?bvid=" id "&high_quality=1")
+                     [_ _ _ "bilibili.com" _ id & query]
+                     (str "https://player.bilibili.com/player.html?bvid=" id "&high_quality=1"
+                          (when-let [page (second query)]
+                            (str "&page=" page)))
 
 
                      :else
                      :else
                      url)]
                      url)]
@@ -1182,16 +1192,20 @@
                (= (first src) "youtube-player"))
                (= (first src) "youtube-player"))
         (youtube/youtube-video (last src))
         (youtube/youtube-video (last src))
         (when src
         (when src
-          [:iframe
-           {:allowfullscreen true
-            :allow "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope"
-            :framespacing "0"
-            :frameborder "no"
-            :border "0"
-            :scrolling "no"
-            :src src
-            :width width
-            :height height}])))))
+          (let [width (min (- (util/get-width) 96) 560)
+                height (int (* width (/ (if (string/includes? src "player.bilibili.com")
+                                          360 315)
+                                        560)))]
+            [:iframe
+             {:allow-full-screen true
+              :allow "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope"
+              :framespacing "0"
+              :frame-border "no"
+              :border "0"
+              :scrolling "no"
+              :src src
+              :width width
+              :height height}]))))))
 
 
 (defn- macro-else-cp
 (defn- macro-else-cp
   [name config arguments]
   [name config arguments]
@@ -1931,7 +1945,10 @@
         (when-not (target-forbidden-edit? target)
         (when-not (target-forbidden-edit? target)
           (cond
           (cond
             (and shift? (state/get-selection-start-block-or-first))
             (and shift? (state/get-selection-start-block-or-first))
-            (editor-handler/highlight-selection-area! block-id)
+            (do
+              (util/stop e)
+              (util/clear-selection!)
+              (editor-handler/highlight-selection-area! block-id))
 
 
             shift?
             shift?
             (util/clear-selection!)
             (util/clear-selection!)

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

@@ -335,6 +335,7 @@
   min-height: 24px;
   min-height: 24px;
   padding: 2px 0;
   padding: 2px 0;
   border-bottom: 1px solid transparent;
   border-bottom: 1px solid transparent;
+  transition: background-color 0.3s cubic-bezier(0.16, 1, 0.3, 1);
 
 
   &.selected {
   &.selected {
     border-bottom-color: var(--ls-primary-background-color);
     border-bottom-color: var(--ls-primary-background-color);

+ 2 - 0
src/main/frontend/components/external.cljs

@@ -1,3 +1,5 @@
+;; deprecated by the onboarding import panel frontend.components.onboarding.setups
+
 (ns frontend.components.external
 (ns frontend.components.external
   (:require [rum.core :as rum]
   (:require [rum.core :as rum]
             [goog.object :as gobj]
             [goog.object :as gobj]

+ 11 - 8
src/main/frontend/components/onboarding.cljs

@@ -23,25 +23,28 @@
                          [:span.mr-1 (t :help/shortcuts)]
                          [:span.mr-1 (t :help/shortcuts)]
                          (ui/icon "command" {:style {:font-size 20}})]]]
                          (ui/icon "command" {:style {:font-size 20}})]]]
                       [(t :help/docs) "https://docs.logseq.com/"]
                       [(t :help/docs) "https://docs.logseq.com/"]
+                      [(t :help/start) "https://docs.logseq.com/#/page/tutorial"]
                       ["FAQ" "https://docs.logseq.com/#/page/faq"]]}
                       ["FAQ" "https://docs.logseq.com/#/page/faq"]]}
-
-          {:title "About"
-           :children [[(t :help/start) "https://docs.logseq.com/#/page/getting%20started"]
-                      [(t :help/about) "https://logseq.com/blog/about"]]}
+          
+          {:title "Community"
+           :children [[(t :help/awesome-logseq) "https://github.com/logseq/awesome-logseq"]
+                      [(t :help/blog) "https://blog.logseq.com"]
+                      [discord-with-icon "https://discord.gg/KpN4eHY"]]}
 
 
           {:title "Development"
           {:title "Development"
            :children [[(t :help/roadmap) "https://trello.com/b/8txSM12G/roadmap"]
            :children [[(t :help/roadmap) "https://trello.com/b/8txSM12G/roadmap"]
                       [(t :help/bug) "https://github.com/logseq/logseq/issues/new?labels=from:in-app&template=bug_report.yaml"]
                       [(t :help/bug) "https://github.com/logseq/logseq/issues/new?labels=from:in-app&template=bug_report.yaml"]
                       [(t :help/feature) "https://github.com/logseq/logseq/issues/new?assignees=&labels=&template=feature_request.md&title="]
                       [(t :help/feature) "https://github.com/logseq/logseq/issues/new?assignees=&labels=&template=feature_request.md&title="]
                       [(t :help/changelog) "https://docs.logseq.com/#/page/changelog"]]}
                       [(t :help/changelog) "https://docs.logseq.com/#/page/changelog"]]}
+          
+          {:title "About"
+           :children [[(t :help/about) "https://logseq.com/blog/about"]]}
 
 
           {:title "Terms"
           {:title "Terms"
            :children [[(t :help/privacy) "https://logseq.com/blog/privacy-policy"]
            :children [[(t :help/privacy) "https://logseq.com/blog/privacy-policy"]
-                      [(t :help/terms) "https://logseq.com/blog/terms"]]}
+                      [(t :help/terms) "https://logseq.com/blog/terms"]]}]]
 
 
-          {:title "Community"
-           :children [[(t :help/awesome-logseq) "https://github.com/logseq/awesome-logseq"]
-                      [discord-with-icon "https://discord.gg/KpN4eHY"]]}]]
+          
 
 
      (map (fn [sublist]
      (map (fn [sublist]
             [[:p.mt-4.mb-1 [:b (:title sublist)]]
             [[:p.mt-4.mb-1 [:b (:title sublist)]]

+ 95 - 41
src/main/frontend/components/onboarding/setups.cljs

@@ -127,54 +127,125 @@
               [:small.opacity-50 label]]]))]]])))
               [:small.opacity-50 label]]]))]]])))
 
 
 (defonce *roam-importing? (atom nil))
 (defonce *roam-importing? (atom nil))
+(defonce *lsq-importing? (atom nil))
 (defonce *opml-importing? (atom nil))
 (defonce *opml-importing? (atom nil))
 (defonce *opml-imported-pages (atom nil))
 (defonce *opml-imported-pages (atom nil))
 
 
+(defn- finished-cb
+  []
+  (notification/show! "Import finished!" :success)
+  (route-handler/redirect-to-home!))
+
+(defn- roam-import-handler
+  [e]
+  (let [file      (first (array-seq (.-files (.-target e))))
+        file-name (gobj/get file "name")]
+    (if (string/ends-with? file-name ".json")
+      (do
+        (reset! *roam-importing? true)
+        (let [reader (js/FileReader.)]
+          (set! (.-onload reader)
+                (fn [e]
+                  (let [text (.. e -target -result)]
+                    (external-handler/import-from-roam-json! text
+                                                             #(do (reset! *roam-importing? false) (finished-cb))))))
+          (.readAsText reader file)))
+      (notification/show! "Please choose a JSON file."
+                          :error))))
+
+(defn- lsq-import-handler
+  [e]
+  (let [file      (first (array-seq (.-files (.-target e))))
+        file-name (some-> (gobj/get file "name")
+                          (string/lower-case))]
+    (cond (string/ends-with? file-name ".edn")
+          (do
+            (reset! *lsq-importing? true)
+            (let [reader (js/FileReader.)]
+              (set! (.-onload reader)
+                    (fn [e]
+                      (let [text (.. e -target -result)]
+                        (external-handler/import-from-edn! text
+                                                           #(do (reset! *lsq-importing? false) (finished-cb))))))
+              (.readAsText reader file)))
+
+          (string/ends-with? file-name ".json")
+          (do
+            (reset! *lsq-importing? true)
+            (let [reader (js/FileReader.)]
+              (set! (.-onload reader)
+                    (fn [e]
+                      (let [text (.. e -target -result)]
+                        (external-handler/import-from-json! text
+                                                            #(do (reset! *lsq-importing? false) (finished-cb))))))
+              (.readAsText reader file)))
+
+          :else
+          (notification/show! "Please choose an EDN or a JSON file."
+                              :error))))
+
+(defn- opml-import-handler
+  [e]
+  (let [file      (first (array-seq (.-files (.-target e))))
+        file-name (gobj/get file "name")]
+    (if (string/ends-with? file-name ".opml")
+      (do
+        (reset! *opml-importing? true)
+        (let [reader (js/FileReader.)]
+          (set! (.-onload reader)
+                (fn [e]
+                  (let [text (.. e -target -result)]
+                    (external-handler/import-from-opml! text
+                                                        (fn [pages]
+                                                          (reset! *opml-imported-pages pages)
+                                                          (reset! *opml-importing? false)
+                                                          (finished-cb))))))
+          (.readAsText reader file)))
+      (notification/show! "Please choose a OPML file."
+                          :error))))
+
 (rum/defc importer < rum/reactive
 (rum/defc importer < rum/reactive
   [{:keys [query-params]}]
   [{:keys [query-params]}]
   (let [roam-importing? (rum/react *roam-importing?)
   (let [roam-importing? (rum/react *roam-importing?)
+        lsq-importing?  (rum/react *lsq-importing?)
         opml-importing? (rum/react *opml-importing?)
         opml-importing? (rum/react *opml-importing?)
-        finished-cb     (fn []
-                          (notification/show! "Finished!" :success)
-                          (route-handler/redirect-to-home!))]
+        importing?      (or roam-importing? lsq-importing? opml-importing?)]
 
 
     (setups-container
     (setups-container
      :importer
      :importer
      [:article.flex.flex-col.items-center.importer.py-16.px-8
      [:article.flex.flex-col.items-center.importer.py-16.px-8
       [:section.c.text-center
       [:section.c.text-center
        [:h1 "Do you already have notes that you want to import?"]
        [:h1 "Do you already have notes that you want to import?"]
-       [:h2 "If they are in a JSON or Markdown format Logseq can work with them."]]
+       [:h2 "If they are in a JSON, EDN or Markdown format Logseq can work with them."]]
       [:section.d.md:flex
       [:section.d.md:flex
        [:label.action-input.flex.items-center.mx-2.my-2
        [:label.action-input.flex.items-center.mx-2.my-2
-        {:disabled (or roam-importing? opml-importing?)}
+        {:disabled importing?}
         [:span.as-flex-center [:i (svg/roam-research 28)]]
         [:span.as-flex-center [:i (svg/roam-research 28)]]
         [:div.flex.flex-col
         [:div.flex.flex-col
          (if roam-importing?
          (if roam-importing?
            (ui/loading "Importing ...")
            (ui/loading "Importing ...")
-           [
-            [:strong "RoamResearch"]
+           [[:strong "RoamResearch"]
             [:small "Import a JSON Export of your Roam graph"]])]
             [:small "Import a JSON Export of your Roam graph"]])]
         [:input.absolute.hidden
         [:input.absolute.hidden
          {:id        "import-roam"
          {:id        "import-roam"
           :type      "file"
           :type      "file"
-          :on-change (fn [e]
-                       (let [file      (first (array-seq (.-files (.-target e))))
-                             file-name (gobj/get file "name")]
-                         (if (string/ends-with? file-name ".json")
-                           (do
-                             (reset! *roam-importing? true)
-                             (let [reader (js/FileReader.)]
-                               (set! (.-onload reader)
-                                     (fn [e]
-                                       (let [text (.. e -target -result)]
-                                         (external-handler/import-from-roam-json! text
-                                                                                  #(do (reset! *roam-importing? false) (finished-cb))))))
-                               (.readAsText reader file)))
-                           (notification/show! "Please choose a JSON file."
-                                               :error))))}]]
+          :on-change roam-import-handler}]]
+
+       [:label.action-input.flex.items-center.mx-2.my-2
+        {:disabled importing?}
+        [:span.as-flex-center [:i (svg/logo 28)]]
+        [:span.flex.flex-col
+         (if lsq-importing?
+           (ui/loading "Importing ...")
+           [[:strong "EDN / JSON"]
+            [:small "Import an EDN or a JSON Export of your Logseq graph"]])]
+        [:input.absolute.hidden
+         {:id        "import-lsq"
+          :type      "file"
+          :on-change lsq-import-handler}]]
 
 
        [:label.action-input.flex.items-center.mx-2.my-2
        [:label.action-input.flex.items-center.mx-2.my-2
-        {:disabled (or roam-importing? opml-importing?)}
+        {:disabled importing?}
         [:span.as-flex-center (ui/icon "sitemap" {:style {:fontSize "26px"}})]
         [:span.as-flex-center (ui/icon "sitemap" {:style {:fontSize "26px"}})]
         [:span.flex.flex-col
         [:span.flex.flex-col
          (if opml-importing?
          (if opml-importing?
@@ -185,24 +256,7 @@
         [:input.absolute.hidden
         [:input.absolute.hidden
          {:id        "import-opml"
          {:id        "import-opml"
           :type      "file"
           :type      "file"
-          :on-change (fn [e]
-                       (let [file      (first (array-seq (.-files (.-target e))))
-                             file-name (gobj/get file "name")]
-                         (if (string/ends-with? file-name ".opml")
-                           (do
-                             (reset! *opml-importing? true)
-                             (let [reader (js/FileReader.)]
-                               (set! (.-onload reader)
-                                     (fn [e]
-                                       (let [text (.. e -target -result)]
-                                         (external-handler/import-from-opml! text
-                                                                             (fn [pages]
-                                                                               (reset! *opml-imported-pages pages)
-                                                                               (reset! *opml-importing? false)
-                                                                               (finished-cb))))))
-                               (.readAsText reader file)))
-                           (notification/show! "Please choose a OPML file."
-                                               :error))))}]]]
+          :on-change opml-import-handler}]]]
 
 
       (when (= "picker" (:from query-params))
       (when (= "picker" (:from query-params))
         [:section.e
         [:section.e

+ 1 - 0
src/main/frontend/components/search.cljs

@@ -298,6 +298,7 @@
         [:a.text-sm.font-medium {:href (rfe/href :search {:q search-q})
         [:a.text-sm.font-medium {:href (rfe/href :search {:q search-q})
                                  :on-click (fn []
                                  :on-click (fn []
                                              (when-not (string/blank? search-q)
                                              (when-not (string/blank? search-q)
+                                               (state/close-modal!)
                                                (search-handler/search (state/get-current-repo) search-q {:limit 1000
                                                (search-handler/search (state/get-current-repo) search-q {:limit 1000
                                                                                                          :more? true})
                                                                                                          :more? true})
                                                (search-handler/clear-search!)))}
                                                (search-handler/clear-search!)))}

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

@@ -619,7 +619,7 @@
      (when (and util/mac? (util/electron?)) (app-auto-update-row t))
      (when (and util/mac? (util/electron?)) (app-auto-update-row t))
      (usage-diagnostics-row t instrument-disabled?)
      (usage-diagnostics-row t instrument-disabled?)
      (when-not (mobile-util/native-platform?) (developer-mode-row t developer-mode?))
      (when-not (mobile-util/native-platform?) (developer-mode-row t developer-mode?))
-     (when (util/electron?) (plugin-system-switcher-row))
+     (when (and (util/electron?) config/enable-plugins?) (plugin-system-switcher-row))
      (flashcards-switcher-row enable-flashcards?)
      (flashcards-switcher-row enable-flashcards?)
      (when (util/electron?) (https-user-agent-row https-agent-opts))
      (when (util/electron?) (https-user-agent-row https-agent-opts))
      (clear-cache-row t)
      (clear-cache-row t)

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

@@ -517,7 +517,8 @@
   [e]
   [e]
   (state/hide-custom-context-menu!)
   (state/hide-custom-context-menu!)
   (when-not (or (gobj/get e "shiftKey")
   (when-not (or (gobj/get e "shiftKey")
-                (util/meta-key? e))
+                (util/meta-key? e)
+                (state/get-edit-input-id))
     (editor-handler/clear-selection!)))
     (editor-handler/clear-selection!)))
 
 
 (rum/defcs ^:large-vars/cleanup-todo sidebar <
 (rum/defcs ^:large-vars/cleanup-todo sidebar <

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

@@ -33,6 +33,15 @@
 (goog-define API-DOMAIN "api.logseq.com")
 (goog-define API-DOMAIN "api.logseq.com")
 (goog-define WS-URL "wss://og96xf1si7.execute-api.us-east-2.amazonaws.com/production?graphuuid=%s")
 (goog-define WS-URL "wss://og96xf1si7.execute-api.us-east-2.amazonaws.com/production?graphuuid=%s")
 
 
+;; feature flags
+(goog-define ENABLE-FILE-SYNC false)
+(defonce enable-file-sync? (or ENABLE-FILE-SYNC dev?)) ;; always enable file-sync when dev
+
+(goog-define ENABLE-PLUGINS true)
+(defonce enable-plugins? ENABLE-PLUGINS)
+
+(swap! state/state assoc :plugin/enabled enable-plugins?)
+
 ;; :TODO: How to do this?
 ;; :TODO: How to do this?
 ;; (defonce desktop? ^boolean goog.DESKTOP)
 ;; (defonce desktop? ^boolean goog.DESKTOP)
 
 

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

@@ -41,7 +41,7 @@
   delete-file-blocks! delete-page-blocks delete-files delete-pages-by-files
   delete-file-blocks! delete-page-blocks delete-files delete-pages-by-files
   filter-only-public-pages-and-blocks get-all-block-contents get-all-tagged-pages
   filter-only-public-pages-and-blocks get-all-block-contents get-all-tagged-pages
   get-all-templates get-block-and-children get-block-by-uuid get-block-children sort-by-left
   get-all-templates get-block-and-children get-block-by-uuid get-block-children sort-by-left
-  get-block-parent get-block-parents parents-collapsed? get-block-referenced-blocks
+  get-block-parent get-block-parents parents-collapsed? get-block-referenced-blocks get-all-referenced-blocks-uuid
   get-block-children-ids get-block-immediate-children get-block-page
   get-block-children-ids get-block-immediate-children get-block-page
   get-custom-css get-date-scheduled-or-deadlines
   get-custom-css get-date-scheduled-or-deadlines
   get-file-blocks get-file-last-modified-at get-file get-file-page get-file-page-id file-exists?
   get-file-blocks get-file-last-modified-at get-file get-file-page get-file-page-id file-exists?
@@ -52,7 +52,8 @@
   get-all-pages get-pages get-pages-relation get-pages-that-mentioned-page get-public-pages get-tag-pages
   get-all-pages get-pages get-pages-relation get-pages-that-mentioned-page get-public-pages get-tag-pages
   journal-page? page-alias-set pull-block
   journal-page? page-alias-set pull-block
   set-file-last-modified-at! page-empty? page-exists? page-empty-or-dummy? get-alias-source-page
   set-file-last-modified-at! page-empty? page-exists? page-empty-or-dummy? get-alias-source-page
-  set-file-content! has-children? get-namespace-pages get-all-namespace-relation get-pages-by-name-partition]
+  set-file-content! has-children? get-namespace-pages get-all-namespace-relation get-pages-by-name-partition
+  get-original-name]
 
 
  [frontend.db.react
  [frontend.db.react
   get-current-page set-key-value
   get-current-page set-key-value
@@ -158,13 +159,13 @@
                 (assoc option
                 (assoc option
                        :listen-handler listen-and-persist!))))
                        :listen-handler listen-and-persist!))))
 
 
-(defn restore-graph!
-  "Restore db from serialized db cache, and swap into the current db status"
-  [repo]
+(defn restore-graph-from-text!
+  "Swap db string into the current db status
+   stored: the text to restore from"
+  [repo stored]
   (p/let [db-name (datascript-db repo)
   (p/let [db-name (datascript-db repo)
           db-conn (d/create-conn db-schema/schema)
           db-conn (d/create-conn db-schema/schema)
           _ (swap! conns assoc db-name db-conn)
           _ (swap! conns assoc db-name db-conn)
-          stored (db-persist/get-serialized-graph db-name)
           _ (when stored
           _ (when stored
               (let [stored-db (try (string->db stored)
               (let [stored-db (try (string->db stored)
                                    (catch js/Error _e
                                    (catch js/Error _e
@@ -178,6 +179,13 @@
                 (conn/reset-conn! db-conn db)))]
                 (conn/reset-conn! db-conn db)))]
     (d/transact! db-conn [{:schema/version db-schema/version}])))
     (d/transact! db-conn [{:schema/version db-schema/version}])))
 
 
+(defn restore-graph!
+  "Restore db from serialized db cache"
+  [repo]
+  (p/let [db-name (datascript-db repo)
+          stored (db-persist/get-serialized-graph db-name)]
+    (restore-graph-from-text! repo stored)))
+
 (defn restore!
 (defn restore!
   [{:keys [repos]} _old-db-schema restore-config-handler]
   [{:keys [repos]} _old-db-schema restore-config-handler]
   (let [repo (or (state/get-current-repo) (:url (first repos)))]
   (let [repo (or (state/get-current-repo) (:url (first repos)))]

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

@@ -71,6 +71,11 @@
        react
        react
        first))))
        first))))
 
 
+(defn get-original-name
+  [page-entity]
+  (or (:block/original-name page-entity)
+      (:block/name page-entity)))
+
 (defn get-tag-pages
 (defn get-tag-pages
   [repo tag-name]
   [repo tag-name]
   (when tag-name
   (when tag-name
@@ -1364,6 +1369,16 @@
          (reset! blocks-count-cache n)
          (reset! blocks-count-cache n)
          n)))))
          n)))))
 
 
+(defn get-all-referenced-blocks-uuid
+  "Get all uuids of blocks with any back link exists."
+  []
+  (when-let [db (conn/get-db)]
+    (d/q '[:find [?refed-uuid ...]
+           :where
+           ;; ?referee-b is block with ref towards ?refed-b
+           [?refed-b   :block/uuid ?refed-uuid]
+           [?referee-b :block/refs ?refed-b]] db)))
+
 ;; block/uuid and block/content
 ;; block/uuid and block/content
 (defn get-all-block-contents
 (defn get-all-block-contents
   []
   []

+ 41 - 3
src/main/frontend/dicts.cljc

@@ -2206,7 +2206,10 @@
            :page/file-sync-versions "Versões da página"
            :page/file-sync-versions "Versões da página"
            :plugin/not-installed "Não instalado"
            :plugin/not-installed "Não instalado"
            :tutorial/dummy-notes "dummy-notes-en.md"
            :tutorial/dummy-notes "dummy-notes-en.md"
-           :tutorial/text "tutorial-en.md"}
+           :tutorial/text "tutorial-en.md"
+           :settings-page/edit-export-css "Editar export.css"
+           :settings-page/enable-flashcards "Flashcards"
+           :settings-page/export-theme "Exportar Tema"}
 
 
    :pt-PT {:on-boarding/demo-graph "Isto é um grafo de demonstração, nenhuma mudança será guardada até abrir uma pasta local."
    :pt-PT {:on-boarding/demo-graph "Isto é um grafo de demonstração, nenhuma mudança será guardada até abrir uma pasta local."
            :on-boarding/add-graph "Adicionar grafo"
            :on-boarding/add-graph "Adicionar grafo"
@@ -2500,7 +2503,39 @@
         :select.graph/add-graph "Sim, adicionar outro grafo"
         :select.graph/add-graph "Sim, adicionar outro grafo"
 
 
         :file-sync/other-user-graph "O gráfico local atual é obrigado ao gráfico remoto de outro usuário. Portanto, não consigo iniciar a sincronização."
         :file-sync/other-user-graph "O gráfico local atual é obrigado ao gráfico remoto de outro usuário. Portanto, não consigo iniciar a sincronização."
-        :file-sync/graph-deleted "O gráfico remoto atual foi excluído"}
+        :file-sync/graph-deleted "O gráfico remoto atual foi excluído"
+        :export-graph "Exportar gráfico"
+        :export-page "Exportar Página"
+        :host "Host"
+        :port "Port"
+        :re-index-discard-unsaved-changes-warning "A reindexação descartará o gráfico atual e processará todos os arquivos novamente conforme estão armazenados no disco. Você perderá as alterações não salvas e pode demorar um pouco. Continuar?"
+        :re-index-multiple-windows-warning "Você precisa fechar as outras janelas antes de reindexar este gráfico."
+        :save "Salvar"
+        :settings-of-plugins "Configurações dos plugins"
+        :sync-from-local-changes-detected "Atualizar detecta e processa arquivos modificados em seu disco e que são diferentes do conteúdo atual da página do Logseq. Continuar?"
+        :type "Tipo"
+        :content/copy-block-emebed "Copiar bloco para incorporar"
+
+        :graph/persist "O Logseq está sincronizando seu status interno, aguarde alguns segundos."
+        :graph/persist-error "Falha na sincronização do status interno."
+        :graph/save "Salvando..."
+        :graph/save-success "Salvo com sucesso"
+        :graph/save-error "Falha ao salvar"
+
+        :page/copy-page-url "Copiar URL da página"
+        :page/file-sync-versions "Versões da Página"
+        :page/open-backup-directory "Abra a listagem de backups de página"
+        :page/unfavorite "Desfavoritar página"
+        :plugin/not-installed "Não instalado"
+        :search/page-names "Procurar nome da página"
+        :settings-page/auto-updater "Auto atualizar"
+        :tutorial/dummy-notes "dummy-notes-en.md"
+        :tutorial/text "tutorial-en.md"
+        :settings-page/edit-export-css "Editar export.css"
+        :settings-page/enable-flashcards "Flashcards"
+        :settings-page/export-theme "Exportar tema"
+        :settings-page/network-proxy "Proxy de rede"
+        :settings-page/plugin-system "Sistema de plugins"}
 
 
    :ru {:on-boarding/demo-graph "Это демонстрационный граф, изменения не будут сохранены, пока вы не откроете локальный файл."
    :ru {:on-boarding/demo-graph "Это демонстрационный граф, изменения не будут сохранены, пока вы не откроете локальный файл."
         :on-boarding/add-graph "Добавить новый граф"
         :on-boarding/add-graph "Добавить новый граф"
@@ -3426,7 +3461,10 @@
 
 
         :file-sync/other-user-graph "Il grafo locale attuale è associato al grafo remoto di un altro utente. Non è quindi possibile avviare la sincronizzazione."
         :file-sync/other-user-graph "Il grafo locale attuale è associato al grafo remoto di un altro utente. Non è quindi possibile avviare la sincronizzazione."
         :file-sync/graph-deleted "Il grafo attuale è stato eliminato"
         :file-sync/graph-deleted "Il grafo attuale è stato eliminato"
-        :settings-page/enable-encryption "Crittografia"}
+        :settings-page/enable-encryption "Crittografia"
+        :settings-page/edit-export-css "Modificare export.css"
+        :settings-page/enable-flashcards "Flashcard"
+        :settings-page/export-theme "Esporta tema"}
 
 
    :tr {:tutorial/text #?(:cljs (rc/inline "tutorial-tr.md")
    :tr {:tutorial/text #?(:cljs (rc/inline "tutorial-tr.md")
                                 :default "tutorial-tr.md")
                                 :default "tutorial-tr.md")

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

@@ -31,7 +31,7 @@
 
 
   &-calc {
   &-calc {
     @apply text-sm;
     @apply text-sm;
-    padding: 0.25em;
+    padding: 23px 0.25em 0.25em 0.25em;
     width: max-content;
     width: max-content;
     text-align: right;
     text-align: right;
 
 

+ 4 - 19
src/main/frontend/extensions/html_parser.cljs

@@ -17,22 +17,10 @@
 (defn- export-hiccup
 (defn- export-hiccup
   [hiccup]
   [hiccup]
   (util/format "#+BEGIN_EXPORT hiccup\n%s\n#+END_EXPORT"
   (util/format "#+BEGIN_EXPORT hiccup\n%s\n#+END_EXPORT"
-
                (str (hiccup-without-style hiccup))))
                (str (hiccup-without-style hiccup))))
 
 
-(def allowed-tags
-  #{:address, :article, :aside, :footer, :header,
-    :h1, :h2, :h3, :h4, :h5, :h6, :hgroup,
-    :main, :nav, :section,
-    :blockquote, :dd, :div, :dl, :dt, :figcaption, :figure,
-    :hr, :li, :ol, :p, :pre, :ul,
-    :a, :abbr, :b, :bdi, :bdo, :br, :cite, :code, :data, :dfn,
-    :em, :i, :kbd, :mark, :q,
-    :rb, :rp, :rt, :rtc, :ruby,
-    :s, :samp, :small, :span, :strong, :sub, :sup, :time, :u, :var, :wbr,
-    :caption, :col, :colgroup, :table, :tbody, :td, :tfoot, :th,
-    :thead, :tr
-    :body :html})
+(def denied-tags
+  #{:script :base :head :link :meta :style :title :comment :xml :svg :frame :frameset :embed :object :canvas :applet})
 
 
 (defn ^:large-vars/cleanup-todo hiccup->doc-inner
 (defn ^:large-vars/cleanup-todo hiccup->doc-inner
   [format hiccup opts]
   [format hiccup opts]
@@ -91,10 +79,7 @@
                                       (if (string? pattern) pattern (apply str (reverse pattern)))))))
                                       (if (string? pattern) pattern (apply str (reverse pattern)))))))
         wrapper (fn [tag content]
         wrapper (fn [tag content]
                   (let [content (cond
                   (let [content (cond
-                                  (not (contains? allowed-tags tag))
-                                  nil
-
-                                  (contains? #{:comment :head :style :xml} tag)
+                                  (contains? denied-tags tag)
                                   nil
                                   nil
 
 
                                   (and (= tag :p) (:in-table? opts))
                                   (and (= tag :p) (:in-table? opts))
@@ -233,7 +218,7 @@
                  (for [x hiccup]
                  (for [x hiccup]
                    (single-hiccup-transform x))
                    (single-hiccup-transform x))
                  (single-hiccup-transform hiccup))]
                  (single-hiccup-transform hiccup))]
-    (apply str result)))
+    (string/replace (apply str result) #"\n\n+" "\n\n")))
 
 
 (defn hiccup->doc
 (defn hiccup->doc
   [format hiccup]
   [format hiccup]

+ 0 - 6
src/main/frontend/fs.cljs

@@ -67,12 +67,6 @@
     (when (= fs bfs-record)
     (when (= fs bfs-record)
       (protocol/rmdir! fs dir))))
       (protocol/rmdir! fs dir))))
 
 
-(defn delete-file!
-  [repo dir path opts]
-  (let [fs-record (get-fs dir)]
-    (when (= fs-record mobile-record)
-      (protocol/delete-file! fs-record repo dir path opts))))
-
 (defn write-file!
 (defn write-file!
   [repo dir path content opts]
   [repo dir path content opts]
   (when content
   (when content

+ 55 - 23
src/main/frontend/fs/capacitor_fs.cljs

@@ -2,6 +2,7 @@
   (:require ["@capacitor/filesystem" :refer [Encoding Filesystem]]
   (:require ["@capacitor/filesystem" :refer [Encoding Filesystem]]
             [cljs-bean.core :as bean]
             [cljs-bean.core :as bean]
             [clojure.string :as string]
             [clojure.string :as string]
+            [frontend.config :as config]
             [frontend.db :as db]
             [frontend.db :as db]
             [frontend.encrypt :as encrypt]
             [frontend.encrypt :as encrypt]
             [frontend.fs.protocol :as protocol]
             [frontend.fs.protocol :as protocol]
@@ -107,6 +108,33 @@
         (= (string/trim decrypted-content) (string/trim db-content)))
         (= (string/trim decrypted-content) (string/trim db-content)))
       (p/resolved (= (string/trim disk-content) (string/trim db-content))))))
       (p/resolved (= (string/trim disk-content) (string/trim db-content))))))
 
 
+(def backup-dir "logseq/bak")
+(defn- get-backup-dir
+  [repo-dir path ext]
+  (let [relative-path (-> (string/replace path repo-dir "")
+                          (string/replace (str "." ext) ""))]
+    (str repo-dir backup-dir "/" relative-path)))
+
+(defn- truncate-old-versioned-files!
+  "reserve the latest 3 version files"
+  [dir]
+  (p/let [files (readdir dir)
+          files (js->clj files :keywordize-keys true)
+          old-versioned-files (drop 3 (reverse (sort-by :mtime files)))]
+    (mapv (fn [file]
+            (.deleteFile Filesystem (clj->js {:path (js/encodeURI (:uri file))})))
+          old-versioned-files)))
+
+(defn backup-file
+  [repo-dir path content ext]
+  (let [backup-dir (get-backup-dir repo-dir path ext)
+        new-path (str backup-dir "/" (string/replace (.toISOString (js/Date.)) ":" "_") "." ext)]
+    (.writeFile Filesystem (clj->js {:data content
+                                     :path new-path
+                                     :encoding (.-UTF8 Encoding)
+                                     :recursive true}))
+    (truncate-old-versioned-files! backup-dir)))
+
 (defn- write-file-impl!
 (defn- write-file-impl!
   [_this repo _dir path content {:keys [ok-handler error-handler old-content skip-compare?]} stat]
   [_this repo _dir path content {:keys [ok-handler error-handler old-content skip-compare?]} stat]
   (if skip-compare?
   (if skip-compare?
@@ -129,6 +157,7 @@
                                         (js/console.error error)
                                         (js/console.error error)
                                         nil)))
                                         nil)))
             disk-content (or disk-content "")
             disk-content (or disk-content "")
+            repo-dir (config/get-local-dir repo)
             ext (string/lower-case (util/get-file-ext path))
             ext (string/lower-case (util/get-file-ext path))
             db-content (or old-content (db/get-file repo (js/decodeURI path)) "")
             db-content (or old-content (db/get-file repo (js/decodeURI path)) "")
             contents-matched? (contents-matched? disk-content db-content)
             contents-matched? (contents-matched? disk-content db-content)
@@ -148,7 +177,12 @@
          (p/let [result (.writeFile Filesystem (clj->js {:path path
          (p/let [result (.writeFile Filesystem (clj->js {:path path
                                                          :data content
                                                          :data content
                                                          :encoding (.-UTF8 Encoding)
                                                          :encoding (.-UTF8 Encoding)
-                                                         :recursive true}))]
+                                                         :recursive true}))
+                 mtime (-> (js->clj stat :keywordize-keys true)
+                           :mtime)]
+           (when-not contents-matched?
+             (backup-file repo-dir path disk-content ext))
+           (db/set-file-last-modified-at! repo path mtime)
            (p/let [content (if (encrypt/encrypted-db? (state/get-current-repo))
            (p/let [content (if (encrypt/encrypted-db? (state/get-current-repo))
                              (encrypt/decrypt content)
                              (encrypt/decrypt content)
                              content)]
                              content)]
@@ -165,11 +199,14 @@
   (let [[dir path] (map #(some-> %
   (let [[dir path] (map #(some-> %
                                  js/decodeURI)
                                  js/decodeURI)
                         [dir path])
                         [dir path])
-        dir (string/replace dir #"/+$" "")
+        dir (some-> dir (string/replace #"/+$" ""))
         path (some-> path (string/replace #"^/+" ""))
         path (some-> path (string/replace #"^/+" ""))
         path (cond (nil? path)
         path (cond (nil? path)
                    dir
                    dir
 
 
+                   (nil? dir)
+                   path
+
                    (string/starts-with? path dir)
                    (string/starts-with? path dir)
                    path
                    path
 
 
@@ -204,13 +241,12 @@
 (defrecord Capacitorfs []
 (defrecord Capacitorfs []
   protocol/Fs
   protocol/Fs
   (mkdir! [_this dir]
   (mkdir! [_this dir]
-    (p/let [result (.mkdir Filesystem
-                           (clj->js
-                            {:path dir
-                             ;; :directory (.-ExternalStorage Directory)
-                             }))]
-      (js/console.log result)
-      result))
+    (-> (.mkdir Filesystem
+                (clj->js
+                 {:path dir}))
+        (p/catch (fn [error]
+                   (log/error :mkdir! {:path dir
+                                       :error error})))))
   (mkdir-recur! [_this dir]
   (mkdir-recur! [_this dir]
     (p/let [result (.mkdir Filesystem
     (p/let [result (.mkdir Filesystem
                            (clj->js
                            (clj->js
@@ -221,8 +257,16 @@
       result))
       result))
   (readdir [_this dir]                  ; recursive
   (readdir [_this dir]                  ; recursive
     (readdir dir))
     (readdir dir))
-  (unlink! [_this _repo _path _opts]
-    nil)
+  (unlink! [this repo path _opts]
+    (p/let [path (get-file-path nil path)
+            repo-dir (config/get-local-dir repo)
+            recycle-dir (str repo-dir config/app-name "/.recycle")
+            file-name (-> (string/replace path repo-dir "")
+                          (string/replace "/" "_")
+                          (string/replace "\\" "_"))
+            new-path (str recycle-dir "/" file-name)]
+      (protocol/mkdir! this recycle-dir)
+      (protocol/rename! this repo path new-path)))
   (rmdir! [_this _dir]
   (rmdir! [_this _dir]
     ;; Too dangerious!!! We'll never implement this.
     ;; Too dangerious!!! We'll never implement this.
     nil)
     nil)
@@ -236,18 +280,6 @@
          content)
          content)
        (p/catch (fn [error]
        (p/catch (fn [error]
                   (log/error :read-file-failed error))))))
                   (log/error :read-file-failed error))))))
-  (delete-file! [_this repo dir path {:keys [ok-handler error-handler]}]
-    (let [path (get-file-path dir path)]
-      (p/catch
-       (p/let [result (.deleteFile Filesystem
-                                   (clj->js
-                                    {:path path}))]
-         (when ok-handler
-           (ok-handler repo path result)))
-       (fn [error]
-         (if error-handler
-           (error-handler error)
-           (log/error :delete-file-failed error))))))
   (write-file! [this repo dir path content opts]
   (write-file! [this repo dir path content opts]
     (let [path (get-file-path dir path)]
     (let [path (get-file-path dir path)]
       (p/let [stat (p/catch
       (p/let [stat (p/catch

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

@@ -9,7 +9,6 @@
   (unlink! [this repo path opts])
   (unlink! [this repo path opts])
   (rmdir! [this dir])
   (rmdir! [this dir])
   (read-file [this dir path opts])
   (read-file [this dir path opts])
-  (delete-file! [this repo dir path opts])
   (write-file! [this repo dir path content opts])
   (write-file! [this repo dir path content opts])
   (rename! [this repo old-path new-path])
   (rename! [this repo old-path new-path])
   (stat [this dir path])
   (stat [this dir path])

+ 22 - 10
src/main/frontend/handler/block.cljs

@@ -150,22 +150,34 @@
 (def *swipe (atom nil))
 (def *swipe (atom nil))
 (def *touch-start (atom nil))
 (def *touch-start (atom nil))
 
 
+(defn- target-disable-swipe?
+  [target]
+  (let [user-defined-tags (get-in (state/get-config)
+                                  [:mobile :gestures/disabled-in-block-with-tags])]
+    (or (.closest target ".dsl-query")
+        (.closest target ".drawer")
+        (.closest target ".draw-wrap")
+        (some #(.closest target (util/format "[data-refs-self*=%s]" %))
+              user-defined-tags))))
+
 (defn on-touch-start
 (defn on-touch-start
   [event uuid]
   [event uuid]
-  (let [input (state/get-input)
+  (let [target (.-target event)
+        input (state/get-input)
         input-id (state/get-edit-input-id)
         input-id (state/get-edit-input-id)
         selection-type (.-type (.getSelection js/document))]
         selection-type (.-type (.getSelection js/document))]
     (reset! *touch-start (js/Date.now))
     (reset! *touch-start (js/Date.now))
     (when-not (and input
     (when-not (and input
                    (string/ends-with? input-id (str uuid)))
                    (string/ends-with? input-id (str uuid)))
       (state/clear-edit!))
       (state/clear-edit!))
-    (when (not= selection-type "Range")
-      (when-let [touches (.-targetTouches event)]
-        (when (= (.-length touches) 1)
-          (let [touch (aget touches 0)
-                x (.-clientX touch)
-                y (.-clientY touch)]
-            (reset! *swipe {:x0 x :y0 y :xi x :yi y :tx x :ty y :direction nil})))))))
+    (when-not (target-disable-swipe? target)
+      (when (not= selection-type "Range")
+        (when-let [touches (.-targetTouches event)]
+          (when (= (.-length touches) 1)
+            (let [touch (aget touches 0)
+                  x (.-clientX touch)
+                  y (.-clientY touch)]
+              (reset! *swipe {:x0 x :y0 y :xi x :yi y :tx x :ty y :direction nil}))))))))
 
 
 (defn on-touch-move
 (defn on-touch-move
   [event block uuid edit? *show-left-menu? *show-right-menu?]
   [event block uuid edit? *show-left-menu? *show-right-menu?]
@@ -197,8 +209,8 @@
               (let [{:keys [x0 y0]} @*swipe
               (let [{:keys [x0 y0]} @*swipe
                     dx (- tx x0)
                     dx (- tx x0)
                     dy (- ty y0)]
                     dy (- ty y0)]
-                (when (and (< (. js/Math abs dy) 20)
-                           (> (. js/Math abs dx) 10))
+                (when (and (< (. js/Math abs dy) 30)
+                           (> (. js/Math abs dx) 30))
                   (let [left (gdom/getElement (str "block-left-menu-" uuid))
                   (let [left (gdom/getElement (str "block-left-menu-" uuid))
                         right (gdom/getElement (str "block-right-menu-" uuid))]
                         right (gdom/getElement (str "block-right-menu-" uuid))]
 
 

+ 33 - 15
src/main/frontend/handler/editor.cljs

@@ -885,6 +885,7 @@
                   content (property/remove-empty-properties content)
                   content (property/remove-empty-properties content)
                   block {:block/uuid block-id
                   block {:block/uuid block-id
                          :block/properties properties
                          :block/properties properties
+                         :block/properties-order (keys properties)
                          :block/content content}]
                          :block/content content}]
               (outliner-core/save-block! block))))))
               (outliner-core/save-block! block))))))
 
 
@@ -942,14 +943,17 @@
               (state/set-edit-content! input-id new-content)
               (state/set-edit-content! input-id new-content)
               (save-block-if-changed! block new-content))))))))
               (save-block-if-changed! block new-content))))))))
 
 
-(defn- set-blocks-id!
+(defn set-blocks-id!
+  "Persist block uuid to file if the uuid is valid, and it's not persisted in file.
+   Accepts a list of uuids."
   [block-ids]
   [block-ids]
   (let [block-ids (remove nil? block-ids)
   (let [block-ids (remove nil? block-ids)
         col (map (fn [block-id]
         col (map (fn [block-id]
-                   (let [block (db/entity [:block/uuid block-id])]
+                   (when-let [block (db/entity [:block/uuid block-id])]
                      (when-not (:block/pre-block? block)
                      (when-not (:block/pre-block? block)
                        [block-id :id (str block-id)])))
                        [block-id :id (str block-id)])))
-                 block-ids)]
+                 block-ids)
+        col (remove nil? col)]
     (batch-set-block-property! col)))
     (batch-set-block-property! col)))
 
 
 (defn copy-block-ref!
 (defn copy-block-ref!
@@ -1915,10 +1919,13 @@
    0))
    0))
 
 
 (defn paste-blocks
 (defn paste-blocks
+  "Given a vec of blocks, insert them into the target page.
+   keep-uuid?: if true, keep the uuid provided in the block structure."
   [blocks {:keys [content-update-fn
   [blocks {:keys [content-update-fn
                   exclude-properties
                   exclude-properties
                   target-block
                   target-block
-                  sibling?]
+                  sibling?
+                  keep-uuid?]
            :or {exclude-properties []}}]
            :or {exclude-properties []}}]
   (let [editing-block (when-let [editing-block (state/get-edit-block)]
   (let [editing-block (when-let [editing-block (state/get-edit-block)]
                         (some-> (db/pull (:db/id editing-block))
                         (some-> (db/pull (:db/id editing-block))
@@ -1945,14 +1952,16 @@
         (let [format (or (:block/format target-block) (state/get-preferred-format))
         (let [format (or (:block/format target-block) (state/get-preferred-format))
               blocks' (map (fn [block]
               blocks' (map (fn [block]
                              (paste-block-cleanup block page exclude-properties format content-update-fn))
                              (paste-block-cleanup block page exclude-properties format content-update-fn))
-                        blocks)
+                           blocks)
               result (outliner-core/insert-blocks! blocks' target-block {:sibling? sibling?
               result (outliner-core/insert-blocks! blocks' target-block {:sibling? sibling?
                                                                          :outliner-op :paste
                                                                          :outliner-op :paste
-                                                                         :replace-empty-target? true})]
+                                                                         :replace-empty-target? true
+                                                                         :keep-uuid? keep-uuid?})]
           (edit-last-block-after-inserted! result))))))
           (edit-last-block-after-inserted! result))))))
 
 
 (defn- block-tree->blocks
 (defn- block-tree->blocks
-  [tree-vec format]
+  "keep-uuid? - maintain the existing :uuid in tree vec"
+  [tree-vec format keep-uuid?]
   (->> (outliner-core/tree-vec-flatten tree-vec)
   (->> (outliner-core/tree-vec-flatten tree-vec)
        (map (fn [block]
        (map (fn [block]
               (let [content (:content block)
               (let [content (:content block)
@@ -1961,22 +1970,31 @@
                                   (property/insert-properties format content props))
                                   (property/insert-properties format content props))
                     ast (mldoc/->edn content* (gp-mldoc/default-config format))
                     ast (mldoc/->edn content* (gp-mldoc/default-config format))
                     blocks (block/extract-blocks ast content* true format)
                     blocks (block/extract-blocks ast content* true format)
-                    fst-block (first blocks)]
+                    fst-block (first blocks)
+                    fst-block (if (and keep-uuid? (uuid? (:uuid block)))
+                                (assoc fst-block :block/uuid (:uuid block))
+                                fst-block)]
                 (assert fst-block "fst-block shouldn't be nil")
                 (assert fst-block "fst-block shouldn't be nil")
                 (assoc fst-block :block/level (:block/level block)))))))
                 (assoc fst-block :block/level (:block/level block)))))))
 
 
-(defn insert-block-tree-after-target
+(defn insert-block-tree
   "`tree-vec`: a vector of blocks.
   "`tree-vec`: a vector of blocks.
-   Block element: {:content :properties :children [block-1, block-2, ...]}"
-  [target-block-id sibling? tree-vec format]
-  (let [blocks (block-tree->blocks tree-vec format)
-        target-block (db/pull target-block-id)
+   A block element: {:content :properties :children [block-1, block-2, ...]}"
+  [tree-vec format {:keys [target-block keep-uuid?] :as opts}]
+  (let [blocks (block-tree->blocks tree-vec format keep-uuid?)
         page-id (:db/id (:block/page target-block))
         page-id (:db/id (:block/page target-block))
         blocks (gp-block/with-parent-and-left page-id blocks)]
         blocks (gp-block/with-parent-and-left page-id blocks)]
     (paste-blocks
     (paste-blocks
      blocks
      blocks
-     {:target-block target-block
-      :sibling? sibling?})))
+     opts)))
+
+(defn insert-block-tree-after-target
+  "`tree-vec`: a vector of blocks.
+   A block element: {:content :properties :children [block-1, block-2, ...]}"
+  [target-block-id sibling? tree-vec format]
+  (insert-block-tree tree-vec format
+                     {:target-block (db/pull target-block-id)
+                      :sibling?     sibling?}))
 
 
 (defn insert-template!
 (defn insert-template!
   ([element-id db-id]
   ([element-id db-id]

+ 118 - 1
src/main/frontend/handler/external.cljs

@@ -1,5 +1,7 @@
 (ns frontend.handler.external
 (ns frontend.handler.external
-  (:require [frontend.external :as external]
+  (:require [clojure.edn :as edn]
+            [clojure.walk :as walk]
+            [frontend.external :as external]
             [frontend.handler.file :as file-handler]
             [frontend.handler.file :as file-handler]
             [frontend.handler.repo :as repo-handler]
             [frontend.handler.repo :as repo-handler]
             [frontend.state :as state]
             [frontend.state :as state]
@@ -13,9 +15,11 @@
             [logseq.graph-parser.date-time-util :as date-time-util]
             [logseq.graph-parser.date-time-util :as date-time-util]
             [frontend.handler.page :as page]
             [frontend.handler.page :as page]
             [frontend.handler.editor :as editor]
             [frontend.handler.editor :as editor]
+            [frontend.handler.notification :as notification]
             [frontend.util :as util]))
             [frontend.util :as util]))
 
 
 (defn index-files!
 (defn index-files!
+  "Create file structure, then parse into DB (client only)"
   [repo files finish-handler]
   [repo files finish-handler]
   (let [titles (->> files
   (let [titles (->> files
                     (map :title)
                     (map :title)
@@ -95,3 +99,116 @@
          {:target-block target-block
          {:target-block target-block
           :sibling? sibling?})
           :sibling? sibling?})
         (finished-ok-handler [page-name])))))
         (finished-ok-handler [page-name])))))
+
+(defn create-page-with-exported-tree!
+  "Create page from the per page object generated in `export-repo-as-edn-v2!`
+   Return page-name (title)
+   Extension to `insert-block-tree-after-target`
+   :id       - page's uuid
+   :title    - page's title (original name)
+   :children - tree
+   "
+  [{:keys [uuid title children] :as tree}]
+  (let [has-children? (seq children)
+        page-format (some-> tree (:children) (first) (:format))]
+    (try (page/create! title {:redirect?  false
+                              :format     page-format
+                              :uuid       uuid})
+         (catch js/Error e
+           (notification/show! (str "Error happens when creating page " title ":\n"
+                                    e
+                                    "\nSkipped and continue the remaining import.") :error)))
+    (when has-children?
+      (let [page-block  (db/entity [:block/name (util/page-name-sanity-lc title)])
+            first-child (first (:block/_left page-block)) ]
+        ;; Missing support for per block format (or deprecated?)
+        (try (editor/insert-block-tree children page-format
+                                       {:target-block first-child
+                                        :sibling?     true
+                                        :keep-uuid?   true})
+             (catch js/Error e
+               (notification/show! (str "Error happens when creating block content of page " title "\n"
+                                        e
+                                        "\nSkipped and continue the remaining import.") :error))))))
+  title)
+
+(defn- pre-transact-uuids
+  "Collect all uuids from page trees and write them to the db before hand."
+  [pages]
+  (let [uuids (map (fn [block]
+                     {:block/uuid (:uuid block)})
+                   (mapcat #(tree-seq map? :children %)
+                           pages))]
+    (db/transact! uuids)
+    pages))
+
+(defn- import-from-tree!
+  "Not rely on file system - backend compatible.
+   tree-translator-fn: translate exported tree structure to the desired tree for import"
+  [data tree-translator-fn]
+  (try (->> (:blocks data)
+            (map tree-translator-fn)
+            (pre-transact-uuids)
+            (mapv create-page-with-exported-tree!))
+       (editor/set-blocks-id! (db/get-all-referenced-blocks-uuid))
+       (catch js/Error e
+         (notification/show! (str "Error happens when importing:\n" e) :error))))
+
+(defn tree-vec-translate-edn
+  "Actions to do for loading edn tree structure.
+   1) Removes namespace `:block/` from all levels of the `tree-vec`
+   2) Rename all :block/page-name to :title
+   3) Rename all :block/id to :uuid"
+  ([tree-vec]
+   (let [kw-trans-fn #(-> %
+                          str
+                          (string/replace ":block/page-name" ":block/title")
+                          (string/replace ":block/id" ":block/uuid")
+                          (string/replace ":block/" "")
+                          keyword)
+         map-trans-fn (fn [acc k v]
+                        (assoc acc (kw-trans-fn k) v))
+         tree-trans-fn (fn [form]
+                         (if (and (map? form)
+                                  (:block/id form))
+                           (reduce-kv map-trans-fn {} form)
+                           form))]
+     (walk/postwalk tree-trans-fn tree-vec))))
+
+(defn import-from-edn!
+  [raw finished-ok-handler]
+  (import-from-tree! (edn/read-string raw) tree-vec-translate-edn)
+  (finished-ok-handler nil)) ;; it was designed to accept a list of imported page names but now deprecated
+
+(defn tree-vec-translate-json
+  "Actions to do for loading json tree structure.
+   1) Rename all :id to :uuid
+   2) Rename all :page-name to :title
+   3) Rename all :format \"markdown\" to :format `:markdown`"
+  ([tree-vec]
+   (let [kw-trans-fn #(-> %
+                          str
+                          (string/replace ":page-name" ":title")
+                          (string/replace ":id" ":uuid")
+                          (string/replace #"^:" "")
+                          keyword)
+         map-trans-fn (fn [acc k v]
+                        (cond (= :format k)
+                              (assoc acc (kw-trans-fn k) (keyword v))
+                              (= :id k)
+                              (assoc acc (kw-trans-fn k) (uuid v))
+                              :else
+                              (assoc acc (kw-trans-fn k) v)))
+         tree-trans-fn (fn [form]
+                         (if (and (map? form)
+                                  (:id form))
+                           (reduce-kv map-trans-fn {} form)
+                           form))]
+     (walk/postwalk tree-trans-fn tree-vec))))
+
+(defn import-from-json!
+  [raw finished-ok-handler]
+  (let [json     (js/JSON.parse raw)
+        clj-data (js->clj json :keywordize-keys true)]
+    (import-from-tree! clj-data tree-vec-translate-json))
+  (finished-ok-handler nil)) ;; it was designed to accept a list of imported page names but now deprecated

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

@@ -15,7 +15,7 @@
             [frontend.handler.user :as user]
             [frontend.handler.user :as user]
             [frontend.fs :as fs]))
             [frontend.fs :as fs]))
 
 
-(def hiding-login&file-sync (not config/dev?))
+(def hiding-login&file-sync (not config/enable-file-sync?))
 (def refresh-file-sync-component (atom false))
 (def refresh-file-sync-component (atom false))
 
 
 (defn graph-txid-exists?
 (defn graph-txid-exists?

+ 2 - 5
src/main/frontend/handler/graph.cljs

@@ -90,8 +90,7 @@
             namespaces (db/get-all-namespace-relation repo)
             namespaces (db/get-all-namespace-relation repo)
             tags (set (map second tagged-pages))
             tags (set (map second tagged-pages))
             full-pages (db/get-all-pages repo)
             full-pages (db/get-all-pages repo)
-            get-original-name (fn [p] (or (:block/original-name p) (:block/name p)))
-            all-pages (map get-original-name full-pages)
+            all-pages (map db/get-original-name full-pages)
             page-name->original-name (zipmap (map :block/name full-pages) all-pages)
             page-name->original-name (zipmap (map :block/name full-pages) all-pages)
             pages-after-journal-filter (if-not journal?
             pages-after-journal-filter (if-not journal?
                                          (remove :block/journal? full-pages)
                                          (remove :block/journal? full-pages)
@@ -164,9 +163,7 @@
                        (distinct))
                        (distinct))
             nodes (build-nodes dark? page links (set tags) nodes namespaces)
             nodes (build-nodes dark? page links (set tags) nodes namespaces)
             full-pages (db/get-all-pages repo)
             full-pages (db/get-all-pages repo)
-            get-original-name (fn [p] (or (:block/original-name p)
-                                         (:block/name p)))
-            all-pages (map get-original-name full-pages)
+            all-pages (map db/get-original-name full-pages)
             page-name->original-name (zipmap (map :block/name full-pages) all-pages)]
             page-name->original-name (zipmap (map :block/name full-pages) all-pages)]
         (normalize-page-name
         (normalize-page-name
          {:nodes nodes
          {:nodes nodes

+ 18 - 16
src/main/frontend/handler/page.cljs

@@ -116,27 +116,35 @@
         [page]))))
         [page]))))
 
 
 (defn create!
 (defn create!
+  "Create page.
+   :redirect?           - when true, redirect to the created page, otherwise return sanitized page name.
+   :split-namespace?    - when true, split hierarchical namespace into levels.
+   :create-first-block? - when true, create an empty block if the page is empty.
+   :uuid                - when set, use this uuid instead of generating a new one."
   ([title]
   ([title]
    (create! title {}))
    (create! title {}))
-  ([title {:keys [redirect? create-first-block? format properties split-namespace? journal?]
+  ([title {:keys [redirect? create-first-block? format properties split-namespace? journal? uuid]
            :or   {redirect?           true
            :or   {redirect?           true
                   create-first-block? true
                   create-first-block? true
                   format              nil
                   format              nil
                   properties          nil
                   properties          nil
-                  split-namespace?    true}}]
-   (let [title (string/trim title)
-         title (gp-util/remove-boundary-slashes title)
-         page-name (util/page-name-sanity-lc title)
-         repo (state/get-current-repo)]
+                  split-namespace?    true
+                  uuid                nil}}]
+   (let [title      (string/trim title)
+         title      (gp-util/remove-boundary-slashes title)
+         page-name  (util/page-name-sanity-lc title)
+         repo       (state/get-current-repo)
+         with-uuid? (if (uuid? uuid) uuid true)] ;; FIXME: prettier validation
      (when (db/page-empty? repo page-name)
      (when (db/page-empty? repo page-name)
        (let [pages    (if split-namespace?
        (let [pages    (if split-namespace?
                         (gp-util/split-namespace-pages title)
                         (gp-util/split-namespace-pages title)
                         [title])
                         [title])
              format   (or format (state/get-preferred-format))
              format   (or format (state/get-preferred-format))
              pages    (map (fn [page]
              pages    (map (fn [page]
-                             (-> (block/page-name->map page true)
+                             ;; only apply uuid to the deepest hierarchy of page to create if provided.
+                             (-> (block/page-name->map page (if (= page title) with-uuid? true))
                                  (assoc :block/format format)))
                                  (assoc :block/format format)))
-                        pages)
+                           pages)
              txs      (->> pages
              txs      (->> pages
                            ;; for namespace pages, only last page need properties
                            ;; for namespace pages, only last page need properties
                            drop-last
                            drop-last
@@ -167,14 +175,8 @@
     (when-not (string/blank? file-path)
     (when-not (string/blank? file-path)
       (db/transact! [[:db.fn/retractEntity [:file/path file-path]]])
       (db/transact! [[:db.fn/retractEntity [:file/path file-path]]])
       (when unlink-file?
       (when unlink-file?
-        (->
-         (p/let [_ (and (config/local-db? repo)
-                        (mobile-util/native-platform?)
-                        ;; TODO: @leizhe remove fs/delete-file! and use fs/unlink!
-                        (fs/delete-file! repo file-path file-path {}))
-                 _ (fs/unlink! repo (config/get-repo-path repo file-path) nil)])
-         (p/catch (fn [err]
-                    (js/console.error "error: " err))))))))
+        (-> (fs/unlink! repo (config/get-repo-path repo file-path) nil)
+            (p/catch (fn [error] (js/console.error error))))))))
 
 
 (defn- compute-new-file-path
 (defn- compute-new-file-path
   [old-path new-name]
   [old-path new-name]

+ 77 - 16
src/main/frontend/modules/shortcut/dicts.cljc

@@ -605,7 +605,62 @@
              :command.editor/backward-kill-word       "Apagar a palavra anterior"
              :command.editor/backward-kill-word       "Apagar a palavra anterior"
              :command.editor/open-edit                "Editar bloco selecionado"
              :command.editor/open-edit                "Editar bloco selecionado"
              :command.editor/delete-selection         "Eliminar blocos selecionados"
              :command.editor/delete-selection         "Eliminar blocos selecionados"
-             :command.editor/toggle-open-blocks       "Alternar blocos abertos (colapsar ou expandir todos)"}
+             :command.editor/toggle-open-blocks       "Alternar blocos abertos (colapsar ou expandir todos)"
+
+             :command.auto-complete/complete          "Auto-completar: Escolha o item selecionado"
+             :command.auto-complete/next              "Auto-completar: Selecione o próximo item"
+             :command.auto-complete/prev              "Auto-completar: Selecione o item anterior"
+             :command.auto-complete/shift-complete    "Auto-completar: Abra o item selecionado na barra lateral"
+             :command.auto-complete/open-link         "Auto-completar: Abra o item selecionado no navegador"
+             :command.cards/forgotten                 "Cartões: Esquecido"
+             :command.cards/next-card                 "Cartões: Próxima carta"
+             :command.cards/recall                    "Cartões: Demorar um pouco para lembrar"
+             :command.cards/remembered                "Cartões: Relembrado"
+             :command.cards/toggle-answers            "Cartões: mostrar/esconder as respostas/clozes"
+             :command.command/run                     "Execute o comando Git"
+             :command.command/toggle-favorite         "Adicionar aos/remover dos favoritos"
+             :command.command-palette/toggle          "Editar atalhos"
+             :command.date-picker/complete            "Escolher data: Escolha o dia selecionado"
+             :command.date-picker/next-day            "Escolher data: Selecione o próximo dia"
+             :command.date-picker/next-week           "Escolher data: Selecione a próxima semana"
+             :command.date-picker/prev-day            "Escolher data: Selecione o dia anterior"
+             :command.date-picker/prev-week           "Escolher data: Selecione a semana anterior"
+             :command.editor/copy-current-file        "Copiar o arquivo atual"
+             :command.editor/select-down              "Selecione o conteúdo abaixo"
+             :command.editor/select-up                "Selecione o conteúdo acima"
+             :command.editor/copy-embed               "Copiar uma incorporação do bloco, apontando para o bloco atual"
+             :command.editor/copy-text                "Copiar seleção como texto"
+             :command.pdf/close                       "Fechar visualização do PDF"
+             :command.editor/escape-editing           "Sair da edição"
+             :command.editor/insert-youtube-timestamp "Inserir timestamp do youtube"
+             :command.editor/paste-text-in-one-block-at-point "Colar texto em um bloco no ponto"
+             :command.editor/replace-block-reference-at-point "Substitua a referência do bloco pelo seu conteúdo no ponto"
+             :command.editor/strike-through           "Tachar"
+             :command.editor/open-file-in-default-app "Abra o arquivo no aplicativo padrão"
+             :command.editor/open-file-in-directory   "Abra o arquivo na pasta"
+             :command.go/all-pages                    "Ir para todas as páginas"
+             :command.go/backward                     "Voltar"
+             :command.go/flashcards                   "Trocar flashcards"
+             :command.go/forward                      "Avançar"
+             :command.go/graph-view                   "Ir para o gráfico"
+             :command.go/home                         "Volar para o inicio"
+             :command.go/keyboard-shortcuts           "Ir para os atalhos do teclado"
+             :command.go/next-journal                 "Ir ao proximo jornal"
+             :command.go/prev-journal                 "Ir ao jornal anterior"
+             :command.go/tomorrow                     "Ir para amanhã"
+             :command.graph/add                       "Adicionar um gráfico"
+             :command.graph/open                      "Selecionar gráfico para abrir"
+             :command.graph/remove                    "Remover um gráfico"
+             :command.pdf/next-page                   "Próxima página do atual pdf doc"
+             :command.pdf/previous-page               "Página anterior do atual pdf doc"
+             :command.sidebar/clear                   "Limpar tudo da barra lateral direita"
+             :command.sidebar/open-today-page         "Abrir a página de hoje na barra lateral direita"
+             :command.ui/select-theme-color           "Selecionar as cores do tema disponível"
+             :command.ui/toggle-cards                 "Trocar cartões"
+             :command.ui/toggle-left-sidebar          "Trocar barra lateral esquerda"
+             :command.graph/save                      "Salvar gráfico atual no computador"
+             :command.misc/copy                       "Copiar (copiar seleção ou referência do bloco)"
+             :command.ui/goto-plugins                 "Ir para o painel de plugins"}
 
 
    :pt-BR   {:shortcut.category/formatting            "Formatação"
    :pt-BR   {:shortcut.category/formatting            "Formatação"
              :shortcut.category/basics                "Básico"
              :shortcut.category/basics                "Básico"
@@ -674,18 +729,18 @@
              :command.auto-complete/next              "Auto-completar: Selecione o próximo item"
              :command.auto-complete/next              "Auto-completar: Selecione o próximo item"
              :command.auto-complete/prev              "Auto-completar: Selecione o item anterior"
              :command.auto-complete/prev              "Auto-completar: Selecione o item anterior"
              :command.auto-complete/shift-complete    "Auto-completar: Abra o item selecionado na barra lateral"
              :command.auto-complete/shift-complete    "Auto-completar: Abra o item selecionado na barra lateral"
-             :command.cards/forgotten                 "Cartas: Esquecido"
-             :command.cards/next-card                 "Cartas: Próxima carta"
-             :command.cards/recall                    "Cartas: Demorar um pouco para lembrar"
-             :command.cards/remembered                "Cartas: Relembrado"
-             :command.cards/toggle-answers            "Cartas: mostrar/esconder as respostas/clozes"
-             :command.command/toggle-favorite         "Adicionar para/remover dos favoritos"
+             :command.cards/forgotten                 "Cartões: Esquecido"
+             :command.cards/next-card                 "Cartões: Próxima carta"
+             :command.cards/recall                    "Cartões: Demorar um pouco para lembrar"
+             :command.cards/remembered                "Cartões: Relembrado"
+             :command.cards/toggle-answers            "Cartões: mostrar/esconder as respostas/clozes"
+             :command.command/toggle-favorite         "Adicionar aos/remover dos favoritos"
              :command.command-palette/toggle          "Editar atalhos"
              :command.command-palette/toggle          "Editar atalhos"
-             :command.date-picker/complete            "Pegar data: Escolha o dia selecionado"
-             :command.date-picker/next-day            "Pegar data: Selecione o próximo dia"
-             :command.date-picker/next-week           "Pegar data: Selecione a próxima semana"
-             :command.date-picker/prev-day            "Pegar data: Selecione o dia anterior"
-             :command.date-picker/prev-week           "Pegar data: Selecione a semana anterior"
+             :command.date-picker/complete            "Escolher data: Escolha o dia selecionado"
+             :command.date-picker/next-day            "Escolher data: Selecione o próximo dia"
+             :command.date-picker/next-week           "Escolher data: Selecione a próxima semana"
+             :command.date-picker/prev-day            "Escolher data: Selecione o dia anterior"
+             :command.date-picker/prev-week           "Escolher data: Selecione a semana anterior"
              :command.editor/escape-editing           "Sair da edição"
              :command.editor/escape-editing           "Sair da edição"
              :command.editor/insert-youtube-timestamp "Inserir timestamp do youtube"
              :command.editor/insert-youtube-timestamp "Inserir timestamp do youtube"
              :command.editor/paste-text-in-one-block-at-point "Colar texto em um bloco no ponto"
              :command.editor/paste-text-in-one-block-at-point "Colar texto em um bloco no ponto"
@@ -709,7 +764,7 @@
              :command.sidebar/clear                   "Limpar tudo da barra lateral direita"
              :command.sidebar/clear                   "Limpar tudo da barra lateral direita"
              :command.sidebar/open-today-page         "Abrir a página de hoje na barra lateral direita"
              :command.sidebar/open-today-page         "Abrir a página de hoje na barra lateral direita"
              :command.ui/select-theme-color           "Selecionar as cores do tema disponível"
              :command.ui/select-theme-color           "Selecionar as cores do tema disponível"
-             :command.ui/toggle-cards                 "Trocar cartas"
+             :command.ui/toggle-cards                 "Trocar cartões"
              :command.ui/toggle-left-sidebar          "Trocar barra lateral esquerda"
              :command.ui/toggle-left-sidebar          "Trocar barra lateral esquerda"
              :command.auto-complete/open-link         "Auto-completar: Abra o item selecionado no navegador"
              :command.auto-complete/open-link         "Auto-completar: Abra o item selecionado no navegador"
              :command.command/run                     "Execute o comando Git"
              :command.command/run                     "Execute o comando Git"
@@ -721,9 +776,12 @@
              :command.ui/goto-plugins                 "Ir para o painel de plugins"
              :command.ui/goto-plugins                 "Ir para o painel de plugins"
             ;;  :command.ui/open-new-window              "Abra uma nova janela"
             ;;  :command.ui/open-new-window              "Abra uma nova janela"
              :command.editor/select-down              "Selecione o conteúdo abaixo"
              :command.editor/select-down              "Selecione o conteúdo abaixo"
-             :command.editor/select-up                "Selecione o conteúdo acima"}
+             :command.editor/select-up                "Selecione o conteúdo acima"
+             :command.editor/copy-embed               "Copiar uma incorporação do bloco, apontando para o bloco atual"
+             :command.editor/copy-text                "Copiar seleção como texto"
+             :command.pdf/close                       "Fechar visualização do PDF"}
 
 
-   :ja      {:shortcut.category/formatting                "フォーマット"
+   :ja      {:shortcut.category/formatting            "フォーマット"
              :shortcut.category/basics                "基本操作"
              :shortcut.category/basics                "基本操作"
              :shortcut.category/navigating            "ナビゲーション"
              :shortcut.category/navigating            "ナビゲーション"
              :shortcut.category/block-editing         "ブロック単位の編集"
              :shortcut.category/block-editing         "ブロック単位の編集"
@@ -953,7 +1011,10 @@
              :shortcut.category/block-command-editing "Modifica comandi blocco"
              :shortcut.category/block-command-editing "Modifica comandi blocco"
              :shortcut.category/block-selection       "Selezione blocco (premi Esc per uscire dalla selezione)"
              :shortcut.category/block-selection       "Selezione blocco (premi Esc per uscire dalla selezione)"
              :shortcut.category/toggle                "Attiva/disattiva"
              :shortcut.category/toggle                "Attiva/disattiva"
-             :shortcut.category/others                "Altri"}
+             :shortcut.category/others                "Altri"
+             :command.editor/copy-embed               "Copia un incorporamento di blocco che punta al blocco corrente"
+             :command.editor/copy-text                "Copia le selezioni come testo"
+             :command.pdf/close                       "Chiudi anteprima PDF"}
 
 
    
    
    :tr      {:shortcut.category/basics "Temel bilgiler"
    :tr      {:shortcut.category/basics "Temel bilgiler"

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

@@ -688,8 +688,7 @@
   (swap! state assoc
   (swap! state assoc
          :selection/mode false
          :selection/mode false
          :selection/blocks nil
          :selection/blocks nil
-         :selection/direction :down
-         :selection/start-block nil))
+         :selection/direction :down))
 
 
 (defn get-selection-blocks
 (defn get-selection-blocks
   []
   []

+ 15 - 7
src/main/frontend/util/marker.cljs

@@ -13,14 +13,22 @@
 (defn add-or-update-marker
 (defn add-or-update-marker
   [content format marker]
   [content format marker]
   (let [[re-pattern new-line-re-pattern]
   (let [[re-pattern new-line-re-pattern]
-        (if (= :org format)
-          [#"\*+\s" #"\n\*+\s"]
-          [#"#+\s" #"\n#+\s"])
+        (case format
+          :org
+          [#"^\*+\s" #"\n\*+\s"]
+
+          (:markdown :md)
+          [#"^#+\s" #"\n#+\s"]
+
+          ;; failback to markdown
+          [#"^#+\s" #"\n#+\s"])
+
         pos
         pos
         (if-let [matches (seq (util/re-pos new-line-re-pattern content))]
         (if-let [matches (seq (util/re-pos new-line-re-pattern content))]
           (let [[start-pos content] (last matches)]
           (let [[start-pos content] (last matches)]
             (+ start-pos (count content)))
             (+ start-pos (count content)))
           (count (util/safe-re-find re-pattern content)))
           (count (util/safe-re-find re-pattern content)))
+
         new-content
         new-content
         (str (subs content 0 pos)
         (str (subs content 0 pos)
              (string/replace-first (subs content pos)
              (string/replace-first (subs content pos)
@@ -57,8 +65,8 @@
   Returns [new-content new-marker]."
   Returns [new-content new-marker]."
   [content marker new-marker format preferred-workflow]
   [content marker new-marker format preferred-workflow]
   (let [content    (string/triml content)
   (let [content    (string/triml content)
-        new-marker (or new-marker
-                       (cycle-marker-state (or marker
-                                               (last (util/safe-re-find (marker-pattern format) content))) ; Returns the last matching group (last vec)
-                                           preferred-workflow))]
+        marker     (or (not-empty marker)
+                       (last (util/safe-re-find (marker-pattern format) content))) ; Returns the last matching group (last vec)
+        new-marker (or (not-empty new-marker)
+                       (cycle-marker-state marker preferred-workflow))]
     [(add-or-update-marker content format new-marker) new-marker]))
     [(add-or-update-marker content format new-marker) new-marker]))

+ 1 - 1
src/main/frontend/util/text.cljs

@@ -5,7 +5,7 @@
 
 
 (defonce between-re #"\(between ([^\)]+)\)")
 (defonce between-re #"\(between ([^\)]+)\)")
 
 
-(def bilibili-regex #"^((?:https?:)?//)?((?:www).)?((?:bilibili.com))(/(?:video/)?)([\w-]+)(\S+)?$")
+(def bilibili-regex #"^((?:https?:)?//)?((?:www).)?((?:bilibili.com))(/(?:video/)?)([\w-]+)(\?p=(\d+))?(\S+)?$")
 (def loom-regex #"^((?:https?:)?//)?((?:www).)?((?:loom.com))(/(?:share/|embed/))([\w-]+)(\S+)?$")
 (def loom-regex #"^((?:https?:)?//)?((?:www).)?((?:loom.com))(/(?:share/|embed/))([\w-]+)(\S+)?$")
 (def vimeo-regex #"^((?:https?:)?//)?((?:www).)?((?:player.vimeo.com|vimeo.com))(/(?:video/)?)([\w-]+)(\S+)?$")
 (def vimeo-regex #"^((?:https?:)?//)?((?:www).)?((?:player.vimeo.com|vimeo.com))(/(?:video/)?)([\w-]+)(\S+)?$")
 (def youtube-regex #"^((?:https?:)?//)?((?:www|m).)?((?:youtube.com|youtu.be|y2u.be|youtube-nocookie.com))(/(?:[\w-]+\?v=|embed/|v/)?)([\w-]+)([\S^\?]+)?$")
 (def youtube-regex #"^((?:https?:)?//)?((?:www|m).)?((?:youtube.com|youtu.be|y2u.be|youtube-nocookie.com))(/(?:[\w-]+\?v=|embed/|v/)?)([\w-]+)([\S^\?]+)?$")

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

@@ -1,3 +1,3 @@
 (ns frontend.version)
 (ns frontend.version)
 
 
-(defonce version "0.7.4")
+(defonce version "0.7.5")

+ 7 - 2
templates/config.edn

@@ -223,6 +223,11 @@
  ;; :mobile/photo
  ;; :mobile/photo
  ;; {:allow-editing? true}
  ;; {:allow-editing? true}
 
 
+ ;; Mobile features options
+ ;; Gestures
+ ;; :mobile
+ ;; {:gestures/disabled-in-block-with-tags ["kanban"]}
+
  ;; Extra CodeMirror options
  ;; Extra CodeMirror options
  ;; :editor/extra-codemirror-options {:keyMap "emacs" :lineWrapping true}
  ;; :editor/extra-codemirror-options {:keyMap "emacs" :lineWrapping true}
 
 
@@ -238,6 +243,6 @@
  ;; You can also reorder them, or even only use one or two of them in the template.
  ;; You can also reorder them, or even only use one or two of them in the template.
  ;; You can also insert or format any text in the template as shown in the following examples.
  ;; You can also insert or format any text in the template as shown in the following examples.
  ;; :quick-capture-templates
  ;; :quick-capture-templates
- ;; {:text "[[quick capture]] **{time}**: \n - {text} from {url}"
- ;;  :media "[[quick capture]] **{time}**: \n - {url}"}
+ ;; {:text "[[quick capture]] **{time}**: {text} from {url}"
+ ;;  :media "[[quick capture]] **{time}**: {url}"}
  }
  }