Просмотр исходного кода

Merge branch 'master' into refactor/libs

charlie 6 дней назад
Родитель
Сommit
35b68df403
26 измененных файлов с 469 добавлено и 109 удалено
  1. 1 0
      deps/db/src/logseq/db/frontend/property.cljs
  2. 11 6
      deps/graph-parser/script/db_import.cljs
  3. 162 82
      deps/graph-parser/src/logseq/graph_parser/exporter.cljs
  4. 118 5
      deps/graph-parser/test/logseq/graph_parser/exporter_test.cljs
  5. BIN
      deps/graph-parser/test/resources/book/it/Understanding EXPLAIN.pdf
  6. 53 0
      deps/graph-parser/test/resources/exporter-test-graph/assets/Understanding EXPLAIN.edn
  7. BIN
      deps/graph-parser/test/resources/exporter-test-graph/assets/Understanding EXPLAIN/8_69787518-9eb8-4fd0-ad3b-b394e28bb547_1769501975346.png
  8. 36 0
      deps/graph-parser/test/resources/exporter-test-graph/assets/zlib.edn
  9. BIN
      deps/graph-parser/test/resources/exporter-test-graph/assets/zlib/1_697873d7-5953-4753-897e-62ac47348634_1769501651181.png
  10. 0 2
      deps/graph-parser/test/resources/exporter-test-graph/journals/2024_02_16.md
  11. 3 1
      deps/graph-parser/test/resources/exporter-test-graph/journals/2024_02_23.md
  12. 2 0
      deps/graph-parser/test/resources/exporter-test-graph/journals/2026_01_01.md
  13. 3 0
      deps/graph-parser/test/resources/exporter-test-graph/logseq/config.edn
  14. 5 0
      deps/graph-parser/test/resources/exporter-test-graph/pages/Understanding EXPLAIN.md
  15. 20 0
      deps/graph-parser/test/resources/exporter-test-graph/pages/hls__Understanding EXPLAIN.md
  16. 15 0
      deps/graph-parser/test/resources/exporter-test-graph/pages/hls__zlib.md
  17. 5 0
      deps/graph-parser/test/resources/exporter-test-graph/pages/zlib.md
  18. BIN
      deps/graph-parser/test/resources/zotero/storage/RX5JS7SY/zlib.pdf
  19. 1 2
      src/electron/electron/handler.cljs
  20. 2 0
      src/electron/electron/window.cljs
  21. 1 1
      src/main/frontend/components/block.cljs
  22. 3 1
      src/main/frontend/components/page.cljs
  23. 2 1
      src/main/frontend/components/property.cljs
  24. 1 6
      src/main/frontend/components/theme.cljs
  25. 5 1
      src/main/frontend/db/model.cljs
  26. 20 1
      src/main/frontend/extensions/pdf/assets.cljs

+ 1 - 0
deps/db/src/logseq/db/frontend/property.cljs

@@ -532,6 +532,7 @@
                                                    :hide? false
                                                    :public? true}
                                           :queryable? true}
