Browse Source

Merge branch 'feat/db' into refactor/dsl-query

Tienson Qin 1 year ago
parent
commit
6ce66e6f09
52 changed files with 1845 additions and 1342 deletions
  1. 6 0
      deps/db/src/logseq/db/frontend/class.cljs
  2. 3 1
      deps/db/src/logseq/db/frontend/delete_blocks.cljs
  3. 3 12
      deps/db/src/logseq/db/frontend/malli_schema.cljs
  4. 26 6
      deps/db/src/logseq/db/frontend/property.cljs
  5. 3 4
      deps/db/src/logseq/db/frontend/schema.cljs
  6. 52 52
      deps/db/test/logseq/db/frontend/rules_test.cljs
  7. 0 1
      deps/db/test/logseq/db_test.cljs
  8. 96 61
      deps/graph-parser/src/logseq/graph_parser/exporter.cljs
  9. 11 4
      deps/graph-parser/test/logseq/graph_parser/exporter_test.cljs
  10. 3 0
      deps/graph-parser/test/resources/exporter-test-graph/journals/2024_10_09.md
  11. 1 1
      deps/graph-parser/test/resources/exporter-test-graph/pages/chat-gpt.md
  12. 6 5
      deps/outliner/src/logseq/outliner/property.cljs
  13. 14 14
      deps/publishing/src/logseq/publishing/db.cljs
  14. 14 14
      deps/publishing/src/logseq/publishing/html.cljs
  15. 1 7
      src/main/frontend/common.css
  16. 21 25
      src/main/frontend/components/all_pages.cljs
  17. 212 163
      src/main/frontend/components/block.cljs
  18. 12 16
      src/main/frontend/components/block.css
  19. 8 0
      src/main/frontend/components/block/views.css
  20. 3 2
      src/main/frontend/components/class.cljs
  21. 8 6
      src/main/frontend/components/export.cljs
  22. 46 0
      src/main/frontend/components/filepicker.cljs
  23. 97 95
      src/main/frontend/components/icon.cljs
  24. 62 50
      src/main/frontend/components/objects.cljs
  25. 30 29
      src/main/frontend/components/page.cljs
  26. 4 0
      src/main/frontend/components/page.css
  27. 15 15
      src/main/frontend/components/page_menu.cljs
  28. 77 77
      src/main/frontend/components/property/config.cljs
  29. 5 3
      src/main/frontend/components/reference.cljs
  30. 7 1
      src/main/frontend/components/table.css
  31. 30 30
      src/main/frontend/components/theme.cljs
  32. 100 71
      src/main/frontend/components/views.cljs
  33. 16 17
      src/main/frontend/config.cljs
  34. 22 0
      src/main/frontend/db/async.cljs
  35. 11 11
      src/main/frontend/db/query_custom.cljs
  36. 228 158
      src/main/frontend/extensions/pdf/assets.cljs
  37. 221 184
      src/main/frontend/extensions/pdf/core.cljs
  38. 13 26
      src/main/frontend/extensions/pdf/pdf.css
  39. 55 57
      src/main/frontend/extensions/pdf/toolbar.cljs
  40. 16 23
      src/main/frontend/extensions/pdf/utils.cljs
  41. 3 3
      src/main/frontend/extensions/tldraw.cljs
  42. 2 2
      src/main/frontend/extensions/zip.cljs
  43. 1 0
      src/main/frontend/handler.cljs
  44. 23 2
      src/main/frontend/handler/assets.cljs
  45. 4 4
      src/main/frontend/handler/db_based/page.cljs
  46. 191 74
      src/main/frontend/handler/editor.cljs
  47. 26 8
      src/main/frontend/handler/export.cljs
  48. 1 0
      src/main/frontend/publishing.cljs
  49. 32 5
      src/main/frontend/worker/db/migrate.cljs
  50. 1 1
      src/main/logseq/sdk/assets.cljs
  51. 2 1
      src/resources/dicts/en.edn
  52. 1 1
      src/test/frontend/db/db_based_model_test.cljs

+ 6 - 0
deps/db/src/logseq/db/frontend/class.cljs

@@ -29,6 +29,12 @@
     :properties {:logseq.property/icon {:type :tabler-icon :id "search"}
                  :logseq.property/parent :logseq.class/Query}}
 
+   :logseq.class/Asset
+   {:title "Asset"
+    :properties {;; :logseq.property/icon {:type :tabler-icon :id "file"}
+                 :logseq.property.class/hide-from-node true}
+    :schema {:properties [:logseq.property.asset/type :logseq.property.asset/size :logseq.property.asset/checksum]}}
+
    ;; TODO: Add more classes such as :book, :paper, :movie, :music, :project
    })
 

+ 3 - 1
deps/db/src/logseq/db/frontend/delete_blocks.cljs

@@ -10,7 +10,9 @@
 
 (defn- replace-ref-with-deleted-block-title
   [block ref-raw-title]
-  (let [block-content (:block/title block)]
+  (let [block-content (if (= "asset" (:block/type block))
+                        ""
+                        (:block/title block))]
     (some-> ref-raw-title
             (string/replace (re-pattern (common-util/format "(?i){{embed \\(\\(%s\\)\\)\\s?}}" (str (:block/uuid block))))
                             block-content)

+ 3 - 12
deps/db/src/logseq/db/frontend/malli_schema.cljs

@@ -122,8 +122,8 @@
                          ;; use explicit call to be nbb compatible
                          [(let [closed-values (entity-plus/lookup-kv-then-entity property :property/closed-values)]
                             (cond-> (assoc (select-keys property [:db/ident :db/valueType :db/cardinality])
-                                    :block/schema
-                                    (select-keys (:block/schema property) [:type]))
+                                           :block/schema
+                                           (select-keys (:block/schema property) [:type]))
                               (seq closed-values)
                               (assoc :property/closed-values closed-values)))
                           v])
@@ -254,7 +254,6 @@
     page-attrs
     page-or-block-attrs)))
 
-
 (def property-common-schema-attrs
   "Property :schema attributes common to all properties"
   [[:hide? {:optional true} :boolean]
@@ -400,11 +399,6 @@
    [:file/created-at inst?]
    [:file/last-modified-at inst?]])
 
-(def asset-block
-  [:map
-   [:asset/uuid :uuid]
-   [:asset/meta :map]])
-
 (def db-ident-key-val
   "A key value map with :db/ident and :kv/value"
   [:map
@@ -435,8 +429,6 @@
                           :file-block
                           (:block/uuid d)
                           :block
-                          (:asset/uuid d)
-                          :asset-block
                           (= (:db/ident d) :logseq.property/empty-placeholder)
                           :property-value-placeholder
                           (:db/ident d)
@@ -448,7 +440,6 @@
     :block block
     :file-block file-block
     :db-ident-key-value db-ident-key-val
-    :asset-block asset-block
     :property-value-placeholder property-value-placeholder}))
 
 (def DB
@@ -480,7 +471,7 @@
                     {}))))
 
 (let [malli-non-ref-attrs (->> (concat property-attrs page-attrs block-attrs page-or-block-attrs (rest normal-page))
-                               (concat (rest file-block) (rest asset-block) (rest property-value-block)
+                               (concat (rest file-block) (rest property-value-block)
                                        (rest db-ident-key-val) (rest class-page))
                                (remove #(= (last %) [:set :int]))
                                (map first)

+ 26 - 6
deps/db/src/logseq/db/frontend/property.cljs

@@ -82,17 +82,16 @@
                                                     :hide? true}}
    :logseq.property/built-in?             {:schema {:type :checkbox
                                                     :hide? true}}
+   :logseq.property/asset   {:title "Asset"
+                             :schema {:type :entity
+                                      :hide? true}}
    :logseq.property/ls-type {:schema {:type :keyword
                                       :hide? true}}
    :logseq.property/hl-type {:schema {:type :keyword :hide? true}}
    :logseq.property/hl-color {:schema {:type :default :hide? true}}
    :logseq.property.pdf/hl-page {:schema {:type :number :hide? true}}
-   :logseq.property.pdf/hl-stamp {:schema {:type :number :hide? true}}
+   :logseq.property.pdf/hl-image {:schema {:type :entity :hide? true}}
    :logseq.property.pdf/hl-value {:schema {:type :map :hide? true}}
-   :logseq.property.pdf/file
-   {:schema {:type :default :hide? true :public? true :view-context :page}}
-   :logseq.property.pdf/file-path
-   {:schema {:type :default :hide? true :public? true :view-context :page}}
    :logseq.property/order-list-type {:name :logseq.order-list-type
                                      :schema {:type :default
                                               :hide? true}}
@@ -202,7 +201,8 @@
              :value value
              :uuid (common-uuid/gen-uuid :db-ident-block-uuid db-ident)})
           [[:logseq.property.view/type.table "Table View"]
-           [:logseq.property.view/type.list "List View"]])}
+           [:logseq.property.view/type.list "List View"]
+           [:logseq.property.view/type.card "Card View"]])}
 
    :logseq.property.table/sorting {:schema
                                    {:type :coll
@@ -234,10 +234,30 @@
                               {:type :node
                                :hide? true
                                :public? false}}
+   :logseq.property.asset/type {:title "File type"
+                                :schema {:type :string
+                                         :hide? true
+                                         :public? false}}
+   :logseq.property.asset/size {:title "File size"
+                                :schema {:type :raw-number
+                                         :hide? true
+                                         :public? false}}
+   :logseq.property.asset/checksum {:title "File checksum"
+                                    :schema {:type :string
+                                             :hide? true
+                                             :public? false}}
+   :logseq.property.asset/last-visit-page {:title "Last visit page"
+                                           :schema {:type :raw-number
+                                                    :hide? true
+                                                    :public? false}}
    :logseq.property.asset/remote-metadata {:schema
                                            {:type :map
                                             :hide? true
                                             :public? false}}
+   :logseq.property.asset/resize-metadata {:title "Asset resize metadata"
+                                           :schema {:type :map
+                                                    :hide? true
+                                                    :public? false}}
    :logseq.property.fsrs/due {:title "Due"
                               :schema
                               {:type :datetime

+ 3 - 4
deps/db/src/logseq/db/frontend/schema.cljs

@@ -2,7 +2,8 @@
   "Main datascript schemas for the Logseq app"
   (:require [clojure.set :as set]))
 
-(def version 29)
+(def version 35)
+
 ;; A page is a special block, a page can corresponds to multiple files with the same ":block/name".
 (def ^:large-vars/data-var schema
   {:db/ident        {:db/unique :db.unique/identity}
@@ -117,9 +118,7 @@
                                   :db/cardinality :db.cardinality/many}
     :property/schema.classes {:db/valueType :db.type/ref
                               :db/cardinality :db.cardinality/many}
-    :property.value/content {}
-    :asset/uuid {:db/unique :db.unique/identity}
-    :asset/meta {}}))
+    :property.value/content {}}))
 
 (def retract-attributes
   #{:block/refs

+ 52 - 52
deps/db/test/logseq/db/frontend/rules_test.cljs

@@ -46,22 +46,22 @@
                 {:page {:block/title "Page A"
                         :build/properties {:foo "bar A"}}}]})]
     (testing "cardinality :one property"
-        (is (= ["Page"]
-               (->> (q-with-rules '[:find (pull ?b [:block/title]) :where (page-property ?b :user.property/foo "bar")]
-                                  @conn)
-                    (map (comp :block/title first))))
-            "page-property returns result when page has property")
-        (is (= []
-               (->> (q-with-rules '[:find (pull ?b [:block/title]) :where (page-property ?b :user.property/foo "baz")]
-                                  @conn)
-                    (map (comp :block/title first))))
-            "page-property returns no result when page doesn't have property value")
-        (is (= #{:user.property/foo}
-               (->> (q-with-rules '[:find [?p ...]
-                                    :where (page-property ?b ?p "bar") [?b :block/title "Page"]]
-                                  @conn)
-                    set))
-            "page-property can bind to property arg with bound property value"))
+      (is (= ["Page"]
+             (->> (q-with-rules '[:find (pull ?b [:block/title]) :where (page-property ?b :user.property/foo "bar")]
+                                @conn)
+                  (map (comp :block/title first))))
+          "page-property returns result when page has property")
+      (is (= []
+             (->> (q-with-rules '[:find (pull ?b [:block/title]) :where (page-property ?b :user.property/foo "baz")]
+                                @conn)
+                  (map (comp :block/title first))))
+          "page-property returns no result when page doesn't have property value")
+      (is (= #{:user.property/foo}
+             (->> (q-with-rules '[:find [?p ...]
+                                  :where (page-property ?b ?p "bar") [?b :block/title "Page"]]
+                                @conn)
+                  set))
+          "page-property can bind to property arg with bound property value"))
 
     (testing "cardinality :many property"
       (is (= ["Page"]
@@ -83,41 +83,41 @@
 
     ;; NOTE: Querying a ref's name is different than before and requires more than just the rule
     (testing ":ref property"
-        (is (= ["Page"]
-               (->> (q-with-rules '[:find (pull ?b [:block/title])
-                                    :where (page-property ?b :user.property/page-many "Page A")]
-                                  @conn)
-                    (map (comp :block/title first))))
-            "page-property returns result when page has property")
-        (is (= []
-               (->> (q-with-rules '[:find (pull ?b [:block/title])
-                                    :where [?b :user.property/page-many ?pv] [?pv :block/title "Page B"]]
-                                  @conn)
-                    (map (comp :block/title first))))
-            "page-property returns no result when page doesn't have property value"))
+      (is (= ["Page"]
+             (->> (q-with-rules '[:find (pull ?b [:block/title])
+                                  :where (page-property ?b :user.property/page-many "Page A")]
+                                @conn)
+                  (map (comp :block/title first))))
+          "page-property returns result when page has property")
+      (is (= []
+             (->> (q-with-rules '[:find (pull ?b [:block/title])
+                                  :where [?b :user.property/page-many ?pv] [?pv :block/title "Page B"]]
+                                @conn)
+                  (map (comp :block/title first))))
+          "page-property returns no result when page doesn't have property value"))
 
     (testing "bindings with property value"
-        (is (= #{:user.property/foo :user.property/number-many :user.property/page-many}
-               (->> (q-with-rules '[:find [?p ...]
-                                    :where (page-property ?b ?p _) [?b :block/title "Page"]]
-                                  @conn)
-                    set))
-            "page-property can bind to property arg with unbound property value")
-        (is (= #{[:user.property/number-many 10]
-                 [:user.property/number-many 5]
-                 [:user.property/foo "bar"]
-                 [:user.property/page-many "Page A"]}
-               (->> (q-with-rules '[:find ?p ?v
-                                    :where (page-property ?b ?p ?v) [?b :block/title "Page"]]
-                                  @conn)
-                    set))
-            "page-property can bind to property and property value args")
-        (is (= #{"Page"}
-               (->> (q-with-rules '[:find (pull ?b [:block/title])
-                                    :where
-                                    [?b :user.property/page-many ?pv]
-                                    (page-property ?pv :user.property/foo "bar A")]
-                                  @conn)
-                    (map (comp :block/title first))
-                    set))
-            "page-property can be used multiple times to query a property value's property"))))
+      (is (= #{:user.property/foo :user.property/number-many :user.property/page-many}
+             (->> (q-with-rules '[:find [?p ...]
+                                  :where (page-property ?b ?p _) [?b :block/title "Page"]]
+                                @conn)
+                  set))
+          "page-property can bind to property arg with unbound property value")
+      (is (= #{[:user.property/number-many 10]
+               [:user.property/number-many 5]
+               [:user.property/foo "bar"]
+               [:user.property/page-many "Page A"]}
+             (->> (q-with-rules '[:find ?p ?v
+                                  :where (page-property ?b ?p ?v) [?b :block/title "Page"]]
+                                @conn)
+                  set))
+          "page-property can bind to property and property value args")
+      (is (= #{"Page"}
+             (->> (q-with-rules '[:find (pull ?b [:block/title])
+                                  :where
+                                  [?b :user.property/page-many ?pv]
+                                  (page-property ?pv :user.property/foo "bar A")]
+                                @conn)
+                  (map (comp :block/title first))
+                  set))
+          "page-property can be used multiple times to query a property value's property"))))

+ 0 - 1
deps/db/test/logseq/db_test.cljs

@@ -5,7 +5,6 @@
             [logseq.db :as ldb]
             [logseq.db.test.helper :as db-test]))
 
-
 ;;; datoms
 ;;; - 1 <----+
 ;;;   - 2    |

+ 96 - 61
deps/graph-parser/src/logseq/graph_parser/exporter.cljs

@@ -313,7 +313,7 @@
   "All built-in property names as a set of keywords"
   (-> built-in-property-name-to-idents keys set
       ;; built-in-properties that map to new properties
-      (set/union #{:filters :query-table :query-properties :query-sort-by :query-sort-desc})))
+      (set/union #{:filters :query-table :query-properties :query-sort-by :query-sort-desc :hl-stamp :file :file-path})))
 
 (def all-built-in-names
   "All built-in properties and classes as a set of keywords"
@@ -359,34 +359,58 @@
         (js/console.error "Translating query properties failed with:" e)
         []))))
 
