瀏覽代碼

Merge branch 'master' into refactor/plugin-api-properties

Tienson Qin 1 周之前
父節點
當前提交
d4f7bf22d9

+ 14 - 6
AGENTS.md

@@ -2,11 +2,11 @@
 - Use clojure-mcp `clojure_inspect_project` to get project structure.
 - `src/`: Core source code
   - `src/main/`: The core logic of the application
-	- `src/main/mobile/`: Mobile app code
-	- `src/main/frontend/inference_worker/`: Code running in a webworker for text-embedding and vector-search
-	- `src/main/frontend/worker/`: Code running in an another webworker
-		- `src/main/frontend/worker/rtc/`: RTC(Real Time Collaboration) related code
-	- `src/main/frontend/components/`: UI components
+    - `src/main/mobile/`: Mobile app code
+    - `src/main/frontend/inference_worker/`: Code running in a webworker for text-embedding and vector-search
+    - `src/main/frontend/worker/`: Code running in an another webworker
+        - `src/main/frontend/worker/rtc/`: RTC(Real Time Collaboration) related code
+    - `src/main/frontend/components/`: UI components
   - `src/electron/`: Code specifically for the Electron desktop application.
   - `src/test/`: unit-tests
 - `deps/`: Internal dependencies/modules
@@ -21,7 +21,15 @@
 - Run single focused unit-test:
   - Add the `:focus` keyword to the test case: `(deftest ^:focus test-name ...)`
   - `bb dev:test -i focus`
-  
+- Run e2e basic tests:
+  - `bb dev:e2e-basic-test`
+- Run e2e rtc extra tests:
+  - `bb dev:e2e-rtc-extra-test`
+
+## Common used cljs keywords
+- All commonly used ClojureScript keywords are defined using `logseq.common.defkeywords/defkeyword`.
+- Search for `defkeywords` to find all the definitions.
+
 ## Code Guidance
 - Keep in mind: @prompts/review.md
 

+ 6 - 0
bb.edn

@@ -164,6 +164,12 @@
   dev:lint-and-test
   logseq.tasks.dev/lint-and-test
 
+  dev:e2e-basic-test
+  logseq.tasks.dev/e2e-basic-test
+
+  dev:e2e-rtc-extra-test
+  logseq.tasks.dev/e2e-rtc-extra-test
+
   dev:gen-malli-kondo-config
   logseq.tasks.dev/gen-malli-kondo-config
 

+ 3 - 1
clj-e2e/deps.edn

@@ -20,4 +20,6 @@
                       {:git/tag "v0.5.1" :git/sha "dfb30dd"}}}
   :dev {:extra-paths ["dev" "test"]}
   :dev-run-rtc-extra-test {:extra-paths ["dev" "test"]
-                           :exec-fn user/run-rtc-extra-test2}}}
+                           :exec-fn user/run-rtc-extra-test2}
+  :dev-run-all-basic-test {:extra-paths ["dev" "test"]
+                           :exec-fn user/run-all-basic-test}}}

+ 5 - 3
clj-e2e/dev/user.clj

@@ -76,7 +76,8 @@
 
 (defn run-rtc-extra-test2
   [& _args]
-  (run-tests 'logseq.e2e.rtc-extra-test))
+  (run-tests 'logseq.e2e.rtc-extra-test)
+  (System/exit 0))
 
 (defn run-editor-basic-test
   []
@@ -89,7 +90,7 @@
        (swap! *futures assoc :tag-basic-test)))
 
 (defn run-all-basic-test
-  []
+  [& _]
   (run-tests 'logseq.e2e.editor-basic-test
              'logseq.e2e.commands-basic-test
              'logseq.e2e.multi-tabs-basic-test
@@ -98,7 +99,8 @@
              'logseq.e2e.plugins-basic-test
              'logseq.e2e.reference-basic-test
              'logseq.e2e.property-basic-test
-             'logseq.e2e.tag-basic-test))
+             'logseq.e2e.tag-basic-test)
+  (System/exit 0))
 
 (defn start
   []

+ 45 - 2
deps/db/src/logseq/db/frontend/property.cljs

@@ -6,6 +6,7 @@
             [flatland.ordered.map :refer [ordered-map]]
             [logseq.common.defkeywords :refer [defkeywords]]
             [logseq.common.uuid :as common-uuid]
+            [logseq.db.common.order :as db-order]
             [logseq.db.frontend.db-ident :as db-ident]
             [logseq.db.frontend.property.type :as db-property-type]))
 
@@ -715,10 +716,52 @@
   ([property-name user-namespace]
    (db-ident/create-db-ident-from-name user-namespace property-name)))
 
+(defn normalize-sorted-entities-block-order
+  "Return tx-data.
+  Generate appropriate :block/order values for sorted-blocks with :block/order value = nil or duplicated"
+  [sorted-entities]
+  (let [parts (partition-by :block/order sorted-entities)
+        [_ tx-data]
+        (reduce (fn [[start-order tx-data] ents]
+                  (let [n (count ents)]
+                    (if (> n 1)
+                      (let [orders (db-order/gen-n-keys n start-order (:block/order (first ents)))
+                            tx-data* (apply conj tx-data (map
+                                                          (fn [order ent]
+                                                            {:db/id (:db/id ent)
+                                                             :block/order order})
+                                                          orders ents))]
+                        [(last orders) tx-data*])
+                      [(:block/order (first ents)) tx-data])))
+                [nil []] parts)]
+    tx-data))
+
+(defn sort-properties
+  "Sort by :block/order and :block/uuid.
+  - nil is greater than non-nil
+  - When block/order is equal, sort by block/uuid"
+  [prop-entities]
+  (sort
+   (fn [a b]
+     (let [order-a (:block/order a)
+           order-b (:block/order b)]
+       (cond
+         (and (nil? order-a) (nil? order-b))
+         (compare (:block/uuid a) (:block/uuid b))
+
+         (nil? order-a) 1
+         (nil? order-b) -1
+
+         (= order-a order-b)
+         (compare (:block/uuid a) (:block/uuid b))
+
+         :else
+         (compare order-a order-b))))
+   prop-entities))
+
 (defn get-class-ordered-properties
   [class-entity]
-  (->> (:logseq.property.class/properties class-entity)
-       (sort-by :block/order)))
+  (sort-properties (:logseq.property.class/properties class-entity)))
 
 (defn property-created-block?
   "`block` has been created in a property and it's not a closed value."

+ 45 - 0
deps/db/test/logseq/db/frontend/property_test.cljs

@@ -0,0 +1,45 @@
+(ns logseq.db.frontend.property-test
+  (:require [cljs.test :refer [deftest is testing]]
+            [logseq.db.frontend.property :as db-property]))
+
+(deftest sort-properties
+  (let [p1 {:db/id 1, :block/order "a", :block/uuid "uuid-a"}
+        p2 {:db/id 2, :block/order "b", :block/uuid "uuid-b"}
+        p3 {:db/id 3, :block/order nil, :block/uuid "uuid-d"}
+        p4 {:db/id 4, :block/order "b", :block/uuid "uuid-c"}
+        p5 {:db/id 5, :block/order nil, :block/uuid "uuid-e"}]
+    (is (= [p1 p2 p4 p3 p5]
+           (db-property/sort-properties [p3 p1 p5 p2 p4])))))
+
+(deftest normalize-block-order-tx-data-test
+  (testing "Generate appropriate :block/order values for sorted-blocks with :block/order value = nil or duplicated"
+    (let [p1 {:db/id 1, :block/order "a0"}
+          p2 {:db/id 2, :block/order "bbb"}
+          p3 {:db/id 3, :block/order "bbb"}
+          p4 {:db/id 4, :block/order nil}
+          p5 {:db/id 5, :block/order nil}
+          sorted-entities [p1 p2 p3 p4 p5]
+          tx-data (db-property/normalize-sorted-entities-block-order sorted-entities)
+          ;; apply tx-data to entities
+          tx-map (into {} (map (juxt :db/id identity) tx-data))
+          updated-entities (map (fn [ent]
+                                  (if-let [tx (get tx-map (:db/id ent))]
+                                    (merge ent tx)
+                                    ent))
+                                sorted-entities)
+          ;; sort again and test
+          final-sorted (db-property/sort-properties updated-entities)]
+      (is (= 5 (count final-sorted)))
+      ;; Check that all orders are now strings
+      (is (every? string? (map :block/order final-sorted)))
+      ;; Check that all orders are unique
+      (is (= 5 (count (set (map :block/order final-sorted)))))
+      ;; Check that the final list is sorted correctly by the new orders
+      (is (= final-sorted (sort-by :block/order final-sorted)))))
+
+  (testing "No changes needed for already valid orders"
+    (let [p1 {:db/id 1, :block/order "b00"}
+          p2 {:db/id 2, :block/order "b01"}
+          sorted-entities [p1 p2]
+          tx-data (db-property/normalize-sorted-entities-block-order sorted-entities)]
+      (is (empty? tx-data)))))

+ 25 - 22
deps/graph-parser/script/db_import.cljs

@@ -49,26 +49,31 @@
   (p/let [s (fsp/readFile (:path file))]
     (str s)))
 