+     ;; need to rename for better alignment with practical purposes
      :logseq.property.asset/external-file-name {:title "External file name"
                                                 :schema {:type :string
                                                          :hide? true

+ 11 - 6
deps/graph-parser/script/db_import.cljs

@@ -103,18 +103,21 @@
     (js/process.exit 1)))
 
 (defn default-export-options
-  [options]
+  [file-graph-dir options]
   {;; common options
    :rpath-key ::rpath
    :notify-user (partial notify-user options)
    :<read-file <read-file
    ;; :set-ui-state prn
+
    ;; config file options
    ;; TODO: Add actual default
    :default-config {}
-   ;; TODO: Add zotero support
-   ;; :<get-file-stat (fn [path])
-   })
+   :<get-file-stat (fn [path]
+                     (let [abs-path (if (node-path/isAbsolute path)
+                                      path
+                                      (node-path/resolve file-graph-dir path))]
+                       (fsp/stat abs-path)))})
 
 (defn- import-file-graph-to-db
   "Import a file graph dir just like UI does. However, unlike the UI the
@@ -125,7 +128,7 @@
         config-file (first (filter #(string/ends-with? (:path %) "logseq/config.edn") *files))
         _ (assert config-file "No 'logseq/config.edn' found for file graph dir")
         options (merge options
-                       (default-export-options options)
+                       (default-export-options file-graph-dir options)
                         ;; asset file options
                        {:<read-and-copy-asset #(<read-and-copy-asset db-graph-dir %1 %2 %3)})]
     (p/with-redefs [d/transact! dev-transact!]
@@ -141,7 +144,9 @@
 (defn- import-files-to-db
   "Import specific doc files for dev purposes"
   [file conn {:keys [files] :as options}]
-  (let [doc-options (gp-exporter/build-doc-options {:macros {}} (merge options (default-export-options options)))
+  (let [doc-options (gp-exporter/build-doc-options {:macros {}}
+                                                   ;; Pass file-graph-dir as nil since individual files don't specify it
+                                                   (merge options (default-export-options nil options)))
         files' (mapv #(hash-map :path %)
                      (into [file] (map resolve-path files)))]
     (p/with-redefs [d/transact! dev-transact!]

+ 162 - 82
deps/graph-parser/src/logseq/graph_parser/exporter.cljs

@@ -612,6 +612,8 @@
       ;; Change to :node as dates can be pages but pages can't be dates
       (= {:from :date :to :node} type-change)
       (do
+        (swap! upstream-properties assoc prop {:schema {:logseq.property/type :node}
+                                               :from-type :date})
         (swap! property-schemas assoc-in [prop :logseq.property/type] :node)
         (update-page-or-date-values page-names-to-uuids val))
 
@@ -934,7 +936,7 @@
       (when (and link id label)
         (when-let [zotero-data-dir (get-in config [:zotero/settings-v2 "default" :zotero-data-directory])]
           {:link (str "zotero://" link)
-           :path (node-path/join zotero-data-dir "storage" id label)
+           :path (path/path-join zotero-data-dir "storage" id label)
            :base label})))))
 
 (defn- walk-ast-blocks
@@ -943,13 +945,15 @@
   [config ast-blocks]
   (let [results (atom {:simple-queries []
                        :asset-links []
-                       :embeds []})]
+                       :embeds []
+                       :zotero-imported-files {}
+                       :zotero-linked-files []})]
     (walk/prewalk
      (fn [x]
        (cond
-         (and (vector? x)
-              (= "Link" (first x))
-              (let [path-or-map (second (:url (second x)))]
+        (and (vector? x)
+             (= "Link" (first x))
+             (let [path-or-map (second (:url (second x)))]
                 (cond
                   (string? path-or-map)
                   (or (common-config/local-relative-asset? path-or-map)
@@ -963,6 +967,19 @@
               (= "Macro" (first x))
               (= "embed" (:name (second x))))
          (swap! results update :embeds conj x)
+        (and (vector? x)
+             (= "Macro" (first x))
+             (= "zotero-imported-file" (:name (second x))))
+        (let [[item-key filename] (:arguments (second x))]
+          (when (and item-key filename)
+            (swap! results update :zotero-imported-files assoc item-key (common-util/safe-read-string filename))))
+        (and (vector? x)
+             (= "Macro" (first x))
+             (= "zotero-linked-file" (:name (second x))))
+        (let [[relative-path] (:arguments (second x))
+              parsed-path (common-util/safe-read-string relative-path)]
+          (when (string? parsed-path)
+            (swap! results update :zotero-linked-files conj parsed-path)))
          (and (vector? x)
               (= "Macro" (first x))
               (= "query" (:name (second x))))
@@ -1229,10 +1246,10 @@
 (defn- build-pdf-annotations-tx
   "Builds tx for pdf annotations when a pdf has an annotations EDN file under assets/"
   [parent-asset-path assets parent-asset pdf-annotation-pages opts]
-  (let [asset-edn-path (path/path-normalize
-                        (node-path/join common-config/local-assets-dir
-                                        (safe-sanitize-file-name
-                                         (node-path/basename (string/replace-first parent-asset-path #"(?i)\.pdf$" ".edn")))))
+  (let [asset-edn-path (path/path-join
+                        common-config/local-assets-dir
+                        (safe-sanitize-file-name
+                         (node-path/basename (string/replace-first parent-asset-path #"(?i)\.pdf$" ".edn"))))
         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)]
@@ -1240,79 +1257,127 @@
         (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- resolve-asset-data
+  [asset-link user-config linked-files linked-base-dir zotero-imported-files]
+  (let [link-map (second asset-link)
+        path* (-> link-map :url second)
+        zotero-path-data (when (map? path*)
+                           (get-zotero-local-pdf-path user-config link-map))
+        zotero-asset? (some? zotero-path-data)
+        linked-relative (when (and linked-files zotero-asset? (seq @linked-files))
+                          (let [value (first @linked-files)]
+                            (swap! linked-files rest)
+                            (string/replace-first value "attachments:" "")))
+        linked-base (when (string? linked-relative)
+                      (node-path/basename linked-relative))
+        linked-path (when (and (string? linked-relative)
+                               (string? linked-base-dir)
+                               (not (string/blank? linked-base-dir)))
+                      (node-path/join linked-base-dir linked-relative))
+        {:keys [path link base]} (cond
+                                   linked-path {:path linked-path
+                                                :link (:link zotero-path-data)
+                                                :base linked-base}
+                                   zotero-asset? zotero-path-data
+                                   :else {:path path*})
+        asset-name (cond
+                     linked-path base
+                     zotero-asset? (or (get zotero-imported-files (last (string/split link #"/"))) base)
+                     :else (some-> path asset-path->name))
+        path (cond
+               linked-path path
+               (and zotero-asset? asset-name) (string/replace path #"[^/]+$" asset-name)
+               :else path)
+        asset-link-or-name (or link asset-name)
+        asset-path (when zotero-asset?
+                     (if linked-path
+                       (str "zotero-link://" linked-relative)
+                       (str "zotero-path://" (string/join ns-util/namespace-char [(last (string/split link #"/")) asset-name]))))]
+    {:asset-link-or-name asset-link-or-name
+     :asset-name asset-name
+     :asset-path asset-path
+     :path path
+     :zotero-asset? zotero-asset?}))
+
+(defn- ensure-asset-data!
+  [assets asset-link-or-name path asset-path <get-file-stat]
+  (when (and asset-link-or-name
+             (not (get @assets asset-link-or-name))
+             (string/ends-with? path ".pdf")
+             (fn? <get-file-stat))
+    (-> (p/let [^js stat (<get-file-stat path)]
+          (swap! assets assoc asset-link-or-name
+                 {:asset-id (d/squuid)
+                  :type "pdf"
+                  ;; avoid using the real checksum since it could be the same with in-graph asset
+                  :checksum "0000000000000000000000000000000000000000000000000000000000000000"
+                  :size (.-size stat)
+                  :external-url (or asset-link-or-name path)
+                  :external-file-name asset-path}))
+        (p/catch (fn [error]
+                   (js/console.error error))))))
+
+(defn- build-asset-tx
+  [asset-data asset-name asset-link-or-name asset-link pdf-annotation-pages opts assets zotero-asset?]
+  (let [new-asset (merge (build-new-asset asset-data)
+                         {:block/title (db-asset/asset-name->title (node-path/basename asset-name))
+                          :block/uuid (get-asset-block-id assets asset-link-or-name)}
+                         (when-let [metadata (not-empty (common-util/safe-read-map-string (:metadata (second asset-link))))]
+                           {:logseq.property.asset/resize-metadata metadata}))
+        pdf-annotations-path (if (and zotero-asset? (string? asset-name))
+                               (path/path-join common-config/local-assets-dir asset-name)
+                               (or asset-name asset-link-or-name))
+        pdf-annotations-tx (when (= "pdf" (path/file-ext pdf-annotations-path))
+                             (build-pdf-annotations-tx pdf-annotations-path assets new-asset pdf-annotation-pages opts))
+        asset-tx (concat [new-asset] pdf-annotations-tx)]
+    ;; (prn :asset-added! (node-path/basename asset-name))
+    ;; (cljs.pprint/pprint asset-link)
+    ;; (prn :debug :asset-tx asset-tx)
+    (swap! assets assoc-in [asset-link-or-name :asset-created?] true)
+    {:asset-name-uuid [asset-link-or-name (:block/uuid new-asset)]
+     :asset-tx asset-tx}))
+
 (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]} {:keys [notify-user <get-file-stat user-config] :as opts}]
-  (if (seq asset-links)
-    (p/let [asset-maps* (p/all (map
-                                (fn [asset-link]
-                                  (p/let [path* (-> asset-link second :url second)
-                                          zotero-asset? (when (map? path*)
-                                                          (= "zotero" (:protocol (second (:url (second asset-link))))))
-                                          {:keys [path link base]} (if (map? path*)
-                                                                     (get-zotero-local-pdf-path user-config (second asset-link))
-                                                                     {:path path*})
-                                          asset-name (some-> path asset-path->name)
-                                          asset-link-or-name (or link (some-> path asset-path->name))
-                                          asset-data* (when asset-link-or-name (get @assets asset-link-or-name))
-                                          _ (when (and asset-link-or-name
-                                                       (not asset-data*)
-                                                       (string/ends-with? path ".pdf")
-                                                       (fn? <get-file-stat)) ; external pdf
-                                              (->
-                                               (p/let [^js stat (<get-file-stat path)]
-                                                 (swap! assets assoc asset-link-or-name
-                                                        {:asset-id (d/squuid)
-                                                         :type "pdf"
-                                                         ;; avoid using the real checksum since it could be the same with in-graph asset
-                                                         :checksum "0000000000000000000000000000000000000000000000000000000000000000"
-                                                         :size (.-size stat)
-                                                         :external-url (or link path)
-                                                         :external-file-name base}))
-                                               (p/catch (fn [error]
-                                                          (js/console.error error)))))
-                                          asset-data (when asset-link-or-name (get @assets asset-link-or-name))]
-                                    (if asset-data
-                                      (cond
-                                        (not (get-asset-block-id assets asset-link-or-name))
-                                        (notify-user {:msg (str "Skipped creating asset " (pr-str asset-link-or-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-link-or-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/uuid (get-asset-block-id assets asset-link-or-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-link-or-name))
-                                                                   (build-pdf-annotations-tx asset-link-or-name assets new-asset pdf-annotation-pages opts))
-                                              asset-tx (concat [new-asset] pdf-annotations-tx)]
-                                          ;; (prn :asset-added! (node-path/basename asset-name))
-                                          ;; (cljs.pprint/pprint asset-link)
-                                          ;; (prn :debug :asset-tx asset-tx)
-                                          (swap! assets assoc-in [asset-link-or-name :asset-created?] true)
-                                          {:asset-name-uuid [asset-link-or-name (:block/uuid new-asset)]
-                                           :asset-tx asset-tx}))
-                                      (when-not zotero-asset? ; no need to report warning for zotero managed pdf files
-                                        (swap! ignored-assets conj
-                                               {:reason "No asset data found for this asset path"
-                                                :path (-> asset-link second :url second)
-                                                :location {:block (:block/title block)}})
-                                        nil))))
-                                asset-links))
-            asset-maps (remove nil? asset-maps*)
-            asset-blocks (mapcat :asset-tx asset-maps)
-            asset-names-to-uuids
-            (into {} (map :asset-name-uuid asset-maps))]
-      (cond-> {:block
-               (update block :block/title update-asset-links-in-block-title asset-names-to-uuids ignored-assets)}
-        (seq asset-blocks)
-        (assoc :asset-blocks-tx asset-blocks)))
-    (p/resolved {:block block})))
+  [block {:keys [asset-links zotero-imported-files zotero-linked-files]} {:keys [assets ignored-assets pdf-annotation-pages]} {:keys [notify-user <get-file-stat user-config] :as opts}]
+  (let [linked-files (when (seq zotero-linked-files) (atom zotero-linked-files))
+        linked-base-dir (when linked-files
+                          (get-in user-config [:zotero/settings-v2 "default" :zotero-linked-attachment-base-directory]))]
+    (if (seq asset-links)
+      (p/let [asset-maps* (p/all (map
+                                  (fn [asset-link]
+                                    (p/let [{:keys [asset-link-or-name asset-name asset-path path zotero-asset?]}
+                                            (resolve-asset-data asset-link user-config linked-files linked-base-dir zotero-imported-files)
+                                            _ (ensure-asset-data! assets asset-link-or-name path asset-path <get-file-stat)
+                                            asset-data (when asset-link-or-name (get @assets asset-link-or-name))]
+                                      (if asset-data
+                                        (cond
+                                          (not (get-asset-block-id assets asset-link-or-name))
+                                          (notify-user {:msg (str "Skipped creating asset " (pr-str asset-link-or-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-link-or-name (:asset-id asset-data)]}
+
+                                          :else
+                                          (build-asset-tx asset-data asset-name asset-link-or-name asset-link pdf-annotation-pages opts assets zotero-asset?))
+                                        (when-not zotero-asset? ; no need to report warning for zotero managed pdf files
+                                          (swap! ignored-assets conj
+                                                 {:reason "No asset data found for this asset path"
+                                                  :path (-> asset-link second :url second)
+                                                  :location {:block (:block/title block)}})
+                                          nil))))
+                                  asset-links))
+              asset-maps (remove nil? asset-maps*)
+              asset-blocks (mapcat :asset-tx asset-maps)
+              asset-names-to-uuids
+              (into {} (map :asset-name-uuid asset-maps))]
+        (cond-> {:block
+                 (update block :block/title update-asset-links-in-block-title asset-names-to-uuids ignored-assets)}
+          (seq asset-blocks)
+          (assoc :asset-blocks-tx asset-blocks)))
+      (p/resolved {:block block}))))
 
 (defn- handle-quotes
   "If a block contains a quote, convert block to #Quote node"
@@ -1359,7 +1424,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 :notify-user :<get-file-stat]))
+          (<handle-assets-in-block block-after-built-in-props walked-ast-blocks import-state (select-keys options [:log-fn :notify-user :<get-file-stat :user-config]))
           ;; :block/page should be [:block/page NAME]
 
           journal-page-created-at (some-> (:block/page block*) second journal-created-ats)
@@ -1976,6 +2041,21 @@
                                  :level :error
                                  :ex-data {:error error}}))))))
 
+(defn- resolve-zotero-config-path
+  [config config-file]
+  (let [config-path (:path config-file)
+        base-dir (when (and (string? config-path)
+                            (node-path/isAbsolute config-path))
+                   ;; config.edn lives in <graph-root>/logseq/config.edn
+                   (node-path/dirname (node-path/dirname config-path)))
+        to-abs (fn [p]
+                 (if (and base-dir (string? p) (not (string/blank? p)) (not (node-path/isAbsolute p)))
+                   (path/path-join base-dir p)
+                   p))]
+    (-> config
+        (update-in [:zotero/settings-v2 "default" :zotero-data-directory] to-abs)
+        (update-in [:zotero/settings-v2 "default" :zotero-linked-attachment-base-directory] to-abs))))
+
 (defn export-config-file
   "Exports logseq/config.edn by saving to database and setting any properties related to config"
   [repo-or-conn config-file <read-file {:keys [<save-file notify-user default-config]
@@ -1988,7 +2068,7 @@
                             ;; Converts a file graph config.edn for use with DB graphs. Unlike common-config/create-config-for-db-graph,
                             ;; manually dissoc deprecated keys for config to be valid
                             (pretty-print-dissoc % (keys common-config/file-only-config)))
-                (let [config (edn/read-string %)]
+                (let [config (resolve-zotero-config-path (edn/read-string %) config-file)]
                   (when-let [title-format (or (:journal/page-title-format config) (:date-formatter config))]
                     (ldb/transact! repo-or-conn [{:db/ident :logseq.class/Journal
                                                   :logseq.property.journal/title-format title-format}]))

+ 118 - 5
deps/graph-parser/test/logseq/graph_parser/exporter_test.cljs

@@ -125,6 +125,12 @@
         options' (merge default-export-options
                         {:user-options (merge {:convert-all-tags? false} (dissoc options :assets :verbose))
                         ;; asset file options
+                         :<get-file-stat (fn [path]
+                                           (let [abs-path (if (node-path/isAbsolute path)
+                                                            path
+                                                            (node-path/resolve file-graph-dir path))]
+                                             ;; inline require to allow cljs tests to run
+                                             (.stat (js/require "fs/promises") abs-path)))
                          :<read-and-copy-asset (fn [file *assets buffer-handler]
                                                  (<read-and-copy-asset file *assets buffer-handler assets))}
                         (select-keys options [:verbose]))]
@@ -210,17 +216,17 @@
 
       ;; Counts
       ;; Includes journals as property values e.g. :logseq.property/deadline
-      (is (= 32 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Journal]] @conn))))
+      (is (= 33 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Journal]] @conn))))
 
-      (is (= 5 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Asset]] @conn))))
+      (is (= 9 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Asset]] @conn))))
       (is (= 5 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Task]] @conn))))
       (is (= 4 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Query]] @conn))))
       (is (= 2 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Card]] @conn))))
       (is (= 5 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Quote-block]] @conn))))