+(defn- translate-linked-ref-filters
+  [prop-value page-names-to-uuids]
+  (try
+    (let [filters (edn/read-string prop-value)
+          filter-by (group-by val filters)
+          includes (->> (filter-by true)
+                        (map first)
+                        (keep #(or (page-names-to-uuids %)
+                                   (js/console.error (str "No uuid found for linked reference filter page " (pr-str %)))))
+                        (mapv #(vector :block/uuid %)))
+          excludes (->> (filter-by false)
+                        (map first)
+                        (keep #(or (page-names-to-uuids %)
+                                   (js/console.error (str "No uuid found for linked reference filter page " (pr-str %)))))
+                        (mapv #(vector :block/uuid %)))]
+      (cond-> []
+        (seq includes)
+        (conj [:logseq.property.linked-references/includes includes])
+        (seq excludes)
+        (conj [:logseq.property.linked-references/excludes excludes])))
+    (catch :default e
+      (js/console.error "Translating linked reference filters failed with: " e))))
+
 (defn- update-built-in-property-values
-  [props {:keys [ignored-properties all-idents]} {:block/keys [title name]} options]
+  [props page-names-to-uuids {:keys [ignored-properties all-idents]} {:block/keys [title name]} options]
   (let [m
         (->> props
-             (keep (fn [[prop prop-value]]
-                     ;; FIXME: Migrate :filters to :logseq.property.linked-references/* properties
-                     (if (#{:icon :filters} prop)
-                       (do (swap! ignored-properties
-                                  conj
-                                  {:property prop :value prop-value :location (if name {:page name} {:block title})})
-                           nil)
-                       (case prop
-                         :query-properties
-                         (when-let [cols (not-empty (translate-query-properties prop-value all-idents options))]
-                           [:logseq.property.table/ordered-columns cols])
-                         :query-table
-                         [:logseq.property.view/type
-                          (if prop-value :logseq.property.view/type.table :logseq.property.view/type.list)]
-                         :query-sort-by
-                         [:logseq.property.table/sorting
-                          [{:id (or (query-table-special-keys (keyword prop-value))
-                                    (get-ident @all-idents (keyword prop-value)))
-                            :asc? true}]]
-                         ;; ignore to handle below
-                         :query-sort-desc
-                         nil
-                         ;; else
-                         [(built-in-property-name-to-idents prop) prop-value]))))
+             (mapcat (fn [[prop prop-value]]
+                       (if (#{:icon} prop)
+                         (do (swap! ignored-properties
+                                    conj
+                                    {:property prop :value prop-value :location (if name {:page name} {:block title})})
+                             nil)
+                         (case prop
+                           :query-properties
+                           (when-let [cols (not-empty (translate-query-properties prop-value all-idents options))]
+                             [[:logseq.property.table/ordered-columns cols]])
+                           :query-table
+                           [[:logseq.property.view/type
+                             (if prop-value :logseq.property.view/type.table :logseq.property.view/type.list)]]
+                           :query-sort-by
+                           [[:logseq.property.table/sorting
+                             [{:id (or (query-table-special-keys (keyword prop-value))
+                                       (get-ident @all-idents (keyword prop-value)))
+                               :asc? true}]]]
+                           ;; ignore to handle below
+                           :query-sort-desc
+                           nil
+                           :filters
+                           (translate-linked-ref-filters prop-value page-names-to-uuids)
+                           ;; else
+                           [[(built-in-property-name-to-idents prop) prop-value]]))))
              (into {}))]
     (cond-> m
       (and (contains? props :query-sort-desc) (:query-sort-by props))
@@ -505,6 +529,7 @@
       {}
       (let [props' (-> (update-built-in-property-values
                         (select-keys props file-built-in-property-names)
+                        page-names-to-uuids
                         (select-keys import-state [:ignored-properties :all-idents])
                         (select-keys block [:block/name :block/title])
                         (select-keys options [:property-classes]))
@@ -769,11 +794,43 @@
       (dissoc :block/whiteboard?)
       (update-page-tags db tag-classes page-names-to-uuids all-idents)))
 
+(defn- get-all-existing-page-ents
+  [db]
+  (->> db
+       ;; don't fetch built-in as that would give the wrong entity if a user used
+       ;; a db-only built-in property name e.g. description
+       (d/q '[:find [?b ...]
+              :where [?b :block/name] [(missing? $ ?b :logseq.property/built-in?)]])
+       (map #(d/entity db %))
+       (map (juxt :block/name identity))
+       (into {})))
+
+(defn- build-existing-page [m db page-uuid page-names-to-uuids {:keys [tag-classes notify-user import-state]}]
+  (let [;; These attributes are not allowed to be transacted because they must not change across files
+        disallowed-attributes [:block/name :block/uuid :block/format :block/title :block/journal-day
+                               :block/created-at :block/updated-at]
+        allowed-attributes (into [:block/tags :block/alias :logseq.property/parent :block/type :db/ident]
+                                 (keep #(when (db-malli-schema/user-property? (key %)) (key %))
+                                       m))
+        block-changes (cond-> (select-keys m allowed-attributes)
+                        ;; disallow any type -> "page" but do allow any conversion to a non-page type
+                        (ldb/internal-page? m)
+                        (dissoc :block/type))]
+    (when-let [ignored-attrs (not-empty (apply dissoc m (into disallowed-attributes allowed-attributes)))]
+      (notify-user {:msg (str "Import ignored the following attributes on page " (pr-str (:block/title m)) ": "
+                              ignored-attrs)}))
+    (when (seq block-changes)
+      (cond-> (merge block-changes {:block/uuid page-uuid})
+        (seq (:block/alias m))
+        (update-page-alias page-names-to-uuids)
+        (:block/tags m)
+        (update-page-tags db tag-classes page-names-to-uuids (:all-idents import-state))))))
+
 (defn- build-pages-tx
   "Given all the pages and blocks parsed from a file, return a map containing
   all non-whiteboard pages to be transacted, pages' properties and additional
   data for subsequent steps"
-  [conn pages blocks {:keys [tag-classes property-classes property-parent-classes notify-user import-state]
+  [conn pages blocks {:keys [tag-classes property-classes property-parent-classes import-state]
                       :as options}]
   (let [all-pages* (->> (extract/with-ref-pages pages blocks)
                         ;; remove unused property pages unless the page has content
@@ -781,17 +838,14 @@
                                       (not (:block/file %))))
                         ;; remove file path relative
                         (map #(dissoc % :block/file)))
-        existing-pages (keep #(first
-                               ;; don't fetch built-in as that would give the wrong entity if a user used
-                               ;; a db-only built-in property name e.g. description
-                               (d/q '[:find [(pull ?b [*]) ...]
-                                      :in $ ?name
-                                      :where [?b :block/name ?name] [(missing? $ ?b :logseq.property/built-in?)]]
-                                    @conn
-                                    (:block/name %)))
-                             all-pages*)
-        existing-page-names-to-uuids (into {} (map (juxt :block/name :block/uuid) existing-pages))
-        existing-page? #(contains? existing-page-names-to-uuids (:block/name %))
+        ;; Fetch all named ents once per import file to speed up named lookups
+        all-existing-page-ents (get-all-existing-page-ents @conn)
+        ;; map of existing pages in current parsed file, indexed by name
+        existing-pages-by-name (into {}
+                                     (keep #(when-let [ent (all-existing-page-ents (:block/name %))]
+                                              [(:block/name ent) (:block/uuid ent)])
+                                           all-pages*))
+        existing-page? #(contains? existing-pages-by-name (:block/name %))
         ;; fix extract incorrectly assigning new user pages built-in uuids
         all-pages (map #(if (and (not (existing-page? %))
                                  (contains? all-built-in-names (keyword (:block/name %))))
@@ -799,32 +853,13 @@
                           %)
                        all-pages*)
         new-pages (remove existing-page? all-pages)
-        page-names-to-uuids (merge existing-page-names-to-uuids
+        page-names-to-uuids (merge (update-vals all-existing-page-ents :block/uuid)
                                    (into {} (map (juxt :block/name :block/uuid) new-pages)))
         all-pages-m (mapv #(handle-page-properties % @conn page-names-to-uuids all-pages options)
                           all-pages)
         pages-tx (keep (fn [m]
-                         (if-let [page-uuid (existing-page-names-to-uuids (:block/name m))]
-                           (let [;; These attributes are not allowed to be transacted because they must not change across files
-                                 disallowed-attributes [:block/name :block/uuid :block/format :block/title :block/journal-day
-                                                        :block/created-at :block/updated-at]
-                                 allowed-attributes (into [:block/tags :block/alias :logseq.property/parent :block/type :db/ident]
-                                                          (keep #(when (db-malli-schema/user-property? (key %)) (key %))
-                                                                m))
-                                 block-changes (cond-> (select-keys m allowed-attributes)
-                                                 ;; disallow any type -> "page" but do allow any conversion to a non-page type
-                                                 (ldb/internal-page? m)
-                                                 (dissoc :block/type))]
-                             (when-let [ignored-attrs (not-empty (apply dissoc m (into disallowed-attributes allowed-attributes)))]
-                               (notify-user {:msg (str "Import ignored the following attributes on page " (pr-str (:block/title m)) ": "
-                                                       ignored-attrs)}))
-                             (when (seq block-changes)
-                               (cond-> (merge block-changes {:block/uuid page-uuid})
-                                 (seq (:block/alias m))
-                                 (update-page-alias page-names-to-uuids)
-                                 (:block/tags m)
-                                 (update-page-tags @conn tag-classes page-names-to-uuids (:all-idents import-state)))))
-
+                         (if-let [page-uuid (existing-pages-by-name (:block/name m))]
+                           (build-existing-page m @conn page-uuid page-names-to-uuids options)
                            (when (or (= "class" (:block/type m))
                                      ;; Don't build a new page if it overwrites an existing class
                                      (not (some-> (get @(:all-idents import-state) (keyword (:block/title m)))
@@ -840,7 +875,7 @@
                        (map :block all-pages-m))]
     {:pages-tx pages-tx
      :page-properties-tx (mapcat :properties-tx all-pages-m)
-     :existing-pages existing-page-names-to-uuids
+     :existing-pages existing-pages-by-name
      :page-names-to-uuids page-names-to-uuids}))
 
 (defn- build-upstream-properties-tx-for-default

+ 11 - 4
deps/graph-parser/test/logseq/graph_parser/exporter_test.cljs

@@ -173,8 +173,8 @@
 
       ;; Counts
       ;; Includes journals as property values e.g. :logseq.task/deadline
-      (is (= 18 (count (d/q '[:find ?b :where [?b :block/type "journal"]] @conn))))
-      (is (= 18 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Journal]] @conn))))
+      (is (= 19 (count (d/q '[:find ?b :where [?b :block/type "journal"]] @conn))))
+      (is (= 19 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Journal]] @conn))))
 
       (is (= 4 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Task]] @conn))))
       (is (= 3 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Query]] @conn))))
@@ -186,7 +186,7 @@
                          (filter ldb/internal-page?))))
           "Correct number of pages with block content")
       (is (= 4 (count (d/datoms @conn :avet :block/type "whiteboard"))))
-      (is (= 1 (count @(:ignored-properties import-state))) ":filters should be the only ignored property")
+      (is (= 0 (count @(:ignored-properties import-state))) ":filters should be the only ignored property")
       (is (= 1 (count @assets))))
 
     (testing "logseq files"
@@ -293,7 +293,14 @@
           "numered block has correct property")
 
       (is (= #{"gpt"}
-             (:block/alias (readable-properties @conn (find-page-by-name @conn "chat-gpt")))))
+             (:block/alias (readable-properties @conn (find-page-by-name @conn "chat-gpt"))))
+          "alias set correctly")
+
+      (is (= {:logseq.property.linked-references/includes #{"Oct 9th, 2024"}
+              :logseq.property.linked-references/excludes #{"ref2"}}
+             (select-keys (readable-properties @conn (find-page-by-name @conn "chat-gpt"))
+                          [:logseq.property.linked-references/excludes :logseq.property.linked-references/includes]))
+          "linked ref filters set correctly")
 
       ;; Queries
       (is (= {:logseq.property.table/sorting [{:id :user.property/prop-num, :asc? false}]

+ 3 - 0
deps/graph-parser/test/resources/exporter-test-graph/journals/2024_10_09.md

@@ -0,0 +1,3 @@
+- test linked ref filters
+	- [[ref1]] [[chat-gpt]]
+	- [[ref2]] [[chat-gpt]]

+ 1 - 1
deps/graph-parser/test/resources/exporter-test-graph/pages/chat-gpt.md

@@ -1,4 +1,4 @@
-filters:: {"contents" true}
+filters:: {"oct 9th, 2024" true, "ref2" false}
 type:: [[LargeLanguageModel]]
 tags:: ai, #fun, [[LargeLanguageModel]] 
 alias:: gpt

+ 6 - 5
deps/outliner/src/logseq/outliner/property.cljs

@@ -12,6 +12,7 @@
             [logseq.db.frontend.property.type :as db-property-type]
             [logseq.db.frontend.db-ident :as db-ident]
             [logseq.db.frontend.entity-plus :as entity-plus]
+            [logseq.db.frontend.schema :as db-schema]
             [logseq.db.sqlite.util :as sqlite-util]
             [logseq.outliner.core :as outliner-core]
             [logseq.outliner.validate :as outliner-validate]
@@ -251,15 +252,15 @@
   (let [block-eid (->eid block-eid)
         _ (assert (qualified-keyword? property-id) "property-id should be a keyword")
         block (d/entity @conn block-eid)
-        property (d/entity @conn property-id)
-        _ (assert (some? property) (str "Property " property-id " doesn't exist yet"))
-        property-type (get-in property [:block/schema :type] :default)
-        db-attribute? (contains? db-property/db-attribute-properties property-id)]
+        db-attribute? (some? (db-schema/schema-for-db-based-graph property-id))]
     (if db-attribute?
       (when-not (and (= property-id :block/alias) (= v (:db/id block))) ; alias can't be itself
         (ldb/transact! conn [{:db/id (:db/id block) property-id v}]
                        {:outliner-op :save-block}))
-      (let [new-value (if (db-property-type/user-ref-property-types property-type)
+      (let [property (d/entity @conn property-id)
+            _ (assert (some? property) (str "Property " property-id " doesn't exist yet"))
+            property-type (get-in property [:block/schema :type] :default)
+            new-value (if (db-property-type/user-ref-property-types property-type)
                         (convert-ref-property-value conn property-id v property-type)
                         v)
             existing-value (get block property-id)]

+ 14 - 14
deps/publishing/src/logseq/publishing/db.cljs

@@ -4,25 +4,25 @@
             [logseq.db.frontend.rules :as rules]
             [clojure.set :as set]
             [clojure.string :as string]
-            [logseq.db.frontend.entity-util :as entity-util]
-            [logseq.db.frontend.property :as db-property]))
+            [logseq.db.frontend.entity-util :as entity-util]))
 
 (defn ^:api get-area-block-asset-url
   "Returns asset url for an area block used by pdf assets. This lives in this ns
   because it is used by this dep and needs to be independent from the frontend app"
   [db block page]
-  (when-some [props (and block page (:block/properties block))]
-    ;; Can't use db-property-util/lookup b/c repo isn't available
-    (let [prop-lookup-fn (if (entity-util/db-based-graph? db)
-                           #(db-property/property-value-content (get %1 %2))
-                           #(get %1 (keyword (name %2))))]
-      (when-some [uuid' (:block/uuid block)]
-        (when-some [stamp (prop-lookup-fn props :logseq.property.pdf/hl-stamp)]
-          (let [group-key      (string/replace-first (:block/title page) #"^hls__" "")
-                hl-page        (prop-lookup-fn props :logseq.property.pdf/hl-page)
-                encoded-chars? (boolean (re-find #"(?i)%[0-9a-f]{2}" group-key))
-                group-key      (if encoded-chars? (js/encodeURI group-key) group-key)]
-            (str "./assets/" group-key "/" (str hl-page "_" uuid' "_" stamp ".png"))))))))
+  (let [db-based? (entity-util/db-based-graph? db)]
+    (when-some [uuid' (:block/uuid block)]
+      (if db-based?
+        (when-let [image (:logseq.property.pdf/hl-image block)]
+          (str "./assets/" (:block/uuid image) ".png"))
+        (let [props (and block page (:block/properties block))
+              prop-lookup-fn #(get %1 (keyword (name %2)))]
+          (when-some [stamp (:hl-stamp props)]
+            (let [group-key      (string/replace-first (:block/title page) #"^hls__" "")
+                  hl-page        (prop-lookup-fn props :logseq.property.pdf/hl-page)
+                  encoded-chars? (boolean (re-find #"(?i)%[0-9a-f]{2}" group-key))
+                  group-key      (if encoded-chars? (js/encodeURI group-key) group-key)]
+              (str "./assets/" group-key "/" (str hl-page "_" uuid' "_" stamp ".png")))))))))
 
 (defn- clean-asset-path-prefix
   [path]

+ 14 - 14
deps/publishing/src/logseq/publishing/html.cljs

@@ -23,22 +23,22 @@ necessary db filtering"
 ;; Copied from https://github.com/babashka/babashka/blob/8c1077af00c818ade9e646dfe1297bbe24b17f4d/examples/notes.clj#L21
 (defn- html [v]
   (cond (vector? v)
-    (let [tag (first v)
-          attrs (second v)
-          attrs (when (map? attrs) attrs)
-          elts (if attrs (nnext v) (next v))
-          tag-name (name tag)]
-      (gstring/format "<%s%s>%s</%s>\n" tag-name (html attrs) (html elts) tag-name))
-    (map? v)
-    (string/join ""
-                 (keep (fn [[k v]]
+        (let [tag (first v)
+              attrs (second v)
+              attrs (when (map? attrs) attrs)
+              elts (if attrs (nnext v) (next v))
+              tag-name (name tag)]
+          (gstring/format "<%s%s>%s</%s>\n" tag-name (html attrs) (html elts) tag-name))
+        (map? v)
+        (string/join ""
+                     (keep (fn [[k v]]
                          ;; Skip nil values because some html tags haven't been
                          ;; given values through html-options
-                         (when (some? v)
-                           (gstring/format " %s=\"%s\"" (name k) v))) v))
-    (seq? v)
-    (string/join " " (map html v))
-    :else (str v)))
+                             (when (some? v)
+                               (gstring/format " %s=\"%s\"" (name k) v))) v))
+        (seq? v)
+        (string/join " " (map html v))
+        :else (str v)))
 
 (defn- ^:large-vars/html publishing-html
   [transit-db app-state options]

+ 1 - 7
src/main/frontend/common.css

@@ -469,7 +469,7 @@ a.tag {
 }
 
 .preview-ref-link {
-  @apply inline-block;
+  @apply inline-flex;
 
   & + &, & + a.tag, a.tag + & {
     @apply mx-[3px];
@@ -478,12 +478,6 @@ a.tag {
   .block-title-wrap.as-heading {
     @apply my-0 pb-0;
   }
-
-  .page-ref {
-    .ui__icon.ti {
-      @apply relative left-[2px] top-[5px];
-    }
-  }
 }
 
 svg.note {

+ 21 - 25
src/main/frontend/components/all_pages.cljs

@@ -13,29 +13,25 @@
             [logseq.shui.ui :as shui]))
 
 (defn- columns
-  [db]
-  (let [db-based? (ldb/db-based-graph? db)]
-    (->> [{:id :block/title
-           :name (t :block/name)
-           :cell (fn [_table row _column]
-                   (component-block/page-cp {} row))
-           :type :string}
-          {:id :block/type
-           :name "Type"
-           :cell (fn [_table row _column]
-                   (let [type (get row :block/type)]
-                     [:div.capitalize (if (= type "class") "tag" type)]))
-           :get-value (fn [row] (get row :block/type))
-           :type :string}
-          (when db-based?
-            {:id :block/tags
-             :name "Tags"})
-          {:id :block.temp/refs-count
-           :name (t :page/backlinks)
-           :cell (fn [_table row _column] (:block.temp/refs-count row))
-           :type :number}]
-         (remove nil?)
-         vec)))
+  []
+  (->> [{:id :block/title
+         :name (t :block/name)
+         :cell (fn [_table row _column]
+                 (component-block/page-cp {:show-icon? true} row))
+         :type :string}
+        {:id :block/type
+         :name "Type"
+         :cell (fn [_table row _column]
+                 (let [type (get row :block/type)]
+                   [:div.capitalize (if (= type "class") "tag" type)]))
+         :get-value (fn [row] (get row :block/type))
+         :type :string}
+        {:id :block.temp/refs-count
+         :name (t :page/backlinks)
+         :cell (fn [_table row _column] (:block.temp/refs-count row))
+         :type :number}]
+       (remove nil?)
+       vec))
 
 (defn- get-all-pages
   []
@@ -46,7 +42,7 @@
   []
   (let [db (db/get-db)
         [data set-data!] (rum/use-state (get-all-pages))
-        columns' (views/build-columns {} (columns db)
+        columns' (views/build-columns {} (columns)
                                       {:with-object-name? false})
         view-entity (first (ldb/get-all-pages-views db))]
     (rum/use-effect!
@@ -57,7 +53,7 @@
                  data (map (fn [row] (assoc row :block.temp/refs-count (get result (:db/id row) 0))) data)]
            (set-data! data))))
      [])
-    [:div.ls-all-pages.w-full
+    [:div.ls-all-pages.w-full.mx-auto
      (views/view view-entity {:data data
                               :set-data! set-data!
                               :title-key :all-pages/table-title

+ 212 - 163
src/main/frontend/components/block.cljs

@@ -275,106 +275,124 @@
                    state)}
   [state config title src metadata full-text local?]
   (let [size (get state ::size)
-        breadcrumb? (:breadcrumb? config)]
-    (ui/resize-provider
-     (ui/resize-consumer
-      (if (and (not (mobile-util/native-platform?))
-               (not breadcrumb?))
-        (cond->
-         {:className "resize image-resize"
-          :onSizeChanged (fn [value]
-                           (when (and (not @*resizing-image?)
-                                      (some? @size)
-                                      (not= value @size))
-                             (reset! *resizing-image? true))
-                           (reset! size value))
-          :onPointerUp (fn []
-                         (when (and @size @*resizing-image?)
-                           (when-let [block-id (:block/uuid config)]
-                             (let [size (bean/->clj @size)]
-                               (editor-handler/resize-image! block-id metadata full-text size))))
-                         (when @*resizing-image?
-                              ;; TODO: need a better way to prevent the clicking to edit current block
-                           (js/setTimeout #(reset! *resizing-image? false) 200)))
-          :onClick (fn [e]
-                     (when @*resizing-image? (util/stop e)))}
-          (and (:width metadata) (not (util/mobile?)))
-          (assoc :style {:width (:width metadata)}))
-        {})
-      [:div.asset-container {:key "resize-asset-container"}
-       [:img.rounded-sm.relative
-        (merge
-         {:loading "lazy"
-          :referrerPolicy "no-referrer"
-          :src src
-          :title title}
-         metadata)]
-       (when-not breadcrumb?
-         [:<>
-          (let [image-src (fs/asset-path-normalize src)]
-            [:.asset-action-bar {:aria-hidden "true"}
-             [:.flex
-              (when-not config/publishing?
-                [:button.asset-action-btn
-                 {:title (t :asset/delete)
-                  :tabIndex "-1"
-                  :on-pointer-down util/stop
-                  :on-click
-                  (fn [e]
-                    (util/stop e)
-                    (when-let [block-id (some-> (.-target e) (.closest "[blockid]") (.getAttribute "blockid") (uuid))]
-                      (let [*local-selected? (atom local?)]
-                        (-> (shui/dialog-confirm!
-                              [:div.text-xs.opacity-60.-my-2
-                               (when local?
-                                 [:label.flex.gap-1.items-center
-                                  (shui/checkbox
-                                    {:default-checked @*local-selected?
-                                     :on-checked-change #(reset! *local-selected? %)})
-                                  (t :asset/physical-delete)])]
-                              {:title (t :asset/confirm-delete (.toLocaleLowerCase (t :text/image)))
-                               :outside-cancel? true})
-                          (p/then (fn []
-                                    (shui/dialog-close!)
-                                    (editor-handler/delete-asset-of-block!
-                                      {:block-id block-id
-                                       :local? local?
-                                       :delete-local? @*local-selected?
-                                       :repo (state/get-current-repo)
-                                       :href src
-                                       :title title
-                                       :full-text full-text})))))))}
-                 (ui/icon "trash")])
-
-              [:button.asset-action-btn
-               {:title (t :asset/copy)
-                :tabIndex "-1"
-                :on-pointer-down util/stop
-                :on-click (fn [e]
-                            (util/stop e)
-                            (-> (util/copy-image-to-clipboard image-src)
-                                (p/then #(notification/show! "Copied!" :success))))}
-               (ui/icon "copy")]
-
-              [:button.asset-action-btn
-               {:title (t :asset/maximize)
-                :tabIndex "-1"
-                :on-pointer-down util/stop
-                :on-click open-lightbox}
-
-               (ui/icon "maximize")]
-
-              (when (util/electron?)
-                [:button.asset-action-btn
-                 {:title (t (if local? :asset/show-in-folder :asset/open-in-browser))
-                  :tabIndex "-1"
-                  :on-pointer-down util/stop
-                  :on-click (fn [e]
-                              (util/stop e)
-                              (if local?
-                                (ipc/ipc "openFileInFolder" image-src)
-                                (js/window.apis.openExternal image-src)))}
-                 (shui/tabler-icon "folder-pin")])]])])]))))
+        breadcrumb? (:breadcrumb? config)
+        asset-block (:asset-block config)
+        asset-container [:div.asset-container {:key "resize-asset-container"}
+                         [:img.rounded-sm.relative
+                          (merge
+                           {:loading "lazy"
+                            :referrerPolicy "no-referrer"
+                            :src src
+                            :title title}
+                           metadata)]
+                         (when-not breadcrumb?
+                           [:<>
+                            (let [image-src (fs/asset-path-normalize src)]
+                              [:.asset-action-bar {:aria-hidden "true"}
+                               [:.flex
+                                (when-not config/publishing?
+                                  [:button.asset-action-btn
+                                   {:title (t :asset/delete)
+                                    :tabIndex "-1"
+                                    :on-pointer-down util/stop
+                                    :on-click
+                                    (fn [e]
+                                      (util/stop e)
+                                      (when-let [block-id (some-> (.-target e) (.closest "[blockid]") (.getAttribute "blockid") (uuid))]
+                                        (let [*local-selected? (atom local?)]
+                                          (-> (shui/dialog-confirm!
+                                               [:div.text-xs.opacity-60.-my-2
+                                                (when (and local? (not= (:block/uuid asset-block) block-id))
+                                                  [:label.flex.gap-1.items-center
+                                                   (shui/checkbox
+                                                    {:default-checked @*local-selected?
+                                                     :on-checked-change #(reset! *local-selected? %)})
+                                                   (t :asset/physical-delete)])]
+                                               {:title (t :asset/confirm-delete (.toLocaleLowerCase (t :text/image)))
+                                                :outside-cancel? true})
+                                              (p/then (fn []
+                                                        (shui/dialog-close!)
+                                                        (editor-handler/delete-asset-of-block!
+                                                         {:block-id block-id
+                                                          :asset-block asset-block
+                                                          :local? local?
+                                                          :delete-local? @*local-selected?
+                                                          :repo (state/get-current-repo)
+                                                          :href src
+                                                          :title title
+                                                          :full-text full-text})))))))}
+                                   (ui/icon "trash")])
+
+                                [:button.asset-action-btn
+                                 {:title (t :asset/copy)
+                                  :tabIndex "-1"
+                                  :on-pointer-down util/stop
+                                  :on-click (fn [e]
+                                              (util/stop e)
+                                              (-> (util/copy-image-to-clipboard image-src)
+                                                  (p/then #(notification/show! "Copied!" :success))))}
+                                 (ui/icon "copy")]
+
+                                [:button.asset-action-btn
+                                 {:title (t :asset/maximize)
+                                  :tabIndex "-1"
+                                  :on-pointer-down util/stop
+                                  :on-click open-lightbox}
+
+                                 (ui/icon "maximize")]
+
+                                (when (util/electron?)
+                                  [:button.asset-action-btn
+                                   {:title (t (if local? :asset/show-in-folder :asset/open-in-browser))
+                                    :tabIndex "-1"
+                                    :on-pointer-down util/stop
+                                    :on-click (fn [e]
+                                                (util/stop e)
+                                                (if local?
+                                                  (ipc/ipc "openFileInFolder" image-src)
+                                                  (js/window.apis.openExternal image-src)))}
+                                   (shui/tabler-icon "folder-pin")])]])])]
+        width (or (get-in asset-block [:logseq.property.asset/resize-metadata :width])
+                  (:width metadata))
+        height (or (get-in asset-block [:logseq.property.asset/resize-metadata :height])
+                   (:height metadata))
+        style (when-not (util/mobile?)
+                (cond (and width height)
+                      {:width width :height height}
+                      width
+                      {:width width}
+                      height
+                      {:height height}
+                      :else
+                      {}))]
+    (if (:disable-resize? config)
+      asset-container
+      (ui/resize-provider
+       (ui/resize-consumer
+        (if (and (not (mobile-util/native-platform?))
+                 (not breadcrumb?))
+          (cond->
+           {:className "resize image-resize"
+            :onSizeChanged (fn [value]
+                             (when (and (not @*resizing-image?)
+                                        (some? @size)
+                                        (not= value @size))
+                               (reset! *resizing-image? true))
+                             (reset! size value))
+            :onPointerUp (fn []
+                           (when (and @size @*resizing-image?)
+                             (when-let [block-id (:block/uuid config)]
+                               (let [size (bean/->clj @size)]
+                                 (editor-handler/resize-image! config block-id metadata full-text size))))
+                           (when @*resizing-image?
+                           ;; TODO:​ need a better way to prevent the clicking to edit current block
+                             (js/setTimeout #(reset! *resizing-image? false) 200)))
+            :onClick (fn [e]
+                       (when @*resizing-image? (util/stop e)))}
+            style
+            (assoc :style style))
+          {})
+        asset-container)))))
 
 (rum/defc audio-cp [src]
   ;; Change protocol to allow media fragment uris to play
@@ -382,18 +400,47 @@
            :controls true
            :on-touch-start #(util/stop %)}])
 
+(defn- open-pdf-file
+  [e block href]
+  (when-let [s (or href (some-> (.-target e) (.-dataset) (.-href)))]
+    (let [load$ (fn []
+                  (p/let [href (or href
+                                   (if (or (mobile-util/native-platform?) (util/electron?))
+                                     s
+                                     (assets-handler/<make-asset-url s)))]
+                    (when-let [current (pdf-assets/inflate-asset s {:block block
+                                                                    :href href})]
+                      (state/set-current-pdf! current)
+                      (util/stop e))))]
+      (-> (load$)
+          (p/catch
+           (fn [^js _e]
+             ;; load pdf asset to indexed db
+             (p/let [[handle] (js/window.showOpenFilePicker
+                               (bean/->js {:multiple false :startIn "documents" :types [{:accept {"application/pdf" [".pdf"]}}]}))
+                     file (.getFile handle)
+                     buffer (.arrayBuffer file)]
+               (when-let [content (some-> buffer (js/Uint8Array.))]
+                 (let [repo (state/get-current-repo)
+                       file-rpath (string/replace s #"^[.\/\\]*assets[\/\\]+" "assets/")
+                       dir (config/get-repo-dir repo)]
+                   (-> (fs/write-file! repo dir file-rpath content nil)
+                       (p/then load$)))))
+             (js/console.error _e)))))))
+
 (rum/defcs asset-link < rum/reactive
   (rum/local nil ::src)
   [state config title href metadata full_text]
   (let [src (::src state)
         granted? (state/sub [:nfs/user-granted? (state/get-current-repo)])
-        href (config/get-local-asset-absolute-path href)]
+        href (config/get-local-asset-absolute-path href)
+        db-based? (config/db-based-graph? (state/get-current-repo))]
     (when (and (or granted?
                    (util/electron?)
                    (mobile-util/native-platform?)
-                   (config/db-based-graph? (state/get-current-repo)))
+                   db-based?)
                (nil? @src))
-      (p/then (assets-handler/make-asset-url href) #(reset! src %)))
+      (p/then (assets-handler/<make-asset-url href) #(reset! src %)))
 
     (when @src
       (let [ext (keyword (or (util/get-file-ext @src)
@@ -416,6 +463,10 @@
           (asset-loader @src
                         #(audio-cp @src))
 
+          (contains? config/video-formats ext)
+          [:video {:src @src
+                   :controls true}]
+
           (contains? (common-config/img-formats) ext)
           (asset-loader @src
                         #(resizable-image config title @src metadata full_text true))
@@ -428,9 +479,16 @@
            title]
 
           (= ext :pdf)
-          [:a.asset-ref.is-pdf {:href @src
-                                :on-click share-fn}
-           title]
+          [:a.asset-ref.is-pdf
+           {:data-href href
+            :draggable true
+            :on-drag-start #(.setData (gobj/get % "dataTransfer") "file" href)
+            :on-click (fn [e]
+                        (util/stop e)
+                        (open-pdf-file e (:asset-block config) @src))}
+           (if db-based?
+             title
+             [:span [:span.opacity-70 "[[📚"] title [:span.opacity-70 "]]"]])]
 
           :else
           [:a.asset-ref.is-doc {:href @src
@@ -560,7 +618,8 @@
                     (util/page-name-sanity-lc (:block/title page-entity)))
         breadcrumb? (:breadcrumb? config)
         config (assoc config :whiteboard-page? whiteboard-page?)
-        untitled? (when page-name (model/untitled-page? (:block/title page-entity)))]
+        untitled? (when page-name (model/untitled-page? (:block/title page-entity)))
+        show-icon? (:show-icon? config)]
     [:a.relative
      {:tabIndex "0"
       :class (cond->
@@ -598,6 +657,10 @@
       :on-key-up (fn [e] (when (and e (= (.-key e) "Enter") (not meta-click?))
                            (state/clear-edit!)
                            (open-page-ref config page-entity e page-name contents-page?)))}
+     (when show-icon?
+       (when-let [icon (icon-component/get-node-icon-cp page-entity {:color? true :not-text-or-page? true})]
+         [:span.mr-1
+          icon]))
      [:span
       (if (and (coll? children) (seq children))
         (for [child children]
@@ -623,7 +686,7 @@
                                      s (cond untitled?
                                              (t :untitled)
 
-                        ;; The page-name-in-block generated by the auto-complete is not page-name-sanitized
+                                             ;; The page-name-in-block generated by the auto-complete is not page-name-sanitized
                                              (pdf-utils/hls-file? page-name)
                                              (pdf-utils/fix-local-asset-pagename page-name)
 
@@ -870,6 +933,15 @@
     (when draw-component
       (draw-component {:file file :block-uuid block-uuid}))))
 
+(rum/defc asset-cp
+  [config block]
+  (let [asset-type (:logseq.property.asset/type block)]
+    (asset-link (assoc config :asset-block block)
+                (:block/title block)
+                (path/path-join (str "../" common-config/local-assets-dir) (str (:block/uuid block) "." asset-type))
+                nil
+                nil)))
+
 (rum/defc page-reference < rum/reactive
   "Component for page reference"
   [html-export? s {:keys [nested-link? show-brackets? id] :as config} label]
@@ -881,7 +953,11 @@
           show-brackets? (if (some? show-brackets?) show-brackets? (state/show-brackets?))
           block-uuid (:block/uuid config)
           contents-page? (= "contents" (string/lower-case (str id)))
-          block (db/get-page s)]
+          block (db/get-page s)
+          config' (assoc config
+                         :label (mldoc/plain->text label)
+                         :contents-page? contents-page?
+                         :show-icon? true?)]
       (cond
         (string/ends-with? s ".excalidraw")
         [:div.draw {:on-click (fn [e]
@@ -895,20 +971,14 @@
                     (not html-export?)
                     (not contents-page?))
            [:span.text-gray-500.bracket page-ref/left-brackets])
-         (page-cp (assoc config
-                         :label (mldoc/plain->text label)
-                         :contents-page? contents-page?)
-                  {:block/name s})
+         (page-cp config' {:block/name s})
          (when (and (or show-brackets? nested-link?)
                     (not html-export?)
                     (not contents-page?))
            [:span.text-gray-500.bracket page-ref/right-brackets])]
 
         :else
-        (page-cp (assoc config
-                        :label (mldoc/plain->text label)
-                        :contents-page? contents-page?)
-                 {:block/name s})))))
+        (page-cp config' {:block/name s})))))
 
 (defn- latex-environment-content
   [name option content]
@@ -1052,10 +1122,12 @@
                          (block-content (assoc config :block-ref? true :stop-events? stop-inner-events?)
                                         block nil (:block/uuid block)
                                         (:slide? config))]
-                  inner (if label
+                  inner (cond
+                          label
                           (->elem
                            :span.block-ref
                            (map-inline config label))
+                          :else
                           title)]
               [:div.block-ref-wrap.inline
                {:data-type    (name (or block-type :default))
@@ -1210,38 +1282,6 @@
       (contains? config/audio-formats ext)
       (audio-link config url s label metadata full_text)
 
-      (= ext :pdf)
-      [:a.asset-ref.is-pdf
-       {:data-href s
-        :on-click (fn [^js e]
-                    (when-let [s (some-> (.-target e) (.-dataset) (.-href))]
-                      (let [load$ (fn []
-                                    (p/let [href (if (or (mobile-util/native-platform?) (util/electron?))
-                                                   s
-                                                   (assets-handler/make-asset-url s))]
-                                      (when-let [current (pdf-assets/inflate-asset s {:href href})]
-                                        (state/set-current-pdf! current)
-                                        (util/stop e))))]
-                        (-> (load$)
-                            (p/catch
-                             (fn [^js _e]
-                              ;; load pdf asset to indexed db
-                               (p/let [[handle] (js/window.showOpenFilePicker
-                                                 (bean/->js {:multiple false :startIn "documents" :types [{:accept {"application/pdf" [".pdf"]}}]}))
-                                       file (.getFile handle)
-                                       buffer (.arrayBuffer file)]
-                                 (when-let [content (some-> buffer (js/Uint8Array.))]
-                                   (let [repo (state/get-current-repo)
-                                         file-rpath (string/replace s #"^[.\/\\]*assets[\/\\]+" "assets/")
-                                         dir (config/get-repo-dir repo)]
-                                     (-> (fs/write-file! repo dir file-rpath content nil)
-                                         (p/then load$)))))
-                               (js/console.error _e)))))))
-        :draggable true
-        :on-drag-start #(.setData (gobj/get % "dataTransfer") "file" s)}
-       (or label-text
-           (->elem :span (map-inline config label)))]
-
       (contains? config/doc-formats ext)
       (asset-link config label-text s metadata full_text)
 
@@ -1977,7 +2017,8 @@
                                (or (db/page? block)
                                    (:logseq.property/icon block)
                                    link?
-                                   (some :logseq.property/icon (:block/tags block))))
+                                   (some :logseq.property/icon (:block/tags block))
+                                   (contains? #{"pdf"} (:logseq.property.asset/type block))))
                           icon
 
                           :else
@@ -2102,7 +2143,8 @@
                        [:label.blank " "]]
 
                       (when (and area?
-                                 (pu/lookup properties :logseq.property.pdf/hl-stamp))
+                                 (or (:hl-stamp properties)
+                                     (:logseq.property.pdf/hl-image properties)))
                         (pdf-assets/area-display block))])]
        (remove-nils
         (concat
@@ -2144,17 +2186,23 @@
   [config block]
   (let [collapsed? (:collapsed? config)
         block' (db/entity (:db/id block))
-        node-type (:logseq.property.node/display-type block')
+        node-display-type (:logseq.property.node/display-type block')
         query? (ldb/class-instance? (db/entity :logseq.class/Query) block')
         query (:logseq.property/query block')
-        advanced-query? (and query? (= :code node-type))]
+        advanced-query? (and query? (= :code node-display-type))]
     (cond
-      (= :code node-type)
+      (:raw-title? config)
+      (text-block-title (dissoc config :raw-title?) block)
+
+      (= "asset" (:block/type block))
+      (asset-cp config block)
+
+      (= :code node-display-type)
       [:div.flex.flex-1.w-full
        (src-cp (assoc config :block block) {:language (:logseq.property.code/lang block)})]
 
       ;; TODO: switched to https://cortexjs.io/mathlive/ for editing
-      (= :math node-type)
+      (= :math node-display-type)
       (latex/latex (str (:container-id config) "-" (:db/id block)) (:block/title block) true false)
 
       (and query?
@@ -2708,19 +2756,17 @@
                      (count (:block/_refs block))
                      (rum/react *refs-count))
         table? (:table? config)
-        editor-block (state/sub :editor/block)
         raw-mode-block (state/sub :editor/raw-mode-block)
         type-block-editor? (and (contains? #{:code} (:logseq.property.node/display-type block))
                                 (not= (:db/id block) (:db/id raw-mode-block)))
-        config (assoc config :block-parent-id block-id)
-        editing-local? (or edit? (and editor-block (= (:db/id editor-block) (:db/id block))))]
+        config (assoc config :block-parent-id block-id)]
     [:div.block-content-or-editor-wrap
      {:class (when (:page-title? config) "ls-page-title-container")
       :data-node-type (some-> (:logseq.property.node/display-type block) name)}
      (when (and db-based? (not table?)) (block-positioned-properties config block :block-left))
      [:div.block-content-or-editor-inner
       [:div.flex.flex-1.flex-row.gap-1.items-center
-       (if (and editor-box editing-local? (not type-block-editor?))
+       (if (and editor-box edit? (not type-block-editor?))
          [:div.editor-wrapper.flex.flex-1
           {:id editor-id
            :class (util/classnames [{:opacity-50 (boolean (or (ldb/built-in? block) (ldb/journal? block)))}])}
@@ -2986,7 +3032,7 @@
               (when (and (config/local-file-based-graph? repo) (not (state/editing?)))
                 ;; Basically the same logic as editor-handler/upload-asset,
                 ;; does not require edting
-                (-> (editor-handler/save-assets! repo (js->clj files))
+                (-> (editor-handler/file-based-save-assets! repo (js->clj files))
                     (p/then
                      (fn [res]
                        (when-let [[asset-file-name file-obj asset-file-fpath matched-alias] (and (seq res) (first res))]
@@ -3276,7 +3322,7 @@
                                                               (:db/id block)
                                                               (pu/get-pid :logseq.property/icon)
                                                               (select-keys icon [:id :type :color]))
-                             ;; del
+                                                             ;; del
                                                              (db-property-handler/remove-block-property!
                                                               (:db/id block)
                                                               (pu/get-pid :logseq.property/icon))))
@@ -3386,7 +3432,10 @@
        (rum/with-key
          (block-container-inner state repo config' block
                                 {:navigating-block navigating-block :navigated? navigated?})
-         (str "block-inner" (:block/uuid block)))))))
+         (str "block-inner-"
+              (:container-id config)
+              "-"
+              (:block/uuid block)))))))
 
 (defn divide-lists
   [[f & l]]

+ 12 - 16
src/main/frontend/components/block.css

@@ -52,6 +52,10 @@
     transform: translate3d(0, 0, 0);
   }
 
+  .image-resize {
+    display: flex;
+  }
+
   .draw [aria-labelledby="shapes-title"] {
     position: absolute;
     left: 50%;
@@ -81,7 +85,13 @@
   @apply justify-between items-center;
 }
 
+.page-ref {
+    @apply inline-flex items-center;
+}
+
 .breadcrumb {
+  @apply flex flex-row items-center;
+
   .asset-container > img {
     height: 18px;
     width: unset !important;
@@ -94,11 +104,6 @@
   }
 
   &.block-parents {
-    .ui__icon {
-      position: relative;
-      top: 4px;
-    }
-
     a {
       color: var(--ls-primary-text-color);
 
@@ -107,10 +112,6 @@
       }
     }
   }
-
-  .page-ref {
-    @apply inline-flex items-baseline;
-  }
 }
 
 .open-block-ref-link {
@@ -345,7 +346,7 @@
 }
 
 .page-reference {
-  @apply rounded transition-[background];
+  @apply inline-flex items-center rounded transition-[background];
 
   .bracket {
     @apply opacity-30;
@@ -357,16 +358,11 @@
 }
 
 .page-ref {
-  @apply inline;
   color: var(--lx-accent-11, var(--ls-link-text-color, hsl(var(--primary)/.8)));
 
   &:hover {
     color: var(--lx-accent-11, var(--ls-link-text-color, hsl(var(--primary))));
   }
-
-  .ui__icon.ti {
-    @apply relative top-[3px] left-0;
-  }
 }
 
 .asset-ref {
@@ -933,7 +929,7 @@ html.is-mac {
   height: 38px;
 }
 
-.ls-page-title .positioned-properties svg {
+.ls-page-title .positioned-properties svg, .page-ref svg {
   width: 16px;
   height: 16px;
 }

+ 8 - 0
src/main/frontend/components/block/views.css

@@ -0,0 +1,8 @@
+.ls-cards {
+  @apply grid grid-cols-[repeat(auto-fill,18rem)] gap-4 content;
+
+  .ls-card-item {
+    @apply shadow-md rounded p-2 overflow-y-scroll;
+    height: 15rem;
+  }
+}

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

@@ -29,7 +29,8 @@
           default-collapsed? (> (count children-pages) 30)]
       [:div.mt-4
        (ui/foldable
-        [:h2.font-medium "Children (" (count children-pages) ")"]
-        [:div.mt-2.ml-1 (class-children-aux class {:default-collapsed? default-collapsed?})]
+        [:div.font-medium.opacity-50
+         (str "Children (" (count children-pages) ")")]
+        [:div.ml-1.mt-2 (class-children-aux class {:default-collapsed? default-collapsed?})]
         {:default-collapsed? false
          :title-trigger? true})])))

+ 8 - 6
src/main/frontend/components/export.cljs

@@ -40,8 +40,8 @@
           :title "Change backup folder"
           :on-click (fn []
                       (p/do!
-                        (db/transact! [[:db/retractEntity :logseq.kv/graph-backup-folder]])
-                        (reset! *backup-folder nil)))
+                       (db/transact! [[:db/retractEntity :logseq.kv/graph-backup-folder]])
+                       (reset! *backup-folder nil)))
           :size :sm}
          (ui/icon "edit"))]
        (shui/button
@@ -68,7 +68,6 @@
                        (export/auto-db-backup! repo {:backup-now? false})))}
         "Backup now"))]))
 
-
 (rum/defc export
   []
   (when-let [current-repo (state/get-current-repo)]
@@ -85,11 +84,15 @@
           [:div
            [:a.font-medium {:on-click #(export/export-repo-as-json! current-repo)}
             (t :export-json)]])
-        (when (config/db-based-graph? current-repo)
+        (when db-based?
           [:div
            [:a.font-medium {:on-click #(export/export-repo-as-sqlite-db! current-repo)}
             (t :export-sqlite-db)]])
-        (when (config/db-based-graph? current-repo)
+        (when db-based?
+          [:div
+           [:a.font-medium {:on-click #(export/export-repo-as-zip! current-repo)}
+            (t :export-zip)]])
+        (when db-based?
           [:div
            [:a.font-medium {:on-click #(export/export-repo-as-debug-json! current-repo)}
             "Export debug JSON"]
@@ -129,7 +132,6 @@
        [:a#export-as-opml.hidden]
        [:a#convert-markdown-to-unordered-list-or-heading.hidden]])))
 
-
 (def *export-block-type (atom :text))
 
 (def text-indent-style-options [{:label "dashes"

+ 46 - 0
src/main/frontend/components/filepicker.cljs

@@ -0,0 +1,46 @@
+(ns frontend.components.filepicker
+  "File picker"
+  (:require [rum.core :as rum]
+            [logseq.shui.ui :as shui]
+            [cljs-drag-n-drop.core :as dnd]
+            [goog.dom :as gdom]))
+
+(rum/defcs picker <
+  (rum/local nil ::input)
+  {:did-mount (fn [state]
+                (let [on-change (:on-change (first (:rum/args state)))]
+                  (when-let [element (gdom/getElement "filepicker")]
+                    (dnd/subscribe!
+                     element
+                     :upload-files
+                     {:drop (fn [e files]
+                              (on-change e files))})))
+                state)
+   :will-unmount (fn [state]
+                   (when-let [el (gdom/getElement "filepicker")]
+                     (dnd/unsubscribe! el :upload-files))
+                   state)}
+  [state {:keys [on-change]}]
+  (assert (fn? on-change))
+  (let [*input (::input state)]
+    [:div#filepicker.border.border-dashed
+     {:on-click (fn [] (.click @*input))}
+     [:div.relative.flex.flex-col.gap-6.overflow-hidden
+      [:div {:tabIndex 0
+             :class "group relative grid h-52 w-full cursor-pointer place-items-center rounded-lg border-2 border-dashed border-muted-foreground/25 px-5 py-2.5 text-center transition hover:bg-muted/25 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"}
+       [:input.hidden
+        {:ref #(reset! *input %)
+         :tabIndex -1
+         :multiple true
+         :type "file"
+         :on-change (fn [e]
+                      (let [files (array-seq (.-files (.-target e)))]
+                        (on-change e files)))}]
+       [:div {:class "flex flex-col items-center justify-center gap-4 sm:px-5"}
+        [:div {:class "rounded-full border border-dashed p-3"}
+         (shui/tabler-icon "upload" {:class "!block text-muted-foreground"
+                                     :style {:width 28
+                                             :height 28}})]
+        [:div {:class "flex flex-col gap-px"}
+         [:div {:class "font-medium text-muted-foreground"}
+          "Drag 'n' drop files here, or click to select files"]]]]]]))

+ 97 - 95
src/main/frontend/components/icon.cljs

@@ -28,7 +28,7 @@
                (and (= :emoji (:type icon')) (:id icon'))
                [:em-emoji (merge {:id (:id icon')
                                   :style {:line-height 1}}
-                            opts)]
+                                 opts)]
 
                (and (= :tabler-icon (:type icon')) (:id icon'))
                (ui/icon (:id icon') opts))]
@@ -39,28 +39,30 @@
 
 (defn get-node-icon
   [node-entity]
-  (let [first-tag-icon (some :logseq.property/icon (sort-by :db/id (:block/tags node-entity)))
-        default-icon-id (cond
-                          (some? first-tag-icon)
-                          first-tag-icon
-                          (ldb/class? node-entity)
-                          "hash"
-                          (ldb/property? node-entity)
-                          "letter-p"
-                          (ldb/whiteboard? node-entity)
-                          "whiteboard"
-                          (ldb/page? node-entity)
-                          "page"
-                          :else
-                          "letter-n")]
+  (let [first-tag-icon (some :logseq.property/icon (sort-by :db/id (:block/tags node-entity)))]
     (or (get node-entity (pu/get-pid :logseq.property/icon))
-        default-icon-id)))
+        (let [asset-type (:logseq.property.asset/type node-entity)]
+          (cond
+            (some? first-tag-icon)
+            first-tag-icon
+            (ldb/class? node-entity)
+            "hash"
+            (ldb/property? node-entity)
+            "letter-p"
+            (ldb/whiteboard? node-entity)
+            "whiteboard"
+            (ldb/page? node-entity)
+            "page"
+            (= asset-type "pdf")
+            "book"
+            :else
+            "letter-n")))))
 
 (defn get-node-icon-cp
   [node-entity opts]
   (let [opts' (assoc opts :size 14)
         node-icon (get-node-icon node-entity)]
-    (when-not (string/blank? node-icon)
+    (when-not (or (string/blank? node-icon) (and (contains? #{"letter-n" "page"} node-icon) (:not-text-or-page? opts)))
       [:span.flex (merge {:style {:color (or (:color node-icon) "inherit")}}
                          (select-keys opts [:class]))
        (icon node-icon opts')])))
@@ -76,11 +78,11 @@
   (if @*tabler-icons
     @*tabler-icons
     (let [result (->> (keys (bean/->clj js/tablerIcons))
-                   (map (fn [k]
-                          (-> (string/replace (csk/->Camel_Snake_Case (name k)) "_" " ")
-                            (string/replace-first "Icon " ""))))
+                      (map (fn [k]
+                             (-> (string/replace (csk/->Camel_Snake_Case (name k)) "_" " ")
+                                 (string/replace-first "Icon " ""))))
                    ;; FIXME: somehow those icons don't work
-                   (remove #{"Ab" "Ab 2" "Ab Off"}))]
+                      (remove #{"Ab" "Ab 2" "Ab Off"}))]
       (reset! *tabler-icons result)
       result)))
 
@@ -111,10 +113,10 @@
                                 :id icon'
                                 :name icon'}))
       :on-mouse-over #(some-> hover
-                        (reset! {:type :tabler-icon
-                                 :id icon'
-                                 :name icon'
-                                 :icon icon'}))
+                              (reset! {:type :tabler-icon
+                                       :id icon'
+                                       :name icon'
+                                       :icon icon'}))
       :on-mouse-out #()})
    (ui/icon icon' {:size 24})])
 
@@ -122,10 +124,10 @@
   [{:keys [id name] :as emoji} {:keys [on-chosen hover]}]
   [:button.text-2xl.w-9.h-9.transition-opacity
    (cond->
-     {:tabIndex "0"
-      :title name
-      :on-click (fn [e]
-                  (on-chosen e (assoc emoji :type :emoji)))}
+    {:tabIndex "0"
+     :title name
+     :on-click (fn [e]
+                 (on-chosen e (assoc emoji :type :emoji)))}
      (not (nil? hover))
      (assoc :on-mouse-over #(reset! hover emoji)
             :on-mouse-out #()))
@@ -197,9 +199,9 @@
 (defn add-used-item!
   [m]
   (let [s (some->> (or (get-used-items) [])
-            (take 24)
-            (filter #(not= m %))
-            (cons m))]
+                   (take 24)
+                   (filter #(not= m %))
+                   (cons m))]
     (storage/set :ui/ls-icons-used s)))
 
 (rum/defc all-cp
@@ -221,8 +223,8 @@
 (rum/defc tab-observer
   [tab {:keys [reset-q!]}]
   (rum/use-effect!
-    #(reset-q!)
-    [tab])
+   #(reset-q!)
+   [tab])
   nil)
 
 (rum/defc select-observer
@@ -246,40 +248,40 @@
                      (do (.focus (rum/deref *input-ref)) (set-current! -1 nil)))))
         down-handler!
         (rum/use-callback
-          (fn [^js e]
-            (let []
-              (if (= 13 (.-keyCode e))
+         (fn [^js e]
+           (let []
+             (if (= 13 (.-keyCode e))
                 ;; enter
-                (some-> (second (rum/deref *current-ref)) (.click))
-                (let [[idx _node] (rum/deref *current-ref)]
-                  (case (.-keyCode e)
+               (some-> (second (rum/deref *current-ref)) (.click))
+               (let [[idx _node] (rum/deref *current-ref)]
+                 (case (.-keyCode e)
                     ;;left
-                    37 (focus! (dec idx) :prev)
+                   37 (focus! (dec idx) :prev)
                     ;; tab & right
-                    (9 39) (focus! (inc idx) :next)
+                   (9 39) (focus! (inc idx) :next)
                     ;; up
-                    38 (do (focus! (- idx 9) :prev) (util/stop e))
+                   38 (do (focus! (- idx 9) :prev) (util/stop e))
                     ;; down
-                    40 (do (focus! (+ idx 9) :next) (util/stop e))
-                    :dune))))) [])]
+                   40 (do (focus! (+ idx 9) :next) (util/stop e))
+                   :dune))))) [])]
 
     (rum/use-effect!
-      (fn []
+     (fn []
         ;; calculate items
-        (let [^js sections (.querySelectorAll (get-cnt) ".pane-section")
-              items (map #(some-> (.querySelectorAll % ".its > button") (js/Array.from) (js->clj)) sections)
-              step 9
-              items (map #(let [count (count %)
-                                m (mod count step)]
-                            (if (> m 0) (concat % (repeat (- step m) false)) %)) items)]
-          (set! (. *items-ref -current) (flatten items))
-          (focus! 0 :next))
+       (let [^js sections (.querySelectorAll (get-cnt) ".pane-section")
+             items (map #(some-> (.querySelectorAll % ".its > button") (js/Array.from) (js->clj)) sections)
+             step 9
+             items (map #(let [count (count %)
+                               m (mod count step)]
+                           (if (> m 0) (concat % (repeat (- step m) false)) %)) items)]
+         (set! (. *items-ref -current) (flatten items))
+         (focus! 0 :next))
 
         ;; handlers
-        (let [^js cnt (get-cnt)]
-          (.addEventListener cnt "keydown" down-handler! false)
-          #(.removeEventListener cnt "keydown" down-handler!)))
-      [])
+       (let [^js cnt (get-cnt)]
+         (.addEventListener cnt "keydown" down-handler! false)
+         #(.removeEventListener cnt "keydown" down-handler!)))
+     [])
     [:span.absolute.hidden {:ref *el-ref}]))
 
 (rum/defc color-picker
@@ -292,28 +294,28 @@
                        [:div.color-picker-presets
                         (for [c colors]
                           (shui/button
-                            {:on-click (fn [] (set-color! c)
-                                         (some-> on-select! (apply [c]))
-                                         (shui/popup-hide!))
-                             :size :sm :variant :outline
-                             :class "it" :style {:background-color c}}
-                            (if c "" (shui/tabler-icon "minus" {:class "scale-75 opacity-70"}))))]))]
+                           {:on-click (fn [] (set-color! c)
+                                        (some-> on-select! (apply [c]))
+                                        (shui/popup-hide!))
+                            :size :sm :variant :outline
+                            :class "it" :style {:background-color c}}
+                           (if c "" (shui/tabler-icon "minus" {:class "scale-75 opacity-70"}))))]))]
     (rum/use-effect!
-      (fn []
-        (when-let [^js picker (some-> (rum/deref *el) (.closest ".cp__emoji-icon-picker"))]
-          (let [color (if (string/blank? color) "inherit" color)]
-            (.setProperty (.-style picker) "--ls-color-icon-preset" color)
-            (storage/set :ls-icon-color-preset color)))
-        (reset! *color color))
-      [color])
+     (fn []
+       (when-let [^js picker (some-> (rum/deref *el) (.closest ".cp__emoji-icon-picker"))]
+         (let [color (if (string/blank? color) "inherit" color)]
+           (.setProperty (.-style picker) "--ls-color-icon-preset" color)
+           (storage/set :ls-icon-color-preset color)))
+       (reset! *color color))
+     [color])
 
     (shui/button {:size :sm
                   :ref *el
                   :class "color-picker"
                   :on-click (fn [^js e] (shui/popup-show! (.-target e) content-fn {:content-props {:side-offset 6}}))
                   :variant :outline}
-      [:strong {:style {:color (or color "inherit")}}
-       (shui/tabler-icon "palette")])))
+                 [:strong {:style {:color (or color "inherit")}}
+                  (shui/tabler-icon "palette")])))
 
 (rum/defcs ^:large-vars/cleanup-todo icon-search <
   (rum/local "" ::q)
@@ -331,12 +333,12 @@
         *result-ref (rum/create-ref)
         result @*result
         opts (assoc opts
-               :on-chosen (fn [e m]
-                            (let [icon? (= (:type m) :tabler-icon)
-                                  m (if (and icon? (not (string/blank? @*color)))
-                                      (assoc m :color @*color) m)]
-                              (and on-chosen (on-chosen e m))
-                              (when (:type m) (add-used-item! m)))))
+                    :on-chosen (fn [e m]
+                                 (let [icon? (= (:type m) :tabler-icon)
+                                       m (if (and icon? (not (string/blank? @*color)))
+                                           (assoc m :color @*color) m)]
+                                   (and on-chosen (on-chosen e m))
+                                   (when (:type m) (add-used-item! m)))))
         *select-mode? (::select-mode? state)
         reset-q! #(when-let [^js input (rum/deref *input-ref)]
                     (reset! *q "")
@@ -395,10 +397,10 @@
          [:div.flex.flex-1.flex-col.gap-1.search-result
           (let [matched (concat (:emojis result) (:icons result))]
             (when (seq matched)
-             (pane-section
-              (util/format "Matched (%s)" (count matched))
-              matched
-              opts)))]
+              (pane-section
+               (util/format "Matched (%s)" (count matched))
+               matched
+               opts)))]
          [:div.flex.flex-1.flex-col.gap-1
           (case @*tab
             :emoji (emojis-cp emojis opts)
@@ -414,13 +416,13 @@
           (for [[id label] tabs
                 :let [active? (= @*tab id)]]
             (shui/button
-              {:variant :ghost
-               :size :sm
-               :class (util/classnames [{:active active?} "tab-item"])
-               :on-mouse-down (fn [e]
-                                (util/stop e)
-                                (reset! *tab id))}
-              label)))]
+             {:variant :ghost
+              :size :sm
+              :class (util/classnames [{:active active?} "tab-item"])
+              :on-mouse-down (fn [e]
+                               (util/stop e)
+                               (reset! *tab id))}
+             label)))]
 
        (when (not= :emoji @*tab)
          (color-picker *color (fn [c]
@@ -431,7 +433,7 @@
        (when del-btn?
          (shui/button {:variant :outline :size :sm :data-action "del"
                        :on-click #(on-chosen nil)}
-           (shui/tabler-icon "trash" {:size 17})))]]]))
+                      (shui/tabler-icon "trash" {:size 17})))]]]))
 
 (rum/defc icon-picker
   [icon-value {:keys [empty-label disabled? initial-open? del-btn? on-chosen icon-props popup-opts]}]
@@ -447,9 +449,9 @@
               :icon-value icon-value
               :del-btn? del-btn?})))]
     (rum/use-effect!
-      (fn []
-        (when initial-open?
-          (js/setTimeout #(some-> (rum/deref *trigger-ref) (.click)) 32)))
+     (fn []
+       (when initial-open?
+         (js/setTimeout #(some-> (rum/deref *trigger-ref) (.click)) 32)))
      [initial-open?])
 
     ;; trigger
@@ -468,7 +470,7 @@
                                           :content-props {:class "ls-icon-picker"
                                                           :onEscapeKeyDown #(.preventDefault %)}}
                                          popup-opts))))}
-        (if has-icon?
+       (if has-icon?
          (if (vector? icon-value)       ; hiccup
            icon-value
            (icon icon-value (merge {:color? true} icon-props)))

+ 62 - 50
src/main/frontend/components/objects.cljs

@@ -18,7 +18,8 @@
             [logseq.shui.ui :as shui]
             [frontend.util :as util]
             [frontend.ui :as ui]
-            [logseq.common.config :as common-config]))
+            [logseq.common.config :as common-config]
+            [frontend.components.filepicker :as filepicker]))
 
 (defn- get-class-objects
   [class]
@@ -104,7 +105,7 @@
         [view-entity set-view-entity!] (rum/use-state class)
         [views set-views!] (rum/use-state [class])
         [data set-data!] (rum/use-state objects)
-        columns (views/build-columns config properties)]
+        columns (views/build-columns (assoc config :class class) properties)]
 
     (rum/use-effect!
      (fn []
@@ -134,31 +135,41 @@
 
        (ui/foldable
         [:div.font-medium.opacity-50 "Tagged Nodes"]
-        (views/view view-entity {:data data
-                                 :set-data! set-data!
-                                 :views-title (class-views class views view-entity {:set-view-entity! set-view-entity!
-                                                                                    :set-views! set-views!})
-                                 :columns columns
-                                 :add-new-object! #(add-new-class-object! class set-data!)
-                                 :show-add-property? true
-                                 :add-property! (fn []
-                                                  (state/pub-event! [:editor/new-property {:block class
-                                                                                           :class-schema? true}]))
-                                 :on-delete-rows (fn [table selected-rows]
-                                                   (let [pages (filter ldb/page? selected-rows)
-                                                         blocks (remove ldb/page? selected-rows)]
-                                                     (p/do!
-                                                      (ui-outliner-tx/transact!
-                                                       {:outliner-op :delete-blocks}
-                                                       (when (seq blocks)
-                                                         (outliner-op/delete-blocks! blocks nil))
-                                                       (let [page-ids (map :db/id pages)
-                                                             tx-data (map (fn [pid] [:db/retract pid :block/tags (:db/id class)]) page-ids)]
-                                                         (when (seq tx-data)
-                                                           (outliner-op/transact! tx-data {:outliner-op :save-block}))))
-                                                      (set-data! (get-class-objects class))
-                                                      (when-let [f (get-in table [:data-fns :set-row-selection!])]
-                                                        (f {})))))})
+        [:div.mt-2
+         (views/view view-entity {:data data
+                                  :set-data! set-data!
+                                  :views-title (class-views class views view-entity {:set-view-entity! set-view-entity!
+                                                                                     :set-views! set-views!})
+                                  :columns columns
+                                  :add-new-object! (if (= :logseq.class/Asset (:db/ident class))
+                                                     (fn [_e]
+                                                       (shui/dialog-open!
+                                                        (fn []
+                                                          [:div.flex.flex-col.gap-2
+                                                           [:div.font-medium "Add assets"]
+                                                           (filepicker/picker
+                                                            {:on-change (fn [_e files]
+                                                                          (editor-handler/upload-asset! nil files :markdown editor-handler/*asset-uploading? true))})])))
+                                                     #(add-new-class-object! class set-data!))
+                                  :show-add-property? true
+                                  :add-property! (fn []
+                                                   (state/pub-event! [:editor/new-property {:block class
+                                                                                            :class-schema? true}]))
+                                  :on-delete-rows (fn [table selected-rows]
+                                                    (let [pages (filter ldb/page? selected-rows)
+                                                          blocks (remove ldb/page? selected-rows)]
+                                                      (p/do!
+                                                       (ui-outliner-tx/transact!
+                                                        {:outliner-op :delete-blocks}
+                                                        (when (seq blocks)
+                                                          (outliner-op/delete-blocks! blocks nil))
+                                                        (let [page-ids (map :db/id pages)
+                                                              tx-data (map (fn [pid] [:db/retract pid :block/tags (:db/id class)]) page-ids)]
+                                                          (when (seq tx-data)
+                                                            (outliner-op/transact! tx-data {:outliner-op :save-block}))))
+                                                       (set-data! (get-class-objects class))
+                                                       (when-let [f (get-in table [:data-fns :set-row-selection!])]
+                                                         (f {})))))})]
         {:disable-on-pointer-down? true})])))
 
 (rum/defcs class-objects < rum/reactive db-mixins/query mixins/container-id
@@ -217,30 +228,31 @@
     (when (false? loading?)
       (ui/foldable
        [:div.font-medium.opacity-50 "Nodes with Property"]
-       (views/view view-entity {:data data
-                                :set-data! set-data!
-                                :title-key :views.table/property-nodes
-                                :columns columns
-                                :add-new-object! #(add-new-property-object! property set-data!)
+       [:div.mt-2
+        (views/view view-entity {:data data
+                                 :set-data! set-data!
+                                 :title-key :views.table/property-nodes
+                                 :columns columns
+                                 :add-new-object! #(add-new-property-object! property set-data!)
                                ;; TODO: Add support for adding column
-                                :show-add-property? false
-                                :on-delete-rows (when-not (contains? #{:logseq.property/built-in? :logseq.property/parent}
-                                                                     (:db/ident property))
-                                                  (fn [table selected-rows]
-                                                    (let [pages (filter ldb/page? selected-rows)
-                                                          blocks (remove ldb/page? selected-rows)]
-                                                      (p/do!
-                                                       (ui-outliner-tx/transact!
-                                                        {:outliner-op :delete-blocks}
-                                                        (when (seq blocks)
-                                                          (outliner-op/delete-blocks! blocks nil))
-                                                        (let [page-ids (map :db/id pages)
-                                                              tx-data (map (fn [pid] [:db/retract pid (:db/ident property)]) page-ids)]
-                                                          (when (seq tx-data)
-                                                            (outliner-op/transact! tx-data {:outliner-op :save-block}))))
-                                                       (set-data! (get-property-related-objects (state/get-current-repo) property))
-                                                       (when-let [f (get-in table [:data-fns :set-row-selection!])]
-                                                         (f {}))))))})
+                                 :show-add-property? false
+                                 :on-delete-rows (when-not (contains? #{:logseq.property/built-in? :logseq.property/parent}
+                                                                      (:db/ident property))
+                                                   (fn [table selected-rows]
+                                                     (let [pages (filter ldb/page? selected-rows)
+                                                           blocks (remove ldb/page? selected-rows)]
+                                                       (p/do!
+                                                        (ui-outliner-tx/transact!
+                                                         {:outliner-op :delete-blocks}
+                                                         (when (seq blocks)
+                                                           (outliner-op/delete-blocks! blocks nil))
+                                                         (let [page-ids (map :db/id pages)
+                                                               tx-data (map (fn [pid] [:db/retract pid (:db/ident property)]) page-ids)]
+                                                           (when (seq tx-data)
+                                                             (outliner-op/transact! tx-data {:outliner-op :save-block}))))
+                                                        (set-data! (get-property-related-objects (state/get-current-repo) property))
+                                                        (when-let [f (get-in table [:data-fns :set-row-selection!])]
+                                                          (f {}))))))})]
        {:disable-on-pointer-down? true}))))
 
 ;; Show all nodes containing the given property

+ 30 - 29
src/main/frontend/components/page.cljs

@@ -218,37 +218,38 @@
           db-based? (config/db-based-graph? repo)]
       [:<>
        (when (and db-based? (or (ldb/class? block) (ldb/property? block)))
-         [:div.font-medium.mt-8.ml-1.pb-3.opacity-50
+         [:div.font-medium.mt-8.pb-3.opacity-50.ml-2
           "Notes"])
 
-       (cond
-         loading?
-         nil
-
-         (and
-          (not loading?)
-          (not block?)
-          (empty? children) page-e)
-         (dummy-block page-e)
-
-         :else
-         (let [document-mode? (state/sub :document/mode?)
-               hiccup-config (merge
-                              {:id (if block? (str block-id) page-name)
-                               :db/id (:db/id block)
-                               :block? block?
-                               :editor-box editor/box
-                               :document/mode? document-mode?}
-                              config)
-               config (common-handler/config-with-document-mode hiccup-config)
-               blocks (if block? [block] (db/sort-by-order children block))]
-           [:div
-            (page-blocks-inner page-e blocks config sidebar? whiteboard? block-id)
-            (when-not config/publishing?
-              (let [args (if block-id
-                           {:block-uuid block-id}
-                           {:page page-name})]
-                (add-button args)))]))])))
+       [:div.ml-1
+        (cond
+          loading?
+          nil
+
+          (and
+           (not loading?)
+           (not block?)
+           (empty? children) page-e)
+          (dummy-block page-e)
+
+          :else
+          (let [document-mode? (state/sub :document/mode?)
+                hiccup-config (merge
+                               {:id (if block? (str block-id) page-name)
+                                :db/id (:db/id block)
+                                :block? block?
+                                :editor-box editor/box
+                                :document/mode? document-mode?}
+                               config)
+                config (common-handler/config-with-document-mode hiccup-config)
+                blocks (if block? [block] (db/sort-by-order children block))]
+            [:div
+             (page-blocks-inner page-e blocks config sidebar? whiteboard? block-id)
+             (when-not config/publishing?
+               (let [args (if block-id
+                            {:block-uuid block-id}
+                            {:page page-name})]
+                 (add-button args)))]))]])))
 
 (rum/defc today-queries < rum/reactive
   [repo today? sidebar?]

+ 4 - 0
src/main/frontend/components/page.css

@@ -141,6 +141,10 @@ html.is-native-iphone-without-notch {
   cursor: e-resize;
 }
 
+.ls-all-pages {
+  max-width: 1400px;
+}
+
 .add-button-link {
   opacity: 0;
   color: var(--ls-primary-text-color);

+ 15 - 15
src/main/frontend/components/page_menu.cljs

@@ -25,26 +25,26 @@
 (defn- delete-page!
   [page]
   (page-handler/<delete! (:block/uuid page)
-                        (fn []
-                          (notification/show! (str "Page " (:block/title page) " was deleted successfully!")
-                                              :success))
-                        {:error-handler (fn [{:keys [msg]}]
-                                          (notification/show! msg :warning))}))
+                         (fn []
+                           (notification/show! (str "Page " (:block/title page) " was deleted successfully!")
+                                               :success))
+                         {:error-handler (fn [{:keys [msg]}]
+                                           (notification/show! msg :warning))}))
 
 (defn delete-page-confirm!
   [page]
   (when page
     (-> (shui/dialog-confirm!
-          {:title [:h3.text-lg.leading-6.font-medium.flex.gap-2.items-center
-                   [:span.top-1.relative
-                    (shui/tabler-icon "alert-triangle")]
-                   (if (config/db-based-graph? (state/get-current-repo))
-                     (t :page/db-delete-confirmation)
-                     (t :page/delete-confirmation))]
-           :content [:p.opacity-60 (str "- " (:block/title page))]
-           :outside-cancel? true})
-      (p/then #(delete-page! page))
-      (p/catch #()))))
+         {:title [:h3.text-lg.leading-6.font-medium.flex.gap-2.items-center
+                  [:span.top-1.relative
+                   (shui/tabler-icon "alert-triangle")]
+                  (if (config/db-based-graph? (state/get-current-repo))
+                    (t :page/db-delete-confirmation)
+                    (t :page/delete-confirmation))]
+          :content [:p.opacity-60 (str "- " (:block/title page))]
+          :outside-cancel? true})
+        (p/then #(delete-page! page))
+        (p/catch #()))))
 
 (defn ^:large-vars/cleanup-todo page-menu
   [page]

+ 77 - 77
src/main/frontend/components/property/config.cljs

@@ -38,12 +38,12 @@
   "Create new closed value and returns its block UUID."
   [property item]
   (p/do!
-    (db-property-handler/upsert-closed-value! (:db/ident property) item)
-    (re-init-commands! property)))
+   (db-property-handler/upsert-closed-value! (:db/ident property) item)
+   (re-init-commands! property)))
 
 (defn- loop-focusable-elements!
   ([^js cnt] (loop-focusable-elements! cnt
-               ".ui__button:not([disabled]), .ui__input, .ui__textarea"))
+                                       ".ui__button:not([disabled]), .ui__input, .ui__textarea"))
   ([^js cnt selectors]
    (when-let [els (some-> cnt (.querySelectorAll selectors) (seq))]
      (let [active js/document.activeElement
@@ -51,7 +51,7 @@
            total-len (count els)
            to-idx (cond
                     (or (= -1 current-idx)
-                      (= total-len (inc current-idx)))
+                        (= total-len (inc current-idx)))
                     0
                     :else
                     (inc current-idx))]
@@ -61,13 +61,13 @@
   [property description]
   (if-let [ent (:logseq.property/description property)]
     (db/transact! (state/get-current-repo)
-      [(outliner-core/block-with-updated-at
-         {:db/id (:db/id ent) :block/title description})]
-      {:outliner-op :save-block})
+                  [(outliner-core/block-with-updated-at
+                    {:db/id (:db/id ent) :block/title description})]
+                  {:outliner-op :save-block})
     (when-not (string/blank? description)
       (db-property-handler/set-block-property!
-        (:db/id property)
-        :logseq.property/description description))))
+       (:db/id property)
+       :logseq.property/description description))))
 
 (defn- <create-class-if-not-exists!
   [value]
@@ -126,10 +126,10 @@
                                         (if (= value :no-tag)
                                           (toggle-fn)
                                           (p/let [result (<create-class-if-not-exists! value)
-                                                 value' (or result value)
-                                                 tx-data [[(if select? :db/add :db/retract) (:db/id property) :property/schema.classes [:block/uuid value']]]
-                                                 _ (db/transact! (state/get-current-repo) tx-data {:outliner-op :update-property})]
-                                           (when-not multiple-choices? (toggle-fn)))))}]
+                                                  value' (or result value)
+                                                  tx-data [[(if select? :db/add :db/retract) (:db/id property) :property/schema.classes [:block/uuid value']]]
+                                                  _ (db/transact! (state/get-current-repo) tx-data {:outliner-op :update-property})]
+                                            (when-not multiple-choices? (toggle-fn)))))}]
 
                  (select/select opts)))]
 
@@ -160,9 +160,9 @@
         description (util/trim-safe (:description form-data))]
 
     (rum/use-effect!
-      (fn []
-        (js/setTimeout #(some-> (rum/deref *el) (.focus)) 32))
-      [])
+     (fn []
+       (js/setTimeout #(some-> (rum/deref *el) (.focus)) 32))
+     [])
 
     [:div.ls-property-name-edit-pane.outline-none
      {:on-key-down (fn [^js e] (when (= "Tab" (.-key e))
@@ -171,10 +171,10 @@
       :ref *el}
      [:div.flex.items-center.input-wrap
       (icon-component/icon-picker (:icon form-data)
-        {:on-chosen (fn [_e icon] (set-form-data! (assoc form-data :icon icon)))
-         :popup-opts {:align "start"}
-         :del-btn? (boolean (:icon form-data))
-         :empty-label "?"})
+                                  {:on-chosen (fn [_e icon] (set-form-data! (assoc form-data :icon icon)))
+                                   :popup-opts {:align "start"}
+                                   :del-btn? (boolean (:icon form-data))
+                                   :empty-label "?"})
       (shui/input {:ref *input-ref :size "sm" :default-value title :placeholder "name"
                    :disabled disabled? :on-change (fn [^js e] (set-form-data! (assoc form-data :title (util/trim-safe (util/evalue e)))))})]
      [:div.pt-2 (shui/textarea {:placeholder "description" :default-value description
@@ -187,74 +187,74 @@
                       :on-click (fn []
                                   (set-saving! true)
                                   (-> [(db-property-handler/upsert-property!
-                                         (:db/ident property)
-                                         (:block/schema property)
-                                         {:property-name title
-                                          :properties {:logseq.property/icon (:icon form-data)}})
+                                        (:db/ident property)
+                                        (:block/schema property)
+                                        {:property-name title
+                                         :properties {:logseq.property/icon (:icon form-data)}})
                                        (when (not= description (:description (rum/deref *form-data)))
                                          (set-property-description! property description))]
-                                    (p/all)
-                                    (p/then #(set-sub-open! false))
-                                    (p/catch #(shui/toast! (str %) :error))
-                                    (p/finally #(set-saving! false))))}
-          "Save")])]))
+                                      (p/all)
+                                      (p/then #(set-sub-open! false))
+                                      (p/catch #(shui/toast! (str %) :error))
+                                      (p/finally #(set-saving! false))))}
+                     "Save")])]))
 
 (rum/defc choice-base-edit-form
   [own-property block]
   (let [create? (:create? block)
         uuid (:block/uuid block)
         *form-data (rum/use-ref
-                     {:value (or (str (db-property/closed-value-content block)) "")
-                      :icon (:logseq.property/icon block)
-                      :description (or (db-property/property-value-content (:logseq.property/description block)) "")})
+                    {:value (or (str (db-property/closed-value-content block)) "")
+                     :icon (:logseq.property/icon block)
+                     :description (or (db-property/property-value-content (:logseq.property/description block)) "")})
         [form-data, set-form-data!] (rum/use-state (rum/deref *form-data))
         *input-ref (rum/use-ref nil)]
 
     (rum/use-effect!
-      (fn []
-        (when create?
-          (js/setTimeout #(some-> (rum/deref *input-ref) (.focus)) 60)))
-      [])
+     (fn []
+       (when create?
+         (js/setTimeout #(some-> (rum/deref *input-ref) (.focus)) 60)))
+     [])
 
     [:div.ls-base-edit-form
      [:div.flex.items-center.input-wrap
       (icon-component/icon-picker
-        (:icon form-data)
-        {:on-chosen (fn [_e icon] (set-form-data! (assoc form-data :icon icon)))
-         :empty-label "?"
-         :del-btn? (boolean (:icon form-data))
-         :popup-opts {:align "start"}})
+       (:icon form-data)
+       {:on-chosen (fn [_e icon] (set-form-data! (assoc form-data :icon icon)))
+        :empty-label "?"
+        :del-btn? (boolean (:icon form-data))
+        :popup-opts {:align "start"}})
 
       (shui/input {:ref *input-ref :size "sm"
                    :default-value (:value form-data)
                    :on-change (fn [^js e] (set-form-data! (assoc form-data :value (util/trim-safe (util/evalue e)))))
                    :placeholder "title"})]
      [:div.pt-2 (shui/textarea
-                  {:placeholder "description" :default-value (:description form-data)
-                   :on-change (fn [^js e] (set-form-data! (assoc form-data :description (util/trim-safe (util/evalue e)))))})]
+                 {:placeholder "description" :default-value (:description form-data)
+                  :on-change (fn [^js e] (set-form-data! (assoc form-data :description (util/trim-safe (util/evalue e)))))})]
      [:div.pt-2.flex.justify-end
       (let [dirty? (not= (rum/deref *form-data) form-data)]
         (shui/button {:size "sm"
                       :disabled (not dirty?)
                       :on-click (fn []
                                   (-> (<upsert-closed-value! own-property
-                                        (cond-> form-data uuid (assoc :id uuid)))
-                                    (p/then #(shui/popup-hide!))
-                                    (p/catch #(shui/toast! (str %) :error))))
+                                                             (cond-> form-data uuid (assoc :id uuid)))
+                                      (p/then #(shui/popup-hide!))
+                                      (p/catch #(shui/toast! (str %) :error))))
                       :variant (if dirty? :default :secondary)}
-          "Save"))]]))
+                     "Save"))]]))
 
 (defn restore-root-highlight-item!
   [id]
   (js/setTimeout
-    #(some-> (gdom/getElement id) (.focus)) 32))
+   #(some-> (gdom/getElement id) (.focus)) 32))
 
 (rum/defc dropdown-editor-menuitem
   [{:keys [id icon title desc submenu-content item-props sub-content-props disabled? toggle-checked? on-toggle-checked-change]}]
   (let [submenu-content (when-not disabled? submenu-content)
         item-props' (if (and disabled? (:on-select item-props))
-                     (assoc item-props :on-select (fn [] nil))
-                     item-props)
+                      (assoc item-props :on-select (fn [] nil))
+                      item-props)
         [sub-open? set-sub-open!] (rum/use-state false)
         toggle? (boolean? toggle-checked?)
         id1 (str (or id icon (random-uuid)))
@@ -302,27 +302,27 @@
   [property block]
   (let [delete-choice! (fn []
                          (p/do!
-                           (db-property-handler/delete-closed-value! (:db/id property) (:db/id block))
-                           (re-init-commands! property)))
+                          (db-property-handler/delete-closed-value! (:db/id property) (:db/id block))
+                          (re-init-commands! property)))
         update-icon! (fn [icon]
                        (property-handler/set-block-property!
-                         (state/get-current-repo) (:block/uuid block) :logseq.property/icon
-                         (select-keys icon [:id :type :color])))
+                        (state/get-current-repo) (:block/uuid block) :logseq.property/icon
+                        (select-keys icon [:id :type :color])))
         icon (:logseq.property/icon block)
         value (db-property/closed-value-content block)]
 
     [:li
      (shui/tabler-icon "grip-vertical" {:size 14})
      (shui/button {:size "sm" :variant :outline}
-       (icon-component/icon-picker icon {:on-chosen (fn [_e icon] (update-icon! icon))
-                                         :popup-opts {:align "start"}
-                                         :del-btn? (boolean icon)
-                                         :empty-label "?"}))
+                  (icon-component/icon-picker icon {:on-chosen (fn [_e icon] (update-icon! icon))
+                                                    :popup-opts {:align "start"}
+                                                    :del-btn? (boolean icon)
+                                                    :empty-label "?"}))
      [:strong {:on-click (fn [^js e]
                            (shui/popup-show! (.-target e)
-                             (fn [] (choice-base-edit-form property block))
-                             {:id :ls-base-edit-form
-                              :align "start"}))}
+                                             (fn [] (choice-base-edit-form property block))
+                                             {:id :ls-base-edit-form
+                                              :align "start"}))}
       value]
      [:a.del {:on-click delete-choice!
               :title "Delete this choice"}
@@ -334,7 +334,7 @@
         values' (if uuid-values?
                   (let [values' (map #(db/entity [:block/uuid %]) values)]
                     (->> (util/distinct-by db-property/closed-value-content values')
-                        (map :block/uuid)))
+                         (map :block/uuid)))
                   values)]
     [:div.flex.flex-col.gap-1.w-64.p-4.overflow-y-auto
      {:class "max-h-[50dvh]"}
@@ -466,12 +466,12 @@
   (let [handle-select! (fn [^js e]
                          (when-let [v (some-> (.-target e) (.-dataset) (.-value))]
                            (p/do!
-                             (db-property-handler/upsert-property!
+                            (db-property-handler/upsert-property!
                              (:db/ident property)
                              (assoc (:block/schema property) :type (keyword v))
                              {})
-                             (set-sub-open! false)
-                             (restore-root-highlight-item! id))))
+                            (set-sub-open! false)
+                            (restore-root-highlight-item! id))))
         item-props {:on-select handle-select!}
         schema-types (->> db-property-type/user-built-in-property-types
                           (map (fn [type]
@@ -548,10 +548,10 @@
                                         (update-cardinality-fn))))}))
 
      (let [group' (->> [(when (and (not (contains? #{:logseq.property/parent :logseq.property.class/properties} (:db/ident property)))
-                                (not
-                                  (and (= :default (get-in property [:block/schema :type]))
-                                    (empty? (:property/closed-values property))
-                                    (contains? #{nil :properties} (:position property-schema)))))
+                                   (not
+                                    (and (= :default (get-in property [:block/schema :type]))
+                                         (empty? (:property/closed-values property))
+                                         (contains? #{nil :properties} (:position property-schema)))))
                           (let [position (:position property-schema)]
                             (dropdown-editor-menuitem {:icon :float-left :title "UI position" :desc (some->> position (get position-labels) (:title))
                                                        :item-props {:class "ui__position-trigger-item"}
@@ -560,8 +560,8 @@
                         (when (not (contains? #{:logseq.property/parent :logseq.property.class/properties} (:db/ident property)))
                           (dropdown-editor-menuitem {:icon :eye-off :title "Hide by default" :toggle-checked? (boolean (:hide? property-schema))
                                                      :on-toggle-checked-change #(db-property-handler/upsert-property! (:db/ident property)
-                                                                                  (assoc property-schema :hide? %) {})}))]
-                    (remove nil?))]
+                                                                                                                      (assoc property-schema :hide? %) {})}))]
+                       (remove nil?))]
        (when (> (count group') 0)
          (cons (shui/dropdown-menu-separator) group')))
 
@@ -569,11 +569,11 @@
        [:<>
         (shui/dropdown-menu-separator)
         (dropdown-editor-menuitem
-          {:icon :share-3 :title "Go to this property" :desc ""
-           :item-props {:class "opacity-90 focus:opacity-100"
-                        :on-select (fn []
-                                     (shui/popup-hide-all!)
-                                     (route-handler/redirect-to-page! (:block/uuid property)))}})])
+         {:icon :share-3 :title "Go to this property" :desc ""
+          :item-props {:class "opacity-90 focus:opacity-100"
+                       :on-select (fn []
+                                    (shui/popup-hide-all!)
+                                    (route-handler/redirect-to-page! (:block/uuid property)))}})])
 
      (when (and owner-block
                 (not (and
@@ -614,4 +614,4 @@
   (let [property (db/sub-block (:db/id property*))
         values (rum/react (::values state))]
     (when-not (= :loading values)
-        (dropdown-editor-impl property owner-block values opts))))
+      (dropdown-editor-impl property owner-block values opts))))

+ 5 - 3
src/main/frontend/components/reference.cljs

@@ -66,8 +66,9 @@
         *collapsed? (atom nil)]
     (ui/foldable
      [:div.flex.flex-row.flex-1.justify-between.items-center
-      [:h2.font-medium (t :linked-references/reference-count (when (or (seq (:included filters))
-                                                                       (seq (:excluded filters))) filter-n) total)]
+      [:div.font-medium.opacity-50
+       (t :linked-references/reference-count (when (or (seq (:included filters))
+                                                       (seq (:excluded filters))) filter-n) total)]
       [:a.filter.fade-link
        {:title (t :linked-references/filter-heading)
         :on-mouse-over (fn [_e]
@@ -219,7 +220,8 @@
       [:div.references.page-unlinked.mt-6.flex-1.flex-row.faster.fade-in
        [:div.content.flex-1
         (ui/foldable
-         [:h2.font-medium (t :unlinked-references/reference-count @n-ref)]
+         [:div.font-medium.opacity-50
+          (t :unlinked-references/reference-count @n-ref)]
          (fn [] (unlinked-references-aux page n-ref))
          {:default-collapsed? true
           :title-trigger? true})]])))

+ 7 - 1
src/main/frontend/components/table.css

@@ -1,4 +1,10 @@
 .ls-table {
+  img {
+    max-height: 48px;
+    min-width: 120px;
+    overflow-y: hidden;
+  }
+
   .ls-block {
     @apply w-full py-0;
   }
@@ -108,4 +114,4 @@ html.is-resizing-buf {
 
 .markdown-table {
   width: 98%;
-}
+}

+ 30 - 30
src/main/frontend/components/theme.cljs

@@ -20,14 +20,14 @@
   []
   (let [*el (rum/use-ref nil)]
     (rum/use-effect!
-      (fn []
-        (when-let [el (rum/deref *el)]
-          (let [w (- (.-offsetWidth el) (.-clientWidth el))
-                c "custom-scrollbar"
-                l (.-classList js/document.documentElement)]
-            (if (or (not util/mac?) (> w 2))
-              (.add l c) (.remove l c)))))
-      [])
+     (fn []
+       (when-let [el (rum/deref *el)]
+         (let [w (- (.-offsetWidth el) (.-clientWidth el))
+               c "custom-scrollbar"
+               l (.-classList js/document.documentElement)]
+           (if (or (not util/mac?) (> w 2))
+             (.add l c) (.remove l c)))))
+     [])
     [:div.fixed.w-16.h-16.overflow-scroll.opacity-0
      {:ref   *el
       :class "top-1/2 -left-1/2 z-[-999]"}]))
@@ -55,26 +55,26 @@
 
     ;; theme color
     (rum/use-effect!
-      #(some-> js/document.documentElement
-         (.setAttribute "data-color"
-           (or accent-color "logseq")))
-      [accent-color])
+     #(some-> js/document.documentElement
+              (.setAttribute "data-color"
+                             (or accent-color "logseq")))
+     [accent-color])
 
     (rum/use-effect!
-      #(some-> js/document.documentElement
-         (.setAttribute "data-font" (or editor-font "default")))
-      [editor-font])
+     #(some-> js/document.documentElement
+              (.setAttribute "data-font" (or editor-font "default")))
+     [editor-font])
 
     (rum/use-effect!
      #(let [doc js/document.documentElement]
         (.setAttribute doc "lang" preferred-language)))
 
     (rum/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
-      [])
+     #(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
+     [])
 
     (rum/use-effect!
      #(when (and restored-sidebar?
@@ -128,16 +128,16 @@
      [system-theme?])
 
     (rum/use-effect!
-      (fn []
-        (if settings-open?
-          (shui/dialog-open!
-            (fn [] [:div.settings-modal (settings/settings settings-open?)])
-            {:label "app-settings"
-             :align :top
-             :content-props {:onOpenAutoFocus #(.preventDefault %)}
-             :id :app-settings})
-          (shui/dialog-close! :app-settings)))
-      [settings-open?])
+     (fn []
+       (if settings-open?
+         (shui/dialog-open!
+          (fn [] [:div.settings-modal (settings/settings settings-open?)])
+          {:label "app-settings"
+           :align :top
+           :content-props {:onOpenAutoFocus #(.preventDefault %)}
+           :id :app-settings})
+         (shui/dialog-close! :app-settings)))
+     [settings-open?])
 
     (rum/use-effect!
      #(storage/set :file-sync/onboarding-state onboarding-state)

+ 100 - 71
src/main/frontend/components/views.cljs

@@ -23,7 +23,8 @@
             [logseq.db.frontend.property :as db-property]
             [logseq.db.frontend.property.type :as db-property-type]
             [logseq.shui.ui :as shui]
-            [rum.core :as rum]))
+            [rum.core :as rum]
+            [frontend.mixins :as mixins]))
 
 (rum/defc header-checkbox < rum/static
   [{:keys [selected-all? selected-some? toggle-selected-all!]}]
@@ -104,81 +105,96 @@
       (reduce + (filter number? col))
       (string/join ", " col))))
 
-(rum/defc block-title < rum/static
+(rum/defc block-container < rum/static
   [config row]
-  (let [block-container (state/get-component :block/container)]
+  (let [container (state/get-component :block/container)]
     [:div.relative.w-full
-     (block-container (assoc config :table? true) row)]))
+     (container config row)]))
 
 (defn build-columns
   [config properties & {:keys [with-object-name?]
                         :or {with-object-name? true}}]
-  (->> (concat
-        [{:id :select
-          :name "Select"
-          :header (fn [table _column] (header-checkbox table))
-          :cell (fn [table row column]
-                  (row-checkbox table row column))
-          :column-list? false
-          :resizable? false}
-         (when with-object-name?
-           {:id :block/title
-            :name "Name"
-            :type :string
+  (let [asset-class? (= :logseq.class/Asset (:db/ident (:class config)))
+        properties (if (some #(= (:db/ident %) :block/tags) properties)
+                     properties
+                     (conj properties (db/entity :block/tags)))]
+    (->> (concat
+          [{:id :select
+            :name "Select"
+            :header (fn [table _column] (header-checkbox table))
+            :cell (fn [table row column]
+                    (row-checkbox table row column))
+            :column-list? false
+            :resizable? false}
+           (when with-object-name?
+             {:id :block/title
+              :name "Name"
+              :type :string
+              :header header-cp
+              :cell (fn [_table row _column]
+                      (block-container (assoc config
+                                              :raw-title? true
+                                              :table? true) row))
+              :disable-hide? true})
+           (when asset-class?
+             {:id :file
+              :name "File"
+              :type :string
+              :header header-cp
+              :cell (fn [_table row _column]
+                      (when-let [asset-cp (state/get-component :block/asset-cp)]
+                        [:div.block-content (asset-cp (assoc config :disable-resize? true) row)]))
+              :disable-hide? true})]
+          (keep
+           (fn [property]
+             (let [ident (or (:db/ident property) (:id property))]
+               (when-not (or (contains? #{:logseq.property/built-in? :logseq.property.asset/checksum :logseq.property.class/properties} ident)
+                             (contains? #{:map :entity} (get-in property [:block/schema :type])))
+                 (let [property (if (de/entity? property)
+                                  property
+                                  (or (db/entity ident) property))
+                       get-value (or (:get-value property)
+                                     (when (de/entity? property)
+                                       (fn [row] (get-property-value-for-search row property))))
+                       closed-values (seq (:property/closed-values property))
+                       closed-value->sort-number (when closed-values
+                                                   (->> (zipmap (map :db/id closed-values) (range 0 (count closed-values)))
+                                                        (into {})))
+                       get-value-for-sort (fn [row]
+                                            (cond
+                                              (= (:db/ident property) :logseq.task/deadline)
+                                              (:block/journal-day (get row :logseq.task/deadline))
+                                              closed-values
+                                              (closed-value->sort-number (:db/id (get row (:db/ident property))))
+                                              :else
+                                              (if (fn? get-value)
+                                                (get-value row)
+                                                (get row ident))))]
+                   {:id ident
+                    :name (or (:name property)
+                              (:block/title property))
+                    :header (or (:header property)
+                                header-cp)
+                    :cell (or (:cell property)
+                              (when (de/entity? property)
+                                (fn [_table row _column]
+                                  (pv/property-value row property (get row (:db/ident property)) {}))))
+                    :get-value get-value
+                    :get-value-for-sort get-value-for-sort
+                    :type (:type property)}))))
+           properties)
+
+          [{:id :block/created-at
+            :name (t :page/created-at)
+            :type :datetime
             :header header-cp
-            :cell (fn [_table row _column]
-                    (block-title config row))
-            :disable-hide? true})]
-        (keep
-         (fn [property]
-           (let [ident (or (:db/ident property) (:id property))]
-             (when-not (or (contains? #{:logseq.property/built-in? :logseq.property.class/properties} ident)
-                           (contains? #{:map :entity} (get-in property [:block/schema :type])))
-               (let [property (if (de/entity? property)
-                                property
-                                (or (db/entity ident) property))
-                     get-value (or (:get-value property)
-                                   (when (de/entity? property)
-                                     (fn [row] (get-property-value-for-search row property))))
-                     closed-values (seq (:property/closed-values property))
-                     closed-value->sort-number (when closed-values
-                                                 (->> (zipmap (map :db/id closed-values) (range 0 (count closed-values)))
-                                                      (into {})))
-                     get-value-for-sort (fn [row]
-                                          (cond
-                                            (= (:db/ident property) :logseq.task/deadline)
-                                            (:block/journal-day (get row :logseq.task/deadline))
-                                            closed-values
-                                            (closed-value->sort-number (:db/id (get row (:db/ident property))))
-                                            :else
-                                            (if (fn? get-value)
-                                              (get-value row)
-                                              (get row ident))))]
-                 {:id ident
-                  :name (or (:name property)
-                            (:block/title property))
-                  :header (or (:header property)
-                              header-cp)
-                  :cell (or (:cell property)
-                            (when (de/entity? property)
-                              (fn [_table row _column]
-                                (pv/property-value row property (get row (:db/ident property)) {}))))
-                  :get-value get-value
-                  :get-value-for-sort get-value-for-sort
-                  :type (:type property)}))))
-         properties)
-
-        [{:id :block/created-at
-          :name (t :page/created-at)
-          :type :datetime
-          :header header-cp
-          :cell timestamp-cell-cp}
-         {:id :block/updated-at
-          :name (t :page/updated-at)
-          :type :datetime
-          :header header-cp
-          :cell timestamp-cell-cp}])
-       (remove nil?)))
+            :cell timestamp-cell-cp}
+           {:id :block/updated-at
+            :name (t :page/updated-at)
+            :type :datetime
+            :header header-cp
+            :cell timestamp-cell-cp}])
+         (remove nil?))))
 
 (defn- sort-columns
   [columns ordered-column-ids]
@@ -1035,7 +1051,7 @@
            (shui/table-footer (add-new-row table)))]]))))
 
 (rum/defc list-view < rum/static
-  [view-entity result config]
+  [config view-entity result]
   (when-let [->hiccup (state/get-component :block/->hiccup)]
     (let [group-by-page? (not (every? db/page? result))
           result (if group-by-page?
@@ -1050,6 +1066,16 @@
                        :group-by-page? group-by-page?
                        :ref? true)))))
 
+(rum/defcs card-view < rum/static mixins/container-id
+  [state config view-entity result]
+  (let [config' (assoc config :container-id (:container-id state))]
+    [:div.ls-cards
+     (for [block result]
+       [:div.ls-card-item
+        {:key (str "view-card-" (:db/id view-entity) "-" (:db/id block))}
+        [:div.-ml-4
+         (block-container (assoc config' :id (str (:block/uuid block))) block)]])]))
+
 (rum/defc view-inner < rum/static
   [view-entity {:keys [data set-data! columns add-new-object! views-title title-key render-empty-title?] :as option
                 :or {render-empty-title? false}}]
@@ -1144,7 +1170,10 @@
 
      (case display-type
        :logseq.property.view/type.list
-       (list-view view-entity (:rows table) (:config option))
+       (list-view (:config option) view-entity (:rows table))
+
+       :logseq.property.view/type.card
+       (card-view (:config option) view-entity (:rows table))
 
        (table-view table option row-selection add-new-object! ready?))]))
 

+ 16 - 17
src/main/frontend/config.cljs

@@ -43,8 +43,7 @@
       (def USER-POOL-ID "us-east-1_dtagLnju8")
       (def IDENTITY-POOL-ID "us-east-1:d6d3b034-1631-402b-b838-b44513e93ee0")
       (def OAUTH-DOMAIN "logseq-prod.auth.us-east-1.amazoncognito.com")
-      (def CONNECTIVITY-TESTING-S3-URL "https://logseq-connectivity-testing-prod.s3.us-east-1.amazonaws.com/logseq-connectivity-testing")
-      )
+      (def CONNECTIVITY-TESTING-S3-URL "https://logseq-connectivity-testing-prod.s3.us-east-1.amazonaws.com/logseq-connectivity-testing"))
 
   (do (def FILE-SYNC-PROD? false)
       (def LOGIN-URL
@@ -59,7 +58,6 @@
       (def OAUTH-DOMAIN "logseq-test2.auth.us-east-2.amazoncognito.com")
       (def CONNECTIVITY-TESTING-S3-URL "https://logseq-connectivity-testing-prod.s3.us-east-1.amazonaws.com/logseq-connectivity-testing")))
 
-
 (goog-define ENABLE-RTC-SYNC-PRODUCTION false)
 (if ENABLE-RTC-SYNC-PRODUCTION
   (def RTC-WS-URL "wss://ws.logseq.com/rtc-sync?token=%s")
@@ -76,9 +74,9 @@
 
 ;; User level configuration for whether plugins are enabled
 (defonce lsp-enabled?
-         (and (util/electron?)
-              (not (false? feature-plugin-system-on?))
-              (state/lsp-enabled?-or-theme)))
+  (and (util/electron?)
+       (not (false? feature-plugin-system-on?))
+       (state/lsp-enabled?-or-theme)))
 
 (defn plugin-config-enabled?
   []
@@ -153,10 +151,10 @@
                      (util/safe-lower-case)
                      (keyword))]
      (boolean
-       (some
-         (fn [s]
-           (contains? s input))
-         formats)))))
+      (some
+       (fn [s]
+         (contains? s input))
+       formats)))))
 
 (defn ext-of-video?
   ([s] (ext-of-video? s true))
@@ -345,7 +343,7 @@
    (demo-graph? (state/get-current-repo)))
   ([repo-url]
    (or (nil? repo-url) (= repo-url demo-repo)
-     (string/ends-with? repo-url demo-repo))))
+       (string/ends-with? repo-url demo-repo))))
 
 (defonce recycle-dir ".recycle")
 (def config-file "config.edn")
@@ -376,10 +374,12 @@
        (string/starts-with? s local-db-prefix)))
 
 (defn db-based-graph?
-  [s]
-  (boolean
-   (and (string? s)
-        (sqlite-util/db-based-graph? s))))
+  ([]
+   (db-based-graph? (state/get-current-repo)))
+  ([s]
+   (boolean
+    (and (string? s)
+         (sqlite-util/db-based-graph? s)))))
 
 (defn get-local-asset-absolute-path
   [s]
@@ -505,8 +505,7 @@
 
 (defn get-current-repo-assets-root
   []
-  (when-let [repo-dir (and (local-file-based-graph? (state/get-current-repo))
-                           (get-repo-dir (state/get-current-repo)))]
+  (when-let [repo-dir (get-repo-dir (state/get-current-repo))]
     (path/path-join repo-dir "assets")))
 
 (defn get-custom-js-path

+ 22 - 0
src/main/frontend/db/async.cljs

@@ -292,6 +292,28 @@
         :where
         [?tag :block/type "class"]]))
 
+(defn <get-asset-with-checksum
+  [graph checksum]
+  (p/let [result (<q graph {:transact-db? true}
+                     '[:find [(pull ?b [*]) ...]
+                       :in $ ?checksum
+                       :where
+                       [?b :logseq.property.asset/checksum ?checksum]]
+                     checksum)]
+    (some-> (first result)
+            :db/id
+            db/entity)))
+
+(defn <get-pdf-annotations
+  [graph pdf-id]
+  (p/let [result (<q graph {:transact-db? true}
+                     '[:find [(pull ?b [*]) ...]
+                       :in $ ?pdf-id
+                       :where
+                       [?b :logseq.property/asset ?pdf-id]]
+                     pdf-id)]
+    result))
+
 (comment
   (defn <fetch-all-pages
     [graph]

+ 11 - 11
src/main/frontend/db/query_custom.cljs

@@ -14,14 +14,14 @@
   [l]
   (let [block-attrs (butlast model/file-graph-block-attrs)]
     (walk/postwalk
-    (fn [f]
-      (if (and (list? f)
-               (= 'pull (first f))
-               (= '?b (second f))
-               (= '[*] (nth f 2)))
-        `(~'pull ~'?b ~block-attrs)
-        f))
-    l)))
+     (fn [f]
+       (if (and (list? f)
+                (= 'pull (first f))
+                (= '?b (second f))
+                (= '[*] (nth f 2)))
+         `(~'pull ~'?b ~block-attrs)
+         f))
+     l)))
 
 (defn- add-rules-to-query
   "Searches query's :where for rules and adds them to query if used"
@@ -60,9 +60,9 @@
                     (fn [rules]
                       (into (or rules [])
                             (rules/extract-rules query-dsl-rules
-                                                   rules-found
-                                                   (when db-graph?
-                                                     {:deps rules/rules-dependencies})))))))
+                                                 rules-found
+                                                 (when db-graph?
+                                                   {:deps rules/rules-dependencies})))))))
       query-m)))
 
 (defn custom-query

+ 228 - 158
src/main/frontend/extensions/pdf/assets.cljs

@@ -35,28 +35,28 @@
   (let [repo-dir (config/get-repo-dir (state/get-current-repo))]
     (when (some-> url (string/trim) (string/includes? repo-dir))
       (some-> (string/split url repo-dir)
-        (last)
-        (string/replace-first "/assets/" "")))))
+              (last)
+              (string/replace-first "/assets/" "")))))
 
 (defn inflate-asset
-  [original-path & {:keys [href]}]
+  [original-path & {:keys [href block]}]
   (let [web-link? (string/starts-with? original-path "http")
         blob-res? (some-> href (string/starts-with? "blob"))
         filename  (util/node-path.basename original-path)
-        ext-name  (util/get-file-ext filename)
+        ext-name  "pdf"
         url       (if blob-res? href
-                    (assets-handler/normalize-asset-resource-url original-path))
+                      (assets-handler/normalize-asset-resource-url original-path))
         filename' (if (or web-link? blob-res?) filename
-                    (some-> (get-in-repo-assets-full-filename url)
-                      (js/decodeURIComponent) (string/replace ' "/" "_")))
+                      (some-> (get-in-repo-assets-full-filename url)
+                              (js/decodeURIComponent) (string/replace '"/" "_")))
         filekey   (util/safe-sanitize-file-name
-                    (subs filename' 0 (- (count filename') (inc (count ext-name)))))]
+                   (subs filename' 0 (- (count filename') (inc (count ext-name)))))]
     (when-let [key (and (not (string/blank? filekey))
-                     (if web-link?
-                       (str filekey "__" (hash url))
-                       filekey))]
-
+                        (if web-link?
+                          (str filekey "__" (hash url))
+                          filekey))]
       {:key           key
+       :block         block
        :identity      (subs key (- (count key) 15))
        :filename      filename
        :url           url
@@ -69,103 +69,135 @@
     (-> (str common-config/local-assets-dir "/" key "/")
         (str (util/format "%s_%s_%s.png" page id img-stamp)))))
 
-(defn ensure-ref-page!
+(defn file-based-ensure-ref-page!
   [pdf-current]
-  (when-let [page-name (util/trim-safe (:key pdf-current))]
-    (p/let [page-name (str "hls__" page-name)
-            repo (state/get-current-repo)
-            page (db-async/<get-block repo page-name)
-            file-path (:original-path pdf-current)
-            format (state/get-preferred-format)
-            repo-dir (config/get-repo-dir repo)
-            asset-dir (util/node-path.join repo-dir common-config/local-assets-dir)
-            url (if (string/includes? file-path asset-dir)
-                  (str ".." (last (string/split file-path repo-dir)))
-                  file-path)]
-      (if-not page
-        (let [label (:filename pdf-current)]
-          (p/do!
-            (page-handler/<create! page-name {:redirect?        false :create-first-block? false
-                                              :split-namespace? false
-                                              :format           format
-                                              :properties       {(pu/get-pid :logseq.property.pdf/file)
-                                                                 (case format
-                                                                   :markdown
-                                                                   (util/format "[%s](%s)" label url)
-
-                                                                   :org
-                                                                   (util/format "[[%s][%s]]" url label)
-
-                                                                   url)
-                                                                 (pu/get-pid :logseq.property.pdf/file-path)
-                                                                 url}})
-            (db-model/get-page page-name)))
+  ;; db version doesn't need a page for highlights data
+  (when-not (config/db-based-graph? (state/get-current-repo))
+    (when-let [page-name (util/trim-safe (:key pdf-current))]
+      (p/let [page-name (str "hls__" page-name)
+              repo (state/get-current-repo)
+              page (db-async/<get-block repo page-name)
+              file-path (:original-path pdf-current)
+              format (state/get-preferred-format)
+              repo-dir (config/get-repo-dir repo)
+              asset-dir (util/node-path.join repo-dir common-config/local-assets-dir)
+              url (if (string/includes? file-path asset-dir)
+                    (str ".." (last (string/split file-path repo-dir)))
+                    file-path)]
+        (if-not page
+          (let [label (:filename pdf-current)]
+            (p/do!
+             (page-handler/<create! page-name {:redirect?        false :create-first-block? false
+                                               :split-namespace? false
+                                               :format           format
+                                               :properties       {:file
+                                                                  (case format
+                                                                    :markdown
+                                                                    (util/format "[%s](%s)" label url)
+
+                                                                    :org
+                                                                    (util/format "[[%s][%s]]" url label)
+
+                                                                    url)
+                                                                  :file-path
+                                                                  url}})
+             (db-model/get-page page-name)))
 
-        (do
+          (do
           ;; try to update file path
-          (when (nil? (some-> page
-                        (:block/properties)
-                        (get (pu/get-pid :logseq.property.pdf/file-path))))
-            (property-handler/add-page-property!
-              page-name (pu/get-pid :logseq.property.pdf/file-path) url))
-          page)))))
+            (when (nil? (some-> page
+                                (:block/properties)
+                                :file-path))
+              (property-handler/add-page-property! page-name :file-path url))
+            page))))))
+
+(defn file-based-ensure-ref-block!
+  [pdf-current {:keys [id content page properties] :as hl} insert-opts]
+  (p/let [ref-page (when pdf-current (file-based-ensure-ref-page! pdf-current))]
+    (when ref-page
+      (let [ref-block (db-model/query-block-by-uuid id)]
+        (if-not (nil? (:block/title ref-block))
+          (do
+            (println "[existed ref block]" ref-block)
+            ref-block)
+          (let [text       (:text content)
+                wrap-props #(if-let [stamp (:image content)]
+                              (assoc %
+                                     :hl-type :area
+                                     :hl-stamp stamp)
+                              %)
+                db-base? (config/db-based-graph? (state/get-current-repo))
+                props (cond->
+                       {(pu/get-pid :logseq.property/ls-type)  :annotation
+                        (pu/get-pid :logseq.property.pdf/hl-page)  page
+                        (pu/get-pid :logseq.property/hl-color) (:color properties)}
+
+                        db-base?
+                        (assoc (pu/get-pid :logseq.property.pdf/hl-value) hl)
+
+                        (not db-base?)
+                         ;; force custom uuid
+                        (assoc :id (if (string? id) (uuid id) id)))
+                properties (wrap-props props)]
+            (when (string? text)
+              (editor-handler/api-insert-new-block!
+               text (merge {:page        (:block/name ref-page)
+                            :custom-uuid id
+                            :properties properties}
+                           insert-opts)))))))))
+
+(defn db-based-ensure-ref-block!
+  [pdf-current {:keys [id content page properties] :as hl} insert-opts]
+  (when-let [pdf-block (:block pdf-current)]
+    (let [ref-block (db-model/query-block-by-uuid id)]
+      (if (:block/title ref-block)
+        (do
+          (println "[existed ref block]" ref-block)
+          ref-block)
+        (let [text       (:text content)
+              properties (cond->
+                          {:logseq.property/ls-type  :annotation
+                           :logseq.property/hl-color (:color properties)
+                           :logseq.property/asset (:db/id pdf-block)
+                           :logseq.property.pdf/hl-page  page
+                           :logseq.property.pdf/hl-value hl}
+                           (:image content)
+                           (assoc :logseq.property/hl-type :area
+                                  :logseq.property.pdf/hl-image (:image content)))]
+          (when (string? text)
+            (editor-handler/api-insert-new-block!
+             text (merge {:block-uuid (:block/uuid pdf-block)
+                          :sibling? false
+                          :custom-uuid id
+                          :properties properties}
+                         insert-opts))))))))
 
 (defn ensure-ref-block!
-  ([pdf hl] (ensure-ref-block! pdf hl nil))
-  ([pdf-current {:keys [id content page properties] :as hl} insert-opts]
-   (p/let [ref-page (when pdf-current (ensure-ref-page! pdf-current))]
-     (when ref-page
-       (let [ref-block (db-model/query-block-by-uuid id)]
-         (if-not (nil? (:block/title ref-block))
-           (do
-             (println "[existed ref block]" ref-block)
-             ref-block)
-           (let [text       (:text content)
-                 wrap-props #(if-let [stamp (:image content)]
-                               (assoc %
-                                      (pu/get-pid :logseq.property/hl-type) :area
-                                      (pu/get-pid :logseq.property.pdf/hl-stamp) stamp)
-                               %)
-                 db-base? (config/db-based-graph? (state/get-current-repo))
-                 props (cond->
-                        {(pu/get-pid :logseq.property/ls-type)  :annotation
-                         (pu/get-pid :logseq.property.pdf/hl-page)  page
-                         (pu/get-pid :logseq.property/hl-color) (:color properties)}
-
-                         db-base?
-                         (assoc (pu/get-pid :logseq.property.pdf/hl-value) hl)
-
-                         (not db-base?)
-                         ;; force custom uuid
-                         (assoc :id (if (string? id) (uuid id) id)))
-                 properties (wrap-props props)]
-             (when (string? text)
-               (editor-handler/api-insert-new-block!
-                text (merge {:page        (:block/name ref-page)
-                             :custom-uuid id
-                             :properties properties}
-                            insert-opts))))))))))
+  [pdf-current hl insert-opts]
+  (if (config/db-based-graph? (state/get-current-repo))
+    (db-based-ensure-ref-block! pdf-current hl insert-opts)
+    (file-based-ensure-ref-block! pdf-current hl insert-opts)))
 
 (defn construct-highlights-from-hls-page
   [hls-page]
   (p/let [blocks (db-async/<get-page-all-blocks (:block/uuid hls-page))]
     {:highlights (keep :logseq.property.pdf/hl-value blocks)}))
 
-(defn load-hls-data$
+(defn file-based-load-hls-data$
   [{:keys [hls-file]}]
   (when hls-file
-    (let [repo(state/get-current-repo)
+    (let [repo (state/get-current-repo)
           repo-dir (config/get-repo-dir repo)
           db-base? (config/db-based-graph? repo)]
       (p/let [_    (fs/create-if-not-exists repo repo-dir hls-file "{:highlights []}")
               res  (fs/read-file repo-dir hls-file)
               data (if res (reader/read-string res) {})]
         (if db-base?
-          (p/let [hls-page (ensure-ref-page! (state/get-current-pdf))]
+          (p/let [hls-page (file-based-ensure-ref-page! (state/get-current-pdf))]
             (construct-highlights-from-hls-page hls-page))
           data)))))
 
-(defn persist-hls-data$
+(defn file-based-persist-hls-data$
   [{:keys [hls-file]} highlights extra]
   (when hls-file
     (let [repo-cur (state/get-current-repo)
@@ -173,16 +205,46 @@
           data     (with-out-str (pprint {:highlights highlights :extra extra}))]
       (fs/write-file! repo-cur repo-dir hls-file data {:skip-compare? true}))))
 
-(defn resolve-hls-data-by-key$
+(defn file-based-resolve-hls-data-by-key$
   [target-key]
   ;; TODO: fuzzy match
   (when-let [hls-file (and target-key (str common-config/local-assets-dir "/" target-key ".edn"))]
-    (load-hls-data$ {:hls-file hls-file})))
+    (file-based-load-hls-data$ {:hls-file hls-file})))
 
 (defn area-highlight?
   [hl]
   (and hl (not (nil? (get-in hl [:content :image])))))
 
+(defn- file-based-persist-hl-area-image
+  [repo-url repo-dir current new-hl old-hl png]
+  (p/let [_          (js/console.time :write-area-image)
+          ^js png    (.arrayBuffer png)
+          {:keys [key]} current
+                                  ;; dir
+          fstamp     (get-in new-hl [:content :image])
+          old-fstamp (and old-hl (get-in old-hl [:content :image]))
+          fname      (str (:page new-hl) "_" (:id new-hl))
+          fdir       (str common-config/local-assets-dir "/" key)
+          _          (fs/mkdir-if-not-exists (path/path-join repo-dir fdir))
+          new-fpath  (str fdir "/" fname "_" fstamp ".png")
+          old-fpath  (and old-fstamp (str fdir "/" fname "_" old-fstamp ".png"))
+          _          (and old-fpath (fs/rename! repo-url old-fpath new-fpath))
+          _          (fs/write-file! repo-url repo-dir new-fpath png {:skip-compare? true})]
+
+    (js/console.timeEnd :write-area-image)))
+
+(defn- db-based-persist-hl-area-image
+  [repo png]
+  (let [file (js/File. #js [png] "pdf area highlight.png")]
+    (editor-handler/db-based-save-assets! repo [file])))
+
+(defn- persist-hl-area-image
+  [repo-url repo-dir current new-hl old-hl png]
+  (if (config/db-based-graph?)
+    (p/let [result (db-based-persist-hl-area-image repo-url png)]
+      (first result))
+    (file-based-persist-hl-area-image repo-url repo-dir current new-hl old-hl png)))
+
 (defn persist-hl-area-image$
   "Save pdf highlight area image"
   [^js viewer current new-hl old-hl {:keys [top left width height]}]
@@ -205,41 +267,22 @@
          (* left dpr) (* top dpr) (* width dpr) (* height dpr)
          0 0 dw dh)
 
-        (let [callback (fn [^js png]
-                         ;; write image file
-                         (p/catch
-                          (p/let [_          (js/console.time :write-area-image)
-                                  ^js png    (.arrayBuffer png)
-                                  {:keys [key]} current
-                                  ;; dir
-                                  fstamp     (get-in new-hl [:content :image])
-                                  old-fstamp (and old-hl (get-in old-hl [:content :image]))
-                                  fname      (str (:page new-hl) "_" (:id new-hl))
-                                  fdir       (str common-config/local-assets-dir "/" key)
-                                  _          (fs/mkdir-if-not-exists (path/path-join repo-dir fdir))
-                                  new-fpath  (str fdir "/" fname "_" fstamp ".png")
-                                  old-fpath  (and old-fstamp (str fdir "/" fname "_" old-fstamp ".png"))
-                                  _          (and old-fpath (fs/rename! repo-url old-fpath new-fpath))
-                                  _          (fs/write-file! repo-url repo-dir new-fpath png {:skip-compare? true})]
-
-                            (js/console.timeEnd :write-area-image))
-
-                          (fn [err]
-                            (js/console.error "[write area image Error]" err))))]
-
-          (.toBlob canvas' callback))
-        ))))
+        (js/Promise.
+         (fn [resolve reject]
+           (.toBlob canvas'
+                    (fn [^js png]
+                      (p/catch
+                       (resolve (persist-hl-area-image repo-url repo-dir current new-hl old-hl png))
+                       (fn [err]
+                         (reject err)
+                         (js/console.error "[write area image Error]" err)))))))))))
 
 (defn update-hl-block!
   [highlight]
   (when-let [block (db-model/get-block-by-uuid (:id highlight))]
-    (doseq [[k v] {(pu/get-pid :logseq.property.pdf/hl-stamp)
-                   (if (area-highlight? highlight)
-                     (get-in highlight [:content :image])
-                     (js/Date.now))
-                   (pu/get-pid :logseq.property/hl-color)
-                   (get-in highlight [:properties :color])}]
-      (property-handler/set-block-property! (state/get-current-repo) (:block/uuid block) k v))))
+    (when-let [color (get-in highlight [:properties :color])]
+      (let [k (pu/get-pid :logseq.property/hl-color)]
+        (property-handler/set-block-property! (state/get-current-repo) (:block/uuid block) k color)))))
 
 (defn unlink-hl-area-image$
   [^js _viewer current hl]
@@ -260,35 +303,54 @@
 
 (defn copy-hl-ref!
   [highlight ^js viewer]
-  (p/let [ref-block (ensure-ref-block! (state/get-current-pdf) highlight)]
+  (p/let [ref-block (ensure-ref-block! (state/get-current-pdf) highlight nil)]
     (when ref-block
       (util/copy-to-clipboard!
        (block-ref/->block-ref (:block/uuid ref-block))
        :owner-window (pdf-windows/resolve-own-window viewer)))))
 
-(defn open-block-ref!
+(defn file-based-open-block-ref!
   [block]
   (let [id (:block/uuid block)
         page (db/entity (:db/id (:block/page block)))
         page-name (:block/title page)
-        file-path (pu/get-block-property-value block :logseq.property.pdf/file-path)
+        file-path (get-in block [:block/properties :file-path])
         hl-page (pu/get-block-property-value block :logseq.property.pdf/hl-page)
-        hl-value (pu/get-block-property-value block :logseq.property.pdf/hl-value)
-        db-base? (config/db-based-graph? (state/get-current-repo))]
+        hl-value (pu/get-block-property-value block :logseq.property.pdf/hl-value)]
     (when-let [target-key (and page-name (subs page-name 5))]
-      (p/let [hls (resolve-hls-data-by-key$ target-key)
+      (p/let [hls (file-based-resolve-hls-data-by-key$ target-key)
               hls (and hls (:highlights hls))
-              file-path (or file-path (str "../assets/" target-key ".pdf"))
-              href (and db-base? (assets-handler/make-asset-url file-path))]
+              file-path (or file-path (str "../assets/" target-key ".pdf"))]
         (if-let [matched (or (and hls (medley/find-first #(= id (:id %)) hls))
-                           (if hl-page {:page hl-page}
-                             (when-let [page (some-> hl-value :page)] {:page page})))]
+                             (if hl-page {:page hl-page}
+                                 (when-let [page (some-> hl-value :page)] {:page page})))]
           (do
             (state/set-state! :pdf/ref-highlight matched)
             ;; open pdf viewer
-            (state/set-current-pdf! (inflate-asset file-path {:href href})))
+            (state/set-current-pdf! (inflate-asset file-path)))
           (js/console.debug "[Unmatched highlight ref]" block))))))
 
+(defn db-based-open-block-ref!
+  [block]
+  (let [hl-value (:logseq.property.pdf/hl-value block)
+        asset (:logseq.property/asset block)
+        file-path (str "../assets/" (:block/uuid asset) ".pdf")]
+    (if asset
+      (->
+       (p/let [href (assets-handler/<make-asset-url file-path)]
+         (state/set-state! :pdf/ref-highlight hl-value)
+        ;; open pdf viewer
+         (state/set-current-pdf! (inflate-asset file-path {:href href :block asset})))
+       (p/catch (fn [error]
+                  (js/console.error error))))
+      (js/console.error "Pdf asset no longer exists"))))
+
+(defn open-block-ref!
+  [block]
+  (if (config/db-based-graph? (state/get-current-repo))
+    (db-based-open-block-ref! block)
+    (file-based-open-block-ref! block)))
+
 (defn goto-block-ref!
   [{:keys [id] :as hl}]
   (when id
@@ -300,8 +362,11 @@
 (defn goto-annotations-page!
   ([current] (goto-annotations-page! current nil))
   ([current id]
-   (when-let [e (some->> (:key current) (str "hls__") (db-model/get-page))]
-     (rfe/push-state :page {:name (str (:block/uuid e))} (if id {:anchor (str "block-content-" + id)} nil)))))
+   (when current
+     (if (config/db-based-graph?)
+       (rfe/push-state :page {:name (:block/uuid (:block current))} (if id {:anchor (str "block-content-" + id)} nil))
+       (when-let [e (some->> (:key current) (str "hls__") (db-model/get-page))]
+         (rfe/push-state :page {:name (str (:block/uuid e))} (if id {:anchor (str "block-content-" + id)} nil)))))))
 
 (defn open-lightbox
   [e]
@@ -323,31 +388,36 @@
     (when (seq images)
       (lightbox/preview-images! images))))
 
-(rum/defc area-display
-  [block]
-  (when-let [asset-path' (and block (publish-db/get-area-block-asset-url
-                                     (conn/get-db (state/get-current-repo))
-                                     block
-                                     (db-utils/pull (:db/id (:block/page block)))))]
-    (let [asset-path (assets-handler/make-asset-url asset-path')]
-      [:span.hl-area
-       [:span.actions
-        (when-not config/publishing?
+(rum/defcs area-display <
+  (rum/local nil ::src)
+  [state block]
+  (let [*src (::src state)]
+    (when-let [asset-path' (and block (publish-db/get-area-block-asset-url
+                                       (conn/get-db (state/get-current-repo))
+                                       block
+                                       (db-utils/pull (:db/id (:block/page block)))))]
+      (when (nil? @*src)
+        (p/let [asset-path (assets-handler/<make-asset-url asset-path')]
+          (reset! *src asset-path)))
+      (when @*src
+        [:span.hl-area
+         [:span.actions
+          (when-not config/publishing?
+            [:button.asset-action-btn.px-1
+             {:title         (t :asset/copy)
+              :tabIndex      "-1"
+              :on-pointer-down util/stop
+              :on-click      (fn [e]
+                               (util/stop e)
+                               (-> (util/copy-image-to-clipboard (common-config/remove-asset-protocol @*src))
+                                   (p/then #(notification/show! "Copied!" :success))))}
+             (ui/icon "copy")])
+
           [:button.asset-action-btn.px-1
-           {:title         (t :asset/copy)
+           {:title         (t :asset/maximize)
             :tabIndex      "-1"
             :on-pointer-down util/stop
-            :on-click      (fn [e]
-                             (util/stop e)
-                             (-> (util/copy-image-to-clipboard (common-config/remove-asset-protocol asset-path))
-                                 (p/then #(notification/show! "Copied!" :success))))}
-           (ui/icon "copy")])
-
-        [:button.asset-action-btn.px-1
-         {:title         (t :asset/maximize)
-          :tabIndex      "-1"
-          :on-pointer-down util/stop
-          :on-click      open-lightbox}
-
-         (ui/icon "maximize")]]
-       [:img {:src asset-path}]])))
+            :on-click      open-lightbox}
+
+           (ui/icon "maximize")]]
+         [:img {:src @*src}]]))))

+ 221 - 184
src/main/frontend/extensions/pdf/core.cljs

@@ -19,7 +19,11 @@
             [medley.core :as medley]
             [promesa.core :as p]
             [rum.core :as rum]
-            [frontend.ui :as ui]))
+            [frontend.ui :as ui]
+            [frontend.db.async :as db-async]
+            [goog.functions :refer [debounce]]
+            [frontend.handler.property :as property-handler]
+            [datascript.impl.entity :as de]))
 
 (declare pdf-container system-embed-playground)
 
@@ -34,7 +38,7 @@
 
 (rum/defcs pdf-highlight-finder
   < rum/static rum/reactive
-    (rum/local false ::mounted?)
+  (rum/local false ::mounted?)
   [state ^js viewer]
   (let [*mounted? (::mounted? state)]
     (when viewer
@@ -71,11 +75,11 @@
   [^js viewer]
   (let [el-ref   (rum/use-ref nil)
         adjust-main-size!
-                 (util/debounce
-                  200 (fn [width]
-                        (let [root-el js/document.documentElement]
-                          (.setProperty (.-style root-el) "--ph-view-container-width" width)
-                          (pdf-utils/adjust-viewer-size! viewer))))
+        (util/debounce
+         200 (fn [width]
+               (let [root-el js/document.documentElement]
+                 (.setProperty (.-style root-el) "--ph-view-container-width" width)
+                 (pdf-utils/adjust-viewer-size! viewer))))
         group-id (.-$groupIdentity viewer)]
 
     ;; draggable handler
@@ -148,8 +152,8 @@
                            "copy"
                            (do
                              (util/copy-to-clipboard!
-                               (or (:text content) (pdf-utils/fix-selection-text-breakline (.toString selection)))
-                               :owner-window (pdf-windows/resolve-own-window viewer))
+                              (or (:text content) (pdf-utils/fix-selection-text-breakline (.toString selection)))
+                              :owner-window (pdf-windows/resolve-own-window viewer))
                              (pdf-utils/clear-all-selection))
 
                            "link"
@@ -171,11 +175,11 @@
                                (let [highlight (merge highlight
                                                       {:id         (pdf-utils/gen-uuid)
                                                        :properties properties})]
-                                 (add-hl! highlight)
-                                 (pdf-utils/clear-all-selection)
-                                 (pdf-assets/copy-hl-ref! highlight viewer))
+                                 (p/let [highlight' (add-hl! highlight)]
+                                   (pdf-utils/clear-all-selection)
+                                   (pdf-assets/copy-hl-ref! highlight' viewer)))
 
-                               ;; update highlight
+;; update highlight
                                (upd-hl! (assoc highlight :properties properties)))
 
                              (reset! *highlight-last-color (keyword action)))))
@@ -208,7 +212,6 @@
       (for [it ["yellow", "red", "green", "blue", "purple"]]
         [:a {:key it :data-color it :data-action it} it])]
 
-
      (and id [:li.item {:data-action "ref"} (t :pdf/copy-ref)])
 
      (and (not area?) [:li.item {:data-action "copy"} (t :pdf/copy-text)])
@@ -228,8 +231,7 @@
 
                                     (when (true? (:clearSelection extras))
                                       (pdf-utils/clear-all-selection)))}
-          label]))
-     ]))
+          label]))]))
 
 (rum/defc pdf-highlights-text-region
   [^js viewer vw-hl hl {:keys [show-ctx-menu!]}]
@@ -250,7 +252,7 @@
         (fn [^js e]
           (when-let [^js dt (and id (.-dataTransfer e))]
             (reset! block/*dragging? true)
-            (pdf-assets/ensure-ref-block! (state/get-current-pdf) hl)
+            (pdf-assets/ensure-ref-block! (state/get-current-pdf) hl nil)
             (.setData dt "text/plain" (str "((" id "))"))))]
 
     [:div.extensions__pdf-hls-text-region
@@ -289,9 +291,9 @@
         update-hl!        (fn [hl] (some-> (rum/deref *ops-ref) (:upd-hl!) (apply [hl])))]
 
     (rum/use-effect!
-      (fn []
-        (rum/set-ref! *ops-ref ops))
-      [ops])
+     (fn []
+       (rum/set-ref! *ops-ref ops))
+     [ops])
 
     ;; resizable
     (rum/use-effect!
@@ -326,19 +328,21 @@
                                                   (let [hl' (assoc hl :position to-sc-pos)
                                                         hl' (assoc-in hl' [:content :image] (js/Date.now))]
 
-                                                    (p/then
-                                                     (pdf-assets/persist-hl-area-image$ viewer
-                                                                                        (:pdf/current @state/state)
-                                                                                        hl' hl (:bounding to-vw-pos))
-                                                     (fn [] (js/setTimeout
-                                                             #(do
-                                                                ;; reset dom effects
-                                                                (set! (.. target -style -transform) (str "translate(0, 0)"))
-                                                                (.removeAttribute target "data-x")
-                                                                (.removeAttribute target "data-y")
-
-                                                                (update-hl! hl')) 200))))
-
+                                                    (p/let [result (pdf-assets/persist-hl-area-image$ viewer
+                                                                                                      (:pdf/current @state/state)
+                                                                                                      hl' hl (:bounding to-vw-pos))]
+
+                                                      (js/setTimeout
+                                                       #(do
+                                                           ;; reset dom effects
+                                                          (set! (.. target -style -transform) (str "translate(0, 0)"))
+                                                          (.removeAttribute target "data-x")
+                                                          (.removeAttribute target "data-y")
+                                                          (let [hl' (if (de/entity? result)
+                                                                      (assoc-in hl' [:content :image] (:db/id result))
+                                                                      hl')]
+                                                            (update-hl! hl')))
+                                                       200)))
 
                                                   (js/setTimeout #(rum/set-ref! *dirty false))))
 
@@ -361,12 +365,10 @@
 
                                                     ;; cache pos
                                                     (.setAttribute target "data-x" ax)
-                                                    (.setAttribute target "data-y" ay))
-                                                  ))}
+                                                    (.setAttribute target "data-y" ay))))}
                            :modifiers [(js/interact.modifiers.restrict
-                                         (bean/->js {:restriction (.closest el ".page")}))]
-                           :inertia   true})
-                         ))]
+                                        (bean/->js {:restriction (.closest el ".page")}))]
+                           :inertia   true})))]
          ;; destroy
          #(.unset it)))
      [hl])
@@ -394,8 +396,7 @@
          (if (get-in hl [:content :image])
            (pdf-highlight-area-region viewer vw-hl hl ops)
            (pdf-highlights-text-region viewer vw-hl hl ops))
-         (:id hl))
-       ))])
+         (:id hl))))])
 
 (rum/defc ^:large-vars/cleanup-todo pdf-highlight-area-selection
   [^js viewer {:keys [show-ctx-menu!]}]
@@ -464,73 +465,73 @@
         disable-text-selection! #(js-invoke viewer-clt (if % "add" "remove") "disabled-text-selection")
 
         fn-move                 (rum/use-callback
-                                  (fn [^js/MouseEvent e]
-                                    (set-end! (calc-coords! (.-pageX e) (.-pageY e))))
-                                  [])]
+                                 (fn [^js/MouseEvent e]
+                                   (set-end! (calc-coords! (.-pageX e) (.-pageY e))))
+                                 [])]
 
     (rum/use-effect!
-      (fn []
-        (when-let [^js/HTMLElement root cnt-el]
-          (let [fn-start (fn [^js/MouseEvent e]
-                           (if (should-start e)
-                             (let [target (.-target e)
-                                   page-el (.closest target ".page")
-                                   [x y] [(.-pageX e) (.-pageY e)]]
-                               (rum/set-ref! *start-el target)
-                               (rum/set-ref! *start-xy [x y])
-                               (rum/set-ref! *page-el page-el)
-                               (rum/set-ref! *page-rect (some-> page-el (.getBoundingClientRect) (.toJSON) (bean/->clj)))
-                               (set-start! (calc-coords! x y))
-                               (disable-text-selection! true)
-
-                               (.addEventListener root "mousemove" fn-move))
+     (fn []
+       (when-let [^js/HTMLElement root cnt-el]
+         (let [fn-start (fn [^js/MouseEvent e]
+                          (if (should-start e)
+                            (let [target (.-target e)
+                                  page-el (.closest target ".page")
+                                  [x y] [(.-pageX e) (.-pageY e)]]
+                              (rum/set-ref! *start-el target)
+                              (rum/set-ref! *start-xy [x y])
+                              (rum/set-ref! *page-el page-el)
+                              (rum/set-ref! *page-rect (some-> page-el (.getBoundingClientRect) (.toJSON) (bean/->clj)))
+                              (set-start! (calc-coords! x y))
+                              (disable-text-selection! true)
+
+                              (.addEventListener root "mousemove" fn-move))
 
                              ;; reset
-                             (do (reset-coords!)
-                                 (disable-text-selection! false))))
-
-                fn-end   (fn [^js/MouseEvent e]
-                           (when-let [start-el (rum/deref *start-el)]
-                             (let [end  (calc-coords! (.-pageX e) (.-pageY e))
-                                   rect (calc-rect start end)]
-
-                               (if (and (> (:width rect) 10)
-                                        (> (:height rect) 10))
-
-                                 (when-let [^js page-el (.closest start-el ".page")]
-                                   (let [page-number (int (.-pageNumber (.-dataset page-el)))
-                                         page-pos    (merge rect {:top  (- (:top rect) (.-offsetTop page-el))
-                                                                  :left (- (:left rect) (.-offsetLeft page-el))})
-                                         vw-pos      {:bounding page-pos :rects [] :page page-number}
-                                         sc-pos      (pdf-utils/vw-to-scaled-pos viewer vw-pos)
-
-                                         point       {:x (.-clientX e) :y (.-clientY e)}
-                                         hl          {:id         nil
-                                                      :page       page-number
-                                                      :position   sc-pos
-                                                      :content    {:text "[:span]" :image (js/Date.now)}
-                                                      :properties {}}]
+                            (do (reset-coords!)
+                                (disable-text-selection! false))))
+
+               fn-end   (fn [^js/MouseEvent e]
+                          (when-let [start-el (rum/deref *start-el)]
+                            (let [end  (calc-coords! (.-pageX e) (.-pageY e))
+                                  rect (calc-rect start end)]
+
+                              (if (and (> (:width rect) 10)
+                                       (> (:height rect) 10))
+
+                                (when-let [^js page-el (.closest start-el ".page")]
+                                  (let [page-number (int (.-pageNumber (.-dataset page-el)))
+                                        page-pos    (merge rect {:top  (- (:top rect) (.-offsetTop page-el))
+                                                                 :left (- (:left rect) (.-offsetLeft page-el))})
+                                        vw-pos      {:bounding page-pos :rects [] :page page-number}
+                                        sc-pos      (pdf-utils/vw-to-scaled-pos viewer vw-pos)
+
+                                        point       {:x (.-clientX e) :y (.-clientY e)}
+                                        hl          {:id         nil
+                                                     :page       page-number
+                                                     :position   sc-pos
+                                                     :content    {:text "" :image (js/Date.now)}
+                                                     :properties {}}]
 
                                      ;; ctx tips for area
-                                     (show-ctx-menu! viewer hl point {:reset-fn #(reset-coords!)}))
+                                    (show-ctx-menu! viewer hl point {:reset-fn #(reset-coords!)}))
 
-                                   (set-area-mode! false))
+                                  (set-area-mode! false))
 
                                  ;; reset
-                                 (reset-coords!)))
+                                (reset-coords!)))
 
-                             (disable-text-selection! false)
-                             (.removeEventListener root "mousemove" fn-move)))]
+                            (disable-text-selection! false)
+                            (.removeEventListener root "mousemove" fn-move)))]
 
-            (doto root
-              (.addEventListener "mousedown" fn-start)
-              (.addEventListener "mouseup" fn-end #js {:once true}))
+           (doto root
+             (.addEventListener "mousedown" fn-start)
+             (.addEventListener "mouseup" fn-end #js {:once true}))
 
             ;; destroy
-            #(doto root
-               (.removeEventListener "mousedown" fn-start)
-               (.removeEventListener "mouseup" fn-end)))))
-      [start])
+           #(doto root
+              (.removeEventListener "mousedown" fn-start)
+              (.removeEventListener "mouseup" fn-end)))))
+     [start])
 
     [:div.extensions__pdf-area-selection
      {:ref *el}
@@ -548,10 +549,10 @@
         [ctx-menu-state, set-ctx-menu-state!] (rum/use-state {:highlight nil :vw-pos nil :selection nil :point nil :reset-fn nil})
 
         clear-ctx-menu! (rum/use-callback
-                          #(let [reset-fn (:reset-fn ctx-menu-state)]
-                             (set-ctx-menu-state! {})
-                             (and (fn? reset-fn) (reset-fn)))
-                          [ctx-menu-state])
+                         #(let [reset-fn (:reset-fn ctx-menu-state)]
+                            (set-ctx-menu-state! {})
+                            (and (fn? reset-fn) (reset-fn)))
+                         [ctx-menu-state])
 
         show-ctx-menu! (fn [^js viewer hl point & ops]
                          (let [vw-pos (pdf-utils/scaled-to-vw-pos viewer (:position hl))]
@@ -560,20 +561,28 @@
         add-hl! (fn [hl]
                   (when (:id hl)
                     ;; fix js object
-                    (let [highlights (pdf-utils/fix-nested-js highlights)]
-                      (set-highlights! (conj highlights hl)))
-
-                    (when-let [vw-pos (and (pdf-assets/area-highlight? hl)
-                                        (pdf-utils/scaled-to-vw-pos viewer (:position hl)))]
-                      ;; exceptions
-                      (pdf-assets/persist-hl-area-image$ viewer (:pdf/current @state/state)
-                        hl nil (:bounding vw-pos)))))
-
+                    (let [highlights (pdf-utils/fix-nested-js highlights)
+                          highlights' (conj highlights hl)]
+                      (set-highlights! highlights')
+
+                      (if-let [vw-pos (and (pdf-assets/area-highlight? hl)
+                                           (pdf-utils/scaled-to-vw-pos viewer (:position hl)))]
+                        ;; exceptions
+                        (->
+                         (p/let [result (pdf-assets/persist-hl-area-image$ viewer (:pdf/current @state/state)
+                                                                           hl nil (:bounding vw-pos))]
+                           (when (de/entity? result)
+                             (let [hl' (assoc-in hl [:content :image] (:db/id result))]
+                               (set-highlights! (map (fn [hl] (if (= (:id hl) (:id hl')) hl' hl)) highlights'))
+                               hl')))
+                         (p/catch (fn [e]
+                                    (js/console.error e))))
+                        hl))))
         upd-hl! (fn [hl]
                   (let [highlights (pdf-utils/fix-nested-js highlights)]
                     (when-let [[target-idx] (medley/find-first
-                                              #(= (:id (second %)) (:id hl))
-                                              (medley/indexed highlights))]
+                                             #(= (:id (second %)) (:id hl))
+                                             (medley/indexed highlights))]
                       (set-highlights! (assoc-in highlights [target-idx] hl))
                       (pdf-assets/update-hl-block! hl))))
 
@@ -694,13 +703,13 @@
      ;; hl context tip menu
      (when-let [_hl (:highlight ctx-menu-state)]
        (js/ReactDOM.createPortal
-         (pdf-highlights-ctx-menu viewer ctx-menu-state
-           {:clear-ctx-menu! clear-ctx-menu!
-            :add-hl! add-hl!
-            :del-hl! del-hl!
-            :upd-hl! upd-hl!})
+        (pdf-highlights-ctx-menu viewer ctx-menu-state
+                                 {:clear-ctx-menu! clear-ctx-menu!
+                                  :add-hl! add-hl!
+                                  :del-hl! del-hl!
+                                  :upd-hl! upd-hl!})
 
-         (.querySelector el ".pp-holder")))
+        (.querySelector el ".pp-holder")))
 
      ;; debug highlights anchor
      ;;(if (seq highlights)
@@ -716,13 +725,11 @@
      (pdf-page-finder viewer)
 
      ;; area selection container
-     (when (pdf-utils/support-area?)
-       (pdf-highlight-area-selection
-         viewer
-         {:clear-ctx-menu! clear-ctx-menu!
-          :show-ctx-menu! show-ctx-menu!
-          :add-hl! add-hl!
-          }))]))
+     (pdf-highlight-area-selection
+      viewer
+      {:clear-ctx-menu! clear-ctx-menu!
+       :show-ctx-menu! show-ctx-menu!
+       :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]
@@ -824,10 +831,10 @@
 
         (when (and page-ready? viewer (not initial-error))
           [(rum/with-key
-            (pdf-highlights
-             (:el state) viewer
-             initial-hls (:loaded-pages ano-state)
-             ops) "pdf-highlights")])]
+             (pdf-highlights
+              (:el state) viewer
+              initial-hls (:loaded-pages ano-state)
+              ops) "pdf-highlights")])]
 
        (when (and page-ready? viewer)
          [(when-not in-system-window?
@@ -852,14 +859,28 @@
 
      [:div.mt-5.sm:mt-4.flex
       (ui/button
-        "Submit"
-        {:on-click (fn []
-                     (let [password @password]
-                       (confirm-fn password)))})]]))
+       "Submit"
+       {:on-click (fn []
+                    (let [password @password]
+                      (confirm-fn password)))})]]))
+
+(defonce debounced-set-property!
+  (debounce property-handler/set-block-property! 300))
+
+(defn- debounce-set-last-visit-page!
+  [asset last-visit-page]
+  (when (and (number? last-visit-page)
+             (> last-visit-page 0))
+    (debounced-set-property! (state/get-current-repo)
+                             (:db/id asset)
+                             :logseq.property.asset/last-visit-page
+                             last-visit-page)))
 
 (rum/defc ^:large-vars/data-var pdf-loader
   [{:keys [url hls-file identity filename] :as pdf-current}]
-  (let [*doc-ref       (rum/use-ref nil)
+  (let [repo           (state/get-current-repo)
+        db-based?      (config/db-based-graph?)
+        *doc-ref       (rum/use-ref nil)
         [loader-state, set-loader-state!] (rum/use-state {:error nil :pdf-document nil :status nil})
         [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
@@ -867,59 +888,75 @@
         set-dirty-hls! (fn [latest-hls]                     ;; TODO: incremental
                          (set-hls-state! #(merge % {:initial-hls [] :latest-hls latest-hls})))
         set-hls-extra! (fn [extra]
-                         (set-hls-state! #(merge % {:extra extra})))]
+                         (if db-based?
+                           (debounce-set-last-visit-page! (:block pdf-current) (:page extra))
+                           (set-hls-state! #(merge % {:extra extra}))))]
 
     ;; current pdf effects
-    (rum/use-effect!
-     (fn []
-       (when pdf-current
-         (pdf-assets/ensure-ref-page! pdf-current)))
-     [pdf-current])
+    (when-not db-based?
+      (rum/use-effect!
+       (fn []
+         (when pdf-current
+           (pdf-assets/file-based-ensure-ref-page! pdf-current)))
+       [pdf-current]))
 
     ;; load highlights
-    (rum/use-effect!
-     (fn []
-       (p/catch
-        (p/let [data (pdf-assets/load-hls-data$ pdf-current)
-                {:keys [highlights extra]} data]
-          (set-initial-page! (or (when-let [page (:page extra)]
-                                   (util/safe-parse-int page)) 1))
-          (set-hls-state! {:initial-hls highlights :latest-hls highlights :extra extra :loaded true}))
-
-        ;; error
-        (fn [^js e]
-          (js/console.error "[load hls error]" e)
-
-          (let [msg (str (util/format "Error: failed to load the highlights file: \"%s\". \n"
-                                      (:hls-file pdf-current))
-                         e)]
-            (notification/show! msg :error)
-            (set-hls-state! {:loaded true :error e}))))
-
-       ;; cancel
-       #())
-     [hls-file])
-
-    ;; cache highlights
-    (let [persist-hls-data!
-          (rum/use-callback
-           (util/debounce
-            4000 (fn [latest-hls extra]
-                   (pdf-assets/persist-hls-data$
-                    pdf-current latest-hls extra))) [pdf-current])]
-
+    (if db-based?
+      (rum/use-effect!
+       (fn []
+         (when pdf-current
+           (let [pdf-block (:block pdf-current)]
+             (p/let [data (db-async/<get-pdf-annotations repo (:db/id pdf-block))
+                     highlights (map :logseq.property.pdf/hl-value data)]
+               (set-initial-page! (or
+                                   (:logseq.property.asset/last-visit-page pdf-block)
+                                   1))
+               (set-hls-state! {:initial-hls highlights :latest-hls highlights :loaded true})))))
+       [pdf-current])
       (rum/use-effect!
        (fn []
-         (when (= :completed (:status loader-state))
-           (p/catch
-            (when-not (:error hls-state)
-              (p/do! (persist-hls-data! (:latest-hls hls-state) (:extra hls-state))))
+         (p/catch
+          (p/let [data (pdf-assets/file-based-load-hls-data$ pdf-current)
+                  {:keys [highlights extra]} data]
+            (set-initial-page! (or (when-let [page (:page extra)]
+                                     (util/safe-parse-int page)) 1))
+            (set-hls-state! {:initial-hls highlights :latest-hls highlights :extra extra :loaded true}))
+
+          ;; error
+          (fn [^js e]
+            (js/console.error "[load hls error]" e)
+
+            (let [msg (str (util/format "Error: failed to load the highlights file: \"%s\". \n"
+                                        (:hls-file pdf-current))
+                           e)]
+              (notification/show! msg :error)
+              (set-hls-state! {:loaded true :error e}))))
+
+         ;; cancel
+         #())
+       [hls-file]))
+
+    ;; cache highlights
+    (when-not db-based?
+      (let [persist-hls-data!
+            (rum/use-callback
+             (util/debounce
+              4000 (fn [latest-hls extra]
+                     (pdf-assets/file-based-persist-hls-data$
+                      pdf-current latest-hls extra))) [pdf-current])]
+
+        (rum/use-effect!
+         (fn []
+           (when (= :completed (:status loader-state))
+             (p/catch
+              (when-not (:error hls-state)
+                (p/do! (persist-hls-data! (:latest-hls hls-state) (:extra hls-state))))
 
             ;; write hls file error
-            (fn [e]
-              (js/console.error "[write hls error]" e)))))
+              (fn [e]
+                (js/console.error "[write hls error]" e)))))
 
-       [(:latest-hls hls-state) (:extra hls-state)]))
+         [(:latest-hls hls-state) (:extra hls-state)])))
 
     ;; load document
     (rum/use-effect!
@@ -969,12 +1006,12 @@
            (do
              (set-loader-state! {:error nil})
              (shui/dialog-open!
-               (fn [{:keys [close]}]
-                 (let [on-password-fn
-                       (fn [password]
-                         (close)
-                         (set-doc-password! password))]
-                   (pdf-password-input on-password-fn)))))
+              (fn [{:keys [close]}]
+                (let [on-password-fn
+                      (fn [password]
+                        (close)
+                        (set-doc-password! password))]
+                  (pdf-password-input on-password-fn)))))
 
            (do
              (notification/show!
@@ -1064,9 +1101,9 @@
 
      (when (and (not system-win?) pdf-current)
        (js/ReactDOM.createPortal
-         (pdf-container-outer
-           (pdf-container pdf-current))
-         (js/document.querySelector "#app-single-container")))]))
+        (pdf-container-outer
+         (pdf-container pdf-current))
+        (js/document.querySelector "#app-single-container")))]))
 
 (rum/defcs system-embed-playground
   < rum/reactive

+ 13 - 26
src/main/frontend/extensions/pdf/pdf.css

@@ -76,25 +76,17 @@ input::-webkit-inner-spin-button {
 
       > .r {
         a.button {
-          user-select: none;
-          display: flex;
-          align-items: center;
-          margin-left: 8px;
-          margin-right: 8px;
-          padding: 4px 2px;
-          color: var(--ls-icon-color);
-          background-color: transparent;
-          transition: none;
-          word-break: normal;
+          @apply select-none flex items-center mx-2 py-1 px-0.5 bg-transparent
+          transition-none break-normal hover:opacity-80;
 
           &.is-active {
-            opacity: 1;
+            @apply opacity-100 rounded-none;
+
             border-bottom: 2px solid #969494;
-            border-radius: 0;
           }
 
           &:active {
-            opacity: .6;
+            @apply opacity-60;
           }
         }
       }
@@ -728,7 +720,7 @@ input::-webkit-inner-spin-button {
   }
 
   &[data-theme=light] {
-    background-color: #FFFFFF;
+    @apply bg-white;
   }
 
   &[data-theme=dark] {
@@ -768,23 +760,18 @@ input::-webkit-inner-spin-button {
 
     .extensions__pdf-toolbar {
       .buttons {
-        background-color: #f6efdf;
+        @apply bg-[#f6efdf];
       }
     }
   }
 }
 
-.asset-ref {
-  &.is-pdf {
-    &:before {
-      content: "[[📚";
-      opacity: .7;
-      margin-right: 4px;
-    }
-
-    &:after {
-      content: "]]";
-      opacity: .7;
+html[data-theme=dark] {
+  .extensions__pdf-container[data-theme=warm] {
+    .extensions__pdf-toolbar {
+      .buttons {
+        @apply bg-background;
+      }
     }
   }
 }

+ 55 - 57
src/main/frontend/extensions/pdf/toolbar.cljs

@@ -57,11 +57,11 @@
      [hl-block-colored?])
 
     (rum/use-effect!
-      (fn []
-        (let [b (boolean auto-open-ctx-menu?)]
-          (state/set-state! :pdf/auto-open-ctx-menu? b)
-          (storage/set "ls-pdf-auto-open-ctx-menu" b)))
-      [auto-open-ctx-menu?])
+     (fn []
+       (let [b (boolean auto-open-ctx-menu?)]
+         (state/set-state! :pdf/auto-open-ctx-menu? b)
+         (storage/set "ls-pdf-auto-open-ctx-menu" b)))
+     [auto-open-ctx-menu?])
 
     (rum/use-effect!
      (fn []
@@ -263,14 +263,14 @@
                    :small? true :on-click #(do (do-find! {:type :again :prev? true}) (util/stop %))})
 
        (ui/button
-         {:icon "chevron-down"
-          :intent "link"
-          :small? true :on-click #(do (do-find! {:type :again}) (util/stop %))})
+        {:icon "chevron-down"
+         :intent "link"
+         :small? true :on-click #(do (do-find! {:type :again}) (util/stop %))})
 
        (ui/button
-         {:icon "x"
-          :intent "link"
-          :small? true :on-click close-finder!})]
+        {:icon "x"
+         :intent "link"
+         :small? true :on-click close-finder!})]
 
       [:div.result-inner
        (when-let [status (and entered-active?
@@ -315,10 +315,10 @@
          (fn [idx itm]
            (let [parent (str parent "-items-" idx)]
              (rum/with-key
-              (pdf-outline-item
-               viewer
-               (merge itm {:parent parent})
-               ops) parent))) items)])]))
+               (pdf-outline-item
+                viewer
+                (merge itm {:parent parent})
+                ops) parent))) items)])]))
 
 (rum/defc pdf-outline
   [^js viewer _visible? set-visible!]
@@ -363,11 +363,11 @@
          [:section
           (map-indexed (fn [idx itm]
                          (rum/with-key
-                          (pdf-outline-item
-                           viewer
-                           (merge itm {:parent idx})
-                           {:upt-outline-node! upt-outline-node!})
-                          idx))
+                           (pdf-outline-item
+                            viewer
+                            (merge itm {:parent idx})
+                            {:upt-outline-node! upt-outline-node!})
+                           idx))
                        outline-data)]
          [:section.is-empty "No outlines"])])))
 
@@ -376,37 +376,36 @@
 
   (let [[active, set-active!] (rum/use-state false)]
     (rum/with-context
-     [hls-state *highlights-ctx*]
-     (let [hls (sort-by :page (or (seq (:initial-hls hls-state))
-                                  (:latest-hls hls-state)))]
-
-       (for [{:keys [id content properties page] :as hl} hls
-             :let [goto-ref! #(pdf-assets/goto-block-ref! hl)]]
-         [:div.extensions__pdf-highlights-list-item
-          {:key             id
-           :class           (when (= active id) "active")
-           :on-click        (fn []
-                              (pdf-utils/scroll-to-highlight viewer hl)
-                              (set-active! id))
-           :on-double-click goto-ref!}
-          [:h6.flex
-           [:span.flex.items-center
-            [:small {:data-color (:color properties)}]
-            [:strong "Page " page]]
-
-           [:button
-            {:title    (t :pdf/linked-ref)
-             :on-click goto-ref!}
-            (ui/icon "external-link")]]
-
-
-          (if-let [img-stamp (:image content)]
-            (let [fpath (pdf-assets/resolve-area-image-file
-                         img-stamp (state/get-current-pdf) hl)
-                  fpath (assets-handler/make-asset-url fpath)]
-              [:p.area-wrap
-               [:img {:src fpath}]])
-            [:p.text-wrap (:text content)])])))))
+      [hls-state *highlights-ctx*]
+      (let [hls (sort-by :page (or (seq (:initial-hls hls-state))
+                                   (:latest-hls hls-state)))]
+
+        (for [{:keys [id content properties page] :as hl} hls
+              :let [goto-ref! #(pdf-assets/goto-block-ref! hl)]]
+          [:div.extensions__pdf-highlights-list-item
+           {:key             id
+            :class           (when (= active id) "active")
+            :on-click        (fn []
+                               (pdf-utils/scroll-to-highlight viewer hl)
+                               (set-active! id))
+            :on-double-click goto-ref!}
+           [:h6.flex
+            [:span.flex.items-center
+             [:small {:data-color (:color properties)}]
+             [:strong "Page " page]]
+
+            [:button
+             {:title    (t :pdf/linked-ref)
+              :on-click goto-ref!}
+             (ui/icon "external-link")]]
+
+           (if-let [img-stamp (:image content)]
+             (let [fpath (pdf-assets/resolve-area-image-file
+                          img-stamp (state/get-current-pdf) hl)
+                   fpath (assets-handler/<make-asset-url fpath)]
+               [:p.area-wrap
+                [:img {:src fpath}]])
+             [:p.text-wrap (:text content)])])))))
 
 (rum/defc pdf-outline-&-highlights
   [^js viewer visible? set-visible!]
@@ -513,12 +512,11 @@
          (svg/adjustments 18)]
 
         ;; selection
-        (when (pdf-utils/support-area?)
-          [:a.button
-           {:title (str "Area highlight (" (if util/mac? "⌘" "Shift") ")")
-            :class (when area-mode? "is-active")
-            :on-click #(set-area-mode! (not area-mode?))}
-           (svg/icon-area 18)])
+        [:a.button
+         {:title (str "Area highlight (" (if util/mac? "⌘" "Shift") ")")
+          :class (when area-mode? "is-active")
+          :on-click #(set-area-mode! (not area-mode?))}
+         (svg/icon-area 18)]
 
         [:a.button
          {:title    "Highlight mode"

+ 16 - 23
src/main/frontend/extensions/pdf/utils.cljs

@@ -2,8 +2,6 @@
   (:require ["/frontend/extensions/pdf/utils" :as js-utils]
             [cljs-bean.core :as bean]
             [clojure.string :as string]
-            [frontend.config :as config]
-            [frontend.state :as state]
             [frontend.util :as util]
             [logseq.common.uuid :as common-uuid]
             [promesa.core :as p]))
@@ -16,11 +14,6 @@
   [filename]
   (and filename (string? filename) (string/starts-with? filename "hls__")))
 
-(defn support-area?
-  []
-  (and (util/electron?)
-    (not (config/db-based-graph? (state/get-current-repo)))))
-
 (defn get-bounding-rect
   [rects]
   (bean/->clj (js-utils/getBoundingRect (bean/->js rects))))
@@ -100,16 +93,16 @@
   [^js viewer]
   (when-let [^js doc (and viewer (.-pdfDocument viewer))]
     (p/create
-      (fn [resolve]
-        (p/catch
-          (p/then (.getMetadata doc)
-                  (fn [^js r]
-                    (js/console.debug "[metadata] " r)
-                    (when-let [^js info (and r (.-info r))]
-                      (resolve (bean/->clj info)))))
-          (fn [e]
-            (resolve nil)
-            (js/console.error e)))))))
+     (fn [resolve]
+       (p/catch
+        (p/then (.getMetadata doc)
+                (fn [^js r]
+                  (js/console.debug "[metadata] " r)
+                  (when-let [^js info (and r (.-info r))]
+                    (resolve (bean/->clj info)))))
+        (fn [e]
+          (resolve nil)
+          (js/console.error e)))))))
 
 (defn clear-all-selection
   []
@@ -117,7 +110,7 @@
 
 (def adjust-viewer-size!
   (util/debounce
-    200 (fn [^js viewer] (set! (. viewer -currentScaleValue) "auto"))))
+   200 (fn [^js viewer] (set! (. viewer -currentScaleValue) "auto"))))
 
 (defn fix-nested-js
   [its]
@@ -206,8 +199,8 @@
     (catch js/Error _e nil)))
 
 (comment
- (fix-selection-text-breakline "this is a\ntest paragraph")
- (fix-selection-text-breakline "he is 1\n8 years old")
- (fix-selection-text-breakline "这是一个\n\n段落")
- (fix-selection-text-breakline "これ\n\nは、段落")
- (fix-selection-text-breakline "this is a te-\nst paragraph"))
+  (fix-selection-text-breakline "this is a\ntest paragraph")
+  (fix-selection-text-breakline "he is 1\n8 years old")
+  (fix-selection-text-breakline "这是一个\n\n段落")
+  (fix-selection-text-breakline "これ\n\nは、段落")
+  (fix-selection-text-breakline "this is a te-\nst paragraph"))

+ 3 - 3
src/main/frontend/extensions/tldraw.cljs

@@ -77,12 +77,12 @@
                           (-> b
                               (update :block/uuid str)
                               (update :block/title #(->> (text-util/cut-by % "$pfts_2lqh>$" "$<pfts_2lqh$")
-                                                           (apply str))))) blocks)]
+                                                         (apply str))))) blocks)]
       (clj->js {:blocks blocks}))))
 
 (defn save-asset-handler
   [file]
-  (-> (editor-handler/save-assets! (state/get-current-repo) [(js->clj file)])
+  (-> (editor-handler/file-based-save-assets! (state/get-current-repo) [(js->clj file)])
       (p/then
        (fn [res]
          (when-let [[asset-file-name _ full-file-path] (and (seq res) (first res))]
@@ -132,7 +132,7 @@
                          (model/whiteboard-page? entity)))
    :isMobile util/mobile?
    :saveAsset save-asset-handler
-   :makeAssetUrl assets-handler/make-asset-url
+   :makeAssetUrl assets-handler/<make-asset-url
    :inflateAsset (fn [src] (clj->js (pdf-assets/inflate-asset src)))
    :setCurrentPdf (fn [src] (state/set-current-pdf! (if src (pdf-assets/inflate-asset src) nil)))
    :copyToClipboard (fn [text, html] (util/copy-to-clipboard! text :html html))

+ 2 - 2
src/main/frontend/extensions/zip.cljs

@@ -10,10 +10,10 @@
     (aset args "lastModified" last-modified)
     (js/File. blob-content file-name args)))
 
-(defn make-zip [zip-filename file-name->content _repo]
+(defn make-zip [zip-filename file-name-content _repo]
   (let [zip (JSZip.)
         folder (.folder zip zip-filename)]
-    (doseq [[file-name content] file-name->content]
+    (doseq [[file-name content] file-name-content]
       (when-not (string/blank? content)
         (.file folder (-> file-name
                           (string/replace #"^/+" ""))

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

@@ -125,6 +125,7 @@
   (state/set-component! :block/embed block/block-embed)
   (state/set-component! :block/page-cp block/page-cp)
   (state/set-component! :block/inline-text block/inline-text)
+  (state/set-component! :block/asset-cp block/asset-cp)
   (state/set-component! :editor/box editor/box)
   (command-palette/register-global-shortcut-commands))
 

+ 23 - 2
src/main/frontend/handler/assets.cljs

@@ -100,7 +100,6 @@
         (path/path-join "file://" (common-util/safe-decode-uri-component path))
         (path/path-join "file://" path))
 
-
       :else ;; relative path or alias path
       (resolve-asset-real-path-url (state/get-current-repo) path))))
 
@@ -153,7 +152,7 @@
 
 (defonce *assets-url-cache (atom {}))
 
-(defn make-asset-url
+(defn <make-asset-url
   "Make asset URL for UI element, to fill img.src"
   [path] ;; path start with "/assets"(editor) or compatible for "../assets"(whiteboards)
   (if config/publishing?
@@ -197,3 +196,25 @@
                 (p/let [url (js/URL.createObjectURL file)]
                   (swap! *assets-url-cache assoc (keyword handle-path) url)
                   url)))))))))
+
+(defn- decode-digest
+  [^js/Uint8Array digest]
+  (.. (js/Array.from digest)
+      (map (fn [s] (.. s (toString 16) (padStart 2 "0"))))
+      (join "")))
+
+(defn get-file-checksum
+  [^js/Blob file]
+  (-> (.arrayBuffer file)
+      (.then (fn [buf] (js/crypto.subtle.digest "SHA-256" buf)))
+      (.then (fn [dig] (js/Uint8Array. dig)))
+      (.then decode-digest)))
+
+(defn <get-all-assets
+  []
+  (when-let [path (config/get-current-repo-assets-root)]
+    (p/let [result (fs/readdir path {:path-only? true})]
+      (p/all (map (fn [path]
+                    (p/let [data (fs/read-file path "" {})]
+                      (let [path' (util/node-path.join "assets" (util/node-path.basename path))]
+                        [path' data]))) result)))))

+ 4 - 4
src/main/frontend/handler/db_based/page.cljs

@@ -48,11 +48,11 @@
   (if (db/page-exists? (:block/title page-entity) "class")
     (notification/show! (str "A tag with the name \"" (:block/title page-entity) "\" already exists.") :warning false)
     (let [class (db-class/build-new-class (db/get-db)
-                                         {:db/id (:db/id page-entity)
-                                          :block/title (:block/title page-entity)
-                                          :block/created-at (:block/created-at page-entity)})]
+                                          {:db/id (:db/id page-entity)
+                                           :block/title (:block/title page-entity)
+                                           :block/created-at (:block/created-at page-entity)})]
 
-     (db/transact! (state/get-current-repo) [class] {:outliner-op :save-block}))))
+      (db/transact! (state/get-current-repo) [class] {:outliner-op :save-block}))))
 
 (defn <create-class!
   "Creates a class page and provides class-specific error handling"

+ 191 - 74
src/main/frontend/handler/editor.cljs

@@ -1386,40 +1386,40 @@
   (p/let [[repo-dir assets-dir] (ensure-assets-dir! (state/get-current-repo))]
     (path/path-join repo-dir assets-dir filename)))
 
-(defn save-assets!
+(defn file-based-save-assets!
   "Save incoming(pasted) assets to assets directory.
 
    Returns: [file-rpath file-obj file-fpath matched-alias]"
   ([repo files]
    (p/let [[repo-dir assets-dir] (ensure-assets-dir! repo)]
-     (save-assets! repo repo-dir assets-dir files
-                   (fn [index file-stem]
+     (file-based-save-assets! repo repo-dir assets-dir files
+                              (fn [index file-stem]
                      ;; TODO: maybe there're other chars we need to handle?
-                     (let [file-base (-> file-stem
-                                         (string/replace " " "_")
-                                         (string/replace "%" "_")
-                                         (string/replace "/" "_"))
-                           file-name (str file-base "_" (.now js/Date) "_" index)]
-                       (string/replace file-name #"_+" "_"))))))
+                                (let [file-base (-> file-stem
+                                                    (string/replace " " "_")
+                                                    (string/replace "%" "_")
+                                                    (string/replace "/" "_"))
+                                      file-name (str file-base "_" (.now js/Date) "_" index)]
+                                  (string/replace file-name #"_+" "_"))))))
   ([repo repo-dir asset-dir-rpath files gen-filename]
    (p/all
     (for [[index ^js file] (map-indexed vector files)]
       ;; WARN file name maybe fully qualified path when paste file
-      (let [file-name (util/node-path.basename (.-name file))
-            [file-stem ext-full ext-base] (if file-name
-                                            (let [ext-base (util/node-path.extname file-name)
-                                                  ext-full (if-not (config/extname-of-supported? ext-base)
-                                                             (util/full-path-extname file-name) ext-base)]
-                                              [(subs file-name 0 (- (count file-name)
-                                                                    (count ext-full))) ext-full ext-base])
-                                            ["" "" ""])
-            filename  (str (gen-filename index file-stem) ext-full)
-            file-rpath  (str asset-dir-rpath "/" filename)
-            matched-alias (assets-handler/get-matched-alias-by-ext ext-base)
-            file-rpath (cond-> file-rpath
-                         (not (nil? matched-alias))
-                         (string/replace #"^[.\/\\]*assets[\/\\]+" ""))
-            dir (or (:dir matched-alias) repo-dir)]
+      (p/let [file-name (util/node-path.basename (.-name file))
+              [file-stem ext-full ext-base] (if file-name
+                                              (let [ext-base (util/node-path.extname file-name)
+                                                    ext-full (if-not (config/extname-of-supported? ext-base)
+                                                               (util/full-path-extname file-name) ext-base)]
+                                                [(subs file-name 0 (- (count file-name)
+                                                                      (count ext-full))) ext-full ext-base])
+                                              ["" "" ""])
+              filename  (str (gen-filename index file-stem) ext-full)
+              file-rpath  (str asset-dir-rpath "/" filename)
+              matched-alias (assets-handler/get-matched-alias-by-ext ext-base)
+              file-rpath (cond-> file-rpath
+                           (not (nil? matched-alias))
+                           (string/replace #"^[.\/\\]*assets[\/\\]+" ""))
+              dir (or (:dir matched-alias) repo-dir)]
         (if (util/electron?)
           (let [from (not-empty (.-path file))]
             (js/console.debug "Debug: Copy Asset #" dir file-rpath from)
@@ -1455,23 +1455,27 @@
                       (js/console.error error))))))))))
 
 (defn delete-asset-of-block!
-  [{:keys [repo href full-text block-id local? delete-local?] :as _opts}]
+  [{:keys [repo asset-block href full-text block-id local? delete-local?] :as _opts}]
   (let [block (db-model/query-block-by-uuid block-id)
-        _ (or block (throw (str block-id " not exists")))
+        _ (or block (throw (ex-info (str block-id " not exists")
+                                    {:block-id block-id})))
         text (:block/title block)
-        content (string/replace text full-text "")]
+        content (if asset-block
+                  (string/replace text (page-ref/->page-ref (:block/uuid asset-block)) "")
+                  (string/replace text full-text ""))]
     (save-block! repo block content)
     (when (and local? delete-local?)
-      (when-let [href (if (util/electron?) href
-                          (second (re-find #"\((.+)\)$" full-text)))]
-        (let [block-file-rpath (db-model/get-block-file-path block)
-              asset-fpath (if (string/starts-with? href "assets://")
-                            (path/url-to-path href)
-                            (config/get-repo-fpath
-                             repo
-                             (path/resolve-relative-path block-file-rpath href)))]
-          (prn ::deleting-asset href asset-fpath)
-          (fs/unlink! repo asset-fpath nil))))))
+      (if asset-block
+        (delete-block-aux! asset-block)
+        (when-let [href (if (util/electron?) href
+                            (second (re-find #"\((.+)\)$" full-text)))]
+          (let [block-file-rpath (db-model/get-block-file-path block)
+                asset-fpath (if (string/starts-with? href "assets://")
+                              (path/url-to-path href)
+                              (config/get-repo-fpath
+                               repo
+                               (path/resolve-relative-path block-file-rpath href)))]
+            (fs/unlink! repo asset-fpath nil)))))))
 
 ;; assets/journals_2021_02_03_1612350230540_0.png
 (defn resolve-relative-path
@@ -1489,39 +1493,146 @@
       (path/get-relative-path current-file-fpath file-path))
     file-path))
 
+(defn file-upload-assets!
+  "Paste asset and insert link to current editing block"
+  [repo id ^js files format uploading? drop-or-paste?]
+  (when (config/local-file-based-graph? repo)
+    (-> (file-based-save-assets! repo (js->clj files))
+          ;; FIXME: only the first asset is handled
+        (p/then
+         (fn [res]
+           (when-let [[asset-file-name file-obj asset-file-fpath matched-alias] (and (seq res) (first res))]
+             (let [image? (config/ext-of-image? asset-file-name)]
+               (insert-command!
+                id
+                (assets-handler/get-asset-file-link format
+                                                    (if matched-alias
+                                                      (str
+                                                       (if image? "../assets/" "")
+                                                       "@" (:name matched-alias) "/" asset-file-name)
+                                                      (resolve-relative-path (or asset-file-fpath asset-file-name)))
+                                                    (if file-obj (.-name file-obj) (if image? "image" "asset"))
+                                                    image?)
+                format
+                {:last-pattern (if drop-or-paste? "" commands/command-trigger)
+                 :restore?     true
+                 :command      :insert-asset})))))
+        (p/catch (fn [e]
+                   (js/console.error e)))
+        (p/finally
+          (fn []
+            (reset! uploading? false)
+            (reset! *asset-uploading? false)
+            (reset! *asset-uploading-process 0))))))
+
+(defn db-based-save-assets!
+  "Save incoming(pasted) assets to assets directory.
+
+   Returns: asset entity"
+  ([repo files]
+   (p/let [[repo-dir assets-dir] (ensure-assets-dir! repo)]
+     (db-based-save-assets! repo repo-dir assets-dir files)))
+  ([repo repo-dir asset-dir-rpath files]
+   (p/all
+    (for [[_index ^js file] (map-indexed vector files)]
+      ;; WARN file name maybe fully qualified path when paste file
+      (p/let [file-name (util/node-path.basename (.-name file))
+              file-name-without-ext (.-name (util/node-path.parse file-name))
+              checksum (assets-handler/get-file-checksum file)
+              existing-asset (db-async/<get-asset-with-checksum repo checksum)]
+        (if existing-asset
+          existing-asset
+          (p/let [block-id (ldb/new-block-id)
+                  ext (when file-name
+                        (string/lower-case (.substr (util/node-path.extname file-name) 1)))
+                  _ (when (string/blank? ext)
+                      (throw (ex-info "File doesn't have a valid ext."
+                                      {:file-name file-name})))
+                  file-path   (str block-id "." ext)
+                  file-rpath  (str asset-dir-rpath "/" file-path)
+                  dir repo-dir
+                  asset (db/entity :logseq.class/Asset)
+                  properties {:block/type "asset"
+                              :logseq.property.asset/type ext
+                              :logseq.property.asset/size (.-size file)
+                              :logseq.property.asset/checksum checksum
+                              :block/tags (:db/id asset)}
+                  insert-opts {:custom-uuid block-id
+                               :edit-block? false
+                               :properties properties}
+                  edit-block (state/get-edit-block)
+                  insert-opts' (if (and (:block/uuid edit-block)
+                                        (string/blank? (:block/title edit-block)))
+                                 (assoc insert-opts
+                                        :block-uuid (:block/uuid edit-block)
+                                        :replace-empty-target? true
+                                        :sibling? true)
+                                 (assoc insert-opts :page (:block/uuid asset)))
+                  result (api-insert-new-block! file-name-without-ext insert-opts')
+                  new-entity (db/entity [:block/uuid (:block/uuid result)])]
+            (if (util/electron?)
+              (let [from (not-empty (.-path file))]
+                (js/console.debug "Debug: Copy Asset #" dir file-rpath from)
+                (-> (js/window.apis.copyFileToAssets dir file-rpath from)
+                    (p/then
+                     (fn [_dest]
+                       new-entity))
+                    (p/catch #(js/console.error "Debug: Copy Asset Error#" %))))
+              (->
+               (p/do! (js/console.debug "Debug: Writing Asset #" dir file-rpath)
+                      (cond
+                        (mobile-util/native-platform?)
+                        ;; capacitor fs accepts Blob, File implements Blob
+                        (p/let [buffer (.arrayBuffer file)
+                                content (base64/encodeByteArray (js/Uint8Array. buffer))
+                                fpath (path/path-join dir file-rpath)]
+                          (capacitor-fs/<write-file-with-base64 fpath content))
+
+                        (config/db-based-graph? repo) ;; memory-fs
+                        (p/let [buffer (.arrayBuffer file)
+                                content (js/Uint8Array. buffer)]
+                          (fs/write-file! repo dir file-rpath content nil))
+
+                        :else
+                        (throw (ex-info "Paste failed"
+                                        {:file-name file-name})))
+                      new-entity)
+               (p/catch (fn [error]
+                          (prn :paste-file-error)
+                          (js/console.error error))))))))))))
+
+(defn db-upload-assets!
+  "Paste asset and insert link to current editing block"
+  [repo id ^js files format uploading? drop-or-paste?]
+  (when (or (config/local-file-based-graph? repo)
+            (config/db-based-graph? repo))
+    (-> (db-based-save-assets! repo (js->clj files))
+          ;; FIXME: only the first asset is handled
+        (p/then
+         (fn [entities]
+           (let [entity (first entities)]
+             (insert-command!
+              id
+              (page-ref/->page-ref (:block/uuid entity))
+              format
+              {:last-pattern (if drop-or-paste? "" commands/command-trigger)
+               :restore?     true
+               :command      :insert-asset}))))
+        (p/catch (fn [e]
+                   (js/console.error e)))
+        (p/finally
+          (fn []
+            (reset! uploading? false)
+            (reset! *asset-uploading? false)
+            (reset! *asset-uploading-process 0))))))
+
 (defn upload-asset!
   "Paste asset and insert link to current editing block"
   [id ^js files format uploading? drop-or-paste?]
   (let [repo (state/get-current-repo)]
-    (when (or (config/local-file-based-graph? repo)
-              (config/db-based-graph? repo))
-      (-> (save-assets! repo (js->clj files))
-          ;; FIXME: only the first asset is handled
-          (p/then
-           (fn [res]
-             (when-let [[asset-file-name file-obj asset-file-fpath matched-alias] (and (seq res) (first res))]
-               (let [image? (config/ext-of-image? asset-file-name)]
-                 (insert-command!
-                  id
-                  (assets-handler/get-asset-file-link format
-                                                      (if matched-alias
-                                                        (str
-                                                         (if image? "../assets/" "")
-                                                         "@" (:name matched-alias) "/" asset-file-name)
-                                                        (resolve-relative-path (or asset-file-fpath asset-file-name)))
-                                                      (if file-obj (.-name file-obj) (if image? "image" "asset"))
-                                                      image?)
-                  format
-                  {:last-pattern (if drop-or-paste? "" commands/command-trigger)
-                   :restore?     true
-                   :command      :insert-asset})))))
-          (p/catch (fn [e]
-                     (js/console.error e)))
-          (p/finally
-            (fn []
-              (reset! uploading? false)
-              (reset! *asset-uploading? false)
-              (reset! *asset-uploading-process 0)))))))
+    (if (config/db-based-graph? repo)
+      (db-upload-assets! repo id ^js files format uploading? drop-or-paste?)
+      (file-upload-assets! repo id ^js files format uploading? drop-or-paste?))))
 
 ;; Editor should track some useful information, like editor modes.
 ;; For example:
@@ -1797,14 +1908,20 @@
     (state/clear-editor-action!)))
 
 (defn resize-image!
-  [block-id metadata full_text size]
-  (let [new-meta (merge metadata size)
-        image-part (first (string/split full_text #"\{"))
-        new-full-text (str image-part (pr-str new-meta))
-        block (db/entity [:block/uuid block-id])
-        value (:block/title block)
-        new-value (string/replace value full_text new-full-text)]
-    (save-block-aux! block new-value {})))
+  [config block-id metadata full_text size]
+  (let [asset (:asset-block config)]
+    (if (and asset (config/db-based-graph?))
+      (property-handler/set-block-property! (state/get-current-repo)
+                                            (:db/id asset)
+                                            :logseq.property.asset/resize-metadata
+                                            size)
+      (let [new-meta (merge metadata size)
+            image-part (first (string/split full_text #"\{"))
+            new-full-text (str image-part (pr-str new-meta))
+            block (db/entity [:block/uuid block-id])
+            value (:block/title block)
+            new-value (string/replace value full_text new-full-text)]
+        (save-block-aux! block new-value {})))))
 
 (defonce *auto-save-timeout (atom nil))
 (defn edit-box-on-change!

+ 26 - 8
src/main/frontend/handler/export.cljs

@@ -20,6 +20,7 @@
    [frontend.persist-db :as persist-db]
    [cljs-bean.core :as bean]
    [frontend.handler.export.common :as export-common-handler]
+   [frontend.handler.assets :as assets-handler]
    [logseq.db.sqlite.common-db :as sqlite-common-db]
    [logseq.db :as ldb]
    [frontend.idb :as idb]
@@ -52,7 +53,20 @@
           (.setAttribute anchor "download" "index.html")
           (.click anchor))))))
 
-(defn export-repo-as-zip!
+(defn db-based-export-repo-as-zip!
+  [repo]
+  (p/let [db-data (persist-db/<export-db repo {:return-data? true})
+          filename "db.sqlite"
+          repo-name (sqlite-common-db/sanitize-db-name repo)
+          assets (assets-handler/<get-all-assets)
+          files (cons [filename db-data] assets)
+          zipfile (zip/make-zip repo-name files repo)]
+    (when-let [anchor (gdom/getElement "download-as-zip")]
+      (.setAttribute anchor "href" (js/window.URL.createObjectURL zipfile))
+      (.setAttribute anchor "download" (.-name zipfile))
+      (.click anchor))))
+
+(defn file-based-export-repo-as-zip!
   [repo]
   (p/let [files (export-common-handler/<get-file-contents repo "md")
           [owner repo-name] (util/get-git-owner-and-repo repo)
@@ -60,11 +74,17 @@
           files (map (fn [{:keys [path content]}] [path content]) files)]
     (when (seq files)
       (p/let [zipfile (zip/make-zip repo-name files repo)]
-        (when-let [anchor (gdom/getElement "download")]
+        (when-let [anchor (gdom/getElement "download-as-zip")]
           (.setAttribute anchor "href" (js/window.URL.createObjectURL zipfile))
           (.setAttribute anchor "download" (.-name zipfile))
           (.click anchor))))))
 
+(defn export-repo-as-zip!
+  [repo]
+  (if (config/db-based-graph? repo)
+    (db-based-export-repo-as-zip! repo)
+    (file-based-export-repo-as-zip! repo)))
+
 (defn- export-file-on-mobile [data path]
   (p/catch
    (.writeFile Filesystem (clj->js {:path path
@@ -76,7 +96,6 @@
       (notification/show! "Export failed!" :error)
       (log/error :export-file-failed error))))
 
-
 ;; FIXME: All uses of :block/properties in this ns
 (defn- dissoc-properties [m ks]
   (if (:block/properties m)
@@ -189,11 +208,10 @@
   (p/let [data (persist-db/<export-db repo {:return-data? true})
           filename (file-name repo "sqlite")
           url (js/URL.createObjectURL (js/Blob. #js [data]))]
-    (when-not (mobile-util/native-platform?)
-      (when-let [anchor (gdom/getElement "download-as-sqlite-db")]
-        (.setAttribute anchor "href" url)
-        (.setAttribute anchor "download" filename)
-        (.click anchor)))))
+    (when-let [anchor (gdom/getElement "download-as-sqlite-db")]
+      (.setAttribute anchor "href" url)
+      (.setAttribute anchor "download" filename)
+      (.click anchor))))
 
 ;;;;;;;;;;;;;;;;;;;;;;;;;
 ;; Export to roam json ;;

+ 1 - 0
src/main/frontend/publishing.cljs

@@ -100,6 +100,7 @@
   (state/set-component! :block/embed block/block-embed)
   (state/set-component! :block/page-cp block/page-cp)
   (state/set-component! :block/inline-text block/inline-text)
+  (state/set-component! :block/asset-cp block/asset-cp)
   (state/set-component! :editor/box editor/box)
   (command-palette/register-global-shortcut-commands))
 

+ 32 - 5
src/main/frontend/worker/db/migrate.cljs

@@ -10,7 +10,10 @@
             [cljs-bean.core :as bean]
             [logseq.db.sqlite.util :as sqlite-util]
             [logseq.common.config :as common-config]
-            [logseq.common.util :as common-util]))
+            [logseq.common.util :as common-util]
+            [logseq.db.frontend.property.build :as db-property-build]
+            [logseq.db.frontend.order :as db-order]
+            [logseq.common.uuid :as common-uuid]))
 
 ;; TODO: fixes/rollback
 
@@ -231,6 +234,23 @@
             query-id (:db/id query)]
         [[:db/add query-id :logseq.property.class/properties :logseq.property/query]]))))
 
+(defn- add-card-view
+  [conn _search-db]
+  (let [db @conn]
+    (when (ldb/db-based-graph? db)
+      (let [ident :logseq.property.view/type.card
+            uuid' (common-uuid/gen-uuid :db-ident-block-uuid ident)
+            property (d/entity db :logseq.property.view/type)
+            m (cond->
+               (db-property-build/build-closed-value-block
+                uuid'
+                "Card view"
+                property
+                {:db-ident :logseq.property.view/type.card})
+                true
+                (assoc :block/order (db-order/gen-key)))]
+        [m]))))
+
 (defn- add-addresses-in-kvs-table
   [^Object sqlite-db]
   (let [columns (->> (.exec sqlite-db #js {:sql "SELECT NAME FROM PRAGMA_TABLE_INFO('kvs')"
@@ -301,7 +321,14 @@
    [26 {:properties [:logseq.property.node/type]}]
    [27 {:properties [:logseq.property.code/mode]}]
    [28 {:fix (rename-properties {:logseq.property.node/type :logseq.property.node/display-type})}]
-   [29 {:properties [:logseq.property.code/lang]}]])
+   [29 {:properties [:logseq.property.code/lang]}]
+   [30 {:classes [:logseq.class/Asset]
+        :properties [:logseq.property.asset/type :logseq.property.asset/size :logseq.property.asset/checksum]}]
+   [31 {:properties [:logseq.property/asset]}]
+   [32 {:properties [:logseq.property.asset/last-visit-page]}]
+   [33 {:properties [:logseq.property.pdf/hl-image]}]
+   [34 {:properties [:logseq.property.asset/resize-metadata]}]
+   [35 {:fix add-card-view}]])
 
 (let [max-schema-version (apply max (map first schema-version->updates))]
   (assert (<= db-schema/version max-schema-version))
@@ -333,7 +360,7 @@
                             schema-version->updates)
               properties (mapcat :properties updates)
               new-properties (->> (select-keys db-property/built-in-properties properties)
-                                ;; property already exists, this should never happen
+                                  ;; property already exists, this should never happen
                                   (remove (fn [[k _]]
                                             (when (d/entity db k)
                                               (assert (str "DB migration: property already exists " k)))))
@@ -342,12 +369,12 @@
                                   (map (fn [b] (assoc b :logseq.property/built-in? true))))
               classes (mapcat :classes updates)
               new-classes (->> (select-keys db-class/built-in-classes classes)
-                             ;; class already exists, this should never happen
+                               ;; class already exists, this should never happen
                                (remove (fn [[k _]]
                                          (when (d/entity db k)
                                            (assert (str "DB migration: class already exists " k)))))
                                (into {})
-                               (#(sqlite-create-graph/build-initial-classes* % {}))
+                               (#(sqlite-create-graph/build-initial-classes* % (zipmap properties properties)))
                                (map (fn [b] (assoc b :logseq.property/built-in? true))))
               fixes (mapcat
                      (fn [update']

+ 1 - 1
src/main/logseq/sdk/assets.cljs

@@ -7,7 +7,7 @@
             [frontend.util :as util]
             [promesa.core :as p]))
 
-(def ^:export make_url assets-handler/make-asset-url)
+(def ^:export make_url assets-handler/<make-asset-url)
 
 (defn ^:export list_files_of_current_graph
   [^js exts]

+ 2 - 1
src/resources/dicts/en.edn

@@ -458,7 +458,8 @@
  :export-opml "Export as OPML"
  :export-public-pages "Export public pages"
  :export-json "Export as JSON"
- :export-sqlite-db "Export as SQLite DB"
+ :export-sqlite-db "Export SQLite DB"
+ :export-zip "Export both SQLite DB and assets"
  :export-roam-json "Export as Roam JSON"
  :export-edn "Export as EDN"
  :export-transparent-background "Transparent background"

+ 1 - 1
src/test/frontend/db/db_based_model_test.cljs

@@ -24,7 +24,7 @@
   (let [opts {:redirect? false :create-first-block? false :class? true}
         _ (test-helper/create-page! "class1" opts)
         _ (test-helper/create-page! "class2" opts)]
-    (is (= ["Card" "Cards" "Journal" "Query" "Root Tag" "Task" "class1" "class2"] (sort (map :block/title (model/get-all-classes repo)))))))
+    (is (= ["Asset" "Card" "Cards" "Journal" "Query" "Root Tag" "Task" "class1" "class2"] (sort (map :block/title (model/get-all-classes repo)))))))
 
 (deftest ^:fix-me get-class-objects-test
   (let [opts {:redirect? false :create-first-block? false :class? true}