瀏覽代碼

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:
     inputs:
       build-target:
-        description: 'Build Target ("nightly"/"beta"/"non-release")'
-        type: string
+        description: 'Build Target (Release Type)'
+        type: choice
         required: true
+        options:
+          - beta
+          - nightly
+          - non-release
         default: "beta"
       git-ref:
-        description: "Release Git Ref(master)"
+        description: "Release Git Ref (Which branch or tag to build?)"
         required: true
         default: "master"
       is-draft:
@@ -24,6 +28,16 @@ on:
         type: boolean
         required: 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
     - cron: '0 14 * * MON-FRI'
 
@@ -87,6 +101,12 @@ jobs:
         run: |
           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
         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"
         minSdkVersion rootProject.ext.minSdkVersion
         targetSdkVersion rootProject.ext.targetSdkVersion
-        versionCode 28
-        versionName "0.7.4"
+        versionCode 29
+        versionName "0.7.5"
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
         aaptOptions {
              // 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.util.Log;
 import android.os.FileObserver;
+
 import android.net.Uri;
 
 import java.io.*;
@@ -133,7 +134,6 @@ public class FsWatcher extends Plugin {
         File f = new File(path);
         obj.put("path", Uri.fromFile(f));
         obj.put("dir", mPath);
-        Log.i("FsWatcher", "prepare event " + obj);
 
         switch (event) {
             case FileObserver.CLOSE_WRITE:
@@ -144,6 +144,7 @@ public class FsWatcher extends Plugin {
                 } catch (IOException | ErrnoException e) {
                     e.printStackTrace();
                 }
+                Log.i("FsWatcher", "prepare event " + obj);
                 obj.put("content", content);
                 break;
             case FileObserver.MOVED_TO:
@@ -155,13 +156,20 @@ public class FsWatcher extends Plugin {
                 } catch (IOException | ErrnoException e) {
                     e.printStackTrace();
                 }
+                Log.i("FsWatcher", "prepare event " + obj);
                 obj.put("content", content);
                 break;
             case FileObserver.MOVE_SELF:
             case FileObserver.MOVED_FROM:
             case FileObserver.DELETE:
             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;
             default:
                 // unreachable?
@@ -218,7 +226,25 @@ public class FsWatcher extends Plugin {
                 Log.d("FsWatcher", "got path=" + path + " event=" + event);
                 if (Pattern.matches("(?i)[^.].*?\\.(md|org|css|edn|js|markdown)$", 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"}
   org.babashka/fswatcher {:version "0.0.3"}}
  :tasks
- {dev:watch
-  logseq.tasks.dev/watch
+ {dev:desktop-watch
+  logseq.tasks.dev.desktop/watch
 
   dev:open-dev-electron-app
-  logseq.tasks.dev/open-dev-electron-app
+  logseq.tasks.dev.desktop/open-dev-electron-app
 
   -dev:electron-start
-  {:depends [dev:watch dev:open-dev-electron-app]}
+  {:depends [dev:desktop-watch dev:open-dev-electron-app]}
 
   dev:electron-start
   {:doc "Start electron dev by watching assets and opening dev app"
    ;; Parallel execution - https://book.babashka.org/#parallel
    :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
   logseq.tasks.spec/validate-local-storage
 

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

@@ -2,7 +2,7 @@
   (:require [clojure.string :as string]))
 
 (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
   (mapv (fn [p]

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

@@ -237,6 +237,10 @@
 (defn page-name->map
   "Create a page's map structure given a original page name (string).
    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.
     Useful when creating new pages from references or namespaces,
     as there's no chance to introduce timestamps via editing in page"
@@ -253,9 +257,10 @@
        {:block/name page-name
         :block/original-name original-page-name}
        (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?
          (let [namespace (first (gp-util/split-last "/" original-page-name))]
            (when-not (string/blank? namespace)

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

@@ -31,7 +31,7 @@
 (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
-    :excalidraw})
+    :excalidraw :sh})
 
 (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
   (testing "Counts"
     (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
            (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
 yarn watch
 # 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
 ```
 

+ 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 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 block.clickNext()
 
@@ -195,7 +196,7 @@ test('auto completion and auto pair', async ({ page, block }) => {
 
   // {{
   await block.mustType('type {{', { toBe: 'type {{}}' })
-
+  await page.waitForTimeout(100);
   // ((
   await block.clickNext()
 

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

@@ -542,7 +542,7 @@
 				INFOPLIST_FILE = App/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				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\"";
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq;
 				PRODUCT_NAME = "$(TARGET_NAME)";
@@ -568,7 +568,7 @@
 				INFOPLIST_FILE = App/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				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_NAME = "$(TARGET_NAME)";
 				SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
@@ -593,7 +593,7 @@
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				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_FAST_MATH = YES;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
@@ -620,7 +620,7 @@
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				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;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
 				PRODUCT_NAME = "$(TARGET_NAME)";

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

@@ -17,9 +17,5 @@
 	},
 	"ios": {
 		"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,
 .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);
   padding: -1px;
 }

+ 1 - 1
resources/package.json

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

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

@@ -1,30 +1,7 @@
 (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
   "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
   "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]
   (println (format

+ 3 - 1
shadow-cljs.edn

@@ -37,7 +37,9 @@
                                                 "externs.js"]
                            :warnings           {:fn-deprecated 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.
         ;;   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/`.")})))
 
 (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.
    `graph identifier` is the name of the graph to open, e.g. `lambda`"
   [^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
         (when (or page-name block-id file)
           (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?
               (state/set-state! :window/once-graph-ready redirect-f)
               (do (win/switch-to-window! window-on-graph)
@@ -57,6 +57,22 @@
           (send-to-renderer win "openNewWindowOfGraph" graph-name)))
       (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
   [^js win parsed-url]
   (let [url-host (.-host parsed-url)] ;; return "" when no pathname provided
@@ -64,6 +80,9 @@
       (= "auth-callback" url-host)
       (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
       (= "graph" url-host)
       (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]
   (let [windows (get-graph-all-windows dir)]
         ;; 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!
   [url default-open]

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

@@ -1,9 +1,12 @@
 (ns electron.listener
   (:require [frontend.state :as state]
             [frontend.context.i18n :refer [t]]
+            [frontend.date :as date]
             [frontend.handler.route :as route-handler]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.ui :as ui-handler]
+            [frontend.config :as config]
+            [clojure.string :as string]
             [cljs-bean.core :as bean]
             [frontend.fs.watcher-handler :as watcher-handler]
             [frontend.fs.sync :as sync]
@@ -34,7 +37,7 @@
    (fn [_req]
      (persist-dbs!))))
 
-(defn listen-to-electron!
+(defn ^:large-vars/cleanup-todo listen-to-electron!
   []
   ;; TODO: move "file-watcher" to electron.ipc.channels
   (js/window.apis.on "file-watcher"
@@ -125,6 +128,28 @@
                      (fn [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"
                      ;; 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

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

@@ -33,6 +33,7 @@
             [frontend.extensions.zotero :as zotero]
             [frontend.format.block :as block]
             [frontend.format.mldoc :as mldoc]
+            [frontend.fs :as fs]
             [frontend.handler.block :as block-handler]
             [frontend.handler.common :as common-handler]
             [frontend.handler.dnd :as dnd]
@@ -48,20 +49,20 @@
             [frontend.security :as security]
             [frontend.state :as state]
             [frontend.template :as template]
-            [logseq.graph-parser.text :as text]
             [frontend.ui :as ui]
             [frontend.util :as util]
             [frontend.util.clock :as clock]
             [frontend.util.drawer :as drawer]
-            [frontend.util.text :as text-util]
             [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.object :as gobj]
             [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]
             [promesa.core :as p]
             [reitit.frontend.easy :as rfe]
@@ -271,12 +272,15 @@
 
     (when @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]
                        (util/stop event)
                        (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
           (contains? config/audio-formats ext)
           (audio-cp @src)
@@ -284,13 +288,20 @@
           (contains? (gp-config/img-formats) ext)
           (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)
           [:a.asset-ref.is-pdf {:href @src
                                 :on-click share-fn}
            title]
 
           :else
-          [:a.asset-ref.is-doc {:ref @src
+          [:a.asset-ref.is-doc {:href @src
                                 :on-click share-fn}
            title])))))
 
@@ -1142,7 +1153,7 @@
       (when-not (string/blank? id)
         (let [width (min (- (util/get-width) 96)
                          560)
-              height (int (* width (/ 315 560)))]
+              height (int (* width (/ 360 560)))]
           [:iframe
            {:allowfullscreen true
             :framespacing "0"
@@ -1156,10 +1167,7 @@
 (defn- macro-video-cp
   [_config 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
                      [_ _ _ (:or "youtube.com" "youtu.be" "y2u.be") _ id _]
                      (if (= (count id) 11) ["youtube-player" id] url)
@@ -1173,8 +1181,10 @@
                      [_ _ _ (_ :guard #(string/ends-with? % "vimeo.com")) _ 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
                      url)]
@@ -1182,16 +1192,20 @@
                (= (first src) "youtube-player"))
         (youtube/youtube-video (last 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
   [name config arguments]
@@ -1931,7 +1945,10 @@
         (when-not (target-forbidden-edit? target)
           (cond
             (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?
             (util/clear-selection!)

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

@@ -335,6 +335,7 @@
   min-height: 24px;
   padding: 2px 0;
   border-bottom: 1px solid transparent;
+  transition: background-color 0.3s cubic-bezier(0.16, 1, 0.3, 1);
 
   &.selected {
     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
   (:require [rum.core :as rum]
             [goog.object :as gobj]

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

@@ -23,25 +23,28 @@
                          [:span.mr-1 (t :help/shortcuts)]
                          (ui/icon "command" {:style {:font-size 20}})]]]
                       [(t :help/docs) "https://docs.logseq.com/"]
+                      [(t :help/start) "https://docs.logseq.com/#/page/tutorial"]
                       ["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"
            :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/feature) "https://github.com/logseq/logseq/issues/new?assignees=&labels=&template=feature_request.md&title="]
                       [(t :help/changelog) "https://docs.logseq.com/#/page/changelog"]]}
+          
+          {:title "About"
+           :children [[(t :help/about) "https://logseq.com/blog/about"]]}
 
           {:title "Terms"
            :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]
             [[: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]]]))]]])))
 
 (defonce *roam-importing? (atom nil))
+(defonce *lsq-importing? (atom nil))
 (defonce *opml-importing? (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
   [{:keys [query-params]}]
   (let [roam-importing? (rum/react *roam-importing?)
+        lsq-importing?  (rum/react *lsq-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
      :importer
      [:article.flex.flex-col.items-center.importer.py-16.px-8
       [:section.c.text-center
        [: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
        [: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)]]
         [:div.flex.flex-col
          (if roam-importing?
            (ui/loading "Importing ...")
-           [
-            [:strong "RoamResearch"]
+           [[:strong "RoamResearch"]
             [:small "Import a JSON Export of your Roam graph"]])]
         [:input.absolute.hidden
          {:id        "import-roam"
           :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
-        {:disabled (or roam-importing? opml-importing?)}
+        {:disabled importing?}
         [:span.as-flex-center (ui/icon "sitemap" {:style {:fontSize "26px"}})]
         [:span.flex.flex-col
          (if opml-importing?
@@ -185,24 +256,7 @@
         [:input.absolute.hidden
          {:id        "import-opml"
           :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))
         [: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})
                                  :on-click (fn []
                                              (when-not (string/blank? search-q)
+                                               (state/close-modal!)
                                                (search-handler/search (state/get-current-repo) search-q {:limit 1000
                                                                                                          :more? true})
                                                (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))
      (usage-diagnostics-row t instrument-disabled?)
      (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?)
      (when (util/electron?) (https-user-agent-row https-agent-opts))
      (clear-cache-row t)

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

@@ -517,7 +517,8 @@
   [e]
   (state/hide-custom-context-menu!)
   (when-not (or (gobj/get e "shiftKey")
-                (util/meta-key? e))
+                (util/meta-key? e)
+                (state/get-edit-input-id))
     (editor-handler/clear-selection!)))
 
 (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 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?
 ;; (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
   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-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-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?
@@ -52,7 +52,8 @@
   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
   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
   get-current-page set-key-value
@@ -158,13 +159,13 @@
                 (assoc option
                        :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)
           db-conn (d/create-conn db-schema/schema)
           _ (swap! conns assoc db-name db-conn)
-          stored (db-persist/get-serialized-graph db-name)
           _ (when stored
               (let [stored-db (try (string->db stored)
                                    (catch js/Error _e
@@ -178,6 +179,13 @@
                 (conn/reset-conn! db-conn db)))]
     (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!
   [{:keys [repos]} _old-db-schema restore-config-handler]
   (let [repo (or (state/get-current-repo) (:url (first repos)))]

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

@@ -71,6 +71,11 @@
        react
        first))))
 
+(defn get-original-name
+  [page-entity]
+  (or (:block/original-name page-entity)
+      (:block/name page-entity)))
+
 (defn get-tag-pages
   [repo tag-name]
   (when tag-name
@@ -1364,6 +1369,16 @@
          (reset! blocks-count-cache 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
 (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"
            :plugin/not-installed "Não instalado"
            :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."
            :on-boarding/add-graph "Adicionar grafo"
@@ -2500,7 +2503,39 @@
         :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/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 "Это демонстрационный граф, изменения не будут сохранены, пока вы не откроете локальный файл."
         :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/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")
                                 :default "tutorial-tr.md")

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

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

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

@@ -17,22 +17,10 @@
 (defn- export-hiccup
   [hiccup]
   (util/format "#+BEGIN_EXPORT hiccup\n%s\n#+END_EXPORT"
-
                (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
   [format hiccup opts]
@@ -91,10 +79,7 @@
                                       (if (string? pattern) pattern (apply str (reverse pattern)))))))
         wrapper (fn [tag content]
                   (let [content (cond
-                                  (not (contains? allowed-tags tag))
-                                  nil
-
-                                  (contains? #{:comment :head :style :xml} tag)
+                                  (contains? denied-tags tag)
                                   nil
 
                                   (and (= tag :p) (:in-table? opts))
@@ -233,7 +218,7 @@
                  (for [x hiccup]
                    (single-hiccup-transform x))
                  (single-hiccup-transform hiccup))]
-    (apply str result)))
+    (string/replace (apply str result) #"\n\n+" "\n\n")))
 
 (defn hiccup->doc
   [format hiccup]

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

@@ -67,12 +67,6 @@
     (when (= fs bfs-record)
       (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!
   [repo dir path content opts]
   (when content

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

@@ -2,6 +2,7 @@
   (:require ["@capacitor/filesystem" :refer [Encoding Filesystem]]
             [cljs-bean.core :as bean]
             [clojure.string :as string]
+            [frontend.config :as config]
             [frontend.db :as db]
             [frontend.encrypt :as encrypt]
             [frontend.fs.protocol :as protocol]
@@ -107,6 +108,33 @@
         (= (string/trim decrypted-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!
   [_this repo _dir path content {:keys [ok-handler error-handler old-content skip-compare?]} stat]
   (if skip-compare?
@@ -129,6 +157,7 @@
                                         (js/console.error error)
                                         nil)))
             disk-content (or disk-content "")
+            repo-dir (config/get-local-dir repo)
             ext (string/lower-case (util/get-file-ext path))
             db-content (or old-content (db/get-file repo (js/decodeURI path)) "")
             contents-matched? (contents-matched? disk-content db-content)
@@ -148,7 +177,12 @@
          (p/let [result (.writeFile Filesystem (clj->js {:path path
                                                          :data content
                                                          :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))
                              (encrypt/decrypt content)
                              content)]
@@ -165,11 +199,14 @@
   (let [[dir path] (map #(some-> %
                                  js/decodeURI)
                         [dir path])
-        dir (string/replace dir #"/+$" "")
+        dir (some-> dir (string/replace #"/+$" ""))
         path (some-> path (string/replace #"^/+" ""))
         path (cond (nil? path)
                    dir
 
+                   (nil? dir)
+                   path
+
                    (string/starts-with? path dir)
                    path
 
@@ -204,13 +241,12 @@
 (defrecord Capacitorfs []
   protocol/Fs
   (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]
     (p/let [result (.mkdir Filesystem
                            (clj->js
@@ -221,8 +257,16 @@
       result))
   (readdir [_this dir]                  ; recursive
     (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]
     ;; Too dangerious!!! We'll never implement this.
     nil)
@@ -236,18 +280,6 @@
          content)
        (p/catch (fn [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]
     (let [path (get-file-path dir path)]
       (p/let [stat (p/catch

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

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

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

@@ -150,22 +150,34 @@
 (def *swipe (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
   [event uuid]
-  (let [input (state/get-input)
+  (let [target (.-target event)
+        input (state/get-input)
         input-id (state/get-edit-input-id)
         selection-type (.-type (.getSelection js/document))]
     (reset! *touch-start (js/Date.now))
     (when-not (and input
                    (string/ends-with? input-id (str uuid)))
       (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
   [event block uuid edit? *show-left-menu? *show-right-menu?]
@@ -197,8 +209,8 @@
               (let [{:keys [x0 y0]} @*swipe
                     dx (- tx x0)
                     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))
                         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)
                   block {:block/uuid block-id
                          :block/properties properties
+                         :block/properties-order (keys properties)
                          :block/content content}]
               (outliner-core/save-block! block))))))
 
@@ -942,14 +943,17 @@
               (state/set-edit-content! input-id 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]
   (let [block-ids (remove nil? block-ids)
         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)
                        [block-id :id (str block-id)])))
-                 block-ids)]
+                 block-ids)
+        col (remove nil? col)]
     (batch-set-block-property! col)))
 
 (defn copy-block-ref!
@@ -1915,10 +1919,13 @@
    0))
 
 (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
                   exclude-properties
                   target-block
-                  sibling?]
+                  sibling?
+                  keep-uuid?]
            :or {exclude-properties []}}]
   (let [editing-block (when-let [editing-block (state/get-edit-block)]
                         (some-> (db/pull (:db/id editing-block))
@@ -1945,14 +1952,16 @@
         (let [format (or (:block/format target-block) (state/get-preferred-format))
               blocks' (map (fn [block]
                              (paste-block-cleanup block page exclude-properties format content-update-fn))
-                        blocks)
+                           blocks)
               result (outliner-core/insert-blocks! blocks' target-block {:sibling? sibling?
                                                                          :outliner-op :paste
-                                                                         :replace-empty-target? true})]
+                                                                         :replace-empty-target? true
+                                                                         :keep-uuid? keep-uuid?})]
           (edit-last-block-after-inserted! result))))))
 
 (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)
        (map (fn [block]
               (let [content (:content block)
@@ -1961,22 +1970,31 @@
                                   (property/insert-properties format content props))
                     ast (mldoc/->edn content* (gp-mldoc/default-config 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")
                 (assoc fst-block :block/level (:block/level block)))))))
 
-(defn insert-block-tree-after-target
+(defn insert-block-tree
   "`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))
         blocks (gp-block/with-parent-and-left page-id blocks)]
     (paste-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!
   ([element-id db-id]

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

@@ -1,5 +1,7 @@
 (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.repo :as repo-handler]
             [frontend.state :as state]
@@ -13,9 +15,11 @@
             [logseq.graph-parser.date-time-util :as date-time-util]
             [frontend.handler.page :as page]
             [frontend.handler.editor :as editor]
+            [frontend.handler.notification :as notification]
             [frontend.util :as util]))
 
 (defn index-files!
+  "Create file structure, then parse into DB (client only)"
   [repo files finish-handler]
   (let [titles (->> files
                     (map :title)
@@ -95,3 +99,116 @@
          {:target-block target-block
           :sibling? sibling?})
         (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.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))
 
 (defn graph-txid-exists?

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

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

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

@@ -116,27 +116,35 @@
         [page]))))
 
 (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]
    (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
                   create-first-block? true
                   format              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)
        (let [pages    (if split-namespace?
                         (gp-util/split-namespace-pages title)
                         [title])
              format   (or format (state/get-preferred-format))
              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)))
-                        pages)
+                           pages)
              txs      (->> pages
                            ;; for namespace pages, only last page need properties
                            drop-last
@@ -167,14 +175,8 @@
     (when-not (string/blank? file-path)
       (db/transact! [[:db.fn/retractEntity [:file/path file-path]]])
       (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
   [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/open-edit                "Editar bloco selecionado"
              :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"
              :shortcut.category/basics                "Básico"
@@ -674,18 +729,18 @@
              :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.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.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/insert-youtube-timestamp "Inserir timestamp do youtube"
              :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/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 cartas"
+             :command.ui/toggle-cards                 "Trocar cartões"
              :command.ui/toggle-left-sidebar          "Trocar barra lateral esquerda"
              :command.auto-complete/open-link         "Auto-completar: Abra o item selecionado no navegador"
              :command.command/run                     "Execute o comando Git"
@@ -721,9 +776,12 @@
              :command.ui/goto-plugins                 "Ir para o painel de plugins"
             ;;  :command.ui/open-new-window              "Abra uma nova janela"
              :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/navigating            "ナビゲーション"
              :shortcut.category/block-editing         "ブロック単位の編集"
@@ -953,7 +1011,10 @@
              :shortcut.category/block-command-editing "Modifica comandi blocco"
              :shortcut.category/block-selection       "Selezione blocco (premi Esc per uscire dalla selezione)"
              :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"

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

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

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

@@ -13,14 +13,22 @@
 (defn add-or-update-marker
   [content format marker]
   (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
         (if-let [matches (seq (util/re-pos new-line-re-pattern content))]
           (let [[start-pos content] (last matches)]
             (+ start-pos (count content)))
           (count (util/safe-re-find re-pattern content)))
+
         new-content
         (str (subs content 0 pos)
              (string/replace-first (subs content pos)
@@ -57,8 +65,8 @@
   Returns [new-content new-marker]."
   [content marker new-marker format preferred-workflow]
   (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]))

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

@@ -5,7 +5,7 @@
 
 (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 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^\?]+)?$")

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

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

+ 7 - 2
templates/config.edn

@@ -223,6 +223,11 @@
  ;; :mobile/photo
  ;; {:allow-editing? true}
 
+ ;; Mobile features options
+ ;; Gestures
+ ;; :mobile
+ ;; {:gestures/disabled-in-block-with-tags ["kanban"]}
+
  ;; Extra CodeMirror options
  ;; :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 insert or format any text in the template as shown in the following examples.
  ;; :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}"}
  }