-      (is (= 2 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Pdf-annotation]] @conn))))
+      (is (= 7 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Pdf-annotation]] @conn))))
 
       ;; Properties and tags aren't included in this count as they aren't a Page
-      (is (= 11
+      (is (= 13
              (->> (d/q '[:find [?b ...]
                          :where
                          [?b :block/title]
@@ -239,7 +245,8 @@
       (is (= 0 (count @(:ignored-properties import-state))) "No ignored properties")
       (is (= 0 (count @(:ignored-assets import-state))) "No ignored assets")
       (is (= 1 (count @(:ignored-files import-state))) "Ignore .edn for now")
-      (is (= 5 (count @assets))))
+      ;; 2 zotero pdf are external files so not counted here
+      (is (= 7 (count @assets))))
 
     (testing "logseq files"
       (is (= ".foo {}\n"
@@ -419,6 +426,28 @@
               :logseq.property.asset/resize-metadata {:height 288, :width 252}}
              (db-test/readable-properties (db-test/find-block-by-content @conn "greg-popovich-thumbs-up_1704749687791_0")))
           "Asset has correct properties")
+      (is (= {:block/tags [:logseq.class/Asset]
+              :logseq.property.asset/type "pdf"
+              :logseq.property.asset/external-url "zotero://select/library/items/QDM8H6EH"
+              :logseq.property.asset/external-file-name "zotero-link://it/Understanding EXPLAIN.pdf"}
+             (select-keys
+              (db-test/readable-properties (db-test/find-block-by-content @conn "Understanding EXPLAIN"))
+              [:block/tags
+               :logseq.property.asset/type
+               :logseq.property.asset/external-url
+               :logseq.property.asset/external-file-name]))
+          "Zotero linked pdf asset has correct external path info")
+      (is (= {:block/tags [:logseq.class/Asset]
+              :logseq.property.asset/type "pdf"
+              :logseq.property.asset/external-url "zotero://select/library/items/RX5JS7SY"
+              :logseq.property.asset/external-file-name "zotero-path://RX5JS7SY/zlib.pdf"}
+             (select-keys
+              (db-test/readable-properties (db-test/find-block-by-content @conn "zlib"))
+              [:block/tags
+               :logseq.property.asset/type
+               :logseq.property.asset/external-url
+               :logseq.property.asset/external-file-name]))
+          "Zotero imported pdf asset has correct external path info")
       (is (= (d/entity @conn :logseq.class/Asset)
              (:block/page (db-test/find-block-by-content @conn "greg-popovich-thumbs-up_1704749687791_0")))
           "Imported into Asset page")
@@ -447,6 +476,90 @@
                           db-test/readable-properties)
                      :logseq.property.pdf/hl-value :logseq.property/ls-type))
           "Pdf area highlight has correct properties")