-(defn- <read-asset-file [file assets]
-  (p/let [buffer (fs/readFileSync (:path file))
-          checksum (db-asset/<get-file-array-buffer-checksum buffer)]
-    (swap! assets assoc
-           (gp-exporter/asset-path->name (:path file))
-           {:size (.-length buffer)
-            :checksum checksum
-            :type (db-asset/asset-path->type (:path file))
-            :path (:path file)})
-    buffer))
+(defn- exceed-limit-size?
+  "Asset size no more than 100M"
+  [^js buffer]
+  (> (.-length buffer) (* 100 1024 1024)))
 
-(defn- <copy-asset-file [asset-m db-graph-dir]
-  (p/let [parent-dir (node-path/join db-graph-dir common-config/local-assets-dir)
-          _ (fsp/mkdir parent-dir #js {:recursive true})]
-    (if (:block/uuid asset-m)
-      (fsp/copyFile (:path asset-m) (node-path/join parent-dir (str (:block/uuid asset-m) "." (:type asset-m))))
-      (when-not (:pdf-annotation? asset-m)
-        (println "[INFO]" "Copied asset" (pr-str (node-path/basename (:path asset-m)))
-                 "by its name since it was unused.")
-        (fsp/copyFile (:path asset-m) (node-path/join parent-dir (node-path/basename (:path asset-m))))))))
+(defn- <read-and-copy-asset [db-graph-dir file assets buffer-handler]
+  (p/let [buffer (fs/readFileSync (:path file))
+          checksum (db-asset/<get-file-array-buffer-checksum buffer)
+          asset-id (d/squuid)
+          asset-name (gp-exporter/asset-path->name (:path file))
+          asset-type (db-asset/asset-path->type (:path file))]
+    (if (exceed-limit-size? buffer)
+      (js/console.log (str "Skipped copying asset " (pr-str (:path file)) " because it is larger than the 100M max."))
+      (p/let [parent-dir (node-path/join db-graph-dir common-config/local-assets-dir)
+              {:keys [with-edn-content pdf-annotation?]} (buffer-handler buffer)]
+        (fsp/mkdir parent-dir #js {:recursive true})
+        (swap! assets assoc asset-name
+               (with-edn-content
+                 {:size (.-length buffer)
+                  :type asset-type
+                  :path (:path file)
+                  :checksum checksum
+                  :asset-id asset-id}))
+        (when-not pdf-annotation?
+          (fsp/copyFile (:path file) (node-path/join parent-dir (str asset-id "." asset-type))))))))
 
 (defn- notify-user [{:keys [continue debug]} m]
   (println (:msg m))
@@ -119,9 +124,7 @@
         options (merge options
                        (default-export-options options)
                         ;; asset file options
-                       {:<copy-asset (fn copy-asset [file]
-                                       (<copy-asset-file file db-graph-dir))
-                        :<read-asset <read-asset-file})]
+                       {:<read-and-copy-asset #(<read-and-copy-asset db-graph-dir %1 %2 %3)})]
     (p/with-redefs [d/transact! dev-transact!]
       (gp-exporter/export-file-graph conn conn config-file *files options))))
 

+ 72 - 67
deps/graph-parser/src/logseq/graph_parser/exporter.cljs

@@ -1120,8 +1120,7 @@
 
 (defn- build-new-asset [asset-data]
   (merge (sqlite-util/block-with-timestamps
-          {:block/uuid (d/squuid)
-           :block/order (db-order/gen-key)
+          {:block/order (db-order/gen-key)
            :block/page :logseq.class/Asset
            :block/parent :logseq.class/Asset})
          {:block/tags [:logseq.class/Asset]
@@ -1129,16 +1128,25 @@
           :logseq.property.asset/checksum (:checksum asset-data)
           :logseq.property.asset/size (:size asset-data)}))
 
+(defn- get-asset-block-id
+  [assets path]
+  (get-in @assets [path :asset-id]))
+
 (defn- build-annotation-images
   "Builds tx for annotation images and provides a map for mapping image asset names
    to their new uuids"
-  [parent-asset-path assets]
+  [parent-asset-path assets {:keys [notify-user]}]
   (let [image-dir (string/replace-first parent-asset-path #"(?i)\.pdf$" "")
         image-paths (filter #(= image-dir (node-path/dirname %)) (keys @assets))
-        txs (mapv #(let [new-asset (merge (build-new-asset (get @assets %))
-                                          {:block/title "pdf area highlight"})]
-                     (swap! assets assoc-in [% :block/uuid] (:block/uuid new-asset))
-                     new-asset)
+        txs (keep #(let [asset-id (get-asset-block-id assets %)]
+                     (if-not asset-id
+                       (notify-user {:msg (str "Skipped creating asset " (pr-str %) " because it has no asset id")
+                                     :level :error})
+                       (let [new-asset (merge (build-new-asset (get @assets %))
+                                              {:block/title "pdf area highlight"
+                                               :block/uuid asset-id})]
+                         (swap! assets assoc-in [% :asset-created?] true)
+                         new-asset)))
                   image-paths)]
     {:txs txs
      :image-asset-name-to-uuids
@@ -1163,25 +1171,32 @@
         asset-md-name (str "hls__" (safe-sanitize-file-name
                                     (node-path/basename (string/replace-first parent-asset-path #"(?i)\.pdf$" ".md"))))]
     (when-let [asset-edn-map (get @assets asset-edn-path)]
-      ;; Mark edn asset so it isn't treated like a normal asset later
-      (swap! assets assoc-in [asset-edn-path :pdf-annotation?] true)
-      (let [{:keys [txs image-asset-name-to-uuids]} (build-annotation-images parent-asset-path assets)]
+      (let [{:keys [txs image-asset-name-to-uuids]} (build-annotation-images parent-asset-path assets opts)]
         (concat txs
                 (build-pdf-annotations-tx* asset-edn-map (get @pdf-annotation-pages asset-md-name) parent-asset image-asset-name-to-uuids opts))))))
 
 (defn- handle-assets-in-block
   "If a block contains assets, creates them as #Asset nodes in the Asset page and references them in the block."
-  [block {:keys [asset-links]} {:keys [assets ignored-assets pdf-annotation-pages]} opts]
+  [block {:keys [asset-links]} {:keys [assets ignored-assets pdf-annotation-pages]} {:keys [notify-user] :as opts}]
   (if (seq asset-links)
     (let [asset-maps
           (keep
            (fn [asset-link]
              (let [asset-name (-> asset-link second :url second asset-path->name)]
                (if-let [asset-data (and asset-name (get @assets asset-name))]
-                 (if (:block/uuid asset-data)
-                   {:asset-name-uuid [asset-name (:block/uuid asset-data)]}
+                 (cond
+                   (not (get-asset-block-id assets asset-name))
+                   (notify-user {:msg (str "Skipped creating asset " (pr-str asset-name) " because it has no asset id")
+                                 :level :error})
+
+                   ;; If asset tx is already built, no need to do it again
+                   (:asset-created? asset-data)
+                   {:asset-name-uuid [asset-name (:asset-id asset-data)]}
+
+                   :else
                    (let [new-asset (merge (build-new-asset asset-data)
-                                          {:block/title (db-asset/asset-name->title (node-path/basename asset-name))}
+                                          {:block/title (db-asset/asset-name->title (node-path/basename asset-name))
+                                           :block/uuid (get-asset-block-id assets asset-name)}
                                           (when-let [metadata (not-empty (common-util/safe-read-map-string (:metadata (second asset-link))))]
                                             {:logseq.property.asset/resize-metadata metadata}))
                          pdf-annotations-tx (when (= "pdf" (path/file-ext asset-name))
@@ -1190,7 +1205,7 @@
                                           (when pdf-annotations-tx pdf-annotations-tx))]
                     ;;  (prn :asset-added! (node-path/basename asset-name))
                     ;;  (cljs.pprint/pprint asset-link)
-                     (swap! assets assoc-in [asset-name :block/uuid] (:block/uuid new-asset))
+                     (swap! assets assoc-in [asset-name :asset-created?] true)
                      {:asset-name-uuid [asset-name (:block/uuid new-asset)]
                       :asset-tx asset-tx}))
                  (do
@@ -1253,7 +1268,7 @@
         {block-after-built-in-props :block deadline-properties-tx :properties-tx}
         (update-block-deadline-and-scheduled block page-names-to-uuids options)
         {block-after-assets :block :keys [asset-blocks-tx]}
-        (handle-assets-in-block block-after-built-in-props walked-ast-blocks import-state (select-keys options [:log-fn]))
+        (handle-assets-in-block block-after-built-in-props walked-ast-blocks import-state (select-keys options [:log-fn :notify-user]))
         ;; :block/page should be [:block/page NAME]
         journal-page-created-at (some-> (:block/page block*) second journal-created-ats)
         prepared-block (cond-> block-after-assets
@@ -1949,49 +1964,41 @@
                                :level :error
                                :ex-data {:error e}})))))
 
-(defn- read-asset-files
-  "Reads files under assets/"
-  [*asset-files <read-asset-file {:keys [notify-user set-ui-state assets]
-                                  :or {set-ui-state (constantly nil)}}]
-  (assert <read-asset-file "read-asset-file fn required")
-  (let [asset-files (mapv #(assoc %1 :idx %2)
-                          ;; Sort files to ensure reproducible import behavior
-                          (sort-by :path *asset-files)
-                          (range 0 (count *asset-files)))
-        read-asset (fn read-asset [{:keys [path] :as file}]
-                     (-> (p/let [byte-array (<read-asset-file file assets)]
-                           (when (= "edn" (path/file-ext (:path file)))
-                             (swap! assets assoc-in
-                                    [(asset-path->name path) :edn-content]
-                                    (common-util/safe-read-map-string (utf8/decode byte-array)))))
-                         (p/catch
-                          (fn [error]
-                            (notify-user {:msg (str "Import failed to read " (pr-str path) " with error:\n" (.-message error))
-                                          :level :error
-                                          :ex-data {:path path :error error}})))))]
+(defn- read-and-copy-asset-files
+  "Reads and copies files under assets/"
+  [*asset-files <read-and-copy-asset-file {:keys [notify-user set-ui-state assets rpath-key]
+                                           :or {set-ui-state (constantly nil)}}]
+  (assert <read-and-copy-asset-file "read-and-copy-asset-file fn required")
+  (let [asset-files (let [assets (if (keyword? rpath-key)
+                                   (common-util/distinct-by rpath-key *asset-files)
+                                   *asset-files)]
+                      (mapv #(assoc %1 :idx %2)
+                            ;; Sort files to ensure reproducible import behavior
+                            (sort-by :path assets)
+                            (range 0 (count assets))))
+        read-and-copy-asset (fn read-and-copy-asset [{:keys [path] :as file}]
+                              (-> (<read-and-copy-asset-file
+                                   file assets
+                                   (fn [buffer]
+                                     (let [edn? (= "edn" (path/file-ext path))
+                                           edn-content (when edn? (common-util/safe-read-map-string (utf8/decode buffer)))
+                                           ;; Have to assume edn file with :highlights is annotation or
+                                           ;; this import step becomes coupled to build-pdf-annotations-tx
+                                           pdf-annotation? (some #{:highlights} (keys edn-content))
+                                           with-edn-content (fn [m]
+                                                              (cond-> m
+                                                                edn-content
+                                                                (assoc :edn-content edn-content)))]
+                                       {:with-edn-content with-edn-content
+                                        :pdf-annotation? pdf-annotation?})))
+                                  (p/catch
+                                   (fn [error]
+                                     (notify-user {:msg (str "Import failed to read and copy " (pr-str path) " with error:\n" (.-message error))
+                                                   :level :error
+                                                   :ex-data {:path path :error error}})))))]
     (when (seq asset-files)
-      (set-ui-state [:graph/importing-state :current-page] "Read asset files")
-      (<safe-async-loop read-asset asset-files notify-user))))
-
-(defn- copy-asset-files
-  "Copy files under assets/"
-  [asset-maps* <copy-asset-file {:keys [notify-user set-ui-state]
-                                 :or {set-ui-state (constantly nil)}}]
-  (assert <copy-asset-file "copy-asset-file fn required")
-  (let [asset-maps (mapv #(assoc %1 :idx %2)
-                          ;; Sort files to ensure reproducible import behavior
-                         (sort-by :path asset-maps*)
-                         (range 0 (count asset-maps*)))
-        copy-asset (fn copy-asset [{:keys [path] :as asset-m}]
-                     (p/catch
-                      (<copy-asset-file asset-m)
-                      (fn [error]
-                        (notify-user {:msg (str "Import failed to copy " (pr-str path) " with error:\n" (.-message error))
-                                      :level :error
-                                      :ex-data {:path path :error error}}))))]
-    (when (seq asset-maps)
-      (set-ui-state [:graph/importing-state :current-page] "Copy asset files")
-      (<safe-async-loop copy-asset asset-maps notify-user))))
+      (set-ui-state [:graph/importing-state :current-page] "Read and copy asset files")
+      (<safe-async-loop read-and-copy-asset asset-files notify-user))))
 
 (defn- insert-favorites
   "Inserts favorited pages as uuids into a new favorite page"
@@ -2071,11 +2078,10 @@
    * :user-options - map of user specific options. See add-file-to-db-graph for more
    * :<save-config-file - fn which saves a config file
    * :<save-logseq-file - fn which saves a logseq file
-   * :<copy-asset - fn which copies asset file
-   * :<read-asset - fn which reads asset file
+   * :<read-and-copy-asset - fn which reads and copies asset file
 
    Note: See export-doc-files for additional options that are only for it"
-  [repo-or-conn conn config-file *files {:keys [<read-file <copy-asset <read-asset rpath-key log-fn]
+  [repo-or-conn conn config-file *files {:keys [<read-file <read-and-copy-asset rpath-key log-fn]
                                          :or {rpath-key :path log-fn println}
                                          :as options}]
   (reset! gp-block/*export-to-db-graph? true)
@@ -2099,13 +2105,11 @@
                              (-> (select-keys options [:notify-user :<save-logseq-file])
                                  (set/rename-keys {:<save-logseq-file :<save-file})))
         ;; Assets are read first as doc-files need data from them to make Asset blocks.
-        ;; Assets are copied after doc-files as they need block/uuid's from them to name assets
-        (read-asset-files asset-files <read-asset (merge (select-keys options [:notify-user :set-ui-state])
-                                                         {:assets (get-in doc-options [:import-state :assets])}))
+        (read-and-copy-asset-files asset-files
+                                   <read-and-copy-asset
+                                   (merge (select-keys options [:notify-user :set-ui-state :rpath-key])
+                                          {:assets (get-in doc-options [:import-state :assets])}))
         (export-doc-files conn doc-files <read-file doc-options)
-        (copy-asset-files (vals @(get-in doc-options [:import-state :assets]))
-                          <copy-asset
-                          (select-keys options [:notify-user :set-ui-state]))
         (export-favorites-from-config-edn conn repo-or-conn config {})
         (export-class-properties conn repo-or-conn)
         (move-top-parent-pages-to-library conn repo-or-conn)
@@ -2117,6 +2121,7 @@
                 (reset! gp-block/*export-to-db-graph? false)))
    (p/catch (fn [e]
               (reset! gp-block/*export-to-db-graph? false)
+              (js/console.error e)
               ((:notify-user options)
                {:msg (str "Import has unexpected error:\n" (.-message e))
                 :level :error

+ 19 - 17
deps/graph-parser/test/logseq/graph_parser/exporter_test.cljs

@@ -1,7 +1,7 @@
 (ns ^:node-only logseq.graph-parser.exporter-test
   (:require ["fs" :as fs]
             ["path" :as node-path]
-            [cljs.test :refer [testing is are deftest]]
+            [cljs.test :refer [are deftest is testing]]
             [clojure.set :as set]
             [clojure.string :as string]
             [datascript.core :as d]
@@ -94,16 +94,23 @@
    ;; TODO: Add actual default
    :default-config {}})
 
-;; Copied from db-import
-(defn- <read-asset-file [file assets]
+;; tweaked from db-import
+(defn- <read-and-copy-asset [file assets buffer-handler *asset-ids]
   (p/let [buffer (fs/readFileSync (:path file))
-          checksum (db-asset/<get-file-array-buffer-checksum buffer)]
-    (swap! assets assoc
-           (gp-exporter/asset-path->name (:path file))
-           {:size (.-length buffer)
-            :checksum checksum
-            :type (db-asset/asset-path->type (:path file))
-            :path (:path file)})
+          checksum (db-asset/<get-file-array-buffer-checksum buffer)
+          asset-id (d/squuid)
+          asset-name (gp-exporter/asset-path->name (:path file))
+          asset-type (db-asset/asset-path->type (:path file))
+          {:keys [with-edn-content pdf-annotation?]} (buffer-handler buffer)]
+    (when-not pdf-annotation?
+      (swap! *asset-ids conj asset-id))
+    (swap! assets assoc asset-name
+           (with-edn-content
+             {:size (.-length buffer)
+              :type asset-type
+              :path (:path file)
+              :checksum checksum
+              :asset-id asset-id}))
     buffer))
 
 ;; Copied from db-import script and tweaked for an in-memory import
@@ -118,13 +125,8 @@
         options' (merge default-export-options
                         {:user-options (merge {:convert-all-tags? false} (dissoc options :assets :verbose))
                         ;; asset file options
-                         :<read-asset <read-asset-file
-                         :<copy-asset (fn copy-asset [m]
-                                        (if (:block/uuid m)
-                                          (swap! assets conj m)
-                                          (when-not (:pdf-annotation? m)
-                                            (println "[INFO]" "Asset" (pr-str (node-path/basename (:path m)))
-                                                     "does not have a :block/uuid"))))}
+                         :<read-and-copy-asset (fn [file *assets buffer-handler]
+                                                 (<read-and-copy-asset file *assets buffer-handler assets))}
                         (select-keys options [:verbose]))]
     (gp-exporter/export-file-graph conn conn config-file *files options')))
 

+ 87 - 10
scripts/install-linux.sh

@@ -38,6 +38,11 @@ This script installs Logseq on Linux systems.
 
 USAGE:
     $0 [VERSION] [OPTIONS]
+    $0 uninstall
+
+COMMANDS:
+    install (default)   Install Logseq
+    uninstall           Removes Logseq installation (keeps user data)
 
 ARGUMENTS:
     VERSION    Version to install (e.g., "0.10.14"). Default: latest
@@ -59,6 +64,70 @@ For more information, visit: https://github.com/logseq/logseq
 HELP
 }
 
+uninstall() {
+    log_info "Searching for Logseq installations..."
+    
+    local user_removed=false
+    local system_removed=false
+    
+    # User installation paths
+    local -a user_paths=(
+        "$HOME/.local/share/logseq"
+        "$HOME/.local/bin/logseq"
+        "$HOME/.local/share/applications/logseq.desktop"
+        "$HOME/.local/share/icons/hicolor/512x512/apps/logseq.png"
+    )
+    
+    # System installation paths
+    local -a system_paths=(
+        "/opt/logseq"
+        "/usr/local/bin/logseq"
+        "/usr/share/applications/logseq.desktop"
+        "/usr/share/icons/hicolor/512x512/apps/logseq.png"
+    )
+    
+    # Remove user installation
+    log_info "Checking user installation..."
+    for path in "${user_paths[@]}"; do
+        if [[ -e "$path" ]] || [[ -L "$path" ]]; then
+            log_info "Removing: $path"
+            rm -rf "$path"
+            user_removed=true
+        fi
+    done
+    
+    # Remove system installation
+    log_info "Checking system-wide installation..."
+    for path in "${system_paths[@]}"; do
+        if [[ -e "$path" ]] || [[ -L "$path" ]]; then
+            if [[ "$EUID" -ne 0 ]]; then
+                log_warn "System-wide installation found at $path, but root privileges required"
+                log_warn "Run with sudo to uninstall system-wide installation"
+            else
+                log_info "Removing: $path"
+                rm -rf "$path"
+                system_removed=true
+            fi
+        fi
+    done
+    
+    # Update desktop databases
+    if [[ "$user_removed" == true ]] && [[ -d "$HOME/.local/share/applications" ]]; then
+        update-desktop-database "$HOME/.local/share/applications" 2>/dev/null || true
+    fi
+    
+    if [[ "$system_removed" == true ]]; then
+        update-desktop-database /usr/share/applications 2>/dev/null || true
+    fi
+    
+    # Final status message
+    if [[ "$user_removed" == true ]] || [[ "$system_removed" == true ]]; then
+        log_info "Logseq has been uninstalled successfully!"
+    else
+        log_warn "No Logseq installation found in default locations"
+    fi
+}
+
 # Parse command line arguments
 VERSION="$DEFAULT_VERSION"
 USER_INSTALL=false
@@ -87,6 +156,10 @@ while [[ $# -gt 0 ]]; do
             VERBOSE=true
             shift
             ;;
+        uninstall)
+            uninstall
+            exit 0
+            ;;
         -*)
             log_error "Unknown option: $1"
             show_help
@@ -183,7 +256,7 @@ else
 fi
 
 # Fix sandbox permissions
-if [[ -f "$INSTALL_DIR/chrome-sandbox" ]]; then
+if [[ "$USER_INSTALL" == false && -f "$INSTALL_DIR/chrome-sandbox" ]]; then
     log_info "Setting sandbox permissions..."
     chown root:root "$INSTALL_DIR/chrome-sandbox"
     chmod 4755 "$INSTALL_DIR/chrome-sandbox"
@@ -192,12 +265,19 @@ fi
 # Desktop integration
 if [[ "$SKIP_DESKTOP" == false ]]; then
     log_info "Creating desktop integration..."
-    
+
     DESKTOP_FILE="/usr/share/applications/logseq.desktop"
     if [[ "$USER_INSTALL" == true ]]; then
         mkdir -p ~/.local/share/applications/
         DESKTOP_FILE="$HOME/.local/share/applications/logseq.desktop"
     fi
+
+    # Copy icon to standard location
+    ICON_DIR="/usr/share/icons/hicolor/512x512/apps/"
+    if [[ "$USER_INSTALL" == true ]]; then
+        ICON_DIR="$HOME/.local/share/icons/hicolor/512x512/apps/"
+        mkdir -p "$ICON_DIR"
+    fi
     
     # Create desktop file
     cat > "$DESKTOP_FILE" << DESKTOP_EOF
@@ -205,8 +285,8 @@ if [[ "$SKIP_DESKTOP" == false ]]; then
 Version=1.0
 Name=Logseq
 Comment=Logseq - A privacy-first, open-source platform for knowledge management and collaboration
-Exec=$INSTALL_DIR/Logseq %U
-Icon=$INSTALL_DIR/resources/app.asar.unpacked/dist/icon.png
+Exec=$INSTALL_DIR/Logseq $([ "$USER_INSTALL" = true ] && echo "--no-sandbox") %U
+Icon=$ICON_DIR/logseq.png
 Terminal=false
 Type=Application
 Categories=Office;Productivity;Utility;TextEditor;
@@ -217,13 +297,7 @@ DESKTOP_EOF
     # Make desktop file executable
     chmod +x "$DESKTOP_FILE"
     
-    # Copy icon to standard location
     if [[ -f "$INSTALL_DIR/resources/app.asar.unpacked/dist/icon.png" ]]; then
-        ICON_DIR="/usr/share/icons/hicolor/512x512/apps/"
-        if [[ "$USER_INSTALL" == true ]]; then
-            ICON_DIR="$HOME/.local/share/icons/hicolor/512x512/apps/"
-            mkdir -p "$ICON_DIR"
-        fi
         
         cp "$INSTALL_DIR/resources/app.asar.unpacked/dist/icon.png" "$ICON_DIR/logseq.png"
         
@@ -232,6 +306,9 @@ DESKTOP_EOF
             sed -i 's|Icon=$INSTALL_DIR/resources/app.asar.unpacked/dist/icon.png|Icon=logseq|' "$DESKTOP_FILE"
         fi
     fi
+    if [[ "$USER_INSTALL" == true && -f "$INSTALL_DIR/resources/app/icon.png" ]]; then
+        cp "$INSTALL_DIR/resources/app/icon.png" "$ICON_DIR/logseq.png"
+    fi
     
     # Update desktop database
     if [[ "$USER_INSTALL" == false ]]; then

+ 11 - 0
scripts/src/logseq/tasks/dev.clj

@@ -4,6 +4,7 @@
   (:require [babashka.cli :as cli]
             [babashka.fs :as fs]
             [babashka.process :refer [shell]]
+            [babashka.tasks :refer [clojure]]
             [clojure.core.async :as async]
             [clojure.data :as data]
             [clojure.edn :as edn]
@@ -26,6 +27,16 @@
   (dev-lint/dev)
   (test "-e" "long" "-e" "fix-me"))
 
+(defn e2e-basic-test
+  "Run e2e basic tests. HTTP server should be available at localhost:3001"
+  [& _]
+  (clojure {:dir "clj-e2e"} "-X:dev-run-all-basic-test"))
+
+(defn e2e-rtc-extra-test
+  "Run e2e rtc extra tests. HTTP server should be available at localhost:3001"
+  [& _]
+  (clojure {:dir "clj-e2e"} "-X:dev-run-rtc-extra-test"))
+
 (defn gen-malli-kondo-config
   "Generate clj-kondo type-mismatch config from malli schema
   .clj-kondo/metosin/malli-types/config.edn"

+ 2 - 2
src/electron/electron/server.cljs

@@ -108,7 +108,7 @@
       (-> (invoke-logseq-api! method (.-args body))
           (p/then #(do
                      ;; Responses with an :error key are unexpected failures from electron.listener
-                     (when-let [msg (aget % "error")]
+                     (when-let [msg (and % (aget % "error"))]
                        (.code rep 500)
                        (js/console.error "Unexpected API error:" msg))
                      (.send rep %)))
@@ -151,7 +151,7 @@
                                                  (string/replace-first "${HOST}" HOST)
                                                  (string/replace-first "${PORT}" PORT))]
                                     (doto rep (.type "text/html")
-                                              (.send html))))))
+                                          (.send html))))))
               ;; listen port
               _     (.listen s (bean/->js (select-keys @*state [:host :port])))]
         (reset! *server s)

+ 33 - 31
src/main/frontend/components/imports.cljs

@@ -1,9 +1,9 @@
 (ns frontend.components.imports
   "Import data into Logseq."
-  (:require ["path" :as node-path]
-            [cljs-time.core :as t]
+  (:require [cljs-time.core :as t]
             [cljs.pprint :as pprint]
             [clojure.string :as string]
+            [datascript.core :as d]
             [frontend.components.onboarding.setups :as setups]
             [frontend.components.repo :as repo]
             [frontend.components.svg :as svg]
@@ -11,6 +11,7 @@
             [frontend.context.i18n :refer [t]]
             [frontend.db :as db]
             [frontend.fs :as fs]
+            [frontend.handler.assets :as assets-handler]
             [frontend.handler.db-based.editor :as db-editor-handler]
             [frontend.handler.db-based.import :as db-import-handler]
             [frontend.handler.file-based.import :as file-import-handler]
@@ -348,33 +349,35 @@
         (log/error :import-error ex-data)))
     (notification/show! msg :warning false)))
 
-(defn- read-asset [file assets]
-  (-> (.arrayBuffer (:file-object file))
-      (p/then (fn [buffer]
-                (p/let [checksum (db-asset/<get-file-array-buffer-checksum buffer)
-                        byte-array (js/Uint8Array. buffer)]
-                  (swap! assets assoc
-                         (gp-exporter/asset-path->name (:path file))
-                         {:size (.-size (:file-object file))
-                          :checksum checksum
-                          :type (db-asset/asset-path->type (:path file))
-                          :path (:path file)
-                          ;; Save array to avoid reading asset twice
-                          ::byte-array byte-array})
-                  byte-array)))))
-
-(defn- copy-asset [repo repo-dir asset-m]
-  (-> (::byte-array asset-m)
-      (p/then (fn [content]
-                (let [assets-dir (path/path-join repo-dir common-config/local-assets-dir)]
-                  (p/do!
-                   (fs/mkdir-if-not-exists assets-dir)
-                   (if (:block/uuid asset-m)
-                     (fs/write-plain-text-file! repo assets-dir (str (:block/uuid asset-m) "." (:type asset-m)) content {:skip-transact? true})
-                     (when-not (:pdf-annotation? asset-m)
-                       (println "Copied asset" (pr-str (node-path/basename (:path asset-m)))
-                                "by its name since it was unused.")
-                       (fs/write-plain-text-file! repo assets-dir (node-path/basename (:path asset-m)) content {:skip-transact? true})))))))))
+(defn- read-and-copy-asset [repo repo-dir file assets buffer-handler]
+  (let [^js file-object (:file-object file)]
+    (if (assets-handler/exceed-limit-size? file-object)
+      (do
+        (js/console.log (str "Skipped copying asset " (pr-str (:path file)) " because it is larger than the 100M max."))
+        ;; This asset will also be included in the ignored-assets count. Better to be explicit about ignoring
+        ;; these so users are aware of this
+        (notification/show!
+         (str "Skipped copying asset " (pr-str (:path file)) " because it is larger than the 100M max.")
+         :info
+         false))
+      (p/let [buffer (.arrayBuffer file-object)
+              bytes-array (js/Uint8Array. buffer)
+              checksum (db-asset/<get-file-array-buffer-checksum buffer)
+              asset-id (d/squuid)
+              asset-name (gp-exporter/asset-path->name (:path file))
+              assets-dir (path/path-join repo-dir common-config/local-assets-dir)
+              asset-type (db-asset/asset-path->type (:path file))
+              {:keys [with-edn-content pdf-annotation?]} (buffer-handler bytes-array)]
+        (swap! assets assoc asset-name
+               (with-edn-content
+                 {:size (.-size file-object)
+                  :type asset-type
+                  :path (:path file)
+                  :checksum checksum
+                  :asset-id asset-id}))
+        (fs/mkdir-if-not-exists assets-dir)
+        (when-not pdf-annotation?
+          (fs/write-plain-text-file! repo assets-dir (str asset-id "." asset-type) bytes-array {:skip-transact? true}))))))
 
 (defn- import-file-graph
   [*files
@@ -404,8 +407,7 @@
                    :<save-logseq-file (fn save-logseq-file [_ path content]
                                         (db-editor-handler/save-file! path content))
                    ;; asset file options
-                   :<read-asset read-asset
-                   :<copy-asset #(copy-asset repo (config/get-repo-dir repo) %)
+                   :<read-and-copy-asset #(read-and-copy-asset repo (config/get-repo-dir repo) %1 %2 %3)
                    ;; doc file options
                    ;; Write to frontend first as writing to worker first is poor ux with slow streaming changes
                    :export-file (fn export-file [conn m opts]

+ 20 - 16
src/main/frontend/components/property.cljs

@@ -506,7 +506,7 @@
                 (pv/property-value block property opts))]]])]))))
 
 (rum/defc ordered-properties
-  [block properties* opts]
+  [block properties* sorted-property-entities opts]
   (let [[properties set-properties!] (hooks/use-state properties*)
         [properties-order set-properties-order!] (hooks/use-state (mapv first properties))
         m (zipmap (map first properties*) (map second properties*))
@@ -530,15 +530,21 @@
                {:sort-by-inner-element? true
                 :on-drag-end (fn [properties-order {:keys [active-id over-id direction]}]
                                (set-properties-order! properties-order)
-                               (let [move-down? (= direction :down)
-                                     over (db/entity (keyword over-id))
-                                     active (db/entity (keyword active-id))
-                                     over-order (:block/order over)
-                                     new-order (if move-down?
-                                                 (let [next-order (db-order/get-next-order (db/get-db) nil (:db/id over))]
-                                                   (db-order/gen-key over-order next-order))
-                                                 (let [prev-order (db-order/get-prev-order (db/get-db) nil (:db/id over))]
-                                                   (db-order/gen-key prev-order over-order)))]
+                               (p/let [;; Before reordering properties,
+                                       ;; check if the :block/order of these properties is reasonable.
+                                       normalize-tx-data (db-property/normalize-sorted-entities-block-order
+                                                          sorted-property-entities)
+                                       _ (when (seq normalize-tx-data)
+                                           (db/transact! (state/get-current-repo) normalize-tx-data))
+                                       move-down? (= direction :down)
+                                       over (db/entity (keyword over-id))
+                                       active (db/entity (keyword active-id))
+                                       over-order (:block/order over)
+                                       new-order (if move-down?
+                                                   (let [next-order (db-order/get-next-order (db/get-db) nil (:db/id over))]
+                                                     (db-order/gen-key over-order next-order))
+                                                   (let [prev-order (db-order/get-prev-order (db/get-db) nil (:db/id over))]
+                                                     (db-order/gen-key prev-order over-order)))]
                                  (db/transact! (state/get-current-repo)
                                                [{:db/id (:db/id active)
                                                  :block/order new-order}
@@ -549,12 +555,10 @@
 (rum/defc properties-section < rum/static
   [block properties opts]
   (when (seq properties)
-      ;; Sort properties by :block/order
-    (let [properties' (sort-by (fn [[k _v]]
-                                 (if (= k :logseq.property.class/properties)
-                                   "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"
-                                   (:block/order (db/entity k)))) properties)]
-      (ordered-properties block properties' opts))))
+    (let [sorted-prop-entities (db-property/sort-properties (map (comp db/entity first) properties))
+          prop-kv-map (reduce (fn [m [p v]] (assoc m p v)) {} properties)
+          properties' (keep (fn [ent] (find prop-kv-map (:db/ident ent))) sorted-prop-entities)]
+      (ordered-properties block properties' sorted-prop-entities opts))))
 
 (rum/defc hidden-properties-cp
   [block hidden-properties {:keys [root-block? sidebar-properties?] :as opts}]

+ 6 - 0
src/main/frontend/components/svg.cljs

@@ -238,6 +238,12 @@
    [:svg {:fill "none" :width size :height size :viewBox "0 0 24 24" :stroke "currentColor"}
     [:path {:stroke-linecap "round" :stroke-linejoin "round" :stroke-width "2" :d "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM13 10H7"}]]))
 
+(defn auto-fit
+  ([] (auto-fit 16))
+  ([size]
+   [:svg {:xmlns "http://www.w3.org/2000/svg" :width size :height size :fill "none" :viewBox "0 0 24 24" :stroke "currentColor"}
+    [:path {:stroke-linecap "round" :stroke-linejoin "round" :stroke-width "2" :d "M4 8V4h4m8 0h4v4M4 16v4h4m8 0h4v-4M10 14l-3-2 3-2m4 4l3-2-3-2"}]]))
+
 (defn icon-area
   ([] (icon-area 16))
   ([size]

+ 127 - 66
src/main/frontend/extensions/pdf/core.cljs

@@ -17,6 +17,7 @@
             [frontend.modules.shortcut.core :as shortcut]
             [frontend.rum :refer [use-atom]]
             [frontend.state :as state]
+            [frontend.storage :as storage]
             [frontend.ui :as ui]
             [frontend.util :as util]
             [goog.functions :refer [debounce]]
@@ -338,7 +339,7 @@
                                                       (js/setTimeout
                                                        #(do
                                                            ;; reset dom effects
-                                                          (set! (.. target -style -transform) (str "translate(0, 0)"))
+                                                          (set! (.. target -style -transform) "translate(0, 0)")
                                                           (.removeAttribute target "data-x")
                                                           (.removeAttribute target "data-y")
                                                           (let [hl' (if (de/entity? result)
@@ -351,10 +352,10 @@
 
                                        :move  (fn [^js/MouseEvent e]
                                                 (let [^js/HTMLElement target (.-target e)
-                                                      x                      (.getAttribute target "data-x")
-                                                      y                      (.getAttribute target "data-y")
-                                                      bx                     (if-not (nil? x) (js/parseFloat x) 0)
-                                                      by                     (if-not (nil? y) (js/parseFloat y) 0)]
+                                                      x (.getAttribute target "data-x")
+                                                      y (.getAttribute target "data-y")
+                                                      bx (if-not (nil? x) (js/parseFloat x) 0)
+                                                      by (if-not (nil? y) (js/parseFloat y) 0)]
 
                                                   ;; update element style
                                                   (set! (.. target -style -width) (str (.. e -rect -width) "px"))
@@ -404,60 +405,60 @@
 (rum/defc ^:large-vars/cleanup-todo pdf-highlight-area-selection
   [^js viewer {:keys [show-ctx-menu!]}]
 
-  (let [^js viewer-clt          (.. viewer -viewer -classList)
-        ^js cnt-el              (.-container viewer)
-        *el                     (rum/use-ref nil)
-        *start-el               (rum/use-ref nil)
-        *cnt-rect               (rum/use-ref nil)
-        *page-el                (rum/use-ref nil)
-        *page-rect              (rum/use-ref nil)
-        *start-xy               (rum/use-ref nil)
+  (let [^js viewer-clt (.. viewer -viewer -classList)
+        ^js cnt-el (.-container viewer)
+        *el (rum/use-ref nil)
+        *start-el (rum/use-ref nil)
+        *cnt-rect (rum/use-ref nil)
+        *page-el (rum/use-ref nil)
+        *page-rect (rum/use-ref nil)
+        *start-xy (rum/use-ref nil)
 
         [start, set-start!] (rum/use-state nil)
         [end, set-end!] (rum/use-state nil)
         [_ set-area-mode!] (use-atom *area-mode?)
 
-        should-start            (fn [^js e]
-                                  (let [^js target (.-target e)]
-                                    (when (and (not (.contains (.-classList target) "extensions__pdf-hls-area-region"))
-                                               (.closest target ".page"))
-                                      (and e (or (.-metaKey e)
-                                                 (.-shiftKey e)
-                                                 @*area-mode?)))))
-
-        reset-coords!           #(do
-                                   (set-start! nil)
-                                   (set-end! nil)
-                                   (rum/set-ref! *start-xy nil)
-                                   (rum/set-ref! *start-el nil)
-                                   (rum/set-ref! *cnt-rect nil)
-                                   (rum/set-ref! *page-el nil)
-                                   (rum/set-ref! *page-rect nil))
-
-        calc-coords!            (fn [page-x page-y]
-                                  (when cnt-el
-                                    (let [cnt-rect    (rum/deref *cnt-rect)
-                                          cnt-rect    (or cnt-rect (bean/->clj (.toJSON (.getBoundingClientRect cnt-el))))
-                                          page-rect   (rum/deref *page-rect)
-                                          [start-x, start-y] (rum/deref *start-xy)
-                                          dx-left?    (> start-x page-x)
-                                          dy-top?     (> start-y page-y)
-                                          page-left   (:left page-rect)
-                                          page-right  (:right page-rect)
-                                          page-top    (:top page-rect)
-                                          page-bottom (:bottom page-rect)
-                                          _           (rum/set-ref! *cnt-rect cnt-rect)]
-
-                                      {:x (-> page-x
-                                              (#(if dx-left?
-                                                  (if (< % page-left) page-left %)
-                                                  (if (> % page-right) page-right %)))
-                                              (+ (.-scrollLeft cnt-el)))
-                                       :y (-> page-y
-                                              (#(if dy-top?
-                                                  (if (< % page-top) page-top %)
-                                                  (if (> % page-bottom) page-bottom %)))
-                                              (+ (.-scrollTop cnt-el)))})))
+        should-start (fn [^js e]
+                       (let [^js target (.-target e)]
+                         (when (and (not (.contains (.-classList target) "extensions__pdf-hls-area-region"))
+                                    (.closest target ".page"))
+                           (and e (or (.-metaKey e)
+                                      (.-shiftKey e)
+                                      @*area-mode?)))))
+
+        reset-coords! #(do
+                         (set-start! nil)
+                         (set-end! nil)
+                         (rum/set-ref! *start-xy nil)
+                         (rum/set-ref! *start-el nil)
+                         (rum/set-ref! *cnt-rect nil)
+                         (rum/set-ref! *page-el nil)
+                         (rum/set-ref! *page-rect nil))
+
+        calc-coords! (fn [page-x page-y]
+                       (when cnt-el
+                         (let [cnt-rect    (rum/deref *cnt-rect)
+                               cnt-rect    (or cnt-rect (bean/->clj (.toJSON (.getBoundingClientRect cnt-el))))
+                               page-rect   (rum/deref *page-rect)
+                               [start-x, start-y] (rum/deref *start-xy)
+                               dx-left?    (> start-x page-x)
+                               dy-top?     (> start-y page-y)
+                               page-left   (:left page-rect)
+                               page-right  (:right page-rect)
+                               page-top    (:top page-rect)
+                               page-bottom (:bottom page-rect)
+                               _           (rum/set-ref! *cnt-rect cnt-rect)]
+
+                           {:x (-> page-x
+                                   (#(if dx-left?
+                                       (if (< % page-left) page-left %)
+                                       (if (> % page-right) page-right %)))
+                                   (+ (.-scrollLeft cnt-el)))
+                            :y (-> page-y
+                                   (#(if dy-top?
+                                       (if (< % page-top) page-top %)
+                                       (if (> % page-bottom) page-bottom %)))
+                                   (+ (.-scrollTop cnt-el)))})))
 
         calc-rect               (fn [start end]
                                   {:left   (min (:x start) (:x end))
@@ -631,12 +632,44 @@
                                     #js {:once true})))
 
              fn-resize
-             (partial pdf-utils/adjust-viewer-size! viewer)]
-
-         ;;(doto (.-eventBus viewer))
+             (partial pdf-utils/adjust-viewer-size! viewer)
+             fn-wheel
+             (fn [^js/WheelEvent e]
+               (when (or (.-ctrlKey e) (.-metaKey e))
+                 (let [bus (.-eventBus viewer)
+                       container (.-container viewer)
+                       rect (.getBoundingClientRect container)
+                       ;; relative position between container and mouse point
+                       mouse-x (- (.-clientX e) (.-left rect))
+                       mouse-y (- (.-clientY e) (.-top rect))
+                       scroll-left (.-scrollLeft container)
+                       scroll-top (.-scrollTop container)
+                       ;; relative position between pdf and mouse point
+                       x-ratio (/ (+ scroll-left mouse-x) (.-scrollWidth container))
+                       y-ratio (/ (+ scroll-top mouse-y) (.-scrollHeight container))
+                       current-scale (.-currentScale viewer)
+                       scale-factor 1.05                    ;; scale sensitivity
+                       new-scale (if (< (.-deltaY e) 0)
+                                   (* current-scale scale-factor) ;; scale up
+                                   (/ current-scale scale-factor))] ;; scale down
+
+                   (.preventDefault e)
+                   ;; dispatch to scale changing event
+                   (.dispatch bus "scaleChanging"
+                              #js {:source "wheel"
+                                   :scale new-scale})
+                   (js/requestAnimationFrame
+                    (fn []
+                      (set! (.-scrollLeft container)
+                            (- (* (.-scrollWidth container) x-ratio) mouse-x))
+                      (set! (.-scrollTop container)
+                            (- (* (.-scrollHeight container) y-ratio) mouse-y)))))))]
+
+;;(doto (.-eventBus viewer))
 
          (when el
-           (.addEventListener el "mousedown" fn-selection))
+           (.addEventListener el "mousedown" fn-selection)
+           (.addEventListener el "wheel" fn-wheel))
 
          (when win
            (.addEventListener win "resize" fn-resize))
@@ -646,7 +679,8 @@
             ;;(doto (.-eventBus viewer))
 
             (when el
-              (.removeEventListener el "mousedown" fn-selection))
+              (.removeEventListener el "mousedown" fn-selection)
+              (.removeEventListener el "wheel" fn-wheel))
 
             (when win
               (.removeEventListener win "resize" fn-resize)))))
@@ -733,8 +767,7 @@
        :add-hl! add-hl!})]))
 
 (rum/defc ^:large-vars/data-var pdf-viewer
-  [_url ^js pdf-document {:keys [identity filename initial-hls initial-page initial-error]} ops]
-
+  [_url ^js pdf-document {:keys [identity filename initial-hls initial-page initial-scale initial-error]} ops]
   (let [*el-ref (rum/create-ref)
         [state, set-state!] (rum/use-state {:viewer nil :bus nil :link nil :el nil})
         [ano-state, set-ano-state!] (rum/use-state {:loaded-pages []})
@@ -768,12 +801,18 @@
            ;; it must be initialized before set-up document
            (.on "pagesinit"
                 (fn []
-                  (set! (. viewer -currentScaleValue) "auto")
+                  (set! (. viewer -currentScaleValue) (or initial-scale "auto"))
                   (set-page-ready! true)))
-
+           (.on "resizing"
+                #(when (= (. viewer -currentScaleValue) "auto")
+                   (set! (. viewer -currentScaleValue) "auto")))
            (.on (name :ls-update-extra-state)
                 #(when-let [extra (bean/->clj %)]
-                   (apply (:set-hls-extra! ops) [extra]))))
+                   (apply (:set-hls-extra! ops) [extra])))
+           (.on (name :scaleChanging)
+                #(when-let [data (bean/->clj %)]
+                   (set! (. viewer -currentScaleValue) (:scale data))
+                   (apply (:set-hls-extra! ops) [data]))))
 
          (p/then (. viewer setDocument pdf-document)
                  #(set-state! {:viewer viewer :bus event-bus :link link-service :el el}))
@@ -868,6 +907,9 @@
 (defonce debounced-set-property!
   (debounce property-handler/set-block-property! 300))
 
+(defonce debounced-set-storage!
+  (debounce storage/set 300))
+
 (defn- debounce-set-last-visit-page!
   [asset last-visit-page]
   (when (and (number? last-visit-page)
@@ -877,6 +919,17 @@
                              :logseq.property.asset/last-visit-page
                              last-visit-page)))
 
+(defn- debounce-set-last-visit-scale!
+  [asset last-visit-scale]
+  (when (or (number? last-visit-scale)
+            (string? last-visit-scale))
+    (debounced-set-storage! (str "pdf-last-visit-scale/" (:db/id asset)) (or last-visit-scale "auto"))))
+
+(defn- get-last-visit-scale
+  [asset]
+  (or (storage/get (str "pdf-last-visit-scale/" (:db/id asset)))
+      "auto"))
+
 (rum/defc ^:large-vars/data-var pdf-loader
   [{:keys [url hls-file identity filename] :as pdf-current}]
   (let [repo           (state/get-current-repo)
@@ -886,11 +939,14 @@
         [hls-state, set-hls-state!] (rum/use-state {:initial-hls nil :latest-hls nil :extra nil :loaded false :error nil})
         [doc-password, set-doc-password!] (rum/use-state nil) ;; use nil to handle empty string
         [initial-page, set-initial-page!] (rum/use-state 1)
+        [initial-scale, set-initial-scale!] (rum/use-state "auto")
         set-dirty-hls! (fn [latest-hls]                     ;; TODO: incremental
                          (set-hls-state! #(merge % {:initial-hls [] :latest-hls latest-hls})))
         set-hls-extra! (fn [extra]
                          (if db-based?
-                           (debounce-set-last-visit-page! (:block pdf-current) (:page extra))
+                           (do
+                             (debounce-set-last-visit-scale! (:block pdf-current) (:scale extra))
+                             (debounce-set-last-visit-page! (:block pdf-current) (:page extra)))
                            (set-hls-state! #(merge % {:extra extra}))))]
 
     ;; current pdf effects
@@ -912,6 +968,7 @@
                (set-initial-page! (or
                                    (:logseq.property.asset/last-visit-page pdf-block)
                                    1))
+               (set-initial-scale! (get-last-visit-scale pdf-block))
                (set-hls-state! {:initial-hls highlights :latest-hls highlights :loaded true})))))
        [pdf-current])
       (hooks/use-effect!
@@ -921,6 +978,7 @@
                   {:keys [highlights extra]} data]
             (set-initial-page! (or (when-let [page (:page extra)]
                                      (util/safe-parse-int page)) 1))
+            (set-initial-scale! (or (:scale extra) "auto"))
             (set-hls-state! {:initial-hls highlights :latest-hls highlights :extra extra :loaded true}))
 
           ;; error
@@ -971,7 +1029,9 @@
                             :cMapUrl       (str (if (some-> js/location.host (string/ends-with? "logseq.com"))
                                                   "./static/" "./") "js/pdfjs/cmaps/")
                             ;:cMapUrl       "https://cdn.jsdelivr.net/npm/[email protected]/cmaps/"
-                            :cMapPacked true}]
+                            :cMapPacked true
+                            :supportsMouseWheelZoomCtrlKey true
+                            :supportsMouseWheelZoomMetaKey true}]
          (set-loader-state! {:status :loading})
 
          (-> (get-doc$ (clj->js opts))
@@ -1043,6 +1103,7 @@
                              :filename      filename
                              :initial-hls   initial-hls
                              :initial-page  initial-page
+                             :initial-scale initial-scale
                              :initial-error initial-error}
                             {:set-dirty-hls! set-dirty-hls!
                              :set-hls-extra! set-hls-extra!}) "pdf-viewer")])))])))

+ 24 - 6
src/main/frontend/extensions/pdf/toolbar.cljs

@@ -26,7 +26,7 @@
 
 (declare make-docinfo-in-modal)
 
-(def *area-dashed? (atom ((fnil identity false) (storage/get (str "ls-pdf-area-is-dashed")))))
+(def *area-dashed? (atom ((fnil identity false) (storage/get "ls-pdf-area-is-dashed"))))
 (def *area-mode? (atom false))
 (def *highlight-mode? (atom false))
 #_:clj-kondo/ignore
@@ -489,7 +489,15 @@
         [viewer-theme, set-viewer-theme!] (rum/use-state (or (storage/get "ls-pdf-viewer-theme") "light"))
         group-id          (.-$groupIdentity viewer)
         in-system-window? (.-$inSystemWindow viewer)
-        doc               (pdf-windows/resolve-own-document viewer)]
+        doc               (pdf-windows/resolve-own-document viewer)
+        dispatch-extra-state!
+        (fn []
+          (js/setTimeout
+           (fn []
+             (let [scale (.-currentScaleValue viewer)]
+               (.dispatch (.-eventBus viewer) (name :ls-update-extra-state)
+                          #js {:page current-page-num :scale scale})))
+           100))]
 
     ;; themes hooks
     (hooks/use-effect!
@@ -504,8 +512,7 @@
     (hooks/use-effect!
      (fn []
        (when viewer
-         (.dispatch (.-eventBus viewer) (name :ls-update-extra-state)
-                    #js {:page current-page-num})))
+         (dispatch-extra-state!)))
      [viewer current-page-num])
 
     ;; pager hooks
@@ -556,14 +563,25 @@
         ;; zoom
         [:a.button
          {:title    "Zoom out"
-          :on-click (partial pdf-utils/zoom-out-viewer viewer)}
+          :on-click (fn []
+                      (pdf-utils/zoom-out-viewer viewer)
+                      (dispatch-extra-state!))}
          (svg/zoom-out 18)]
 
         [:a.button
          {:title    "Zoom in"
-          :on-click (partial pdf-utils/zoom-in-viewer viewer)}
+          :on-click (fn []
+                      (pdf-utils/zoom-in-viewer viewer)
+                      (dispatch-extra-state!))}
          (svg/zoom-in 18)]
 
+        [:a.button
+         {:title    "Auto fit"
+          :on-click (fn []
+                      (pdf-utils/reset-viewer-auto! viewer)
+                      (dispatch-extra-state!))}
+         (svg/auto-fit 18)]
+
         [:a.button
          {:title    "Outline"
           :on-click #(set-outline-visible! (not outline-visible?))}

+ 7 - 5
src/main/frontend/extensions/pdf/utils.cljs

@@ -2,7 +2,6 @@
   (:require ["/frontend/extensions/pdf/utils" :as js-utils]
             [cljs-bean.core :as bean]
             [clojure.string :as string]
-            [frontend.util :as util]
             [logseq.common.uuid :as common-uuid]
             [promesa.core :as p]))
 
@@ -109,10 +108,13 @@
   ([^js win]
    (some-> win (.getSelection) (.removeAllRanges))))
 
-(def adjust-viewer-size!
-  (util/debounce
-   (fn [^js viewer] (set! (. viewer -currentScaleValue) "auto"))
-   200))
+(defn adjust-viewer-size!
+  [^js viewer]
+  (let [bus (.-eventBus viewer)]
+    (.dispatch bus "resizing")))
+
+(defn reset-viewer-auto! [^js viewer]
+  (set! (. viewer -currentScaleValue) "auto"))
 
 (defn fix-nested-js
   [its]

+ 5 - 0
src/main/frontend/handler/assets.cljs

@@ -16,6 +16,11 @@
             [promesa.core :as p])
   (:import [missionary Cancelled]))
 
+(defn exceed-limit-size?
+  "Asset size no more than 100M"
+  [^js file]
+  (> (.-size file) (* 100 1024 1024)))
+
 (defn alias-enabled?
   []
   (and (util/electron?)

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

@@ -1555,10 +1555,10 @@
                    dir repo-dir
                    asset (db/entity :logseq.class/Asset)]
 
-             (if (> (.-size file) (* 100 1024 1024)) ; 100m
+             (if (assets-handler/exceed-limit-size? file)
                (do
                  (notification/show! [:div "Asset size shouldn't be larger than 100M"]
-                                     :error
+                                     :warning
                                      false)
                  (throw (ex-info "Asset size shouldn't be larger than 100M" {:file-name file-name})))
                (p/let [properties {:logseq.property.asset/type ext

+ 2 - 2
src/main/frontend/worker/db_worker.cljs

@@ -86,8 +86,8 @@
   (when-not @*sqlite
     (p/let [href (.. js/location -href)
             publishing? (string/includes? href "publishing=true")
-            sqlite (sqlite3InitModule (clj->js {:print js/console.log
-                                                :printErr js/console.error}))]
+            sqlite (sqlite3InitModule (clj->js {:print #(log/info :init-sqlite-module! %)
+                                                :printErr #(log/error :init-sqlite-module! %)}))]
       (reset! *publishing? publishing?)
       (reset! *sqlite sqlite)
       nil)))