+      (is (= {:block/tags [:logseq.class/Pdf-annotation]
+              :logseq.property/asset "Understanding EXPLAIN"
+              :logseq.property.pdf/hl-color :logseq.property/color.yellow
+              :logseq.property.pdf/hl-page 6}
+             (select-keys
+              (db-test/readable-properties (db-test/find-block-by-content @conn #"EXPLAIN is a really nice command"))
+              [:block/tags
+               :logseq.property/asset
+               :logseq.property.pdf/hl-color
+               :logseq.property.pdf/hl-page]))
+          "Zotero linked pdf text highlight links to correct asset")
+      (is (= {:block/tags [:logseq.class/Pdf-annotation]
+              :logseq.property/asset "zlib"
+              :logseq.property.pdf/hl-color :logseq.property/color.red
+              :logseq.property.pdf/hl-page 1}
+             (select-keys
+              (db-test/readable-properties (db-test/find-block-by-content @conn #"The zlib library is a general purpose data compression library"))
+              [:block/tags
+               :logseq.property/asset
+               :logseq.property.pdf/hl-color
+               :logseq.property.pdf/hl-page]))
+          "Zotero imported pdf text highlight links to correct asset")
+      (let [area-hl (d/q '[:find (pull ?b [:block/title
+                                           {:block/tags [:db/ident]}
+                                           {:logseq.property/asset [:block/title]}
+                                           {:logseq.property.pdf/hl-image [:block/title]}
+                                           {:logseq.property.pdf/hl-color [:db/ident]}
+                                           :logseq.property.pdf/hl-type
+                                           :logseq.property.pdf/hl-page]) .
+                           :where
+                           [?asset :block/title "Understanding EXPLAIN"]
+                           [?asset :block/tags :logseq.class/Asset]
+                           [?b :block/title "[:span]"]
+                           [?b :logseq.property/asset ?asset]]
+                         @conn)]
+        (is (= {:logseq.property.pdf/hl-color :logseq.property/color.green
+                :logseq.property.pdf/hl-page 8
+                :block/tags [:logseq.class/Pdf-annotation]
+                :logseq.property/asset "Understanding EXPLAIN"
+                :logseq.property.pdf/hl-image "pdf area highlight"
+                :logseq.property.pdf/hl-type :area}
+               (-> area-hl
+                   (update :block/tags #(mapv :db/ident %))
+                   (update :logseq.property/asset #(:block/title %))
+                   (update :logseq.property.pdf/hl-color #(:db/ident %))
+                   (update :logseq.property.pdf/hl-image #(:block/title %))
+                   (select-keys [:block/tags
+                                 :logseq.property/asset
+                                 :logseq.property.pdf/hl-color
+                                 :logseq.property.pdf/hl-page
+                                 :logseq.property.pdf/hl-image
+                                 :logseq.property.pdf/hl-type])))
+            "Zotero linked pdf area highlight links to correct asset"))
+      (let [area-hl (d/q '[:find (pull ?b [:block/title
+                                           {:block/tags [:db/ident]}
+                                           {:logseq.property/asset [:block/title]}
+                                           {:logseq.property.pdf/hl-image [:block/title]}
+                                           {:logseq.property.pdf/hl-color [:db/ident]}
+                                           :logseq.property.pdf/hl-type
+                                           :logseq.property.pdf/hl-page]) .
+                           :where
+                           [?asset :block/title "zlib"]
+                           [?asset :block/tags :logseq.class/Asset]
+                           [?b :block/title "[:span]"]
+                           [?b :logseq.property/asset ?asset]]
+                         @conn)]
+        (is (= {:logseq.property.pdf/hl-color :logseq.property/color.blue
+                :logseq.property.pdf/hl-page 1
+                :block/tags [:logseq.class/Pdf-annotation]
+                :logseq.property/asset "zlib"
+                :logseq.property.pdf/hl-image "pdf area highlight"
+                :logseq.property.pdf/hl-type :area}
+               (-> area-hl
+                   (update :block/tags #(mapv :db/ident %))
+                   (update :logseq.property/asset #(:block/title %))
+                   (update :logseq.property.pdf/hl-color #(:db/ident %))
+                   (update :logseq.property.pdf/hl-image #(:block/title %))
+                   (select-keys [:block/tags
+                                 :logseq.property/asset
+                                 :logseq.property.pdf/hl-color
+                                 :logseq.property.pdf/hl-page
+                                 :logseq.property.pdf/hl-image
+                                 :logseq.property.pdf/hl-type])))
+            "Zotero imported pdf area highlight links to correct asset"))
 
       ;; Quotes
       (is (= {:block/tags [:logseq.class/Quote-block]

BIN
deps/graph-parser/test/resources/book/it/Understanding EXPLAIN.pdf


+ 53 - 0
deps/graph-parser/test/resources/exporter-test-graph/assets/Understanding EXPLAIN.edn

@@ -0,0 +1,53 @@
+{:highlights [{:id #uuid "697874ba-26e9-4e8a-a55d-0639d337dcc6",
+               :page 6,
+               :position {:bounding {:x1 67.03125,
+                                     :y1 344.90625,
+                                     :x2 612.7423095703125,
+                                     :y2 379.3333740234375,
+                                     :width 702,
+                                     :height 993.418487394958},
+                          :rects ({:x1 67.03125,
+                                   :y1 344.90625,
+                                   :x2 612.7423095703125,
+                                   :y2 364.2396240234375,
+                                   :width 702,
+                                   :height 993.418487394958}
+                                  {:x1 67.03125,
+                                   :y1 360,
+                                   :x2 238.0740203857422,
+                                   :y2 379.3333740234375,
+                                   :width 702,
+                                   :height 993.418487394958}),
+                          :page 6},
+               :content {:text "EXPLAIN is a really nice command that gives you lots of information but it's often easy to not know what to do with all this"},
+               :properties {:color "yellow"}}
+              {:id #uuid "69787518-9eb8-4fd0-ad3b-b394e28bb547",
+               :page 8,
+               :position {:bounding {:x1 84,
+                                     :y1 163,
+                                     :x2 637,
+                                     :y2 327,
+                                     :width 702,
+                                     :height 993.418487394958},
+                          :rects (),
+                          :page 8},
+               :content {:text "[:span]", :image 1769501975346},
+               :properties {:color "green"}}
+              {:id #uuid "69787540-7a52-40af-a70a-cedd315934cf",
+               :page 42,
+               :position {:bounding {:x1 119.05208587646484,
+                                     :y1 95.67709350585938,
+                                     :x2 243.25509643554688,
+                                     :y2 133.01040649414062,
+                                     :width 702,
+                                     :height 993.418487394958},
+                          :rects ({:x1 119.05208587646484,
+                                   :y1 95.67709350585938,
+                                   :x2 243.25509643554688,
+                                   :y2 133.01040649414062,
+                                   :width 702,
+                                   :height 993.418487394958}),
+                          :page 42},
+               :content {:text "Conclusion"},
+               :properties {:color "blue"}}],
+ :extra {:page 8}}

BIN
deps/graph-parser/test/resources/exporter-test-graph/assets/Understanding EXPLAIN/8_69787518-9eb8-4fd0-ad3b-b394e28bb547_1769501975346.png


+ 36 - 0
deps/graph-parser/test/resources/exporter-test-graph/assets/zlib.edn

@@ -0,0 +1,36 @@
+{:highlights [{:id #uuid "69787399-f51c-45a2-8971-78dc6c66a0ec",
+               :page 1,
+               :position {:bounding {:x1 123.89583587646484,
+                                     :y1 164.3125,
+                                     :x2 438.8349304199219,
+                                     :y2 180.9791717529297,
+                                     :width 702,
+                                     :height 908.470588235294},
+                          :rects ({:x1 123.89583587646484,
+                                   :y1 164.3125,
+                                   :x2 438.8349304199219,
+                                   :y2 180.9791717529297,
+                                   :width 702,
+                                   :height 908.470588235294}
+                                  {:x1 143.625,
+                                   :y1 166.9791717529297,
+                                   :x2 432.40625,
+                                   :y2 178.4479217529297,
+                                   :width 702,
+                                   :height 908.470588235294}),
+                          :page 1},
+               :content {:text "The zlib library is a general purpose data compression library."},
+               :properties {:color "red"}}
+              {:id #uuid "697873d7-5953-4753-897e-62ac47348634",
+               :page 1,
+               :position {:bounding {:x1 116,
+                                     :y1 530,
+                                     :x2 627,
+                                     :y2 690,
+                                     :width 702,
+                                     :height 908.470588235294},
+                          :rects (),
+                          :page 1},
+               :content {:text "[:span]", :image 1769501651181},
+               :properties {:color "blue"}}],
+ :extra {:page 2}}

BIN
deps/graph-parser/test/resources/exporter-test-graph/assets/zlib/1_697873d7-5953-4753-897e-62ac47348634_1769501651181.png


+ 0 - 2
deps/graph-parser/test/resources/exporter-test-graph/journals/2024_02_16.md

@@ -1,8 +1,6 @@
 - test :dates
   startedAt:: [[Feb 6th, 2024]]
   finishedAt:: [[Feb 7th, 2024]]
-- test :date -> :node
-  finishedAt:: [[Gabriel]]
 - MEETING TITLE
   template:: meeting
   participants:: TODO

+ 3 - 1
deps/graph-parser/test/resources/exporter-test-graph/journals/2024_02_23.md

@@ -1,3 +1,5 @@
 - The Creator
   type:: [[Movie]]
-  testTagClass:: #Movie
+  testTagClass:: #Movie
+- test :date -> :node
+  finishedAt:: [[Gabriel]]

+ 2 - 0
deps/graph-parser/test/resources/exporter-test-graph/journals/2026_01_01.md

@@ -0,0 +1,2 @@
+- [[zlib]]
+- [[Understanding EXPLAIN]]

+ 3 - 0
deps/graph-parser/test/resources/exporter-test-graph/logseq/config.edn

@@ -399,4 +399,7 @@
  ;;   - :triple-lowbar (default)
  ;;      ;use triple underscore `___` for slash `/` in page title
  ;;      ;use Percent-encoding for other invalid characters
+
+ ;; use relative path here only for ci test, won't work on the Electron client
+ :zotero/settings-v2 {"default" {:type-id "1234567", :prefer-citekey? false, :attachments-block-text "Attachments", :page-insert-prefix "", :extra-tags "", :zotero-linked-attachment-base-directory "../book", :zotero-data-directory "../zotero"}}
  :file/name-format :triple-lowbar}

+ 5 - 0
deps/graph-parser/test/resources/exporter-test-graph/pages/Understanding EXPLAIN.md

@@ -0,0 +1,5 @@
+- Attachments
+	- [Understanding EXPLAIN.pdf](zotero://select/library/items/QDM8H6EH) {{zotero-linked-file "it/Understanding EXPLAIN.pdf"}}
+- Notes
+	- ((697874ba-26e9-4e8a-a55d-0639d337dcc6)) definition
+	- ((69787518-9eb8-4fd0-ad3b-b394e28bb547)) select

+ 20 - 0
deps/graph-parser/test/resources/exporter-test-graph/pages/hls__Understanding EXPLAIN.md

@@ -0,0 +1,20 @@
+file:: [Understanding EXPLAIN.pdf](../../book/it/Understanding EXPLAIN.pdf)
+file-path:: ../../book/it/Understanding EXPLAIN.pdf
+
+- EXPLAIN is a really nice command that gives you lots of information but it's often easy to not know what to do with all this
+  ls-type:: annotation
+  hl-page:: 6
+  hl-color:: yellow
+  id:: 697874ba-26e9-4e8a-a55d-0639d337dcc6
+- [:span]
+  ls-type:: annotation
+  hl-page:: 8
+  hl-color:: green
+  id:: 69787518-9eb8-4fd0-ad3b-b394e28bb547
+  hl-type:: area
+  hl-stamp:: 1769501975346
+- Conclusion
+  ls-type:: annotation
+  hl-page:: 42
+  hl-color:: blue
+  id:: 69787540-7a52-40af-a70a-cedd315934cf

+ 15 - 0
deps/graph-parser/test/resources/exporter-test-graph/pages/hls__zlib.md

@@ -0,0 +1,15 @@
+file:: [zlib.pdf](../../zotero/RX5JS7SY/zlib.pdf)
+file-path:: ../../zotero/storage/RX5JS7SY/zlib.pdf
+
+- The zlib library is a general purpose data compression library.
+  ls-type:: annotation
+  hl-page:: 1
+  hl-color:: red
+  id:: 69787399-f51c-45a2-8971-78dc6c66a0ec
+- [:span]
+  ls-type:: annotation
+  hl-page:: 1
+  hl-color:: blue
+  id:: 697873d7-5953-4753-897e-62ac47348634
+  hl-type:: area
+  hl-stamp:: 1769501651181

+ 5 - 0
deps/graph-parser/test/resources/exporter-test-graph/pages/zlib.md

@@ -0,0 +1,5 @@
+- Attachments
+	- [PDF](zotero://select/library/items/RX5JS7SY) {{zotero-imported-file RX5JS7SY, "zlib.pdf"}}
+- Notes
+	- ((69787399-f51c-45a2-8971-78dc6c66a0ec)) definition
+	- ((697873d7-5953-4753-897e-62ac47348634)) links

BIN
deps/graph-parser/test/resources/zotero/storage/RX5JS7SY/zlib.pdf


+ 1 - 2
src/electron/electron/handler.cljs

@@ -428,8 +428,7 @@
   (.close win))
 
 (defmethod handle :theme-loaded [^js win]
-  (.manage (windowStateKeeper) win)
-  (.show win))
+  (.manage (windowStateKeeper) win))
 
 (defmethod handle :keychain/save-e2ee-password [_window [_ key encrypted-text]]
   (keychain/<set-password! key encrypted-text))

+ 2 - 0
src/electron/electron/window.cljs

@@ -67,6 +67,8 @@
                                (callback (bean/->js
                                           {:cancel         false
                                            :requestHeaders headers})))))
+     ;; Show window as soon as it's ready
+     (.once win "ready-to-show" #(.show win))
      (.loadURL win url)
      ;;(when dev? (.. win -webContents (openDevTools)))
      win)))

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

@@ -370,7 +370,7 @@
   [e block href]
   (let [href (if-let [url (:logseq.property.asset/external-url block)]
                (if (string/starts-with? url "zotero://")
-                 (zotero/zotero-full-path (last (string/split url #"/")) (:logseq.property.asset/external-file-name block))
+                 (pdf-assets/get-zotero-local-pdf-path (:logseq.property.asset/external-file-name block) :id (last (string/split url #"/")))
                  url)
                href)]
     (when-let [s (or href (some-> (.-target e) (.-dataset) (.-href)))]

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

@@ -913,7 +913,9 @@
   {:will-unmount (fn [state]
                    (reset! *n-hops nil)
                    (reset! *focus-nodes [])
-                   (state/set-search-mode! :global)
+                   ;; Clear graph search mode on leave so block selection action bar
+                   ;; can open normally on non-graph routes.
+                   (state/set-search-mode! nil)
                    state)}
   [state]
   (let [settings (state/graph-settings)

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

@@ -182,7 +182,8 @@
                                        (if plugin?
                                          [:span.pt-1 (shui/tabler-icon "puzzle" {:size 15 :class "opacity-40"})]
                                          [:span.pt-1 (shui/tabler-icon "letter-t" {:size 15 :class "opacity-40"})])
-                                       [:strong.font-normal (:block/title x)]]))
+                                       [:strong.font-normal (:block/title x)
+                                        (when plugin? [:span.ml-1.text-xs.opacity-40 (str "" _plugin-name)])]]))
                            :value (:block/uuid x)
                            :block/title (:block/title x)
                            :convert-page-to-property? convert?})) properties)

+ 1 - 6
src/main/frontend/components/theme.cljs

@@ -33,8 +33,6 @@
      {:ref   *el
       :class "top-1/2 -left-1/2 z-[-999]"}]))
 
-(defonce *once-theme-loaded? (volatile! false))
-
 (rum/defc ^:large-vars/cleanup-todo container < rum/static
   [{:keys [route theme accent-color editor-font on-click current-repo db-restoring?
            settings-open? sidebar-open? system-theme? sidebar-blocks-len preferred-language]} child]
@@ -76,10 +74,7 @@
      [preferred-language])
 
     (hooks/use-effect!
-     #(js/setTimeout
-       (fn [] (when-not @*once-theme-loaded?
-                (ipc/ipc :theme-loaded)
-                (vreset! *once-theme-loaded? true))) 100) ; Wait for the theme to be applied
+     #(ipc/ipc :theme-loaded)
      [])
 
     (hooks/use-effect!

+ 5 - 1
src/main/frontend/db/model.cljs

@@ -341,7 +341,11 @@ independent of format as format specific heading characters are stripped"
                  remove-non-queryable-built-in-property? false
                  remove-ui-non-suitable-properties? false}}]
   (let [db (conn/get-db graph)
-        result (sort-by (juxt ldb/built-in? :block/title)
+        result (sort-by (juxt (fn [p]
+                                (some-> (:db/ident p)
+                                        (db-property/plugin-property?)))
+                              ldb/built-in?
+                              :block/title)
                         (ldb/get-all-properties db))]
     (cond->> result
       remove-built-in-property?

+ 20 - 1
src/main/frontend/extensions/pdf/assets.cljs

@@ -198,12 +198,31 @@
        (ref/->block-ref (:block/uuid ref-block))
        :owner-window (pdf-windows/resolve-own-window viewer)))))
 
+(defn get-zotero-local-pdf-path
+  [path & {:keys [id]}]
+  (let [zotero-config (get-in (state/sub-config) [:zotero/settings-v2 "default"])
+        zotero-data-directory (:zotero-data-directory zotero-config)
+        zotero-linked-attachment-base-directory (:zotero-linked-attachment-base-directory zotero-config)
+        relative-path (subs path 14)]
+    (cond
+      (string/starts-with? path "zotero-link://")
+      (str "file://" (util/node-path.join zotero-linked-attachment-base-directory relative-path))
+
+      (string/starts-with? path "zotero-path://")
+      (str "file://" (util/node-path.join zotero-data-directory "storage" relative-path))
+
+      :else ;; compatible with commit 33db791
+      (str "file://" (util/node-path.join zotero-data-directory "storage" id path)))))
+
 (defn db-based-open-block-ref!
   [block]
   (let [hl-value (:logseq.property.pdf/hl-value block)
         asset (:logseq.property/asset block)
         external-url (:logseq.property.asset/external-url asset)
-        file-path (or external-url (str "../assets/" (:block/uuid asset) ".pdf"))]
+        file-path (or external-url (str "../assets/" (:block/uuid asset) ".pdf"))
+        file-path (if (string/starts-with? file-path "zotero://")
+                    (get-zotero-local-pdf-path (:logseq.property.asset/external-file-name asset))
+                    file-path)]
     (if asset
       (->
        (p/let [href (assets-handler/<make-asset-url file-path)]