Browse Source

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

Tienson Qin 1 year ago
parent
commit
03b3291486
92 changed files with 12165 additions and 1112 deletions
  1. 0 7
      README.md
  2. 1 1
      deps/db/script/create_graph.cljs
  3. 1 3
      deps/db/src/logseq/db.cljs
  4. 3 3
      deps/db/src/logseq/db/frontend/class.cljs
  5. 18 16
      deps/db/src/logseq/db/frontend/property.cljs
  6. 1 1
      deps/db/src/logseq/db/frontend/schema.cljs
  7. 11 8
      deps/db/src/logseq/db/sqlite/create_graph.cljs
  8. 7 0
      deps/db/src/logseq/db/sqlite/util.cljs
  9. 5 2
      deps/graph-parser/src/logseq/graph_parser/block.cljs
  10. 191 98
      deps/graph-parser/src/logseq/graph_parser/exporter.cljs
  11. 2 2
      deps/graph-parser/src/logseq/graph_parser/extract.cljc
  12. 44 6
      deps/graph-parser/test/logseq/graph_parser/exporter_test.cljs
  13. 5 0
      deps/graph-parser/test/resources/exporter-test-graph/journals/2024_10_17.md
  14. 4 0
      deps/graph-parser/test/resources/exporter-test-graph/pages/n1___x___y.md
  15. 11 10
      deps/outliner/src/logseq/outliner/cli.cljs
  16. 16 9
      deps/outliner/src/logseq/outliner/property.cljs
  17. 1 1
      deps/publishing/README.md
  18. 1 0
      deps/publishing/package.json
  19. 25 9
      deps/publishing/script/publishing.cljs
  20. 37 16
      deps/publishing/src/logseq/publishing/db.cljs
  21. 3 3
      deps/publishing/src/logseq/publishing/export.cljs
  22. 6 5
      deps/publishing/src/logseq/publishing/html.cljs
  23. 228 1
      deps/publishing/yarn.lock
  24. 1 1
      docs/contributing-to-translations.md
  25. 1 0
      externs.js
  26. 7784 0
      resources/js/pdf_viewer2.js
  27. 2 2
      scripts/src/logseq/tasks/dev.clj
  28. 23 5
      scripts/src/logseq/tasks/lang.clj
  29. 24 6
      src/main/frontend/common.css
  30. 3 3
      src/main/frontend/common/missionary_util.clj
  31. 40 32
      src/main/frontend/common/missionary_util.cljs
  32. 1 4
      src/main/frontend/components/all_pages.cljs
  33. 137 88
      src/main/frontend/components/block.cljs
  34. 40 11
      src/main/frontend/components/block.css
  35. 13 3
      src/main/frontend/components/block/views.css
  36. 10 0
      src/main/frontend/components/container.cljs
  37. 5 17
      src/main/frontend/components/export.cljs
  38. 15 7
      src/main/frontend/components/objects.cljs
  39. 30 19
      src/main/frontend/components/page.cljs
  40. 32 25
      src/main/frontend/components/property.cljs
  41. 25 6
      src/main/frontend/components/property.css
  42. 13 7
      src/main/frontend/components/property/value.cljs
  43. 1 1
      src/main/frontend/components/query.cljs
  44. 78 72
      src/main/frontend/components/repo.cljs
  45. 7 8
      src/main/frontend/components/right_sidebar.cljs
  46. 87 46
      src/main/frontend/components/views.cljs
  47. 4 3
      src/main/frontend/db/async.cljs
  48. 33 26
      src/main/frontend/db/async/util.cljs
  49. 75 1
      src/main/frontend/db/rtc/debug_ui.cljs
  50. 6 2
      src/main/frontend/dicts.cljc
  51. 9 6
      src/main/frontend/extensions/pdf/assets.cljs
  52. 1 1
      src/main/frontend/extensions/pdf/core.cljs
  53. 1 1
      src/main/frontend/extensions/pdf/utils.cljs
  54. 9 6
      src/main/frontend/extensions/pdf/windows.cljs
  55. 11 10
      src/main/frontend/handler/editor.cljs
  56. 4 0
      src/main/frontend/handler/events.cljs
  57. 2 1
      src/main/frontend/handler/export.cljs
  58. 19 18
      src/main/frontend/handler/import.cljs
  59. 8 9
      src/main/frontend/handler/page.cljs
  60. 7 6
      src/main/frontend/handler/paste.cljs
  61. 19 17
      src/main/frontend/handler/repo.cljs
  62. 3 1
      src/main/frontend/handler/route.cljs
  63. 38 11
      src/main/frontend/handler/ui.cljs
  64. 3 0
      src/main/frontend/handler/worker.cljs
  65. 7 0
      src/main/frontend/modules/outliner/pipeline.cljs
  66. 17 21
      src/main/frontend/publishing.cljs
  67. 1 1
      src/main/frontend/routes.cljs
  68. 30 25
      src/main/frontend/state.cljs
  69. 3 5
      src/main/frontend/ui.cljs
  70. 7 9
      src/main/frontend/ui.css
  71. 124 0
      src/main/frontend/worker/crypt.cljs
  72. 47 38
      src/main/frontend/worker/db/migrate.cljs
  73. 99 17
      src/main/frontend/worker/db_worker.cljs
  74. 208 0
      src/main/frontend/worker/device.cljs
  75. 10 3
      src/main/frontend/worker/handler/page/db_based/page.cljs
  76. 3 1
      src/main/frontend/worker/pipeline.cljs
  77. 26 12
      src/main/frontend/worker/rtc/client_op.cljs
  78. 41 1
      src/main/frontend/worker/rtc/const.cljs
  79. 143 162
      src/main/frontend/worker/rtc/core.cljs
  80. 96 94
      src/main/frontend/worker/rtc/full_upload_download_graph.cljs
  81. 44 22
      src/main/frontend/worker/rtc/ws_util.cljs
  82. 1 1
      src/main/frontend/worker/search.cljs
  83. 2 3
      src/main/frontend/worker/state.cljs
  84. 720 0
      src/resources/dicts/ca.edn
  85. 735 0
      src/resources/dicts/cs.edn
  86. 25 2
      src/resources/dicts/ja.edn
  87. 449 11
      src/resources/dicts/pl.edn
  88. 1 1
      src/resources/dicts/tr.edn
  89. 14 0
      src/resources/tutorials/dummy-notes-ca.md
  90. 14 0
      src/resources/tutorials/dummy-notes-cs.md
  91. 26 0
      src/resources/tutorials/tutorial-ca.md
  92. 27 0
      src/resources/tutorials/tutorial-cs.md

+ 0 - 7
README.md

@@ -165,13 +165,6 @@ We want to express our sincere gratitude to our [Open Collective](https://openco
 <p align="center">
     <a href="https://opencollective.com/logseq#sponsor"> [Become a sponsor]</a>
 </p>
-<!-- Deta Logo -->
-<p align="center">
-    <a href="https://www.deta.sh/" alt="Deta">
-        <img src="https://uploads-ssl.webflow.com/5eb96efa78dc680fc15be3be/5ebd24f6cbf6e9ebd674656e_Logo.svg"
-        style="width: 200px; height: 100px;"/></a>
-</p>
-
 <p align="center">
     <a href="https://opencollective.com/logseq" alt="Sponsors on Open Collective">
         <img src="https://opencollective.com/logseq/tiers/sponsors.svg?avatarHeight=42&width=600"/></a>

+ 1 - 1
deps/db/script/create_graph.cljs

@@ -27,7 +27,7 @@
                         [(node-path/join (os/homedir) "logseq" "graphs") graph-dir])
         sqlite-build-edn (merge {:auto-create-ontology? true}
                                 (-> (resolve-path edn-path) fs/readFileSync str edn/read-string))
-        conn (outliner-cli/init-conn dir db-name {:classpath (cp/get-classpath)})
+        conn (outliner-cli/init-conn dir db-name {:classpath (cp/get-classpath) :import-type :cli/create-graph})
         {:keys [init-tx block-props-tx]} (outliner-cli/build-blocks-tx sqlite-build-edn)]
     (println "Generating" (count (filter :block/name init-tx)) "pages and"
              (count (filter :block/title init-tx)) "blocks ...")

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

@@ -449,9 +449,7 @@
    (d/datoms db :avet :block/name)
    (keep (fn [d]
            (let [e (d/entity db (:e d))]
-             (when (and (page? e)
-                        (not (hidden? e))
-                        (not (common-util/uuid-string? (:block/name e))))
+             (when-not (hidden? e)
                e))))))
 
 (defn built-in?

+ 3 - 3
deps/db/src/logseq/db/frontend/class.cljs

@@ -57,10 +57,10 @@
    :logseq.class/Pdf-annotation
    {:title "PDF Annotation"
     :properties {:logseq.property.class/hide-from-node true}
-    :schema {:properties [:logseq.property/ls-type :logseq.property/hl-color :logseq.property/asset
+    :schema {:properties [:logseq.property/ls-type :logseq.property.pdf/hl-color :logseq.property/asset
                           :logseq.property.pdf/hl-page :logseq.property.pdf/hl-value
-                          :logseq.property/hl-type :logseq.property.pdf/hl-image]
-             :required-properties [:logseq.property/ls-type :logseq.property/hl-color :logseq.property/asset
+                          :logseq.property.pdf/hl-type :logseq.property.pdf/hl-image]
+             :required-properties [:logseq.property/ls-type :logseq.property.pdf/hl-color :logseq.property/asset
                                    :logseq.property.pdf/hl-page :logseq.property.pdf/hl-value]}}
 
 ;; TODO: Add more classes such as :book, :paper, :movie, :music, :project)

+ 18 - 16
deps/db/src/logseq/db/frontend/property.cljs

@@ -169,10 +169,12 @@
    :logseq.property/asset   {:title "Asset"
                              :schema {:type :entity
                                       :hide? true}}
+   ;; used by pdf and whiteboard
    :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-type {:schema {:type :keyword :hide? true}}
+   :logseq.property.pdf/hl-color {:schema {:type :default :hide? true}}
    :logseq.property.pdf/hl-page {: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}}
@@ -214,8 +216,8 @@
    ;;                                :public? false}}
 
    ;; Task props
-   :logseq.task/status
-   {:title "Status"
+   :logseq.task/priority
+   {:title "Priority"
     :schema
     {:type :default
      :public? true
@@ -226,14 +228,12 @@
              :value value
              :uuid (common-uuid/gen-uuid :db-ident-block-uuid db-ident)
              :icon {:type :tabler-icon :id icon}})
-          [[:logseq.task/status.backlog "Backlog" "Backlog"]
-           [:logseq.task/status.todo "Todo" "Todo"]
-           [:logseq.task/status.doing "Doing" "InProgress50"]
-           [:logseq.task/status.in-review "In Review" "InReview"]
-           [:logseq.task/status.done "Done" "Done"]
-           [:logseq.task/status.canceled "Canceled" "Cancelled"]])}
-   :logseq.task/priority
-   {:title "Priority"
+          [[:logseq.task/priority.low "Low" "priorityLvlLow"]
+           [:logseq.task/priority.medium "Medium" "priorityLvlMedium"]
+           [:logseq.task/priority.high "High" "priorityLvlHigh"]
+           [:logseq.task/priority.urgent "Urgent" "priorityLvlUrgent"]])}
+   :logseq.task/status
+   {:title "Status"
     :schema
     {:type :default
      :public? true
@@ -244,10 +244,12 @@
              :value value
              :uuid (common-uuid/gen-uuid :db-ident-block-uuid db-ident)
              :icon {:type :tabler-icon :id icon}})
-          [[:logseq.task/priority.low "Low" "priorityLvlLow"]
-           [:logseq.task/priority.medium "Medium" "priorityLvlMedium"]
-           [:logseq.task/priority.high "High" "priorityLvlHigh"]
-           [:logseq.task/priority.urgent "Urgent" "priorityLvlUrgent"]])}
+          [[:logseq.task/status.backlog "Backlog" "Backlog"]
+           [:logseq.task/status.todo "Todo" "Todo"]
+           [:logseq.task/status.doing "Doing" "InProgress50"]
+           [:logseq.task/status.in-review "In Review" "InReview"]
+           [:logseq.task/status.done "Done" "Done"]
+           [:logseq.task/status.canceled "Canceled" "Cancelled"]])}
    :logseq.task/deadline
    {:title "Deadline"
     :schema {:type :date

+ 1 - 1
deps/db/src/logseq/db/frontend/schema.cljs

@@ -2,7 +2,7 @@
   "Main datascript schemas for the Logseq app"
   (:require [clojure.set :as set]))
 
-(def version 42)
+(def version 43)
 
 ;; A page is a special block, a page can corresponds to multiple files with the same ":block/name".
 (def ^:large-vars/data-var schema

+ 11 - 8
deps/db/src/logseq/db/sqlite/create_graph.cljs

@@ -103,14 +103,17 @@
 (defn build-db-initial-data
   "Builds tx of initial data for a new graph including key values, initial files,
    built-in properties and built-in classes"
-  [config-content]
-  (let [initial-data [(sqlite-util/kv :logseq.kv/db-type "db")
-                      (sqlite-util/kv :logseq.kv/schema-version db-schema/version)
-                      (sqlite-util/kv :logseq.kv/graph-initial-schema-version db-schema/version)
-                      (sqlite-util/kv :logseq.kv/graph-created-at (common-util/time-ms))
-                      ;; Empty property value used by db.type/ref properties
-                      {:db/ident :logseq.property/empty-placeholder}
-                      {:db/ident :logseq.class/Root}]
+  [config-content & {:keys [import-type]}]
+  (let [initial-data (cond->
+                      [(sqlite-util/kv :logseq.kv/db-type "db")
+                       (sqlite-util/kv :logseq.kv/schema-version db-schema/version)
+                       (sqlite-util/kv :logseq.kv/graph-initial-schema-version db-schema/version)
+                       (sqlite-util/kv :logseq.kv/graph-created-at (common-util/time-ms))
+                       ;; Empty property value used by db.type/ref properties
+                       {:db/ident :logseq.property/empty-placeholder}
+                       {:db/ident :logseq.class/Root}]
+                       import-type
+                       (into (sqlite-util/import-tx import-type)))
         initial-files [{:block/uuid (d/squuid)
                         :file/path (str "logseq/" "config.edn")
                         :file/content config-content

+ 7 - 0
deps/db/src/logseq/db/sqlite/util.cljs

@@ -136,3 +136,10 @@
   {:pre [(= "logseq.kv" (namespace k))]}
   {:db/ident k
    :kv/value value})
+
+(defn import-tx
+  "Creates tx for an import given an import-type"
+  [import-type]
+  [(kv :logseq.kv/import-type import-type)
+   ;; Timestamp is useful as this can occur much later than :logseq.kv/graph-created-at
+   (kv :logseq.kv/imported-at (common-util/time-ms))])

+ 5 - 2
deps/graph-parser/src/logseq/graph_parser/block.cljs

@@ -298,13 +298,16 @@
 
 (def convert-page-if-journal (memoize convert-page-if-journal-impl))
 
+;; Hack to detect export as some fns are too deeply nested to be refactored to get explicit option
+(def *export-to-db-graph? (atom false))
+
 (defn- page-name-string->map
   [original-page-name db date-formatter
    {:keys [with-timestamp? page-uuid from-page class? skip-existing-page-check?]}]
   (let [db-based? (ldb/db-based-graph? db)
         original-page-name (common-util/remove-boundary-slashes original-page-name)
         [original-page-name' page-name journal-day] (convert-page-if-journal original-page-name date-formatter)
-        namespace? (and (not db-based?)
+        namespace? (and (or (not db-based?) @*export-to-db-graph?)
                         (not (boolean (text/get-nested-page-name original-page-name')))
                         (text/namespace-page? original-page-name'))
         page-entity (when (and db (not skip-existing-page-check?))
@@ -433,7 +436,7 @@
 
 (defn- with-page-refs-and-tags
   [{:keys [title body tags refs marker priority] :as block} db date-formatter parse-block]
-  (let [db-based? (ldb/db-based-graph? db)
+  (let [db-based? (and (ldb/db-based-graph? db) (not *export-to-db-graph?))
         refs (->> (concat tags refs (when-not db-based? [marker priority]))
                   (remove string/blank?)
                   (distinct))

+ 191 - 98
deps/graph-parser/src/logseq/graph_parser/exporter.cljs

@@ -27,7 +27,8 @@
             [logseq.db.frontend.property.build :as db-property-build]
             [logseq.db.frontend.malli-schema :as db-malli-schema]
             [logseq.graph-parser.property :as gp-property]
-            [logseq.graph-parser.block :as gp-block]))
+            [logseq.graph-parser.block :as gp-block]
+            [logseq.common.util.namespace :as ns-util]))
 
 (defn- add-missing-timestamps
   "Add updated-at or created-at timestamps if they doesn't exist"
@@ -49,17 +50,33 @@
       (swap! all-idents assoc (keyword class-name) (:db/ident m))
       m)))
 
+(defn- get-page-uuid [page-names-to-uuids page-name]
+  (or (get @page-names-to-uuids (if (string/includes? (str page-name) "#")
+                                  (string/lower-case (gp-block/sanitize-hashtag-name page-name))
+                                  page-name))
+      (throw (ex-info (str "No uuid found for page name " (pr-str page-name))
+                      {:page-name page-name}))))
+
+(defn- find-or-gen-class-uuid [page-names-to-uuids page-name db-ident]
+  (or (get @page-names-to-uuids page-name)
+      (let [new-uuid (common-uuid/gen-uuid :db-ident-block-uuid db-ident)]
+        (swap! page-names-to-uuids assoc page-name new-uuid)
+        new-uuid)))
+
+(defn- convert-tag? [tag-name {:keys [convert-all-tags? tag-classes]}]
+  (or convert-all-tags?
+      (contains? tag-classes tag-name)))
+
 (defn- convert-tag-to-class
   "Converts a tag block with class or returns nil if this tag should be removed
    because it has been moved"
-  [db tag-block page-names-to-uuids tag-classes all-idents]
+  [db tag-block page-names-to-uuids user-options all-idents]
   (if-let [new-class (:block.temp/new-class tag-block)]
     (let [class-m (find-or-create-class db new-class all-idents)]
       (merge class-m
-             (if-let [existing-tag-uuid (get page-names-to-uuids (common-util/page-name-sanity-lc new-class))]
-               {:block/uuid existing-tag-uuid}
-               {:block/uuid (common-uuid/gen-uuid :db-ident-block-uuid (:db/ident class-m))})))
-    (when (contains? tag-classes (:block/name tag-block))
+             {:block/uuid
+              (find-or-gen-class-uuid page-names-to-uuids (common-util/page-name-sanity-lc new-class) (:db/ident class-m))}))
+    (when (convert-tag? (:block/name tag-block) user-options)
       (if-let [existing-tag-uuid (first
                                   (d/q '[:find [?uuid ...]
                                          :in $ ?name
@@ -68,30 +85,25 @@
                                        (:block/name tag-block)))]
         [:block/uuid existing-tag-uuid]
         ;; Creates or updates page within same tx
-        (-> (merge tag-block
-                   (find-or-create-class db (:block/title tag-block) all-idents))
-            ;; override with imported timestamps
-            (dissoc :block/created-at :block/updated-at)
-            (merge (add-missing-timestamps
-                    (select-keys tag-block [:block/created-at :block/updated-at]))))))))
-
-(defn- get-page-uuid [page-names-to-uuids page-name]
-  (or (get page-names-to-uuids (if (string/includes? (str page-name) "#")
-                                 (string/lower-case (gp-block/sanitize-hashtag-name page-name))
-                                 page-name))
-      (throw (ex-info (str "No uuid found for page name " (pr-str page-name))
-                      {:page-name page-name}))))
+        (let [class-m (find-or-create-class db (:block/title tag-block) all-idents)]
+          (-> (merge tag-block class-m
+                     (when-not (:block/uuid tag-block)
+                       {:block/uuid (find-or-gen-class-uuid page-names-to-uuids (:block/name tag-block) (:db/ident class-m))}))
+              ;; override with imported timestamps
+              (dissoc :block/created-at :block/updated-at)
+              (merge (add-missing-timestamps
+                      (select-keys tag-block [:block/created-at :block/updated-at])))))))))
 
 (defn- logseq-class-ident?
   [k]
   (and (qualified-keyword? k) (= "logseq.class" (namespace k))))
 
 (defn- update-page-tags
-  [block db tag-classes page-names-to-uuids all-idents]
+  [block db user-options page-names-to-uuids all-idents]
   (if (seq (:block/tags block))
     (let [page-tags (->> (:block/tags block)
                          (remove #(or (:block.temp/new-class %)
-                                      (contains? tag-classes (:block/name %))
+                                      (convert-tag? (:block/name %) user-options)
                                       ;; Ignore new class tags from extract e.g. :logseq.class/Journal
                                       (logseq-class-ident? %)))
                          (map #(vector :block/uuid (get-page-uuid page-names-to-uuids (:block/name %))))
@@ -103,7 +115,7 @@
                   ;; Don't lazy load as this needs to build before the page does
                   (vec (keep #(if (logseq-class-ident? %)
                                 %
-                                (convert-tag-to-class db % page-names-to-uuids tag-classes all-idents)) tags))))
+                                (convert-tag-to-class db % page-names-to-uuids user-options all-idents)) tags))))
         (seq page-tags)
         (merge {:logseq.property/page-tags page-tags})))
     block))
@@ -125,29 +137,30 @@
    (string/trim)))
 
 (defn- update-block-tags
-  [block db tag-classes page-names-to-uuids all-idents]
+  [block db user-options page-names-to-uuids all-idents]
   (let [block'
         (if (seq (:block/tags block))
           (let [original-tags (remove #(or (:block.temp/new-class %)
                                            ;; Filter out new classes already set on a block e.g. :logseq.class/Query
                                            (logseq-class-ident? %))
-                                      (:block/tags block))]
+                                      (:block/tags block))
+                convert-tag?' #(convert-tag? (:block/name %) user-options)]
             (-> block
                 (update :block/title
                         content-without-tags-ignore-case
                         (->> original-tags
-                             (filter #(tag-classes (:block/name %)))
+                             (filter convert-tag?')
                              (map :block/title)))
                 (update :block/title
                         db-content/replace-tags-with-page-refs
                         (->> original-tags
-                             (remove #(tag-classes (:block/name %)))
+                             (remove convert-tag?')
                              (map #(add-uuid-to-page-map % page-names-to-uuids))))
                 (update :block/tags
                         (fn [tags]
                           (vec (keep #(if (logseq-class-ident? %)
                                         %
-                                        (convert-tag-to-class db % page-names-to-uuids tag-classes all-idents))
+                                        (convert-tag-to-class db % page-names-to-uuids user-options all-idents))
                                      tags))))))
           block)]
     block'))
@@ -366,12 +379,12 @@
           filter-by (group-by val filters)
           includes (->> (filter-by true)
                         (map first)
-                        (keep #(or (page-names-to-uuids %)
+                        (keep #(or (get @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 %)
+                        (keep #(or (get @page-names-to-uuids %)
                                    (js/console.error (str "No uuid found for linked reference filter page " (pr-str %)))))
                         (mapv #(vector :block/uuid %)))]
       (cond-> []
@@ -525,7 +538,7 @@
       (swap! (:block-properties-text-values import-state)
              assoc
              ;; For pages, valid uuid is in page-names-to-uuids, not in block
-             (if (:block/name block) (get-page-uuid page-names-to-uuids (:block/name block)) (:block/uuid block))
+             (if (:block/name block) (get-page-uuid page-names-to-uuids ((some-fn ::original-name :block/name) block)) (:block/uuid block))
              properties-text-values))
     ;; TODO: Add import support for :template. Ignore for now as they cause invalid property types
     (if (contains? props :template)
@@ -608,7 +621,7 @@
               options' (assoc options :property-changes property-changes)
               {:keys [block-properties pvalues-tx]}
               (build-properties-and-values properties' db page-names-to-uuids
-                                           (select-keys block [:block/properties-text-values :block/name :block/title :block/uuid])
+                                           (select-keys block [:block/properties-text-values :block/name :block/title :block/uuid ::original-name])
                                            options')]
           {:block
            (cond-> block
@@ -624,6 +637,7 @@
       (update :block dissoc :block/properties :block/properties-text-values :block/properties-order :block/invalid-properties)))
 
 (defn- handle-page-properties
+  "Adds page properties including special handling for :logseq.property/parent"
   [{:block/keys [properties] :as block*} db page-names-to-uuids refs
    {:keys [property-parent-classes log-fn import-state] :as options}]
   (let [{:keys [block properties-tx]} (handle-page-and-block-properties block* db page-names-to-uuids refs options)
@@ -642,11 +656,14 @@
                        (when (> (count parent-classes-from-properties) 1)
                          (log-fn :skipped-parent-classes "Only one parent class is allowed so skipped ones after the first one" :classes parent-classes-from-properties))
                        (merge class-m
-                              (if-let [existing-tag-uuid (get page-names-to-uuids (common-util/page-name-sanity-lc new-class))]
-                                {:block/uuid existing-tag-uuid}
-                                {:block/uuid (common-uuid/gen-uuid :db-ident-block-uuid (:db/ident class-m))}))))))
-          (dissoc block* :block/properties))]
-    {:block block' :properties-tx properties-tx}))
+                              {:block/uuid (find-or-gen-class-uuid page-names-to-uuids (common-util/page-name-sanity-lc new-class) (:db/ident class-m))})))))
+          (dissoc block* :block/properties))
+        block'' (if (:block/namespace block')
+                  (-> (dissoc block' :block/namespace)
+                      (assoc :logseq.property/parent
+                             {:block/uuid (get-page-uuid page-names-to-uuids (get-in block' [:block/namespace :block/name]))}))
+                  block')]
+    {:block block'' :properties-tx properties-tx}))
 
 (defn- handle-block-properties
   "Does everything page properties does and updates a couple of block specific attributes"
@@ -721,7 +738,7 @@
                    ;; Only keep :block/uuid as we don't want to re-transact page refs
                    (if (map? ref)
                      ;; a new page's uuid can change across blocks so rely on consistent one from pages-tx
-                     (if-let [existing-uuid (some->> (:block/name ref) (get page-names-to-uuids))]
+                     (if-let [existing-uuid (some->> (:block/name ref) (get @page-names-to-uuids))]
                        [:block/uuid existing-uuid]
                        [:block/uuid (:block/uuid ref)])
                      ref))
@@ -755,7 +772,7 @@
     (assoc :block/parent {:block/uuid (get-page-uuid page-names-to-uuids (:block/name (:block/parent block)))})))
 
 (defn- build-block-tx
-  [db block* pre-blocks page-names-to-uuids {:keys [tag-classes import-state] :as options}]
+  [db block* pre-blocks page-names-to-uuids {:keys [import-state] :as options}]
   ;; (prn ::block-in block*)
   (let [;; needs to come before update-block-refs to detect new property schemas
         {:keys [block properties-tx]}
@@ -765,7 +782,7 @@
                    (fix-pre-block-references pre-blocks page-names-to-uuids)
                    (fix-block-name-lookup-ref page-names-to-uuids)
                    (update-block-refs page-names-to-uuids options)
-                   (update-block-tags db tag-classes page-names-to-uuids (:all-idents import-state))
+                   (update-block-tags db (select-keys options [:convert-all-tags? :tag-classes]) page-names-to-uuids (:all-idents import-state))
                    (update-block-marker options)
                    (update-block-priority options)
                    add-missing-timestamps
@@ -784,7 +801,7 @@
                                 aliases))))
 
 (defn- build-new-page-or-class
-  [m db tag-classes page-names-to-uuids all-idents]
+  [m db user-options page-names-to-uuids all-idents]
   (-> (cond-> m
         ;; Fix pages missing :block/title. Shouldn't happen
         (not (:block/title m))
@@ -795,9 +812,12 @@
       ;; TODO: org-mode content needs to be handled
       (assoc :block/format :markdown)
       (dissoc :block/whiteboard?)
-      (update-page-tags db tag-classes page-names-to-uuids all-idents)))
+      (update-page-tags db user-options page-names-to-uuids all-idents)))
 
-(defn- get-all-existing-page-ents
+(defn- get-all-existing-page-uuids
+  "Returns a map of unique page names mapped to their uuids. The page names
+   are in a format that is compatible with extract/extract e.g. namespace pages have
+   their full hierarchy in the name"
   [db]
   (->> db
        ;; don't fetch built-in as that would give the wrong entity if a user used
@@ -805,10 +825,18 @@
        (d/q '[:find [?b ...]
               :where [?b :block/name] [(missing? $ ?b :logseq.property/built-in?)]])
        (map #(d/entity db %))
-       (map (juxt :block/name identity))
+       (map #(vector
+              (if-let [parents (and (ldb/internal-page? %) (ldb/get-page-parents %))]
+                ;; Build a :block/name for namespace pages that matches data from extract/extract
+                (string/join ns-util/namespace-char (conj (mapv :block/name parents) (:block/name %)))
+                (:block/name %))
+              (or (:block/uuid %)
+                  (throw (ex-info (str "No uuid for existing page " (pr-str (:block/name %)))
+                                  (select-keys % [:block/name :block/type]))))))
        (into {})))
 
-(defn- build-existing-page [m db page-uuid page-names-to-uuids {:keys [tag-classes notify-user import-state]}]
+(defn- build-existing-page
+  [m db page-uuid page-names-to-uuids {:keys [notify-user import-state] :as options}]
   (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]
@@ -827,13 +855,46 @@
         (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))))))
+        (update-page-tags db (select-keys options [:tag-classes :convert-all-tags?]) page-names-to-uuids (:all-idents import-state))))))
+
+(defn- modify-page-tx
+  "Modifies page tx from graph-parser for use with DB graphs. Currently modifies
+  namespaces and blocks with built-in page names"
+  [page all-existing-page-uuids]
+  (let [page'
+        (if (contains? all-existing-page-uuids (:block/name page))
+          (cond-> page
+            (:block/namespace page)
+            ;; Fix uuid for existing pages as graph-parser's :block/name is different than
+            ;; the DB graph's version e.g. 'b/c/d' vs 'd'
+            (assoc :block/uuid
+                   (or (all-existing-page-uuids (:block/name page))
+                       (throw (ex-info (str "No uuid found for existing namespace page " (pr-str (:block/name page)))
+                                       (select-keys page [:block/name :block/namespace]))))))
+          (cond-> page
+            ;; fix extract incorrectly assigning new user pages built-in uuids
+            (contains? all-built-in-names (keyword (:block/name page)))
+            (assoc :block/uuid (d/squuid))
+            ;; only happens for few file built-ins like tags and alias
+            (and (contains? all-built-in-names (keyword (:block/name page)))
+                 (not (:block/type page)))
+            (assoc :block/type "page")))]
+    (cond-> page'
+      (:block/namespace page)
+      ((fn [block']
+         (let [new-title (ns-util/get-last-part (:block/title block'))]
+           (merge block'
+                  {;; DB graphs only have child name of namespace
+                   :block/title new-title
+                   ;; save original name b/c it's still used for a few name lookups
+                   ::original-name (:block/name block')
+                   :block/name (common-util/page-name-sanity-lc new-title)})))))))
 
 (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 import-state]
+  [conn pages blocks {:keys [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
@@ -842,43 +903,30 @@
                         ;; remove file path relative
                         (map #(dissoc % :block/file)))
         ;; 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 %))))
-                          (assoc % :block/uuid (d/squuid))
-                          %)
-                       all-pages*)
-        new-pages (remove existing-page? all-pages)
-        page-names-to-uuids (merge (update-vals all-existing-page-ents :block/uuid)
-                                   (into {} (map (juxt :block/name :block/uuid) new-pages)))
+        all-existing-page-uuids (get-all-existing-page-uuids @conn)
+        all-pages (map #(modify-page-tx % all-existing-page-uuids) all-pages*)
+        ;; Stateful because new page uuids can occur via tags
+        page-names-to-uuids (atom (merge all-existing-page-uuids
+                                         (into {} (map (juxt (some-fn ::original-name :block/name) :block/uuid)
+                                                       (remove all-existing-page-uuids all-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-pages-by-name (:block/name m))]
-                           (build-existing-page m @conn page-uuid page-names-to-uuids options)
+                         (if-let [page-uuid (if (::original-name m)
+                                              (all-existing-page-uuids (::original-name m))
+                                              (all-existing-page-uuids (:block/name m)))]
+                           (build-existing-page (dissoc m ::original-name) @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)))
                                                   db-malli-schema/class?)))
-                             (let [m' (if (contains? all-built-in-names (keyword (:block/name m)))
-                                        ;; Use fixed uuid from above
-                                        (cond-> (assoc m :block/uuid (get page-names-to-uuids (:block/name m)))
-                                          ;; only happens for few file built-ins like tags and alias
-                                          (not (:block/type m))
-                                          (assoc :block/type "page"))
-                                        m)]
-                               (build-new-page-or-class m' @conn tag-classes page-names-to-uuids (:all-idents import-state))))))
+                             (build-new-page-or-class (dissoc m ::original-name) @conn
+                                                      (select-keys options [:tag-classes :convert-all-tags?])
+                                                      page-names-to-uuids (:all-idents import-state)))))
                        (map :block all-pages-m))]
     {:pages-tx pages-tx
      :page-properties-tx (mapcat :properties-tx all-pages-m)
-     :existing-pages existing-pages-by-name
+     :existing-pages (select-keys all-existing-page-uuids (map :block/name all-pages*))
      :page-names-to-uuids page-names-to-uuids}))
 
 (defn- build-upstream-properties-tx-for-default
@@ -966,6 +1014,7 @@
     ;; Track per file changes to make to existing properties
     ;; Map of property names (keyword) and their changes (map)
     :upstream-properties (atom {})
+    :convert-all-tags? (:convert-all-tags? user-options)
     :tag-classes (set (map string/lower-case (:tag-classes user-options)))
     :property-classes (set/difference
                        (set (map (comp keyword string/lower-case) (:property-classes user-options)))
@@ -1021,7 +1070,40 @@
              (assoc :block/title (:block/content b)))))
        blocks))
 
+(defn- fix-extracted-block-tags
+  "A tag can have different :block/uuid's across extracted blocks. This makes
+   sense for most in-app uses but not for importing where we want consistent identity.
+   This fn fixes that issue"
+  [blocks]
+  (let [name-uuids (atom {})
+        fix-block-uuids
+        (fn fix-block-uuids [tags-or-refs]
+          ;; mapv to determinastically process in order
+          (mapv (fn [b]
+                  (if-let [existing-uuid (some->> (:block/name b) (get @name-uuids))]
+                    (if (not= existing-uuid (:block/uuid b))
+                      ;; fix unequal uuids for same name
+                      (assoc b :block/uuid existing-uuid)
+                      b)
+                    (if (vector? b)
+                      ;; ignore [:block/uuid] refs
+                      b
+                      (do
+                        (assert (and (:block/name b) (:block/uuid b))
+                                (str "Extracted block tag/ref must have a name and uuid: " (pr-str b)))
+                        (swap! name-uuids assoc (:block/name b) (:block/uuid b))
+                        b))))
+                tags-or-refs))]
+    (map (fn [b]
+           (if (seq (:block/tags b))
+             (-> b
+                 (update :block/tags fix-block-uuids)
+                 (update :block/refs fix-block-uuids))
+             b))
+         blocks)))
+
 (defn- extract-pages-and-blocks
+  "Main fn which calls graph-parser to convert markdown into data"
   [db file content {:keys [extract-options notify-user]}]
   (let [format (common-util/get-format file)
         extract-options' (merge {:block-pattern (common-config/get-block-pattern format)
@@ -1034,7 +1116,8 @@
     (cond (contains? common-config/mldoc-support-formats format)
           (-> (extract/extract file content extract-options')
               (update :pages (fn [pages]
-                               (map #(dissoc % :block.temp/original-page-name) pages))))
+                               (map #(dissoc % :block.temp/original-page-name) pages)))
+              (update :blocks fix-extracted-block-tags))
 
           (common-config/whiteboard? file)
           (-> (extract/extract-whiteboard-edn file content extract-options')
@@ -1296,7 +1379,7 @@
                          :filename-format (or (:file/name-format config) :legacy)
                          :verbose (:verbose options)}
        :user-config config
-       :user-options (select-keys options [:tag-classes :property-classes :property-parent-classes])
+       :user-options (select-keys options [:tag-classes :property-classes :property-parent-classes :convert-all-tags?])
        :import-state (new-import-state)
        :macros (or (:macros options) (:macros config))}
       (merge (select-keys options [:set-ui-state :export-file :notify-user]))))
@@ -1321,26 +1404,36 @@
   [repo-or-conn conn config-file *files {:keys [<read-file <copy-asset rpath-key log-fn]
                                          :or {rpath-key :path log-fn println}
                                          :as options}]
-  (p/let [config (export-config-file
-                  repo-or-conn config-file <read-file
-                  (-> (select-keys options [:notify-user :default-config :<save-config-file])
-                      (set/rename-keys {:<save-config-file :<save-file})))]
-    (let [files (common-config/remove-hidden-files *files config rpath-key)
-          logseq-file? #(string/starts-with? (get % rpath-key) "logseq/")
-          doc-files (->> files
-                         (remove logseq-file?)
-                         (filter #(contains? #{"md" "org" "markdown" "edn"} (path/file-ext (:path %)))))
-          asset-files (filter #(string/starts-with? (get % rpath-key) "assets/") files)
-          doc-options (build-doc-options config options)]
-      (log-fn "Importing" (count files) "files ...")
-      ;; These export* fns are all the major export/import steps
-      (p/do!
-       (export-logseq-files repo-or-conn (filter logseq-file? files) <read-file
-                            (-> (select-keys options [:notify-user :<save-logseq-file])
-                                (set/rename-keys {:<save-logseq-file :<save-file})))
-       (export-asset-files asset-files <copy-asset (select-keys options [:notify-user :set-ui-state]))
-       (export-doc-files conn doc-files <read-file doc-options)
-       (export-favorites-from-config-edn conn repo-or-conn config {})
-       (export-class-properties conn repo-or-conn)
-       {:import-state (:import-state doc-options)
-        :files files}))))
+  (reset! gp-block/*export-to-db-graph? true)
+  (->
+   (p/let [config (export-config-file
+                   repo-or-conn config-file <read-file
+                   (-> (select-keys options [:notify-user :default-config :<save-config-file])
+                       (set/rename-keys {:<save-config-file :<save-file})))]
+     (let [files (common-config/remove-hidden-files *files config rpath-key)
+           logseq-file? #(string/starts-with? (get % rpath-key) "logseq/")
+           doc-files (->> files
+                          (remove logseq-file?)
+                          (filter #(contains? #{"md" "org" "markdown" "edn"} (path/file-ext (:path %)))))
+           asset-files (filter #(string/starts-with? (get % rpath-key) "assets/") files)
+           doc-options (build-doc-options config options)]
+       (log-fn "Importing" (count doc-files) "files ...")
+       ;; These export* fns are all the major export/import steps
+       (p/do!
+        (export-logseq-files repo-or-conn (filter logseq-file? files) <read-file
+                             (-> (select-keys options [:notify-user :<save-logseq-file])
+                                 (set/rename-keys {:<save-logseq-file :<save-file})))
+        (export-asset-files asset-files <copy-asset (select-keys options [:notify-user :set-ui-state]))
+        (export-doc-files conn doc-files <read-file doc-options)
+        (export-favorites-from-config-edn conn repo-or-conn config {})
+        (export-class-properties conn repo-or-conn)
+        {:import-state (:import-state doc-options)
+         :files files})))
+   (p/finally (fn [_]
+                (reset! gp-block/*export-to-db-graph? false)))
+   (p/catch (fn [e]
+              (reset! gp-block/*export-to-db-graph? false)
+              ((:notify-user options)
+               {:msg (str "Import has unexpected error:\n" (.-message e))
+                :level :error
+                :ex-data {:error e}})))))

+ 2 - 2
deps/graph-parser/src/logseq/graph_parser/extract.cljc

@@ -166,7 +166,7 @@
                   :block/file {:file/path (common-util/path-normalize file)}))
                 (extract-page-alias-and-tags page-name properties))]
     (cond->
-      page-m
+     page-m
 
       (seq valid-properties)
       (assoc :block/properties valid-properties
@@ -238,7 +238,7 @@
              (:block/properties-text-values (first blocks))]
             [properties [] {}])
           page-map (build-page-map properties invalid-properties properties-text-values file page page-name (assoc options' :from-page page))
-          namespace-pages (when-not db-based?
+          namespace-pages (when (or (not db-based?) (:export-to-db-graph? options))
                             (let [page (:block/title page-map)]
                               (when (text/namespace-page? page)
                                 (->> (common-util/split-namespace-pages page)

+ 44 - 6
deps/graph-parser/test/logseq/graph_parser/exporter_test.cljs

@@ -112,7 +112,7 @@
   "Import a file graph dir just like UI does. However, unlike the UI the
   exporter receives file maps containing keys :path and ::rpath since :path
   are full paths"
-  [file-graph-dir conn {:keys [assets] :as options}]
+  [file-graph-dir conn {:keys [assets] :or {assets (atom [])} :as options}]
   (let [*files (build-graph-files file-graph-dir)
         config-file (first (filter #(string/ends-with? (:path %) "logseq/config.edn") *files))
         _ (assert config-file "No 'logseq/config.edn' found for file graph dir")
@@ -155,9 +155,8 @@
   (p/let [file-graph-dir "test/resources/docs-0.10.9"
           _ (docs-graph-helper/clone-docs-repo-if-not-exists file-graph-dir "v0.10.9")
           conn (db-test/create-conn)
-          assets (atom [])
           {:keys [import-state]}
-          (import-file-graph-to-db file-graph-dir conn {:assets assets})]
+          (import-file-graph-to-db file-graph-dir conn {})]
 
     (is (empty? (map :entity (:errors (db-validate/validate-db! @conn))))
         "Created graph has no validation errors")
@@ -179,18 +178,23 @@
 
       ;; Counts
       ;; Includes journals as property values e.g. :logseq.task/deadline
-      (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 (= 20 (count (d/q '[:find ?b :where [?b :block/type "journal"]] @conn))))
+      (is (= 20 (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))))
 
       ;; Don't count pages like url.md that have properties but no content
-      (is (= 8
+      (is (= 9
              (count (->> (d/q '[:find [(pull ?b [:block/title :block/type]) ...]
                                 :where [?b :block/title] [_ :block/page ?b]] @conn)
                          (filter ldb/internal-page?))))
           "Correct number of pages with block content")
+      (is (= 0 (->> @conn
+                    (d/q '[:find [?ident ...]
+                           :where [?b :block/type "class"] [?b :db/ident ?ident] (not [?b :logseq.property/built-in?])])
+                    count))
+          "Correct number of user classes")
       (is (= 4 (count (d/datoms @conn :avet :block/type "whiteboard"))))
       (is (= 0 (count @(:ignored-properties import-state))) ":filters should be the only ignored property")
       (is (= 1 (count @assets))))
@@ -329,6 +333,23 @@
              (:block/title (find-block-by-content @conn #"tasks with")))
           "Advanced query has custom title migrated"))
 
+    (testing "namespaces"
+      (let [expand-children (fn expand-children [ent parent]
+                              (if-let [children (:logseq.property/_parent ent)]
+                                (cons {:parent (:block/title parent) :child (:block/title ent)}
+                                      (mapcat #(expand-children % ent) children))
+                                [{:parent (:block/title parent) :child (:block/title ent)}]))]
+        (is (= [{:parent "n1" :child "x"}
+                {:parent "x" :child "z"}
+                {:parent "x" :child "y"}]
+               (rest (expand-children (d/entity @conn (:db/id (find-page-by-name @conn "n1"))) nil)))
+            "First namespace tests duplicate parent page name")
+        (is (= [{:parent "n2" :child "x"}
+                {:parent "x" :child "z"}
+                {:parent "n2" :child "alias"}]
+               (rest (expand-children (d/entity @conn (:db/id (find-page-by-name @conn "n2"))) nil)))
+            "First namespace tests duplicate child page name and built-in page name")))
+
     (testing "db attributes"
       (is (= true
              (:block/collapsed? (find-block-by-content @conn "collapsed block")))
@@ -442,6 +463,23 @@
                (:logseq.property/page-tags (readable-properties @conn (find-page-by-name @conn "chat-gpt"))))
             "tagged page has new page and other pages marked with '#' and '[[]]` imported as tags to page-tags")))))
 
+(deftest-async export-basic-graph-with-convert-all-tags
+  (p/let [file-graph-dir "test/resources/exporter-test-graph"
+          conn (db-test/create-conn)
+          ;; Simulate frontend path-refs being calculated
+          _ (db-pipeline/add-listener conn)
+          {:keys [import-state]}
+          (import-file-graph-to-db file-graph-dir conn {:convert-all-tags? true})]
+
+    (is (empty? (map :entity (:errors (db-validate/validate-db! @conn))))
+        "Created graph has no validation errors")
+    (is (= 0 (count @(:ignored-properties import-state))) "No ignored properties")
+    (is (= 9 (->> @conn
+                  (d/q '[:find [?ident ...]
+                         :where [?b :block/type "class"] [?b :db/ident ?ident] (not [?b :logseq.property/built-in?])])
+                  count))
+        "Correct number of user classes")))
+
 (deftest-async export-files-with-tag-classes-option
   (p/let [file-graph-dir "test/resources/exporter-test-graph"
           files (mapv #(node-path/join file-graph-dir %) ["journals/2024_02_07.md" "pages/Interstellar.md"])

+ 5 - 0
deps/graph-parser/test/resources/exporter-test-graph/journals/2024_10_17.md

@@ -0,0 +1,5 @@
+- test namespaces
+	- [[y]]
+	- [[n1/x/z]]
+	- [[n2/x/z]]
+	- [[n2/alias]] tests name that overlaps with built-in ones

+ 4 - 0
deps/graph-parser/test/resources/exporter-test-graph/pages/n1___x___y.md

@@ -0,0 +1,4 @@
+alias:: y
+description:: page property triggers aliased namespace bug
+
+- some content

+ 11 - 10
deps/outliner/src/logseq/outliner/cli.cljs

@@ -1,13 +1,13 @@
 (ns ^:node-only logseq.outliner.cli
   "Primary ns for outliner CLI fns"
-    (:require [clojure.string :as string]
-              [datascript.core :as d]
-              [logseq.db.sqlite.create-graph :as sqlite-create-graph]
-              [logseq.db.sqlite.build :as sqlite-build]
-              [logseq.db.sqlite.cli :as sqlite-cli]
-              [logseq.outliner.db-pipeline :as db-pipeline]
-              ["fs" :as fs]
-              ["path" :as node-path]))
+  (:require [clojure.string :as string]
+            [datascript.core :as d]
+            [logseq.db.sqlite.create-graph :as sqlite-create-graph]
+            [logseq.db.sqlite.build :as sqlite-build]
+            [logseq.db.sqlite.cli :as sqlite-cli]
+            [logseq.outliner.db-pipeline :as db-pipeline]
+            ["fs" :as fs]
+            ["path" :as node-path]))
 
 (defn- find-on-classpath [classpath rel-path]
   (some (fn [dir]
@@ -17,7 +17,8 @@
 
 (defn- setup-init-data
   "Setup initial data same as frontend.handler.repo/create-db"
-  [conn {:keys [additional-config classpath]}]
+  [conn {:keys [additional-config classpath import-type]
+         :or {import-type :cli/default}}]
   (let [config-content
         (cond-> (or (some-> (find-on-classpath classpath "templates/config.edn") fs/readFileSync str)
                     (do (println "Setting graph's config to empty since no templates/config.edn was found.")
@@ -27,7 +28,7 @@
           (string/replace-first #"(:file/name-format :triple-lowbar)"
                                 (str "$1 "
                                      (string/replace-first (str additional-config) #"^\{(.*)\}$" "$1"))))]
-    (d/transact! conn (sqlite-create-graph/build-db-initial-data config-content))))
+    (d/transact! conn (sqlite-create-graph/build-db-initial-data config-content {:import-type import-type}))))
 
 (defn init-conn
   "Create sqlite DB, initialize datascript connection and sync listener and then

+ 16 - 9
deps/outliner/src/logseq/outliner/property.cljs

@@ -27,11 +27,14 @@
                     {:property property-ident}))))
 
 (defn- build-property-value-tx-data
-  ([block property-id value]
-   (build-property-value-tx-data block property-id value (= property-id :logseq.task/status)))
-  ([block property-id value status?]
+  ([conn block property-id value]
+   (build-property-value-tx-data conn block property-id value (= property-id :logseq.task/status)))
+  ([conn block property-id value status?]
    (when (some? value)
      (let [old-value (get block property-id)
+           property (d/entity @conn property-id)
+           multiple-values? (= :db.cardinality/many (:db/cardinality property))
+           retract-multiple-values? (and multiple-values? (coll? value))
            multiple-values-empty? (and (coll? old-value)
                                        (= 1 (count old-value))
                                        (= :logseq.property/empty-placeholder (:db/ident (first old-value))))
@@ -42,6 +45,8 @@
                            (assoc :block/tags :logseq.class/Task))]
        [(when multiple-values-empty?
           [:db/retract (:db/id block) property-id :logseq.property/empty-placeholder])
+        (when retract-multiple-values?
+          [:db/retract (:db/id block) property-id])
         block-tx-data]))))
 
 (defn- get-property-value-schema
@@ -119,7 +124,7 @@
                         (when (seq properties)
                           (mapcat
                            (fn [[property-id v]]
-                             (build-property-value-tx-data property property-id v)) properties)))
+                             (build-property-value-tx-data conn property property-id v)) properties)))
         many->one? (and (db-property/many? property) (= :one (:cardinality schema)))]
     (when (and many->one? (seq (d/datoms @conn :avet db-ident)))
       (throw (ex-info "Disallowed many to one conversion"
@@ -185,7 +190,7 @@
                          :payload {:message msg'
                                    :type :warning}})))
       (let [status? (= :logseq.task/status (:db/ident property))
-            tx-data (build-property-value-tx-data block property-id new-value status?)]
+            tx-data (build-property-value-tx-data conn block property-id new-value status?)]
         (ldb/transact! conn tx-data {:outliner-op :save-block})))))
 
 (defn create-property-text-block!
@@ -289,7 +294,7 @@
         txs (mapcat
              (fn [eid]
                (if-let [block (d/entity @conn eid)]
-                 (build-property-value-tx-data block property-id v' status?)
+                 (build-property-value-tx-data conn block property-id v' status?)
                  (js/console.error "Skipping setting a block's property because the block id could not be found:" eid)))
              block-eids)]
     (when (seq txs)
@@ -394,9 +399,11 @@
   (let [property (d/entity db property-id)
         schema (:block/schema property)]
     (and
-     (or (some? (get block-properties property-id)) ; property value exists
-         (contains? #{:checkbox} (:type schema)))
-     (= (:position schema) position))))
+     (= (:position schema) position)
+     (not (get-in property [:block/schema :hide?]))
+     (not (and
+           (= (:position schema) :block-below)
+           (nil? (get block-properties property-id)))))))
 
 (defn property-with-other-position?
   [property]

+ 1 - 1
deps/publishing/README.md

@@ -14,7 +14,7 @@ provides two namespaces for node/CLI contexts, `logseq.publishing` and
 
 ## Usage
 
-See `logseq.tasks.dev.publishing` for a CLI example. See the frontend for cljs usage.
+See `script/publishing.cljs` for a CLI example. See the frontend for cljs usage.
 
 ## Dev
 

+ 1 - 0
deps/publishing/package.json

@@ -7,6 +7,7 @@
     "mldoc": "^1.5.9"
   },
   "dependencies": {
+    "better-sqlite3": "9.3.0",
     "fs-extra": "9.1.0"
   },
   "scripts": {

+ 25 - 9
scripts/src/logseq/tasks/dev/publishing.cljs → deps/publishing/script/publishing.cljs

@@ -1,4 +1,4 @@
-(ns logseq.tasks.dev.publishing
+(ns publishing
   "Basic script for publishing from CLI"
   (:require [logseq.graph-parser.cli :as gp-cli]
             [logseq.publishing :as publishing]
@@ -6,8 +6,9 @@
             ["fs" :as fs]
             ["path" :as node-path]
             [clojure.edn :as edn]
-            [datascript.core :as d]))
-
+            [datascript.core :as d]
+            [logseq.db.sqlite.util :as sqlite-util]
+            [nbb.core :as nbb]))
 
 (defn- get-db [graph-dir]
   (let [{:keys [conn]} (gp-cli/parse-graph graph-dir {:verbose false})] @conn))
@@ -18,7 +19,10 @@
                        static-dir
                        graph-dir
                        output-path
-                       (merge options {:repo-config repo-config :ui/theme "dark" :ui/radix-color :purple}))))
+                       (merge options {:repo (node-path/basename graph-dir)
+                                       :repo-config repo-config
+                                       :ui/theme "dark"
+                                       :ui/radix-color :purple}))))
 
 (defn- publish-db-graph [static-dir graph-dir output-path opts]
   (let [db-name (node-path/basename graph-dir)
@@ -32,17 +36,29 @@
                        static-dir
                        graph-dir
                        output-path
-                       (merge opts {:repo-config repo-config :db-graph? true :ui/theme "dark" :ui/radix-color :cyan}))))
+                       (merge opts {:repo (str sqlite-util/db-version-prefix db-name)
+                                    :repo-config repo-config
+                                    :db-graph? true
+                                    :ui/theme "dark"
+                                    :ui/radix-color :cyan}))))
+
+(defn- resolve-path
+  "If relative path, resolve with $ORIGINAL_PWD"
+  [path]
+  (if (node-path/isAbsolute path)
+    path
+    (node-path/join (or js/process.env.ORIGINAL_PWD ".") path)))
 
 (defn -main
-  [& args]
+  [args]
   (when (< (count args) 3)
     (println "Usage: $0 STATIC-DIR GRAPH-DIR OUT-DIR")
     (js/process.exit 1))
-  (let [[static-dir graph-dir output-path]
-        ;; Offset relative paths since they are run in a different directory than user is in
-        (map #(if (node-path/isAbsolute %) % (node-path/resolve ".." %)) args)
+  (let [[static-dir graph-dir output-path] (map resolve-path args)
         options {:dev? (contains? (set args) "--dev")}]
     (if (sqlite-cli/db-graph-directory? graph-dir)
       (publish-db-graph static-dir graph-dir output-path options)
       (publish-file-graph static-dir graph-dir output-path options))))
+
+(when (= nbb/*file* (:file (meta #'-main)))
+  (-main *command-line-args*))

+ 37 - 16
deps/publishing/src/logseq/publishing/db.cljs

@@ -4,7 +4,8 @@
             [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.entity-util :as entity-util]
+            [logseq.db.frontend.malli-schema :as db-malli-schema]))
 
 (defn ^:api get-area-block-asset-url
   "Returns asset url for an area block used by pdf assets. This lives in this ns
@@ -106,7 +107,7 @@
   [db]
   (if (entity-util/db-based-graph? db)
     (fn [datom]
-      (and (= :logseq.property/hl-type (:a datom))
+      (and (= :logseq.property.pdf/hl-type (:a datom))
            (= (keyword (:v datom)) :area)))
     (fn [datom]
       (and
@@ -179,6 +180,26 @@
     ;; (prn :datoms (count datoms) :assets (count assets))
     [@(d/conn-from-datoms datoms (:schema db)) assets]))
 
+(defn- file-filter-only-public
+  [public-pages db datom]
+  (let [ns' (namespace (:a datom))]
+    (and
+     (not (contains? #{:block/file} (:a datom)))
+     (not= ns' "file")
+     (or
+      (not (contains? #{"block" "recent"} ns'))
+      (and (= ns' "block")
+           (or
+            (contains? public-pages (:e datom))
+            (contains? public-pages (:db/id (:block/page (d/entity db (:e datom)))))))))))
+
+(defn- db-filter-only-public
+  [public-pages internal-ents db datom]
+  (or
+   (contains? internal-ents (:e datom))
+   (contains? public-pages (:e datom))
+   (contains? public-pages (:db/id (:block/page (d/entity db (:e datom)))))))
+
 (defn filter-only-public-pages-and-blocks
   "Prepares a database assuming all pages are private unless a page has a 'public:: true'"
   [db {:keys [db-graph?]}]
@@ -186,20 +207,20 @@
   (let [public-pages* (seq (if db-graph? (get-db-public-pages db) (get-public-pages db)))
         public-pages (set/union (set public-pages*)
                                 (get-aliases-for-page-ids db public-pages*))
-        exported-namespace? #(contains? #{"block" "recent"} %)
-        filtered-db (d/filter db
-                              (fn [db datom]
-                                (let [ns' (namespace (:a datom))]
-                                  (and
-                                   (not (contains? #{:block/file} (:a datom)))
-                                   (not= ns' "file")
-                                   (or
-                                    (not (exported-namespace? ns'))
-                                    (and (= ns' "block")
-                                         (or
-                                          (contains? public-pages (:e datom))
-                                          (contains? public-pages (:db/id (:block/page (d/entity db (:e datom))))))))))))
+        internal-ents (when db-graph?
+                        (set/union
+                         (->> (d/datoms db :eavt)
+                              (keep #(when (and (= :db/ident (:a %)) (db-malli-schema/internal-ident? (:v %)))
+                                       (:e %)))
+                              set)
+                         (->> (d/datoms db :avet :logseq.property/built-in? true)
+                              (map :e)
+                              set)))
+        filter-fn (if db-graph?
+                    (partial db-filter-only-public public-pages internal-ents)
+                    (partial file-filter-only-public public-pages))
+        filtered-db (d/filter db filter-fn)
         datoms (d/datoms filtered-db :eavt)
         assets (get-assets db datoms)]
-    ;; (prn :datoms (count datoms) :assets (count assets))
+    ;; (prn :counts :internal (count internal-ents) :datoms (count datoms) :assets (count assets))
     [@(d/conn-from-datoms datoms (:schema db)) assets]))

+ 3 - 3
deps/publishing/src/logseq/publishing/export.cljs

@@ -8,7 +8,7 @@
 
 (def ^:api js-files
   "js files from publishing release build"
-  ["shared.js" "main.js" "code-editor.js" "excalidraw.js" "tldraw.js" "db-worker.js"])
+  ["shared.js" "shared.js.map" "main.js" "main.js.map" "code-editor.js" "excalidraw.js" "tldraw.js" "db-worker.js" "db-worker.js.map"])
 
 (def ^:api static-dirs
   "dirs under static dir to copy over"
@@ -97,8 +97,8 @@
                 custom-js (if (fs/existsSync custom-js-path) (str (fs/readFileSync custom-js-path)) "")
                 _ (fs/writeFileSync (node-path/join output-static-dir "js" "custom.js") custom-js)
                 _ (cleanup-js-dir output-static-dir static-dir options)]
-               (notification-fn {:type "success"
-                                 :payload (str "Export public pages and publish assets to " output-dir " successfully 🎉")}))
+          (notification-fn {:type "success"
+                            :payload (str "Export public pages and publish assets to " output-dir " successfully 🎉")}))
         (p/catch (fn [error]
                    (notification-fn {:type "error"
                                      :payload (str "Export public pages unexpectedly failed with: " error)}))))))

+ 6 - 5
deps/publishing/src/logseq/publishing/html.cljs

@@ -5,8 +5,8 @@ necessary db filtering"
             [goog.string :as gstring]
             [goog.string.format]
             [datascript.transit :as dt]
-            [logseq.publishing.db :as db]
-            [logseq.db.sqlite.util :as sqlite-util]))
+            [datascript.core :as d]
+            [logseq.publishing.db :as db]))
 
 ;; Copied from hiccup but tweaked for publish usage
 ;; Any changes here should also be made in frontend.publishing/unescape-html
@@ -136,21 +136,22 @@ necessary db filtering"
 (defn build-html
   "Given the graph's db, filters the db using the given options and returns the
 generated index.html string and assets used by the html"
-  [db* {:keys [app-state repo-config html-options db-graph?]}]
+  [db* {:keys [repo app-state repo-config html-options db-graph? dev?]}]
   (let [all-pages-public? (if-let [value (:publishing/all-pages-public? repo-config)]
                             value
                             (:all-pages-public? repo-config))
         [db asset-filenames'] (if all-pages-public?
                                 (db/clean-export! db* {:db-graph? db-graph?})
                                 (db/filter-only-public-pages-and-blocks db* {:db-graph? db-graph?}))
+        _ (when dev?
+            (println "Exporting" (count (d/datoms db :eavt)) "of" (count (d/datoms db* :eavt)) "datoms..."))
         asset-filenames (remove nil? asset-filenames')
 
         db-str (dt/write-transit-str db)
-        repo-name (if db-graph? (str sqlite-util/db-version-prefix "Demo") "Demo")
         ;; The repo-name is used by the client and thus determines whether
         ;; it's a db graph or not
         state (assoc app-state
-                     :config {repo-name repo-config})
+                     :config {repo repo-config})
         raw-html-str (publishing-html db-str state html-options)]
     {:html raw-html-str
      :asset-filenames asset-filenames}))

+ 228 - 1
deps/publishing/yarn.lock

@@ -23,11 +23,53 @@ at-least-node@^1.0.0:
   resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
   integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==
 
+base64-js@^1.3.1:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
+  integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
+
[email protected]:
+  version "9.3.0"
+  resolved "https://registry.yarnpkg.com/better-sqlite3/-/better-sqlite3-9.3.0.tgz#2a8aaad65fa0210a4df5e8a0bcbc9156f6138d56"
+  integrity sha512-ww73jVpQhRRdS9uMr761ixlkl4bWoXi8hMQlBGhoN6vPNlUHpIsNmw4pKN6kjknlt/wopdvXHvLk1W75BI+n0Q==
+  dependencies:
+    bindings "^1.5.0"
+    prebuild-install "^7.1.1"
+
+bindings@^1.5.0:
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df"
+  integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==
+  dependencies:
+    file-uri-to-path "1.0.0"
+
+bl@^4.0.3:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
+  integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==
+  dependencies:
+    buffer "^5.5.0"
+    inherits "^2.0.4"
+    readable-stream "^3.4.0"
+
+buffer@^5.5.0:
+  version "5.7.1"
+  resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
+  integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
+  dependencies:
+    base64-js "^1.3.1"
+    ieee754 "^1.1.13"
+
 camelcase@^5.0.0:
   version "5.3.1"
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
   integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
 
+chownr@^1.1.1:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
+  integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
+
 cliui@^4.0.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49"
@@ -58,7 +100,24 @@ decamelize@^1.2.0:
   resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
   integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==
 
-end-of-stream@^1.1.0:
+decompress-response@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc"
+  integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==
+  dependencies:
+    mimic-response "^3.1.0"
+
+deep-extend@^0.6.0:
+  version "0.6.0"
+  resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
+  integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
+
+detect-libc@^2.0.0:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700"
+  integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==
+
+end-of-stream@^1.1.0, end-of-stream@^1.4.1:
   version "1.4.4"
   resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
   integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
@@ -78,6 +137,16 @@ execa@^1.0.0:
     signal-exit "^3.0.0"
     strip-eof "^1.0.0"
 
+expand-template@^2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c"
+  integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==
+
[email protected]:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
+  integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==
+
 find-up@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73"
@@ -85,6 +154,11 @@ find-up@^3.0.0:
   dependencies:
     locate-path "^3.0.0"
 
+fs-constants@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
+  integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
+
 [email protected]:
   version "9.1.0"
   resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d"
@@ -107,16 +181,36 @@ get-stream@^4.0.0:
   dependencies:
     pump "^3.0.0"
 
[email protected]:
+  version "0.0.0"
+  resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce"
+  integrity sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==
+
 graceful-fs@^4.1.6, graceful-fs@^4.2.0:
   version "4.2.11"
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
   integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
 
+ieee754@^1.1.13:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
+  integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
+
 import-meta-resolve@^2.1.0:
   version "2.2.2"
   resolved "https://registry.yarnpkg.com/import-meta-resolve/-/import-meta-resolve-2.2.2.tgz#75237301e72d1f0fbd74dbc6cca9324b164c2cc9"
   integrity sha512-f8KcQ1D80V7RnqVm+/lirO9zkOxjGxhaTC1IPrBGd3MEfNgmNG67tSUO9gTi2F3Blr2Az6g1vocaxzkVnWl9MA==
 
+inherits@^2.0.3, inherits@^2.0.4:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
+  integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
+ini@~1.3.0:
+  version "1.3.8"
+  resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
+  integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
+
 invert-kv@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02"
@@ -189,6 +283,21 @@ mimic-fn@^2.0.0:
   resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
   integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
 
+mimic-response@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9"
+  integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==
+
+minimist@^1.2.0, minimist@^1.2.3:
+  version "1.2.8"
+  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
+  integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
+
+mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3:
+  version "0.5.3"
+  resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
+  integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
+
 mldoc@^1.5.9:
   version "1.5.9"
   resolved "https://registry.yarnpkg.com/mldoc/-/mldoc-1.5.9.tgz#43d740351c64285f0f4988ac9497922d54ae66fc"
@@ -196,11 +305,23 @@ mldoc@^1.5.9:
   dependencies:
     yargs "^12.0.2"
 
+napi-build-utils@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806"
+  integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==
+
 nice-try@^1.0.4:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
   integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
 
+node-abi@^3.3.0:
+  version "3.71.0"
+  resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.71.0.tgz#52d84bbcd8575efb71468fbaa1f9a49b2c242038"
+  integrity sha512-SZ40vRiy/+wRTf21hxkkEjPJZpARzUMVcJoQse2EF8qkUWbbO2z7vd5oA/H6bVH6SZQ5STGcu0KRDS7biNRfxw==
+  dependencies:
+    semver "^7.3.5"
+
 npm-run-path@^2.0.0:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
@@ -273,6 +394,24 @@ path-key@^2.0.0, path-key@^2.0.1:
   resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
   integrity sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==
 
+prebuild-install@^7.1.1:
+  version "7.1.2"
+  resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.2.tgz#a5fd9986f5a6251fbc47e1e5c65de71e68c0a056"
+  integrity sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==
+  dependencies:
+    detect-libc "^2.0.0"
+    expand-template "^2.0.3"
+    github-from-package "0.0.0"
+    minimist "^1.2.3"
+    mkdirp-classic "^0.5.3"
+    napi-build-utils "^1.0.1"
+    node-abi "^3.3.0"
+    pump "^3.0.0"
+    rc "^1.2.7"
+    simple-get "^4.0.0"
+    tar-fs "^2.0.0"
+    tunnel-agent "^0.6.0"
+
 pump@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
@@ -281,6 +420,25 @@ pump@^3.0.0:
     end-of-stream "^1.1.0"
     once "^1.3.1"
 
+rc@^1.2.7:
+  version "1.2.8"
+  resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
+  integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
+  dependencies:
+    deep-extend "^0.6.0"
+    ini "~1.3.0"
+    minimist "^1.2.0"
+    strip-json-comments "~2.0.1"
+
+readable-stream@^3.1.1, readable-stream@^3.4.0:
+  version "3.6.2"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967"
+  integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==
+  dependencies:
+    inherits "^2.0.3"
+    string_decoder "^1.1.1"
+    util-deprecate "^1.0.1"
+
 require-directory@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
@@ -291,11 +449,21 @@ require-main-filename@^1.0.1:
   resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
   integrity sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug==
 
+safe-buffer@^5.0.1, safe-buffer@~5.2.0:
+  version "5.2.1"
+  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
+  integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
+
 semver@^5.5.0:
   version "5.7.1"
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
   integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
 
+semver@^7.3.5:
+  version "7.6.3"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143"
+  integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==
+
 set-blocking@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
@@ -318,6 +486,20 @@ signal-exit@^3.0.0:
   resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
   integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
 
+simple-concat@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f"
+  integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==
+
+simple-get@^4.0.0:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543"
+  integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==
+  dependencies:
+    decompress-response "^6.0.0"
+    once "^1.3.1"
+    simple-concat "^1.0.0"
+
 string-width@^1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
@@ -335,6 +517,13 @@ string-width@^2.0.0, string-width@^2.1.1:
     is-fullwidth-code-point "^2.0.0"
     strip-ansi "^4.0.0"
 
+string_decoder@^1.1.1:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
+  integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
+  dependencies:
+    safe-buffer "~5.2.0"
+
 strip-ansi@^3.0.0, strip-ansi@^3.0.1:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
@@ -354,11 +543,49 @@ strip-eof@^1.0.0:
   resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
   integrity sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==
 
+strip-json-comments@~2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
+  integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==
+
+tar-fs@^2.0.0:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784"
+  integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==
+  dependencies:
+    chownr "^1.1.1"
+    mkdirp-classic "^0.5.2"
+    pump "^3.0.0"
+    tar-stream "^2.1.4"
+
+tar-stream@^2.1.4:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287"
+  integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==
+  dependencies:
+    bl "^4.0.3"
+    end-of-stream "^1.4.1"
+    fs-constants "^1.0.0"
+    inherits "^2.0.3"
+    readable-stream "^3.1.1"
+
+tunnel-agent@^0.6.0:
+  version "0.6.0"
+  resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
+  integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==
+  dependencies:
+    safe-buffer "^5.0.1"
+
 universalify@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
   integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
 
+util-deprecate@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+  integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
+
 which-module@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"

+ 1 - 1
docs/contributing-to-translations.md

@@ -102,7 +102,7 @@ you'll need to ensure it doesn't fail. Mistakes that it catches:
 [lang.clj](https://github.com/logseq/logseq/blob/master/scripts/src/logseq/tasks/lang.clj) for your language
 with a list of duplicated entries e.g. `:nb-NO #{:port ...}`.
 
-Nonexistent entries can be removed by running `bb lang:validate-translations --fix`.
+Nonexistent and some invalid entries can be removed by running `bb lang:validate-translations --fix`.
 
 ## Add a Language
 

+ 1 - 0
externs.js

@@ -143,6 +143,7 @@ dummy.ELEMENT = function() {};
 dummy.TEXT = function() {};
 dummy.isAbsolute = function() {};
 dummy._address = function() {};
+dummy.Consumer = {}
 
 var utils = {}
 utils.withFileTypes = true;

+ 7784 - 0
resources/js/pdf_viewer2.js

@@ -0,0 +1,7784 @@
+/**
+ * @licstart The following is the entire license notice for the
+ * JavaScript code in this page
+ *
+ * Copyright 2023 Mozilla Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * @licend The above is the entire license notice for the
+ * JavaScript code in this page
+ */
+
+(function webpackUniversalModuleDefinition(root, factory) {
+	if(typeof exports === 'object' && typeof module === 'object')
+		module.exports = root.pdfjsViewer = factory();
+	else if(typeof define === 'function' && define.amd)
+		define("pdfjs-dist/web/pdf_viewer", [], () => { return (root.pdfjsViewer = factory()); });
+	else if(typeof exports === 'object')
+		exports["pdfjs-dist/web/pdf_viewer"] = root.pdfjsViewer = factory();
+	else
+		root["pdfjs-dist/web/pdf_viewer"] = root.pdfjsViewer = factory();
+})(globalThis, () => {
+return /******/ (() => { // webpackBootstrap
+/******/ 	"use strict";
+/******/ 	var __webpack_modules__ = ([
+/* 0 */,
+/* 1 */
+/***/ ((__unused_webpack_module, exports, __w_pdfjs_require__) => {
+
+
+
+Object.defineProperty(exports, "__esModule", ({
+  value: true
+}));
+exports.PDFFindController = exports.FindState = void 0;
+var _ui_utils = __w_pdfjs_require__(2);
+var _pdf_find_utils = __w_pdfjs_require__(3);
+var _pdfjsLib = __w_pdfjs_require__(4);
+const FindState = {
+  FOUND: 0,
+  NOT_FOUND: 1,
+  WRAPPED: 2,
+  PENDING: 3
+};
+exports.FindState = FindState;
+const FIND_TIMEOUT = 250;
+const MATCH_SCROLL_OFFSET_TOP = -50;
+const MATCH_SCROLL_OFFSET_LEFT = -400;
+const CHARACTERS_TO_NORMALIZE = {
+  "\u2010": "-",
+  "\u2018": "'",
+  "\u2019": "'",
+  "\u201A": "'",
+  "\u201B": "'",
+  "\u201C": '"',
+  "\u201D": '"',
+  "\u201E": '"',
+  "\u201F": '"',
+  "\u00BC": "1/4",
+  "\u00BD": "1/2",
+  "\u00BE": "3/4"
+};
+const DIACRITICS_EXCEPTION = new Set([0x3099, 0x309a, 0x094d, 0x09cd, 0x0a4d, 0x0acd, 0x0b4d, 0x0bcd, 0x0c4d, 0x0ccd, 0x0d3b, 0x0d3c, 0x0d4d, 0x0dca, 0x0e3a, 0x0eba, 0x0f84, 0x1039, 0x103a, 0x1714, 0x1734, 0x17d2, 0x1a60, 0x1b44, 0x1baa, 0x1bab, 0x1bf2, 0x1bf3, 0x2d7f, 0xa806, 0xa82c, 0xa8c4, 0xa953, 0xa9c0, 0xaaf6, 0xabed, 0x0c56, 0x0f71, 0x0f72, 0x0f7a, 0x0f7b, 0x0f7c, 0x0f7d, 0x0f80, 0x0f74]);
+let DIACRITICS_EXCEPTION_STR;
+const DIACRITICS_REG_EXP = /\p{M}+/gu;
+const SPECIAL_CHARS_REG_EXP = /([.*+?^${}()|[\]\\])|(\p{P})|(\s+)|(\p{M})|(\p{L})/gu;
+const NOT_DIACRITIC_FROM_END_REG_EXP = /([^\p{M}])\p{M}*$/u;
+const NOT_DIACRITIC_FROM_START_REG_EXP = /^\p{M}*([^\p{M}])/u;
+const SYLLABLES_REG_EXP = /[\uAC00-\uD7AF\uFA6C\uFACF-\uFAD1\uFAD5-\uFAD7]+/g;
+const SYLLABLES_LENGTHS = new Map();
+const FIRST_CHAR_SYLLABLES_REG_EXP = "[\\u1100-\\u1112\\ud7a4-\\ud7af\\ud84a\\ud84c\\ud850\\ud854\\ud857\\ud85f]";
+const NFKC_CHARS_TO_NORMALIZE = new Map();
+let noSyllablesRegExp = null;
+let withSyllablesRegExp = null;
+function normalize(text) {
+  const syllablePositions = [];
+  let m;
+  while ((m = SYLLABLES_REG_EXP.exec(text)) !== null) {
+    let {
+      index
+    } = m;
+    for (const char of m[0]) {
+      let len = SYLLABLES_LENGTHS.get(char);
+      if (!len) {
+        len = char.normalize("NFD").length;
+        SYLLABLES_LENGTHS.set(char, len);
+      }
+      syllablePositions.push([len, index++]);
+    }
+  }
+  let normalizationRegex;
+  if (syllablePositions.length === 0 && noSyllablesRegExp) {
+    normalizationRegex = noSyllablesRegExp;
+  } else if (syllablePositions.length > 0 && withSyllablesRegExp) {
+    normalizationRegex = withSyllablesRegExp;
+  } else {
+    const replace = Object.keys(CHARACTERS_TO_NORMALIZE).join("");
+    const toNormalizeWithNFKC = (0, _pdf_find_utils.getNormalizeWithNFKC)();
+    const CJK = "(?:\\p{Ideographic}|[\u3040-\u30FF])";
+    const HKDiacritics = "(?:\u3099|\u309A)";
+    const regexp = `([${replace}])|([${toNormalizeWithNFKC}])|(${HKDiacritics}\\n)|(\\p{M}+(?:-\\n)?)|(\\S-\\n)|(${CJK}\\n)|(\\n)`;
+    if (syllablePositions.length === 0) {
+      normalizationRegex = noSyllablesRegExp = new RegExp(regexp + "|(\\u0000)", "gum");
+    } else {
+      normalizationRegex = withSyllablesRegExp = new RegExp(regexp + `|(${FIRST_CHAR_SYLLABLES_REG_EXP})`, "gum");
+    }
+  }
+  const rawDiacriticsPositions = [];
+  while ((m = DIACRITICS_REG_EXP.exec(text)) !== null) {
+    rawDiacriticsPositions.push([m[0].length, m.index]);
+  }
+  let normalized = text.normalize("NFD");
+  const positions = [[0, 0]];
+  let rawDiacriticsIndex = 0;
+  let syllableIndex = 0;
+  let shift = 0;
+  let shiftOrigin = 0;
+  let eol = 0;
+  let hasDiacritics = false;
+  normalized = normalized.replace(normalizationRegex, (match, p1, p2, p3, p4, p5, p6, p7, p8, i) => {
+    i -= shiftOrigin;
+    if (p1) {
+      const replacement = CHARACTERS_TO_NORMALIZE[p1];
+      const jj = replacement.length;
+      for (let j = 1; j < jj; j++) {
+        positions.push([i - shift + j, shift - j]);
+      }
+      shift -= jj - 1;
+      return replacement;
+    }
+    if (p2) {
+      let replacement = NFKC_CHARS_TO_NORMALIZE.get(p2);
+      if (!replacement) {
+        replacement = p2.normalize("NFKC");
+        NFKC_CHARS_TO_NORMALIZE.set(p2, replacement);
+      }
+      const jj = replacement.length;
+      for (let j = 1; j < jj; j++) {
+        positions.push([i - shift + j, shift - j]);
+      }
+      shift -= jj - 1;
+      return replacement;
+    }
+    if (p3) {
+      hasDiacritics = true;
+      if (i + eol === rawDiacriticsPositions[rawDiacriticsIndex]?.[1]) {
+        ++rawDiacriticsIndex;
+      } else {
+        positions.push([i - 1 - shift + 1, shift - 1]);
+        shift -= 1;
+        shiftOrigin += 1;
+      }
+      positions.push([i - shift + 1, shift]);
+      shiftOrigin += 1;
+      eol += 1;
+      return p3.charAt(0);
+    }
+    if (p4) {
+      const hasTrailingDashEOL = p4.endsWith("\n");
+      const len = hasTrailingDashEOL ? p4.length - 2 : p4.length;
+      hasDiacritics = true;
+      let jj = len;
+      if (i + eol === rawDiacriticsPositions[rawDiacriticsIndex]?.[1]) {
+        jj -= rawDiacriticsPositions[rawDiacriticsIndex][0];
+        ++rawDiacriticsIndex;
+      }
+      for (let j = 1; j <= jj; j++) {
+        positions.push([i - 1 - shift + j, shift - j]);
+      }
+      shift -= jj;
+      shiftOrigin += jj;
+      if (hasTrailingDashEOL) {
+        i += len - 1;
+        positions.push([i - shift + 1, 1 + shift]);
+        shift += 1;
+        shiftOrigin += 1;
+        eol += 1;
+        return p4.slice(0, len);
+      }
+      return p4;
+    }
+    if (p5) {
+      const len = p5.length - 2;
+      positions.push([i - shift + len, 1 + shift]);
+      shift += 1;
+      shiftOrigin += 1;
+      eol += 1;
+      return p5.slice(0, -2);
+    }
+    if (p6) {
+      const len = p6.length - 1;
+      positions.push([i - shift + len, shift]);
+      shiftOrigin += 1;
+      eol += 1;
+      return p6.slice(0, -1);
+    }
+    if (p7) {
+      positions.push([i - shift + 1, shift - 1]);
+      shift -= 1;
+      shiftOrigin += 1;
+      eol += 1;
+      return " ";
+    }
+    if (i + eol === syllablePositions[syllableIndex]?.[1]) {
+      const newCharLen = syllablePositions[syllableIndex][0] - 1;
+      ++syllableIndex;
+      for (let j = 1; j <= newCharLen; j++) {
+        positions.push([i - (shift - j), shift - j]);
+      }
+      shift -= newCharLen;
+      shiftOrigin += newCharLen;
+    }
+    return p8;
+  });
+  positions.push([normalized.length, shift]);
+  return [normalized, positions, hasDiacritics];
+}
+function getOriginalIndex(diffs, pos, len) {
+  if (!diffs) {
+    return [pos, len];
+  }
+  const start = pos;
+  const end = pos + len - 1;
+  let i = (0, _ui_utils.binarySearchFirstItem)(diffs, x => x[0] >= start);
+  if (diffs[i][0] > start) {
+    --i;
+  }
+  let j = (0, _ui_utils.binarySearchFirstItem)(diffs, x => x[0] >= end, i);
+  if (diffs[j][0] > end) {
+    --j;
+  }
+  const oldStart = start + diffs[i][1];
+  const oldEnd = end + diffs[j][1];
+  const oldLen = oldEnd + 1 - oldStart;
+  return [oldStart, oldLen];
+}
+class PDFFindController {
+  #state = null;
+  #updateMatchesCountOnProgress = true;
+  #visitedPagesCount = 0;
+  constructor({
+    linkService,
+    eventBus,
+    updateMatchesCountOnProgress = true
+  }) {
+    this._linkService = linkService;
+    this._eventBus = eventBus;
+    this.#updateMatchesCountOnProgress = updateMatchesCountOnProgress;
+    this.onIsPageVisible = null;
+    this.#reset();
+    eventBus._on("find", this.#onFind.bind(this));
+    eventBus._on("findbarclose", this.#onFindBarClose.bind(this));
+  }
+  get highlightMatches() {
+    return this._highlightMatches;
+  }
+  get pageMatches() {
+    return this._pageMatches;
+  }
+  get pageMatchesLength() {
+    return this._pageMatchesLength;
+  }
+  get selected() {
+    return this._selected;
+  }
+  get state() {
+    return this.#state;
+  }
+  setDocument(pdfDocument) {
+    if (this._pdfDocument) {
+      this.#reset();
+    }
+    if (!pdfDocument) {
+      return;
+    }
+    this._pdfDocument = pdfDocument;
+    this._firstPageCapability.resolve();
+  }
+  #onFind(state) {
+    if (!state) {
+      return;
+    }
+    if (state.phraseSearch === false) {
+      console.error("The `phraseSearch`-parameter was removed, please provide " + "an Array of strings in the `query`-parameter instead.");
+      if (typeof state.query === "string") {
+        state.query = state.query.match(/\S+/g);
+      }
+    }
+    const pdfDocument = this._pdfDocument;
+    const {
+      type
+    } = state;
+    if (this.#state === null || this.#shouldDirtyMatch(state)) {
+      this._dirtyMatch = true;
+    }
+    this.#state = state;
+    if (type !== "highlightallchange") {
+      this.#updateUIState(FindState.PENDING);
+    }
+    this._firstPageCapability.promise.then(() => {
+      if (!this._pdfDocument || pdfDocument && this._pdfDocument !== pdfDocument) {
+        return;
+      }
+      this.#extractText();
+      const findbarClosed = !this._highlightMatches;
+      const pendingTimeout = !!this._findTimeout;
+      if (this._findTimeout) {
+        clearTimeout(this._findTimeout);
+        this._findTimeout = null;
+      }
+      if (!type) {
+        this._findTimeout = setTimeout(() => {
+          this.#nextMatch();
+          this._findTimeout = null;
+        }, FIND_TIMEOUT);
+      } else if (this._dirtyMatch) {
+        this.#nextMatch();
+      } else if (type === "again") {
+        this.#nextMatch();
+        if (findbarClosed && this.#state.highlightAll) {
+          this.#updateAllPages();
+        }
+      } else if (type === "highlightallchange") {
+        if (pendingTimeout) {
+          this.#nextMatch();
+        } else {
+          this._highlightMatches = true;
+        }
+        this.#updateAllPages();
+      } else {
+        this.#nextMatch();
+      }
+    });
+  }
+  scrollMatchIntoView({
+    element = null,
+    selectedLeft = 0,
+    pageIndex = -1,
+    matchIndex = -1
+  }) {
+    if (!this._scrollMatches || !element) {
+      return;
+    } else if (matchIndex === -1 || matchIndex !== this._selected.matchIdx) {
+      return;
+    } else if (pageIndex === -1 || pageIndex !== this._selected.pageIdx) {
+      return;
+    }
+    this._scrollMatches = false;
+    const spot = {
+      top: MATCH_SCROLL_OFFSET_TOP,
+      left: selectedLeft + MATCH_SCROLL_OFFSET_LEFT
+    };
+    (0, _ui_utils.scrollIntoView)(element, spot, true);
+  }
+  #reset() {
+    this._highlightMatches = false;
+    this._scrollMatches = false;
+    this._pdfDocument = null;
+    this._pageMatches = [];
+    this._pageMatchesLength = [];
+    this.#visitedPagesCount = 0;
+    this.#state = null;
+    this._selected = {
+      pageIdx: -1,
+      matchIdx: -1
+    };
+    this._offset = {
+      pageIdx: null,
+      matchIdx: null,
+      wrapped: false
+    };
+    this._extractTextPromises = [];
+    this._pageContents = [];
+    this._pageDiffs = [];
+    this._hasDiacritics = [];
+    this._matchesCountTotal = 0;
+    this._pagesToSearch = null;
+    this._pendingFindMatches = new Set();
+    this._resumePageIdx = null;
+    this._dirtyMatch = false;
+    clearTimeout(this._findTimeout);
+    this._findTimeout = null;
+    this._firstPageCapability = new _pdfjsLib.PromiseCapability();
+  }
+  get #query() {
+    const {
+      query
+    } = this.#state;
+    if (typeof query === "string") {
+      if (query !== this._rawQuery) {
+        this._rawQuery = query;
+        [this._normalizedQuery] = normalize(query);
+      }
+      return this._normalizedQuery;
+    }
+    return (query || []).filter(q => !!q).map(q => normalize(q)[0]);
+  }
+  #shouldDirtyMatch(state) {
+    const newQuery = state.query,
+      prevQuery = this.#state.query;
+    const newType = typeof newQuery,
+      prevType = typeof prevQuery;
+    if (newType !== prevType) {
+      return true;
+    }
+    if (newType === "string") {
+      if (newQuery !== prevQuery) {
+        return true;
+      }
+    } else if (JSON.stringify(newQuery) !== JSON.stringify(prevQuery)) {
+      return true;
+    }
+    switch (state.type) {
+      case "again":
+        const pageNumber = this._selected.pageIdx + 1;
+        const linkService = this._linkService;
+        return pageNumber >= 1 && pageNumber <= linkService.pagesCount && pageNumber !== linkService.page && !(this.onIsPageVisible?.(pageNumber) ?? true);
+      case "highlightallchange":
+        return false;
+    }
+    return true;
+  }
+  #isEntireWord(content, startIdx, length) {
+    let match = content.slice(0, startIdx).match(NOT_DIACRITIC_FROM_END_REG_EXP);
+    if (match) {
+      const first = content.charCodeAt(startIdx);
+      const limit = match[1].charCodeAt(0);
+      if ((0, _pdf_find_utils.getCharacterType)(first) === (0, _pdf_find_utils.getCharacterType)(limit)) {
+        return false;
+      }
+    }
+    match = content.slice(startIdx + length).match(NOT_DIACRITIC_FROM_START_REG_EXP);
+    if (match) {
+      const last = content.charCodeAt(startIdx + length - 1);
+      const limit = match[1].charCodeAt(0);
+      if ((0, _pdf_find_utils.getCharacterType)(last) === (0, _pdf_find_utils.getCharacterType)(limit)) {
+        return false;
+      }
+    }
+    return true;
+  }
+  #calculateRegExpMatch(query, entireWord, pageIndex, pageContent) {
+    const matches = this._pageMatches[pageIndex] = [];
+    const matchesLength = this._pageMatchesLength[pageIndex] = [];
+    if (!query) {
+      return;
+    }
+    const diffs = this._pageDiffs[pageIndex];
+    let match;
+    while ((match = query.exec(pageContent)) !== null) {
+      if (entireWord && !this.#isEntireWord(pageContent, match.index, match[0].length)) {
+        continue;
+      }
+      const [matchPos, matchLen] = getOriginalIndex(diffs, match.index, match[0].length);
+      if (matchLen) {
+        matches.push(matchPos);
+        matchesLength.push(matchLen);
+      }
+    }
+  }
+  #convertToRegExpString(query, hasDiacritics) {
+    const {
+      matchDiacritics
+    } = this.#state;
+    let isUnicode = false;
+    query = query.replaceAll(SPECIAL_CHARS_REG_EXP, (match, p1, p2, p3, p4, p5) => {
+      if (p1) {
+        return `[ ]*\\${p1}[ ]*`;
+      }
+      if (p2) {
+        return `[ ]*${p2}[ ]*`;
+      }
+      if (p3) {
+        return "[ ]+";
+      }
+      if (matchDiacritics) {
+        return p4 || p5;
+      }
+      if (p4) {
+        return DIACRITICS_EXCEPTION.has(p4.charCodeAt(0)) ? p4 : "";
+      }
+      if (hasDiacritics) {
+        isUnicode = true;
+        return `${p5}\\p{M}*`;
+      }
+      return p5;
+    });
+    const trailingSpaces = "[ ]*";
+    if (query.endsWith(trailingSpaces)) {
+      query = query.slice(0, query.length - trailingSpaces.length);
+    }
+    if (matchDiacritics) {
+      if (hasDiacritics) {
+        DIACRITICS_EXCEPTION_STR ||= String.fromCharCode(...DIACRITICS_EXCEPTION);
+        isUnicode = true;
+        query = `${query}(?=[${DIACRITICS_EXCEPTION_STR}]|[^\\p{M}]|$)`;
+      }
+    }
+    return [isUnicode, query];
+  }
+  #calculateMatch(pageIndex) {
+    let query = this.#query;
+    if (query.length === 0) {
+      return;
+    }
+    const {
+      caseSensitive,
+      entireWord
+    } = this.#state;
+    const pageContent = this._pageContents[pageIndex];
+    const hasDiacritics = this._hasDiacritics[pageIndex];
+    let isUnicode = false;
+    if (typeof query === "string") {
+      [isUnicode, query] = this.#convertToRegExpString(query, hasDiacritics);
+    } else {
+      query = query.sort().reverse().map(q => {
+        const [isUnicodePart, queryPart] = this.#convertToRegExpString(q, hasDiacritics);
+        isUnicode ||= isUnicodePart;
+        return `(${queryPart})`;
+      }).join("|");
+    }
+    const flags = `g${isUnicode ? "u" : ""}${caseSensitive ? "" : "i"}`;
+    query = query ? new RegExp(query, flags) : null;
+    this.#calculateRegExpMatch(query, entireWord, pageIndex, pageContent);
+    if (this.#state.highlightAll) {
+      this.#updatePage(pageIndex);
+    }
+    if (this._resumePageIdx === pageIndex) {
+      this._resumePageIdx = null;
+      this.#nextPageMatch();
+    }
+    const pageMatchesCount = this._pageMatches[pageIndex].length;
+    this._matchesCountTotal += pageMatchesCount;
+    if (this.#updateMatchesCountOnProgress) {
+      if (pageMatchesCount > 0) {
+        this.#updateUIResultsCount();
+      }
+    } else if (++this.#visitedPagesCount === this._linkService.pagesCount) {
+      this.#updateUIResultsCount();
+    }
+  }
+  #extractText() {
+    if (this._extractTextPromises.length > 0) {
+      return;
+    }
+    let promise = Promise.resolve();
+    const textOptions = {
+      disableNormalization: true
+    };
+    for (let i = 0, ii = this._linkService.pagesCount; i < ii; i++) {
+      const extractTextCapability = new _pdfjsLib.PromiseCapability();
+      this._extractTextPromises[i] = extractTextCapability.promise;
+      promise = promise.then(() => {
+        return this._pdfDocument.getPage(i + 1).then(pdfPage => {
+          return pdfPage.getTextContent(textOptions);
+        }).then(textContent => {
+          const strBuf = [];
+          for (const textItem of textContent.items) {
+            strBuf.push(textItem.str);
+            if (textItem.hasEOL) {
+              strBuf.push("\n");
+            }
+          }
+          [this._pageContents[i], this._pageDiffs[i], this._hasDiacritics[i]] = normalize(strBuf.join(""));
+          extractTextCapability.resolve();
+        }, reason => {
+          console.error(`Unable to get text content for page ${i + 1}`, reason);
+          this._pageContents[i] = "";
+          this._pageDiffs[i] = null;
+          this._hasDiacritics[i] = false;
+          extractTextCapability.resolve();
+        });
+      });
+    }
+  }
+  #updatePage(index) {
+    if (this._scrollMatches && this._selected.pageIdx === index) {
+      this._linkService.page = index + 1;
+    }
+    this._eventBus.dispatch("updatetextlayermatches", {
+      source: this,
+      pageIndex: index
+    });
+  }
+  #updateAllPages() {
+    this._eventBus.dispatch("updatetextlayermatches", {
+      source: this,
+      pageIndex: -1
+    });
+  }
+  #nextMatch() {
+    const previous = this.#state.findPrevious;
+    const currentPageIndex = this._linkService.page - 1;
+    const numPages = this._linkService.pagesCount;
+    this._highlightMatches = true;
+    if (this._dirtyMatch) {
+      this._dirtyMatch = false;
+      this._selected.pageIdx = this._selected.matchIdx = -1;
+      this._offset.pageIdx = currentPageIndex;
+      this._offset.matchIdx = null;
+      this._offset.wrapped = false;
+      this._resumePageIdx = null;
+      this._pageMatches.length = 0;
+      this._pageMatchesLength.length = 0;
+      this.#visitedPagesCount = 0;
+      this._matchesCountTotal = 0;
+      this.#updateAllPages();
+      for (let i = 0; i < numPages; i++) {
+        if (this._pendingFindMatches.has(i)) {
+          continue;
+        }
+        this._pendingFindMatches.add(i);
+        this._extractTextPromises[i].then(() => {
+          this._pendingFindMatches.delete(i);
+          this.#calculateMatch(i);
+        });
+      }
+    }
+    const query = this.#query;
+    if (query.length === 0) {
+      this.#updateUIState(FindState.FOUND);
+      return;
+    }
+    if (this._resumePageIdx) {
+      return;
+    }
+    const offset = this._offset;
+    this._pagesToSearch = numPages;
+    if (offset.matchIdx !== null) {
+      const numPageMatches = this._pageMatches[offset.pageIdx].length;
+      if (!previous && offset.matchIdx + 1 < numPageMatches || previous && offset.matchIdx > 0) {
+        offset.matchIdx = previous ? offset.matchIdx - 1 : offset.matchIdx + 1;
+        this.#updateMatch(true);
+        return;
+      }
+      this.#advanceOffsetPage(previous);
+    }
+    this.#nextPageMatch();
+  }
+  #matchesReady(matches) {
+    const offset = this._offset;
+    const numMatches = matches.length;
+    const previous = this.#state.findPrevious;
+    if (numMatches) {
+      offset.matchIdx = previous ? numMatches - 1 : 0;
+      this.#updateMatch(true);
+      return true;
+    }
+    this.#advanceOffsetPage(previous);
+    if (offset.wrapped) {
+      offset.matchIdx = null;
+      if (this._pagesToSearch < 0) {
+        this.#updateMatch(false);
+        return true;
+      }
+    }
+    return false;
+  }
+  #nextPageMatch() {
+    if (this._resumePageIdx !== null) {
+      console.error("There can only be one pending page.");
+    }
+    let matches = null;
+    do {
+      const pageIdx = this._offset.pageIdx;
+      matches = this._pageMatches[pageIdx];
+      if (!matches) {
+        this._resumePageIdx = pageIdx;
+        break;
+      }
+    } while (!this.#matchesReady(matches));
+  }
+  #advanceOffsetPage(previous) {
+    const offset = this._offset;
+    const numPages = this._linkService.pagesCount;
+    offset.pageIdx = previous ? offset.pageIdx - 1 : offset.pageIdx + 1;
+    offset.matchIdx = null;
+    this._pagesToSearch--;
+    if (offset.pageIdx >= numPages || offset.pageIdx < 0) {
+      offset.pageIdx = previous ? numPages - 1 : 0;
+      offset.wrapped = true;
+    }
+  }
+  #updateMatch(found = false) {
+    let state = FindState.NOT_FOUND;
+    const wrapped = this._offset.wrapped;
+    this._offset.wrapped = false;
+    if (found) {
+      const previousPage = this._selected.pageIdx;
+      this._selected.pageIdx = this._offset.pageIdx;
+      this._selected.matchIdx = this._offset.matchIdx;
+      state = wrapped ? FindState.WRAPPED : FindState.FOUND;
+      if (previousPage !== -1 && previousPage !== this._selected.pageIdx) {
+        this.#updatePage(previousPage);
+      }
+    }
+    this.#updateUIState(state, this.#state.findPrevious);
+    if (this._selected.pageIdx !== -1) {
+      this._scrollMatches = true;
+      this.#updatePage(this._selected.pageIdx);
+    }
+  }
+  #onFindBarClose(evt) {
+    const pdfDocument = this._pdfDocument;
+    this._firstPageCapability.promise.then(() => {
+      if (!this._pdfDocument || pdfDocument && this._pdfDocument !== pdfDocument) {
+        return;
+      }
+      if (this._findTimeout) {
+        clearTimeout(this._findTimeout);
+        this._findTimeout = null;
+      }
+      if (this._resumePageIdx) {
+        this._resumePageIdx = null;
+        this._dirtyMatch = true;
+      }
+      this.#updateUIState(FindState.FOUND);
+      this._highlightMatches = false;
+      this.#updateAllPages();
+    });
+  }
+  #requestMatchesCount() {
+    const {
+      pageIdx,
+      matchIdx
+    } = this._selected;
+    let current = 0,
+      total = this._matchesCountTotal;
+    if (matchIdx !== -1) {
+      for (let i = 0; i < pageIdx; i++) {
+        current += this._pageMatches[i]?.length || 0;
+      }
+      current += matchIdx + 1;
+    }
+    if (current < 1 || current > total) {
+      current = total = 0;
+    }
+    return {
+      current,
+      total
+    };
+  }
+  #updateUIResultsCount() {
+    this._eventBus.dispatch("updatefindmatchescount", {
+      source: this,
+      matchesCount: this.#requestMatchesCount()
+    });
+  }
+  #updateUIState(state, previous = false) {
+    if (!this.#updateMatchesCountOnProgress && (this.#visitedPagesCount !== this._linkService.pagesCount || state === FindState.PENDING)) {
+      return;
+    }
+    this._eventBus.dispatch("updatefindcontrolstate", {
+      source: this,
+      state,
+      previous,
+      matchesCount: this.#requestMatchesCount(),
+      rawQuery: this.#state?.query ?? null
+    });
+  }
+}
+exports.PDFFindController = PDFFindController;
+
+/***/ }),
+/* 2 */
+/***/ ((__unused_webpack_module, exports) => {
+
+
+
+Object.defineProperty(exports, "__esModule", ({
+  value: true
+}));
+exports.animationStarted = exports.VERTICAL_PADDING = exports.UNKNOWN_SCALE = exports.TextLayerMode = exports.SpreadMode = exports.SidebarView = exports.ScrollMode = exports.SCROLLBAR_PADDING = exports.RenderingStates = exports.ProgressBar = exports.PresentationModeState = exports.OutputScale = exports.MIN_SCALE = exports.MAX_SCALE = exports.MAX_AUTO_SCALE = exports.DEFAULT_SCALE_VALUE = exports.DEFAULT_SCALE_DELTA = exports.DEFAULT_SCALE = exports.CursorTool = exports.AutoPrintRegExp = void 0;
+exports.apiPageLayoutToViewerModes = apiPageLayoutToViewerModes;
+exports.apiPageModeToSidebarView = apiPageModeToSidebarView;
+exports.approximateFraction = approximateFraction;
+exports.backtrackBeforeAllVisibleElements = backtrackBeforeAllVisibleElements;
+exports.binarySearchFirstItem = binarySearchFirstItem;
+exports.docStyle = void 0;
+exports.getActiveOrFocusedElement = getActiveOrFocusedElement;
+exports.getPageSizeInches = getPageSizeInches;
+exports.getVisibleElements = getVisibleElements;
+exports.isPortraitOrientation = isPortraitOrientation;
+exports.isValidRotation = isValidRotation;
+exports.isValidScrollMode = isValidScrollMode;
+exports.isValidSpreadMode = isValidSpreadMode;
+exports.normalizeWheelEventDelta = normalizeWheelEventDelta;
+exports.normalizeWheelEventDirection = normalizeWheelEventDirection;
+exports.parseQueryString = parseQueryString;
+exports.removeNullCharacters = removeNullCharacters;
+exports.roundToDivide = roundToDivide;
+exports.scrollIntoView = scrollIntoView;
+exports.toggleCheckedBtn = toggleCheckedBtn;
+exports.toggleExpandedBtn = toggleExpandedBtn;
+exports.watchScroll = watchScroll;
+const DEFAULT_SCALE_VALUE = "auto";
+exports.DEFAULT_SCALE_VALUE = DEFAULT_SCALE_VALUE;
+const DEFAULT_SCALE = 1.0;
+exports.DEFAULT_SCALE = DEFAULT_SCALE;
+const DEFAULT_SCALE_DELTA = 1.1;
+exports.DEFAULT_SCALE_DELTA = DEFAULT_SCALE_DELTA;
+const MIN_SCALE = 0.1;
+exports.MIN_SCALE = MIN_SCALE;
+const MAX_SCALE = 10.0;
+exports.MAX_SCALE = MAX_SCALE;
+const UNKNOWN_SCALE = 0;
+exports.UNKNOWN_SCALE = UNKNOWN_SCALE;
+const MAX_AUTO_SCALE = 1.25;
+exports.MAX_AUTO_SCALE = MAX_AUTO_SCALE;
+const SCROLLBAR_PADDING = 40;
+exports.SCROLLBAR_PADDING = SCROLLBAR_PADDING;
+const VERTICAL_PADDING = 5;
+exports.VERTICAL_PADDING = VERTICAL_PADDING;
+const RenderingStates = {
+  INITIAL: 0,
+  RUNNING: 1,
+  PAUSED: 2,
+  FINISHED: 3
+};
+exports.RenderingStates = RenderingStates;
+const PresentationModeState = {
+  UNKNOWN: 0,
+  NORMAL: 1,
+  CHANGING: 2,
+  FULLSCREEN: 3
+};
+exports.PresentationModeState = PresentationModeState;
+const SidebarView = {
+  UNKNOWN: -1,
+  NONE: 0,
+  THUMBS: 1,
+  OUTLINE: 2,
+  ATTACHMENTS: 3,
+  LAYERS: 4
+};
+exports.SidebarView = SidebarView;
+const TextLayerMode = {
+  DISABLE: 0,
+  ENABLE: 1,
+  ENABLE_PERMISSIONS: 2
+};
+exports.TextLayerMode = TextLayerMode;
+const ScrollMode = {
+  UNKNOWN: -1,
+  VERTICAL: 0,
+  HORIZONTAL: 1,
+  WRAPPED: 2,
+  PAGE: 3
+};
+exports.ScrollMode = ScrollMode;
+const SpreadMode = {
+  UNKNOWN: -1,
+  NONE: 0,
+  ODD: 1,
+  EVEN: 2
+};
+exports.SpreadMode = SpreadMode;
+const CursorTool = {
+  SELECT: 0,
+  HAND: 1,
+  ZOOM: 2
+};
+exports.CursorTool = CursorTool;
+const AutoPrintRegExp = /\bprint\s*\(/;
+exports.AutoPrintRegExp = AutoPrintRegExp;
+class OutputScale {
+  constructor() {
+    const pixelRatio = window.devicePixelRatio || 1;
+    this.sx = pixelRatio;
+    this.sy = pixelRatio;
+  }
+  get scaled() {
+    return this.sx !== 1 || this.sy !== 1;
+  }
+}
+exports.OutputScale = OutputScale;
+function scrollIntoView(element, spot, scrollMatches = false) {
+  let parent = element.offsetParent;
+  if (!parent) {
+    console.error("offsetParent is not set -- cannot scroll");
+    return;
+  }
+  let offsetY = element.offsetTop + element.clientTop;
+  let offsetX = element.offsetLeft + element.clientLeft;
+  while (parent.clientHeight === parent.scrollHeight && parent.clientWidth === parent.scrollWidth || scrollMatches && (parent.classList.contains("markedContent") || getComputedStyle(parent).overflow === "hidden")) {
+    offsetY += parent.offsetTop;
+    offsetX += parent.offsetLeft;
+    parent = parent.offsetParent;
+    if (!parent) {
+      return;
+    }
+  }
+  if (spot) {
+    if (spot.top !== undefined) {
+      offsetY += spot.top;
+    }
+    if (spot.left !== undefined) {
+      offsetX += spot.left;
+      parent.scrollLeft = offsetX;
+    }
+  }
+  parent.scrollTop = offsetY;
+}
+function watchScroll(viewAreaElement, callback) {
+  const debounceScroll = function (evt) {
+    if (rAF) {
+      return;
+    }
+    rAF = window.requestAnimationFrame(function viewAreaElementScrolled() {
+      rAF = null;
+      const currentX = viewAreaElement.scrollLeft;
+      const lastX = state.lastX;
+      if (currentX !== lastX) {
+        state.right = currentX > lastX;
+      }
+      state.lastX = currentX;
+      const currentY = viewAreaElement.scrollTop;
+      const lastY = state.lastY;
+      if (currentY !== lastY) {
+        state.down = currentY > lastY;
+      }
+      state.lastY = currentY;
+      callback(state);
+    });
+  };
+  const state = {
+    right: true,
+    down: true,
+    lastX: viewAreaElement.scrollLeft,
+    lastY: viewAreaElement.scrollTop,
+    _eventHandler: debounceScroll
+  };
+  let rAF = null;
+  viewAreaElement.addEventListener("scroll", debounceScroll, true);
+  return state;
+}
+function parseQueryString(query) {
+  const params = new Map();
+  for (const [key, value] of new URLSearchParams(query)) {
+    params.set(key.toLowerCase(), value);
+  }
+  return params;
+}
+const InvisibleCharactersRegExp = /[\x01-\x1F]/g;
+function removeNullCharacters(str, replaceInvisible = false) {
+  if (typeof str !== "string") {
+    console.error(`The argument must be a string.`);
+    return str;
+  }
+  if (replaceInvisible) {
+    str = str.replaceAll(InvisibleCharactersRegExp, " ");
+  }
+  return str.replaceAll("\x00", "");
+}
+function binarySearchFirstItem(items, condition, start = 0) {
+  let minIndex = start;
+  let maxIndex = items.length - 1;
+  if (maxIndex < 0 || !condition(items[maxIndex])) {
+    return items.length;
+  }
+  if (condition(items[minIndex])) {
+    return minIndex;
+  }
+  while (minIndex < maxIndex) {
+    const currentIndex = minIndex + maxIndex >> 1;
+    const currentItem = items[currentIndex];
+    if (condition(currentItem)) {
+      maxIndex = currentIndex;
+    } else {
+      minIndex = currentIndex + 1;
+    }
+  }
+  return minIndex;
+}
+function approximateFraction(x) {
+  if (Math.floor(x) === x) {
+    return [x, 1];
+  }
+  const xinv = 1 / x;
+  const limit = 8;
+  if (xinv > limit) {
+    return [1, limit];
+  } else if (Math.floor(xinv) === xinv) {
+    return [1, xinv];
+  }
+  const x_ = x > 1 ? xinv : x;
+  let a = 0,
+    b = 1,
+    c = 1,
+    d = 1;
+  while (true) {
+    const p = a + c,
+      q = b + d;
+    if (q > limit) {
+      break;
+    }
+    if (x_ <= p / q) {
+      c = p;
+      d = q;
+    } else {
+      a = p;
+      b = q;
+    }
+  }
+  let result;
+  if (x_ - a / b < c / d - x_) {
+    result = x_ === x ? [a, b] : [b, a];
+  } else {
+    result = x_ === x ? [c, d] : [d, c];
+  }
+  return result;
+}
+function roundToDivide(x, div) {
+  const r = x % div;
+  return r === 0 ? x : Math.round(x - r + div);
+}
+function getPageSizeInches({
+  view,
+  userUnit,
+  rotate
+}) {
+  const [x1, y1, x2, y2] = view;
+  const changeOrientation = rotate % 180 !== 0;
+  const width = (x2 - x1) / 72 * userUnit;
+  const height = (y2 - y1) / 72 * userUnit;
+  return {
+    width: changeOrientation ? height : width,
+    height: changeOrientation ? width : height
+  };
+}
+function backtrackBeforeAllVisibleElements(index, views, top) {
+  if (index < 2) {
+    return index;
+  }
+  let elt = views[index].div;
+  let pageTop = elt.offsetTop + elt.clientTop;
+  if (pageTop >= top) {
+    elt = views[index - 1].div;
+    pageTop = elt.offsetTop + elt.clientTop;
+  }
+  for (let i = index - 2; i >= 0; --i) {
+    elt = views[i].div;
+    if (elt.offsetTop + elt.clientTop + elt.clientHeight <= pageTop) {
+      break;
+    }
+    index = i;
+  }
+  return index;
+}
+function getVisibleElements({
+  scrollEl,
+  views,
+  sortByVisibility = false,
+  horizontal = false,
+  rtl = false
+}) {
+  const top = scrollEl.scrollTop,
+    bottom = top + scrollEl.clientHeight;
+  const left = scrollEl.scrollLeft,
+    right = left + scrollEl.clientWidth;
+  function isElementBottomAfterViewTop(view) {
+    const element = view.div;
+    const elementBottom = element.offsetTop + element.clientTop + element.clientHeight;
+    return elementBottom > top;
+  }
+  function isElementNextAfterViewHorizontally(view) {
+    const element = view.div;
+    const elementLeft = element.offsetLeft + element.clientLeft;
+    const elementRight = elementLeft + element.clientWidth;
+    return rtl ? elementLeft < right : elementRight > left;
+  }
+  const visible = [],
+    ids = new Set(),
+    numViews = views.length;
+  let firstVisibleElementInd = binarySearchFirstItem(views, horizontal ? isElementNextAfterViewHorizontally : isElementBottomAfterViewTop);
+  if (firstVisibleElementInd > 0 && firstVisibleElementInd < numViews && !horizontal) {
+    firstVisibleElementInd = backtrackBeforeAllVisibleElements(firstVisibleElementInd, views, top);
+  }
+  let lastEdge = horizontal ? right : -1;
+  for (let i = firstVisibleElementInd; i < numViews; i++) {
+    const view = views[i],
+      element = view.div;
+    const currentWidth = element.offsetLeft + element.clientLeft;
+    const currentHeight = element.offsetTop + element.clientTop;
+    const viewWidth = element.clientWidth,
+      viewHeight = element.clientHeight;
+    const viewRight = currentWidth + viewWidth;
+    const viewBottom = currentHeight + viewHeight;
+    if (lastEdge === -1) {
+      if (viewBottom >= bottom) {
+        lastEdge = viewBottom;
+      }
+    } else if ((horizontal ? currentWidth : currentHeight) > lastEdge) {
+      break;
+    }
+    if (viewBottom <= top || currentHeight >= bottom || viewRight <= left || currentWidth >= right) {
+      continue;
+    }
+    const hiddenHeight = Math.max(0, top - currentHeight) + Math.max(0, viewBottom - bottom);
+    const hiddenWidth = Math.max(0, left - currentWidth) + Math.max(0, viewRight - right);
+    const fractionHeight = (viewHeight - hiddenHeight) / viewHeight,
+      fractionWidth = (viewWidth - hiddenWidth) / viewWidth;
+    const percent = fractionHeight * fractionWidth * 100 | 0;
+    visible.push({
+      id: view.id,
+      x: currentWidth,
+      y: currentHeight,
+      view,
+      percent,
+      widthPercent: fractionWidth * 100 | 0
+    });
+    ids.add(view.id);
+  }
+  const first = visible[0],
+    last = visible.at(-1);
+  if (sortByVisibility) {
+    visible.sort(function (a, b) {
+      const pc = a.percent - b.percent;
+      if (Math.abs(pc) > 0.001) {
+        return -pc;
+      }
+      return a.id - b.id;
+    });
+  }
+  return {
+    first,
+    last,
+    views: visible,
+    ids
+  };
+}
+function normalizeWheelEventDirection(evt) {
+  let delta = Math.hypot(evt.deltaX, evt.deltaY);
+  const angle = Math.atan2(evt.deltaY, evt.deltaX);
+  if (-0.25 * Math.PI < angle && angle < 0.75 * Math.PI) {
+    delta = -delta;
+  }
+  return delta;
+}
+function normalizeWheelEventDelta(evt) {
+  const deltaMode = evt.deltaMode;
+  let delta = normalizeWheelEventDirection(evt);
+  const MOUSE_PIXELS_PER_LINE = 30;
+  const MOUSE_LINES_PER_PAGE = 30;
+  if (deltaMode === WheelEvent.DOM_DELTA_PIXEL) {
+    delta /= MOUSE_PIXELS_PER_LINE * MOUSE_LINES_PER_PAGE;
+  } else if (deltaMode === WheelEvent.DOM_DELTA_LINE) {
+    delta /= MOUSE_LINES_PER_PAGE;
+  }
+  return delta;
+}
+function isValidRotation(angle) {
+  return Number.isInteger(angle) && angle % 90 === 0;
+}
+function isValidScrollMode(mode) {
+  return Number.isInteger(mode) && Object.values(ScrollMode).includes(mode) && mode !== ScrollMode.UNKNOWN;
+}
+function isValidSpreadMode(mode) {
+  return Number.isInteger(mode) && Object.values(SpreadMode).includes(mode) && mode !== SpreadMode.UNKNOWN;
+}
+function isPortraitOrientation(size) {
+  return size.width <= size.height;
+}
+const animationStarted = new Promise(function (resolve) {
+  window.requestAnimationFrame(resolve);
+});
+exports.animationStarted = animationStarted;
+const docStyle = document.documentElement.style;
+exports.docStyle = docStyle;
+function clamp(v, min, max) {
+  return Math.min(Math.max(v, min), max);
+}
+class ProgressBar {
+  #classList = null;
+  #disableAutoFetchTimeout = null;
+  #percent = 0;
+  #style = null;
+  #visible = true;
+  constructor(bar) {
+    this.#classList = bar.classList;
+    this.#style = bar.style;
+  }
+  get percent() {
+    return this.#percent;
+  }
+  set percent(val) {
+    this.#percent = clamp(val, 0, 100);
+    if (isNaN(val)) {
+      this.#classList.add("indeterminate");
+      return;
+    }
+    this.#classList.remove("indeterminate");
+    this.#style.setProperty("--progressBar-percent", `${this.#percent}%`);
+  }
+  setWidth(viewer) {
+    if (!viewer) {
+      return;
+    }
+    const container = viewer.parentNode;
+    const scrollbarWidth = container.offsetWidth - viewer.offsetWidth;
+    if (scrollbarWidth > 0) {
+      this.#style.setProperty("--progressBar-end-offset", `${scrollbarWidth}px`);
+    }
+  }
+  setDisableAutoFetch(delay = 5000) {
+    if (isNaN(this.#percent)) {
+      return;
+    }
+    if (this.#disableAutoFetchTimeout) {
+      clearTimeout(this.#disableAutoFetchTimeout);
+    }
+    this.show();
+    this.#disableAutoFetchTimeout = setTimeout(() => {
+      this.#disableAutoFetchTimeout = null;
+      this.hide();
+    }, delay);
+  }
+  hide() {
+    if (!this.#visible) {
+      return;
+    }
+    this.#visible = false;
+    this.#classList.add("hidden");
+  }
+  show() {
+    if (this.#visible) {
+      return;
+    }
+    this.#visible = true;
+    this.#classList.remove("hidden");
+  }
+}
+exports.ProgressBar = ProgressBar;
+function getActiveOrFocusedElement() {
+  let curRoot = document;
+  let curActiveOrFocused = curRoot.activeElement || curRoot.querySelector(":focus");
+  while (curActiveOrFocused?.shadowRoot) {
+    curRoot = curActiveOrFocused.shadowRoot;
+    curActiveOrFocused = curRoot.activeElement || curRoot.querySelector(":focus");
+  }
+  return curActiveOrFocused;
+}
+function apiPageLayoutToViewerModes(layout) {
+  let scrollMode = ScrollMode.VERTICAL,
+    spreadMode = SpreadMode.NONE;
+  switch (layout) {
+    case "SinglePage":
+      scrollMode = ScrollMode.PAGE;
+      break;
+    case "OneColumn":
+      break;
+    case "TwoPageLeft":
+      scrollMode = ScrollMode.PAGE;
+    case "TwoColumnLeft":
+      spreadMode = SpreadMode.ODD;
+      break;
+    case "TwoPageRight":
+      scrollMode = ScrollMode.PAGE;
+    case "TwoColumnRight":
+      spreadMode = SpreadMode.EVEN;
+      break;
+  }
+  return {
+    scrollMode,
+    spreadMode
+  };
+}
+function apiPageModeToSidebarView(mode) {
+  switch (mode) {
+    case "UseNone":
+      return SidebarView.NONE;
+    case "UseThumbs":
+      return SidebarView.THUMBS;
+    case "UseOutlines":
+      return SidebarView.OUTLINE;
+    case "UseAttachments":
+      return SidebarView.ATTACHMENTS;
+    case "UseOC":
+      return SidebarView.LAYERS;
+  }
+  return SidebarView.NONE;
+}
+function toggleCheckedBtn(button, toggle, view = null) {
+  button.classList.toggle("toggled", toggle);
+  button.setAttribute("aria-checked", toggle);
+  view?.classList.toggle("hidden", !toggle);
+}
+function toggleExpandedBtn(button, toggle, view = null) {
+  button.classList.toggle("toggled", toggle);
+  button.setAttribute("aria-expanded", toggle);
+  view?.classList.toggle("hidden", !toggle);
+}
+
+/***/ }),
+/* 3 */
+/***/ ((__unused_webpack_module, exports) => {
+
+
+
+Object.defineProperty(exports, "__esModule", ({
+  value: true
+}));
+exports.CharacterType = void 0;
+exports.getCharacterType = getCharacterType;
+exports.getNormalizeWithNFKC = getNormalizeWithNFKC;
+const CharacterType = {
+  SPACE: 0,
+  ALPHA_LETTER: 1,
+  PUNCT: 2,
+  HAN_LETTER: 3,
+  KATAKANA_LETTER: 4,
+  HIRAGANA_LETTER: 5,
+  HALFWIDTH_KATAKANA_LETTER: 6,
+  THAI_LETTER: 7
+};
+exports.CharacterType = CharacterType;
+function isAlphabeticalScript(charCode) {
+  return charCode < 0x2e80;
+}
+function isAscii(charCode) {
+  return (charCode & 0xff80) === 0;
+}
+function isAsciiAlpha(charCode) {
+  return charCode >= 0x61 && charCode <= 0x7a || charCode >= 0x41 && charCode <= 0x5a;
+}
+function isAsciiDigit(charCode) {
+  return charCode >= 0x30 && charCode <= 0x39;
+}
+function isAsciiSpace(charCode) {
+  return charCode === 0x20 || charCode === 0x09 || charCode === 0x0d || charCode === 0x0a;
+}
+function isHan(charCode) {
+  return charCode >= 0x3400 && charCode <= 0x9fff || charCode >= 0xf900 && charCode <= 0xfaff;
+}
+function isKatakana(charCode) {
+  return charCode >= 0x30a0 && charCode <= 0x30ff;
+}
+function isHiragana(charCode) {
+  return charCode >= 0x3040 && charCode <= 0x309f;
+}
+function isHalfwidthKatakana(charCode) {
+  return charCode >= 0xff60 && charCode <= 0xff9f;
+}
+function isThai(charCode) {
+  return (charCode & 0xff80) === 0x0e00;
+}
+function getCharacterType(charCode) {
+  if (isAlphabeticalScript(charCode)) {
+    if (isAscii(charCode)) {
+      if (isAsciiSpace(charCode)) {
+        return CharacterType.SPACE;
+      } else if (isAsciiAlpha(charCode) || isAsciiDigit(charCode) || charCode === 0x5f) {
+        return CharacterType.ALPHA_LETTER;
+      }
+      return CharacterType.PUNCT;
+    } else if (isThai(charCode)) {
+      return CharacterType.THAI_LETTER;
+    } else if (charCode === 0xa0) {
+      return CharacterType.SPACE;
+    }
+    return CharacterType.ALPHA_LETTER;
+  }
+  if (isHan(charCode)) {
+    return CharacterType.HAN_LETTER;
+  } else if (isKatakana(charCode)) {
+    return CharacterType.KATAKANA_LETTER;
+  } else if (isHiragana(charCode)) {
+    return CharacterType.HIRAGANA_LETTER;
+  } else if (isHalfwidthKatakana(charCode)) {
+    return CharacterType.HALFWIDTH_KATAKANA_LETTER;
+  }
+  return CharacterType.ALPHA_LETTER;
+}
+let NormalizeWithNFKC;
+function getNormalizeWithNFKC() {
+  NormalizeWithNFKC ||= ` ¨ª¯²-µ¸-º¼-¾IJ-ijĿ-ŀʼnſDŽ-njDZ-dzʰ-ʸ˘-˝ˠ-ˤʹͺ;΄-΅·ϐ-ϖϰ-ϲϴ-ϵϹևٵ-ٸक़-य़ড়-ঢ়য়ਲ਼ਸ਼ਖ਼-ਜ਼ਫ਼ଡ଼-ଢ଼ำຳໜ-ໝ༌གྷཌྷདྷབྷཛྷཀྵჼᴬ-ᴮᴰ-ᴺᴼ-ᵍᵏ-ᵪᵸᶛ-ᶿẚ-ẛάέήίόύώΆ᾽-῁ΈΉ῍-῏ΐΊ῝-῟ΰΎ῭-`ΌΏ´-῾ - ‑‗․-… ″-‴‶-‷‼‾⁇-⁉⁗ ⁰-ⁱ⁴-₎ₐ-ₜ₨℀-℃℅-ℇ℉-ℓℕ-№ℙ-ℝ℠-™ℤΩℨK-ℭℯ-ℱℳ-ℹ℻-⅀ⅅ-ⅉ⅐-ⅿ↉∬-∭∯-∰〈-〉①-⓪⨌⩴-⩶⫝̸ⱼ-ⱽⵯ⺟⻳⼀-⿕ 〶〸-〺゛-゜ゟヿㄱ-ㆎ㆒-㆟㈀-㈞㈠-㉇㉐-㉾㊀-㏿ꚜ-ꚝꝰꟲ-ꟴꟸ-ꟹꭜ-ꭟꭩ豈-嗀塚晴凞-羽蘒諸逸-都飯-舘並-龎ff-stﬓ-ﬗיִײַ-זּטּ-לּמּנּ-סּףּ-פּצּ-ﮱﯓ-ﴽﵐ-ﶏﶒ-ﷇﷰ-﷼︐-︙︰-﹄﹇-﹒﹔-﹦﹨-﹫ﹰ-ﹲﹴﹶ-ﻼ!-하-ᅦᅧ-ᅬᅭ-ᅲᅳ-ᅵ¢-₩`;
+  return NormalizeWithNFKC;
+}
+
+/***/ }),
+/* 4 */
+/***/ ((module) => {
+
+
+
+module.exports = globalThis.pdfjsLib;
+
+/***/ }),
+/* 5 */
+/***/ ((__unused_webpack_module, exports, __w_pdfjs_require__) => {
+
+
+
+Object.defineProperty(exports, "__esModule", ({
+  value: true
+}));
+exports.SimpleLinkService = exports.PDFLinkService = exports.LinkTarget = void 0;
+var _ui_utils = __w_pdfjs_require__(2);
+const DEFAULT_LINK_REL = "noopener noreferrer nofollow";
+const LinkTarget = {
+  NONE: 0,
+  SELF: 1,
+  BLANK: 2,
+  PARENT: 3,
+  TOP: 4
+};
+exports.LinkTarget = LinkTarget;
+function addLinkAttributes(link, {
+  url,
+  target,
+  rel,
+  enabled = true
+} = {}) {
+  if (!url || typeof url !== "string") {
+    throw new Error('A valid "url" parameter must provided.');
+  }
+  if (enabled) {
+    link.href = link.title = url;
+  } else {
+    link.href = "";
+    link.title = `Disabled: ${url}`;
+    link.onclick = () => {
+      return false;
+    };
+  }
+  let targetStr = "";
+  switch (target) {
+    case LinkTarget.NONE:
+      break;
+    case LinkTarget.SELF:
+      targetStr = "_self";
+      break;
+    case LinkTarget.BLANK:
+      targetStr = "_blank";
+      break;
+    case LinkTarget.PARENT:
+      targetStr = "_parent";
+      break;
+    case LinkTarget.TOP:
+      targetStr = "_top";
+      break;
+  }
+  link.target = targetStr;
+  link.rel = typeof rel === "string" ? rel : DEFAULT_LINK_REL;
+}
+class PDFLinkService {
+  #pagesRefCache = new Map();
+  constructor({
+    eventBus,
+    externalLinkTarget = null,
+    externalLinkRel = null,
+    ignoreDestinationZoom = false
+  } = {}) {
+    this.eventBus = eventBus;
+    this.externalLinkTarget = externalLinkTarget;
+    this.externalLinkRel = externalLinkRel;
+    this.externalLinkEnabled = true;
+    this._ignoreDestinationZoom = ignoreDestinationZoom;
+    this.baseUrl = null;
+    this.pdfDocument = null;
+    this.pdfViewer = null;
+    this.pdfHistory = null;
+  }
+  setDocument(pdfDocument, baseUrl = null) {
+    this.baseUrl = baseUrl;
+    this.pdfDocument = pdfDocument;
+    this.#pagesRefCache.clear();
+  }
+  setViewer(pdfViewer) {
+    this.pdfViewer = pdfViewer;
+  }
+  setHistory(pdfHistory) {
+    this.pdfHistory = pdfHistory;
+  }
+  get pagesCount() {
+    return this.pdfDocument ? this.pdfDocument.numPages : 0;
+  }
+  get page() {
+    return this.pdfViewer.currentPageNumber;
+  }
+  set page(value) {
+    this.pdfViewer.currentPageNumber = value;
+  }
+  get rotation() {
+    return this.pdfViewer.pagesRotation;
+  }
+  set rotation(value) {
+    this.pdfViewer.pagesRotation = value;
+  }
+  get isInPresentationMode() {
+    return this.pdfViewer.isInPresentationMode;
+  }
+  #goToDestinationHelper(rawDest, namedDest = null, explicitDest) {
+    const destRef = explicitDest[0];
+    let pageNumber;
+    if (typeof destRef === "object" && destRef !== null) {
+      pageNumber = this._cachedPageNumber(destRef);
+      if (!pageNumber) {
+        this.pdfDocument.getPageIndex(destRef).then(pageIndex => {
+          this.cachePageRef(pageIndex + 1, destRef);
+          this.#goToDestinationHelper(rawDest, namedDest, explicitDest);
+        }).catch(() => {
+          console.error(`PDFLinkService.#goToDestinationHelper: "${destRef}" is not ` + `a valid page reference, for dest="${rawDest}".`);
+        });
+        return;
+      }
+    } else if (Number.isInteger(destRef)) {
+      pageNumber = destRef + 1;
+    } else {
+      console.error(`PDFLinkService.#goToDestinationHelper: "${destRef}" is not ` + `a valid destination reference, for dest="${rawDest}".`);
+      return;
+    }
+    if (!pageNumber || pageNumber < 1 || pageNumber > this.pagesCount) {
+      console.error(`PDFLinkService.#goToDestinationHelper: "${pageNumber}" is not ` + `a valid page number, for dest="${rawDest}".`);
+      return;
+    }
+    if (this.pdfHistory) {
+      this.pdfHistory.pushCurrentPosition();
+      this.pdfHistory.push({
+        namedDest,
+        explicitDest,
+        pageNumber
+      });
+    }
+    this.pdfViewer.scrollPageIntoView({
+      pageNumber,
+      destArray: explicitDest,
+      ignoreDestinationZoom: this._ignoreDestinationZoom
+    });
+  }
+  async goToDestination(dest) {
+    if (!this.pdfDocument) {
+      return;
+    }
+    let namedDest, explicitDest;
+    if (typeof dest === "string") {
+      namedDest = dest;
+      explicitDest = await this.pdfDocument.getDestination(dest);
+    } else {
+      namedDest = null;
+      explicitDest = await dest;
+    }
+    if (!Array.isArray(explicitDest)) {
+      console.error(`PDFLinkService.goToDestination: "${explicitDest}" is not ` + `a valid destination array, for dest="${dest}".`);
+      return;
+    }
+    this.#goToDestinationHelper(dest, namedDest, explicitDest);
+  }
+  goToPage(val) {
+    if (!this.pdfDocument) {
+      return;
+    }
+    const pageNumber = typeof val === "string" && this.pdfViewer.pageLabelToPageNumber(val) || val | 0;
+    if (!(Number.isInteger(pageNumber) && pageNumber > 0 && pageNumber <= this.pagesCount)) {
+      console.error(`PDFLinkService.goToPage: "${val}" is not a valid page.`);
+      return;
+    }
+    if (this.pdfHistory) {
+      this.pdfHistory.pushCurrentPosition();
+      this.pdfHistory.pushPage(pageNumber);
+    }
+    this.pdfViewer.scrollPageIntoView({
+      pageNumber
+    });
+  }
+  addLinkAttributes(link, url, newWindow = false) {
+    addLinkAttributes(link, {
+      url,
+      target: newWindow ? LinkTarget.BLANK : this.externalLinkTarget,
+      rel: this.externalLinkRel,
+      enabled: this.externalLinkEnabled
+    });
+  }
+  getDestinationHash(dest) {
+    if (typeof dest === "string") {
+      if (dest.length > 0) {
+        return this.getAnchorUrl("#" + escape(dest));
+      }
+    } else if (Array.isArray(dest)) {
+      const str = JSON.stringify(dest);
+      if (str.length > 0) {
+        return this.getAnchorUrl("#" + escape(str));
+      }
+    }
+    return this.getAnchorUrl("");
+  }
+  getAnchorUrl(anchor) {
+    return this.baseUrl ? this.baseUrl + anchor : anchor;
+  }
+  setHash(hash) {
+    if (!this.pdfDocument) {
+      return;
+    }
+    let pageNumber, dest;
+    if (hash.includes("=")) {
+      const params = (0, _ui_utils.parseQueryString)(hash);
+      if (params.has("search")) {
+        const query = params.get("search").replaceAll('"', ""),
+          phrase = params.get("phrase") === "true";
+        this.eventBus.dispatch("findfromurlhash", {
+          source: this,
+          query: phrase ? query : query.match(/\S+/g)
+        });
+      }
+      if (params.has("page")) {
+        pageNumber = params.get("page") | 0 || 1;
+      }
+      if (params.has("zoom")) {
+        const zoomArgs = params.get("zoom").split(",");
+        const zoomArg = zoomArgs[0];
+        const zoomArgNumber = parseFloat(zoomArg);
+        if (!zoomArg.includes("Fit")) {
+          dest = [null, {
+            name: "XYZ"
+          }, zoomArgs.length > 1 ? zoomArgs[1] | 0 : null, zoomArgs.length > 2 ? zoomArgs[2] | 0 : null, zoomArgNumber ? zoomArgNumber / 100 : zoomArg];
+        } else if (zoomArg === "Fit" || zoomArg === "FitB") {
+          dest = [null, {
+            name: zoomArg
+          }];
+        } else if (zoomArg === "FitH" || zoomArg === "FitBH" || zoomArg === "FitV" || zoomArg === "FitBV") {
+          dest = [null, {
+            name: zoomArg
+          }, zoomArgs.length > 1 ? zoomArgs[1] | 0 : null];
+        } else if (zoomArg === "FitR") {
+          if (zoomArgs.length !== 5) {
+            console.error('PDFLinkService.setHash: Not enough parameters for "FitR".');
+          } else {
+            dest = [null, {
+              name: zoomArg
+            }, zoomArgs[1] | 0, zoomArgs[2] | 0, zoomArgs[3] | 0, zoomArgs[4] | 0];
+          }
+        } else {
+          console.error(`PDFLinkService.setHash: "${zoomArg}" is not a valid zoom value.`);
+        }
+      }
+      if (dest) {
+        this.pdfViewer.scrollPageIntoView({
+          pageNumber: pageNumber || this.page,
+          destArray: dest,
+          allowNegativeOffset: true
+        });
+      } else if (pageNumber) {
+        this.page = pageNumber;
+      }
+      if (params.has("pagemode")) {
+        this.eventBus.dispatch("pagemode", {
+          source: this,
+          mode: params.get("pagemode")
+        });
+      }
+      if (params.has("nameddest")) {
+        this.goToDestination(params.get("nameddest"));
+      }
+    } else {
+      dest = unescape(hash);
+      try {
+        dest = JSON.parse(dest);
+        if (!Array.isArray(dest)) {
+          dest = dest.toString();
+        }
+      } catch {}
+      if (typeof dest === "string" || PDFLinkService.#isValidExplicitDestination(dest)) {
+        this.goToDestination(dest);
+        return;
+      }
+      console.error(`PDFLinkService.setHash: "${unescape(hash)}" is not a valid destination.`);
+    }
+  }
+  executeNamedAction(action) {
+    switch (action) {
+      case "GoBack":
+        this.pdfHistory?.back();
+        break;
+      case "GoForward":
+        this.pdfHistory?.forward();
+        break;
+      case "NextPage":
+        this.pdfViewer.nextPage();
+        break;
+      case "PrevPage":
+        this.pdfViewer.previousPage();
+        break;
+      case "LastPage":
+        this.page = this.pagesCount;
+        break;
+      case "FirstPage":
+        this.page = 1;
+        break;
+      default:
+        break;
+    }
+    this.eventBus.dispatch("namedaction", {
+      source: this,
+      action
+    });
+  }
+  async executeSetOCGState(action) {
+    const pdfDocument = this.pdfDocument;
+    const optionalContentConfig = await this.pdfViewer.optionalContentConfigPromise;
+    if (pdfDocument !== this.pdfDocument) {
+      return;
+    }
+    let operator;
+    for (const elem of action.state) {
+      switch (elem) {
+        case "ON":
+        case "OFF":
+        case "Toggle":
+          operator = elem;
+          continue;
+      }
+      switch (operator) {
+        case "ON":
+          optionalContentConfig.setVisibility(elem, true);
+          break;
+        case "OFF":
+          optionalContentConfig.setVisibility(elem, false);
+          break;
+        case "Toggle":
+          const group = optionalContentConfig.getGroup(elem);
+          if (group) {
+            optionalContentConfig.setVisibility(elem, !group.visible);
+          }
+          break;
+      }
+    }
+    this.pdfViewer.optionalContentConfigPromise = Promise.resolve(optionalContentConfig);
+  }
+  cachePageRef(pageNum, pageRef) {
+    if (!pageRef) {
+      return;
+    }
+    const refStr = pageRef.gen === 0 ? `${pageRef.num}R` : `${pageRef.num}R${pageRef.gen}`;
+    this.#pagesRefCache.set(refStr, pageNum);
+  }
+  _cachedPageNumber(pageRef) {
+    if (!pageRef) {
+      return null;
+    }
+    const refStr = pageRef.gen === 0 ? `${pageRef.num}R` : `${pageRef.num}R${pageRef.gen}`;
+    return this.#pagesRefCache.get(refStr) || null;
+  }
+  static #isValidExplicitDestination(dest) {
+    if (!Array.isArray(dest)) {
+      return false;
+    }
+    const destLength = dest.length;
+    if (destLength < 2) {
+      return false;
+    }
+    const page = dest[0];
+    if (!(typeof page === "object" && Number.isInteger(page.num) && Number.isInteger(page.gen)) && !(Number.isInteger(page) && page >= 0)) {
+      return false;
+    }
+    const zoom = dest[1];
+    if (!(typeof zoom === "object" && typeof zoom.name === "string")) {
+      return false;
+    }
+    let allowNull = true;
+    switch (zoom.name) {
+      case "XYZ":
+        if (destLength !== 5) {
+          return false;
+        }
+        break;
+      case "Fit":
+      case "FitB":
+        return destLength === 2;
+      case "FitH":
+      case "FitBH":
+      case "FitV":
+      case "FitBV":
+        if (destLength !== 3) {
+          return false;
+        }
+        break;
+      case "FitR":
+        if (destLength !== 6) {
+          return false;
+        }
+        allowNull = false;
+        break;
+      default:
+        return false;
+    }
+    for (let i = 2; i < destLength; i++) {
+      const param = dest[i];
+      if (!(typeof param === "number" || allowNull && param === null)) {
+        return false;
+      }
+    }
+    return true;
+  }
+}
+exports.PDFLinkService = PDFLinkService;
+class SimpleLinkService {
+  constructor() {
+    this.externalLinkEnabled = true;
+  }
+  get pagesCount() {
+    return 0;
+  }
+  get page() {
+    return 0;
+  }
+  set page(value) {}
+  get rotation() {
+    return 0;
+  }
+  set rotation(value) {}
+  get isInPresentationMode() {
+    return false;
+  }
+  async goToDestination(dest) {}
+  goToPage(val) {}
+  addLinkAttributes(link, url, newWindow = false) {
+    addLinkAttributes(link, {
+      url,
+      enabled: this.externalLinkEnabled
+    });
+  }
+  getDestinationHash(dest) {
+    return "#";
+  }
+  getAnchorUrl(hash) {
+    return "#";
+  }
+  setHash(hash) {}
+  executeNamedAction(action) {}
+  executeSetOCGState(action) {}
+  cachePageRef(pageNum, pageRef) {}
+}
+exports.SimpleLinkService = SimpleLinkService;
+
+/***/ }),
+/* 6 */
+/***/ ((__unused_webpack_module, exports, __w_pdfjs_require__) => {
+
+
+
+Object.defineProperty(exports, "__esModule", ({
+  value: true
+}));
+exports.AnnotationLayerBuilder = void 0;
+var _pdfjsLib = __w_pdfjs_require__(4);
+var _l10n_utils = __w_pdfjs_require__(7);
+var _ui_utils = __w_pdfjs_require__(2);
+class AnnotationLayerBuilder {
+  #onPresentationModeChanged = null;
+  constructor({
+    pageDiv,
+    pdfPage,
+    linkService,
+    downloadManager,
+    annotationStorage = null,
+    imageResourcesPath = "",
+    renderForms = true,
+    l10n = _l10n_utils.NullL10n,
+    enableScripting = false,
+    hasJSActionsPromise = null,
+    fieldObjectsPromise = null,
+    annotationCanvasMap = null,
+    accessibilityManager = null
+  }) {
+    this.pageDiv = pageDiv;
+    this.pdfPage = pdfPage;
+    this.linkService = linkService;
+    this.downloadManager = downloadManager;
+    this.imageResourcesPath = imageResourcesPath;
+    this.renderForms = renderForms;
+    this.l10n = l10n;
+    this.annotationStorage = annotationStorage;
+    this.enableScripting = enableScripting;
+    this._hasJSActionsPromise = hasJSActionsPromise || Promise.resolve(false);
+    this._fieldObjectsPromise = fieldObjectsPromise || Promise.resolve(null);
+    this._annotationCanvasMap = annotationCanvasMap;
+    this._accessibilityManager = accessibilityManager;
+    this.annotationLayer = null;
+    this.div = null;
+    this._cancelled = false;
+    this._eventBus = linkService.eventBus;
+  }
+  async render(viewport, intent = "display") {
+    if (this.div) {
+      if (this._cancelled || !this.annotationLayer) {
+        return;
+      }
+      this.annotationLayer.update({
+        viewport: viewport.clone({
+          dontFlip: true
+        })
+      });
+      return;
+    }
+    const [annotations, hasJSActions, fieldObjects] = await Promise.all([this.pdfPage.getAnnotations({
+      intent
+    }), this._hasJSActionsPromise, this._fieldObjectsPromise]);
+    if (this._cancelled) {
+      return;
+    }
+    const div = this.div = document.createElement("div");
+    div.className = "annotationLayer";
+    this.pageDiv.append(div);
+    if (annotations.length === 0) {
+      this.hide();
+      return;
+    }
+    this.annotationLayer = new _pdfjsLib.AnnotationLayer({
+      div,
+      accessibilityManager: this._accessibilityManager,
+      annotationCanvasMap: this._annotationCanvasMap,
+      l10n: this.l10n,
+      page: this.pdfPage,
+      viewport: viewport.clone({
+        dontFlip: true
+      })
+    });
+    await this.annotationLayer.render({
+      annotations,
+      imageResourcesPath: this.imageResourcesPath,
+      renderForms: this.renderForms,
+      linkService: this.linkService,
+      downloadManager: this.downloadManager,
+      annotationStorage: this.annotationStorage,
+      enableScripting: this.enableScripting,
+      hasJSActions,
+      fieldObjects
+    });
+    if (this.linkService.isInPresentationMode) {
+      this.#updatePresentationModeState(_ui_utils.PresentationModeState.FULLSCREEN);
+    }
+    if (!this.#onPresentationModeChanged) {
+      this.#onPresentationModeChanged = evt => {
+        this.#updatePresentationModeState(evt.state);
+      };
+      this._eventBus?._on("presentationmodechanged", this.#onPresentationModeChanged);
+    }
+  }
+  cancel() {
+    this._cancelled = true;
+    if (this.#onPresentationModeChanged) {
+      this._eventBus?._off("presentationmodechanged", this.#onPresentationModeChanged);
+      this.#onPresentationModeChanged = null;
+    }
+  }
+  hide() {
+    if (!this.div) {
+      return;
+    }
+    this.div.hidden = true;
+  }
+  #updatePresentationModeState(state) {
+    if (!this.div) {
+      return;
+    }
+    let disableFormElements = false;
+    switch (state) {
+      case _ui_utils.PresentationModeState.FULLSCREEN:
+        disableFormElements = true;
+        break;
+      case _ui_utils.PresentationModeState.NORMAL:
+        break;
+      default:
+        return;
+    }
+    for (const section of this.div.childNodes) {
+      if (section.hasAttribute("data-internal-link")) {
+        continue;
+      }
+      section.inert = disableFormElements;
+    }
+  }
+}
+exports.AnnotationLayerBuilder = AnnotationLayerBuilder;
+
+/***/ }),
+/* 7 */
+/***/ ((__unused_webpack_module, exports) => {
+
+
+
+Object.defineProperty(exports, "__esModule", ({
+  value: true
+}));
+exports.NullL10n = void 0;
+exports.getL10nFallback = getL10nFallback;
+const DEFAULT_L10N_STRINGS = {
+  of_pages: "of {{pagesCount}}",
+  page_of_pages: "({{pageNumber}} of {{pagesCount}})",
+  document_properties_kb: "{{size_kb}} KB ({{size_b}} bytes)",
+  document_properties_mb: "{{size_mb}} MB ({{size_b}} bytes)",
+  document_properties_date_string: "{{date}}, {{time}}",
+  document_properties_page_size_unit_inches: "in",
+  document_properties_page_size_unit_millimeters: "mm",
+  document_properties_page_size_orientation_portrait: "portrait",
+  document_properties_page_size_orientation_landscape: "landscape",
+  document_properties_page_size_name_a3: "A3",
+  document_properties_page_size_name_a4: "A4",
+  document_properties_page_size_name_letter: "Letter",
+  document_properties_page_size_name_legal: "Legal",
+  document_properties_page_size_dimension_string: "{{width}} × {{height}} {{unit}} ({{orientation}})",
+  document_properties_page_size_dimension_name_string: "{{width}} × {{height}} {{unit}} ({{name}}, {{orientation}})",
+  document_properties_linearized_yes: "Yes",
+  document_properties_linearized_no: "No",
+  additional_layers: "Additional Layers",
+  page_landmark: "Page {{page}}",
+  thumb_page_title: "Page {{page}}",
+  thumb_page_canvas: "Thumbnail of Page {{page}}",
+  find_reached_top: "Reached top of document, continued from bottom",
+  find_reached_bottom: "Reached end of document, continued from top",
+  "find_match_count[one]": "{{current}} of {{total}} match",
+  "find_match_count[other]": "{{current}} of {{total}} matches",
+  "find_match_count_limit[one]": "More than {{limit}} match",
+  "find_match_count_limit[other]": "More than {{limit}} matches",
+  find_not_found: "Phrase not found",
+  page_scale_width: "Page Width",
+  page_scale_fit: "Page Fit",
+  page_scale_auto: "Automatic Zoom",
+  page_scale_actual: "Actual Size",
+  page_scale_percent: "{{scale}}%",
+  loading_error: "An error occurred while loading the PDF.",
+  invalid_file_error: "Invalid or corrupted PDF file.",
+  missing_file_error: "Missing PDF file.",
+  unexpected_response_error: "Unexpected server response.",
+  rendering_error: "An error occurred while rendering the page.",
+  annotation_date_string: "{{date}}, {{time}}",
+  printing_not_supported: "Warning: Printing is not fully supported by this browser.",
+  printing_not_ready: "Warning: The PDF is not fully loaded for printing.",
+  web_fonts_disabled: "Web fonts are disabled: unable to use embedded PDF fonts.",
+  free_text2_default_content: "Start typing…",
+  editor_free_text2_aria_label: "Text Editor",
+  editor_ink2_aria_label: "Draw Editor",
+  editor_ink_canvas_aria_label: "User-created image",
+  editor_alt_text_button_label: "Alt text",
+  editor_alt_text_edit_button_label: "Edit alt text",
+  editor_alt_text_decorative_tooltip: "Marked as decorative"
+};
+{
+  DEFAULT_L10N_STRINGS.print_progress_percent = "{{progress}}%";
+}
+function getL10nFallback(key, args) {
+  switch (key) {
+    case "find_match_count":
+      key = `find_match_count[${args.total === 1 ? "one" : "other"}]`;
+      break;
+    case "find_match_count_limit":
+      key = `find_match_count_limit[${args.limit === 1 ? "one" : "other"}]`;
+      break;
+  }
+  return DEFAULT_L10N_STRINGS[key] || "";
+}
+function formatL10nValue(text, args) {
+  if (!args) {
+    return text;
+  }
+  return text.replaceAll(/\{\{\s*(\w+)\s*\}\}/g, (all, name) => {
+    return name in args ? args[name] : "{{" + name + "}}";
+  });
+}
+const NullL10n = {
+  async getLanguage() {
+    return "en-us";
+  },
+  async getDirection() {
+    return "ltr";
+  },
+  async get(key, args = null, fallback = getL10nFallback(key, args)) {
+    return formatL10nValue(fallback, args);
+  },
+  async translate(element) {}
+};
+exports.NullL10n = NullL10n;
+
+/***/ }),
+/* 8 */
+/***/ ((__unused_webpack_module, exports, __w_pdfjs_require__) => {
+
+
+
+Object.defineProperty(exports, "__esModule", ({
+  value: true
+}));
+exports.DownloadManager = void 0;
+var _pdfjsLib = __w_pdfjs_require__(4);
+;
+function download(blobUrl, filename) {
+  const a = document.createElement("a");
+  if (!a.click) {
+    throw new Error('DownloadManager: "a.click()" is not supported.');
+  }
+  a.href = blobUrl;
+  a.target = "_parent";
+  if ("download" in a) {
+    a.download = filename;
+  }
+  (document.body || document.documentElement).append(a);
+  a.click();
+  a.remove();
+}
+class DownloadManager {
+  #openBlobUrls = new WeakMap();
+  downloadUrl(url, filename, _options) {
+    if (!(0, _pdfjsLib.createValidAbsoluteUrl)(url, "http://example.com")) {
+      console.error(`downloadUrl - not a valid URL: ${url}`);
+      return;
+    }
+    download(url + "#pdfjs.action=download", filename);
+  }
+  downloadData(data, filename, contentType) {
+    const blobUrl = URL.createObjectURL(new Blob([data], {
+      type: contentType
+    }));
+    download(blobUrl, filename);
+  }
+  openOrDownloadData(element, data, filename) {
+    const isPdfData = (0, _pdfjsLib.isPdfFile)(filename);
+    const contentType = isPdfData ? "application/pdf" : "";
+    this.downloadData(data, filename, contentType);
+    return false;
+  }
+  download(blob, url, filename, _options) {
+    const blobUrl = URL.createObjectURL(blob);
+    download(blobUrl, filename);
+  }
+}
+exports.DownloadManager = DownloadManager;
+
+/***/ }),
+/* 9 */
+/***/ ((__unused_webpack_module, exports) => {
+
+
+
+Object.defineProperty(exports, "__esModule", ({
+  value: true
+}));
+exports.WaitOnType = exports.EventBus = exports.AutomationEventBus = void 0;
+exports.waitOnEventOrTimeout = waitOnEventOrTimeout;
+const WaitOnType = {
+  EVENT: "event",
+  TIMEOUT: "timeout"
+};
+exports.WaitOnType = WaitOnType;
+function waitOnEventOrTimeout({
+  target,
+  name,
+  delay = 0
+}) {
+  return new Promise(function (resolve, reject) {
+    if (typeof target !== "object" || !(name && typeof name === "string") || !(Number.isInteger(delay) && delay >= 0)) {
+      throw new Error("waitOnEventOrTimeout - invalid parameters.");
+    }
+    function handler(type) {
+      if (target instanceof EventBus) {
+        target._off(name, eventHandler);
+      } else {
+        target.removeEventListener(name, eventHandler);
+      }
+      if (timeout) {
+        clearTimeout(timeout);
+      }
+      resolve(type);
+    }
+    const eventHandler = handler.bind(null, WaitOnType.EVENT);
+    if (target instanceof EventBus) {
+      target._on(name, eventHandler);
+    } else {
+      target.addEventListener(name, eventHandler);
+    }
+    const timeoutHandler = handler.bind(null, WaitOnType.TIMEOUT);
+    const timeout = setTimeout(timeoutHandler, delay);
+  });
+}
+class EventBus {
+  #listeners = Object.create(null);
+  on(eventName, listener, options = null) {
+    this._on(eventName, listener, {
+      external: true,
+      once: options?.once
+    });
+  }
+  off(eventName, listener, options = null) {
+    this._off(eventName, listener, {
+      external: true,
+      once: options?.once
+    });
+  }
+  dispatch(eventName, data) {
+    const eventListeners = this.#listeners[eventName];
+    if (!eventListeners || eventListeners.length === 0) {
+      return;
+    }
+    let externalListeners;
+    for (const {
+      listener,
+      external,
+      once
+    } of eventListeners.slice(0)) {
+      if (once) {
+        this._off(eventName, listener);
+      }
+      if (external) {
+        (externalListeners ||= []).push(listener);
+        continue;
+      }
+      listener(data);
+    }
+    if (externalListeners) {
+      for (const listener of externalListeners) {
+        listener(data);
+      }
+      externalListeners = null;
+    }
+  }
+  _on(eventName, listener, options = null) {
+    const eventListeners = this.#listeners[eventName] ||= [];
+    eventListeners.push({
+      listener,
+      external: options?.external === true,
+      once: options?.once === true
+    });
+  }
+  _off(eventName, listener, options = null) {
+    const eventListeners = this.#listeners[eventName];
+    if (!eventListeners) {
+      return;
+    }
+    for (let i = 0, ii = eventListeners.length; i < ii; i++) {
+      if (eventListeners[i].listener === listener) {
+        eventListeners.splice(i, 1);
+        return;
+      }
+    }
+  }
+}
+exports.EventBus = EventBus;
+class AutomationEventBus extends EventBus {
+  dispatch(eventName, data) {
+    throw new Error("Not implemented: AutomationEventBus.dispatch");
+  }
+}
+exports.AutomationEventBus = AutomationEventBus;
+
+/***/ }),
+/* 10 */
+/***/ ((__unused_webpack_module, exports, __w_pdfjs_require__) => {
+
+
+
+Object.defineProperty(exports, "__esModule", ({
+  value: true
+}));
+exports.GenericL10n = void 0;
+__w_pdfjs_require__(11);
+var _l10n_utils = __w_pdfjs_require__(7);
+const PARTIAL_LANG_CODES = {
+  en: "en-US",
+  es: "es-ES",
+  fy: "fy-NL",
+  ga: "ga-IE",
+  gu: "gu-IN",
+  hi: "hi-IN",
+  hy: "hy-AM",
+  nb: "nb-NO",
+  ne: "ne-NP",
+  nn: "nn-NO",
+  pa: "pa-IN",
+  pt: "pt-PT",
+  sv: "sv-SE",
+  zh: "zh-CN"
+};
+function fixupLangCode(langCode) {
+  return PARTIAL_LANG_CODES[langCode?.toLowerCase()] || langCode;
+}
+class GenericL10n {
+  constructor(lang) {
+    const {
+      webL10n
+    } = document;
+    this._lang = lang;
+    this._ready = new Promise((resolve, reject) => {
+      webL10n.setLanguage(fixupLangCode(lang), () => {
+        resolve(webL10n);
+      });
+    });
+  }
+  async getLanguage() {
+    const l10n = await this._ready;
+    return l10n.getLanguage();
+  }
+  async getDirection() {
+    const l10n = await this._ready;
+    return l10n.getDirection();
+  }
+  async get(key, args = null, fallback = (0, _l10n_utils.getL10nFallback)(key, args)) {
+    const l10n = await this._ready;
+    return l10n.get(key, args, fallback);
+  }
+  async translate(element) {
+    const l10n = await this._ready;
+    return l10n.translate(element);
+  }
+}
+exports.GenericL10n = GenericL10n;
+
+/***/ }),
+/* 11 */
+/***/ (() => {
+
+
+
+document.webL10n = function (window, document) {
+  var gL10nData = {};
+  var gTextData = '';
+  var gTextProp = 'textContent';
+  var gLanguage = '';
+  var gMacros = {};
+  var gReadyState = 'loading';
+  var gAsyncResourceLoading = true;
+  function getL10nResourceLinks() {
+    return document.querySelectorAll('link[type="application/l10n"]');
+  }
+  function getL10nDictionary() {
+    var script = document.querySelector('script[type="application/l10n"]');
+    return script ? JSON.parse(script.innerHTML) : null;
+  }
+  function getTranslatableChildren(element) {
+    return element ? element.querySelectorAll('*[data-l10n-id]') : [];
+  }
+  function getL10nAttributes(element) {
+    if (!element) return {};
+    var l10nId = element.getAttribute('data-l10n-id');
+    var l10nArgs = element.getAttribute('data-l10n-args');
+    var args = {};
+    if (l10nArgs) {
+      try {
+        args = JSON.parse(l10nArgs);
+      } catch (e) {
+        console.warn('could not parse arguments for #' + l10nId);
+      }
+    }
+    return {
+      id: l10nId,
+      args: args
+    };
+  }
+  function xhrLoadText(url, onSuccess, onFailure) {
+    onSuccess = onSuccess || function _onSuccess(data) {};
+    onFailure = onFailure || function _onFailure() {};
+    var xhr = new XMLHttpRequest();
+    xhr.open('GET', url, gAsyncResourceLoading);
+    if (xhr.overrideMimeType) {
+      xhr.overrideMimeType('text/plain; charset=utf-8');
+    }
+    xhr.onreadystatechange = function () {
+      if (xhr.readyState == 4) {
+        if (xhr.status == 200 || xhr.status === 0) {
+          onSuccess(xhr.responseText);
+        } else {
+          onFailure();
+        }
+      }
+    };
+    xhr.onerror = onFailure;
+    xhr.ontimeout = onFailure;
+    try {
+      xhr.send(null);
+    } catch (e) {
+      onFailure();
+    }
+  }
+  function parseResource(href, lang, successCallback, failureCallback) {
+    var baseURL = href.replace(/[^\/]*$/, '') || './';
+    function evalString(text) {
+      if (text.lastIndexOf('\\') < 0) return text;
+      return text.replace(/\\\\/g, '\\').replace(/\\n/g, '\n').replace(/\\r/g, '\r').replace(/\\t/g, '\t').replace(/\\b/g, '\b').replace(/\\f/g, '\f').replace(/\\{/g, '{').replace(/\\}/g, '}').replace(/\\"/g, '"').replace(/\\'/g, "'");
+    }
+    function parseProperties(text, parsedPropertiesCallback) {
+      var dictionary = {};
+      var reBlank = /^\s*|\s*$/;
+      var reComment = /^\s*#|^\s*$/;
+      var reSection = /^\s*\[(.*)\]\s*$/;
+      var reImport = /^\s*@import\s+url\((.*)\)\s*$/i;
+      var reSplit = /^([^=\s]*)\s*=\s*(.+)$/;
+      function parseRawLines(rawText, extendedSyntax, parsedRawLinesCallback) {
+        var entries = rawText.replace(reBlank, '').split(/[\r\n]+/);
+        var currentLang = '*';
+        var genericLang = lang.split('-', 1)[0];
+        var skipLang = false;
+        var match = '';
+        function nextEntry() {
+          while (true) {
+            if (!entries.length) {
+              parsedRawLinesCallback();
+              return;
+            }
+            var line = entries.shift();
+            if (reComment.test(line)) continue;
+            if (extendedSyntax) {
+              match = reSection.exec(line);
+              if (match) {
+                currentLang = match[1].toLowerCase();
+                skipLang = currentLang !== '*' && currentLang !== lang && currentLang !== genericLang;
+                continue;
+              } else if (skipLang) {
+                continue;
+              }
+              match = reImport.exec(line);
+              if (match) {
+                loadImport(baseURL + match[1], nextEntry);
+                return;
+              }
+            }
+            var tmp = line.match(reSplit);
+            if (tmp && tmp.length == 3) {
+              dictionary[tmp[1]] = evalString(tmp[2]);
+            }
+          }
+        }
+        nextEntry();
+      }
+      function loadImport(url, callback) {
+        xhrLoadText(url, function (content) {
+          parseRawLines(content, false, callback);
+        }, function () {
+          console.warn(url + ' not found.');
+          callback();
+        });
+      }
+      parseRawLines(text, true, function () {
+        parsedPropertiesCallback(dictionary);
+      });
+    }
+    xhrLoadText(href, function (response) {
+      gTextData += response;
+      parseProperties(response, function (data) {
+        for (var key in data) {
+          var id,
+            prop,
+            index = key.lastIndexOf('.');
+          if (index > 0) {
+            id = key.substring(0, index);
+            prop = key.substring(index + 1);
+          } else {
+            id = key;
+            prop = gTextProp;
+          }
+          if (!gL10nData[id]) {
+            gL10nData[id] = {};
+          }
+          gL10nData[id][prop] = data[key];
+        }
+        if (successCallback) {
+          successCallback();
+        }
+      });
+    }, failureCallback);
+  }
+  function loadLocale(lang, callback) {
+    if (lang) {
+      lang = lang.toLowerCase();
+    }
+    callback = callback || function _callback() {};
+    clear();
+    gLanguage = lang;
+    var langLinks = getL10nResourceLinks();
+    var langCount = langLinks.length;
+    if (langCount === 0) {
+      var dict = getL10nDictionary();
+      if (dict && dict.locales && dict.default_locale) {
+        console.log('using the embedded JSON directory, early way out');
+        gL10nData = dict.locales[lang];
+        if (!gL10nData) {
+          var defaultLocale = dict.default_locale.toLowerCase();
+          for (var anyCaseLang in dict.locales) {
+            anyCaseLang = anyCaseLang.toLowerCase();
+            if (anyCaseLang === lang) {
+              gL10nData = dict.locales[lang];
+              break;
+            } else if (anyCaseLang === defaultLocale) {
+              gL10nData = dict.locales[defaultLocale];
+            }
+          }
+        }
+        callback();
+      } else {
+        console.log('no resource to load, early way out');
+      }
+      gReadyState = 'complete';
+      return;
+    }
+    var onResourceLoaded = null;
+    var gResourceCount = 0;
+    onResourceLoaded = function () {
+      gResourceCount++;
+      if (gResourceCount >= langCount) {
+        callback();
+        gReadyState = 'complete';
+      }
+    };
+    function L10nResourceLink(link) {
+      var href = link.href;
+      this.load = function (lang, callback) {
+        parseResource(href, lang, callback, function () {
+          console.warn(href + ' not found.');
+          console.warn('"' + lang + '" resource not found');
+          gLanguage = '';
+          callback();
+        });
+      };
+    }
+    for (var i = 0; i < langCount; i++) {
+      var resource = new L10nResourceLink(langLinks[i]);
+      resource.load(lang, onResourceLoaded);
+    }
+  }
+  function clear() {
+    gL10nData = {};
+    gTextData = '';
+    gLanguage = '';
+  }
+  function getPluralRules(lang) {
+    var locales2rules = {
+      'af': 3,
+      'ak': 4,
+      'am': 4,
+      'ar': 1,
+      'asa': 3,
+      'az': 0,
+      'be': 11,
+      'bem': 3,
+      'bez': 3,
+      'bg': 3,
+      'bh': 4,
+      'bm': 0,
+      'bn': 3,
+      'bo': 0,
+      'br': 20,
+      'brx': 3,
+      'bs': 11,
+      'ca': 3,
+      'cgg': 3,
+      'chr': 3,
+      'cs': 12,
+      'cy': 17,
+      'da': 3,
+      'de': 3,
+      'dv': 3,
+      'dz': 0,
+      'ee': 3,
+      'el': 3,
+      'en': 3,
+      'eo': 3,
+      'es': 3,
+      'et': 3,
+      'eu': 3,
+      'fa': 0,
+      'ff': 5,
+      'fi': 3,
+      'fil': 4,
+      'fo': 3,
+      'fr': 5,
+      'fur': 3,
+      'fy': 3,
+      'ga': 8,
+      'gd': 24,
+      'gl': 3,
+      'gsw': 3,
+      'gu': 3,
+      'guw': 4,
+      'gv': 23,
+      'ha': 3,
+      'haw': 3,
+      'he': 2,
+      'hi': 4,
+      'hr': 11,
+      'hu': 0,
+      'id': 0,
+      'ig': 0,
+      'ii': 0,
+      'is': 3,
+      'it': 3,
+      'iu': 7,
+      'ja': 0,
+      'jmc': 3,
+      'jv': 0,
+      'ka': 0,
+      'kab': 5,
+      'kaj': 3,
+      'kcg': 3,
+      'kde': 0,
+      'kea': 0,
+      'kk': 3,
+      'kl': 3,
+      'km': 0,
+      'kn': 0,
+      'ko': 0,
+      'ksb': 3,
+      'ksh': 21,
+      'ku': 3,
+      'kw': 7,
+      'lag': 18,
+      'lb': 3,
+      'lg': 3,
+      'ln': 4,
+      'lo': 0,
+      'lt': 10,
+      'lv': 6,
+      'mas': 3,
+      'mg': 4,
+      'mk': 16,
+      'ml': 3,
+      'mn': 3,
+      'mo': 9,
+      'mr': 3,
+      'ms': 0,
+      'mt': 15,
+      'my': 0,
+      'nah': 3,
+      'naq': 7,
+      'nb': 3,
+      'nd': 3,
+      'ne': 3,
+      'nl': 3,
+      'nn': 3,
+      'no': 3,
+      'nr': 3,
+      'nso': 4,
+      'ny': 3,
+      'nyn': 3,
+      'om': 3,
+      'or': 3,
+      'pa': 3,
+      'pap': 3,
+      'pl': 13,
+      'ps': 3,
+      'pt': 3,
+      'rm': 3,
+      'ro': 9,
+      'rof': 3,
+      'ru': 11,
+      'rwk': 3,
+      'sah': 0,
+      'saq': 3,
+      'se': 7,
+      'seh': 3,
+      'ses': 0,
+      'sg': 0,
+      'sh': 11,
+      'shi': 19,
+      'sk': 12,
+      'sl': 14,
+      'sma': 7,
+      'smi': 7,
+      'smj': 7,
+      'smn': 7,
+      'sms': 7,
+      'sn': 3,
+      'so': 3,
+      'sq': 3,
+      'sr': 11,
+      'ss': 3,
+      'ssy': 3,
+      'st': 3,
+      'sv': 3,
+      'sw': 3,
+      'syr': 3,
+      'ta': 3,
+      'te': 3,
+      'teo': 3,
+      'th': 0,
+      'ti': 4,
+      'tig': 3,
+      'tk': 3,
+      'tl': 4,
+      'tn': 3,
+      'to': 0,
+      'tr': 0,
+      'ts': 3,
+      'tzm': 22,
+      'uk': 11,
+      'ur': 3,
+      've': 3,
+      'vi': 0,
+      'vun': 3,
+      'wa': 4,
+      'wae': 3,
+      'wo': 0,
+      'xh': 3,
+      'xog': 3,
+      'yo': 0,
+      'zh': 0,
+      'zu': 3
+    };
+    function isIn(n, list) {
+      return list.indexOf(n) !== -1;
+    }
+    function isBetween(n, start, end) {
+      return start <= n && n <= end;
+    }
+    var pluralRules = {
+      '0': function (n) {
+        return 'other';
+      },
+      '1': function (n) {
+        if (isBetween(n % 100, 3, 10)) return 'few';
+        if (n === 0) return 'zero';
+        if (isBetween(n % 100, 11, 99)) return 'many';
+        if (n == 2) return 'two';
+        if (n == 1) return 'one';
+        return 'other';
+      },
+      '2': function (n) {
+        if (n !== 0 && n % 10 === 0) return 'many';
+        if (n == 2) return 'two';
+        if (n == 1) return 'one';
+        return 'other';
+      },
+      '3': function (n) {
+        if (n == 1) return 'one';
+        return 'other';
+      },
+      '4': function (n) {
+        if (isBetween(n, 0, 1)) return 'one';
+        return 'other';
+      },
+      '5': function (n) {
+        if (isBetween(n, 0, 2) && n != 2) return 'one';
+        return 'other';
+      },
+      '6': function (n) {
+        if (n === 0) return 'zero';
+        if (n % 10 == 1 && n % 100 != 11) return 'one';
+        return 'other';
+      },
+      '7': function (n) {
+        if (n == 2) return 'two';
+        if (n == 1) return 'one';
+        return 'other';
+      },
+      '8': function (n) {
+        if (isBetween(n, 3, 6)) return 'few';
+        if (isBetween(n, 7, 10)) return 'many';
+        if (n == 2) return 'two';
+        if (n == 1) return 'one';
+        return 'other';
+      },
+      '9': function (n) {
+        if (n === 0 || n != 1 && isBetween(n % 100, 1, 19)) return 'few';
+        if (n == 1) return 'one';
+        return 'other';
+      },
+      '10': function (n) {
+        if (isBetween(n % 10, 2, 9) && !isBetween(n % 100, 11, 19)) return 'few';
+        if (n % 10 == 1 && !isBetween(n % 100, 11, 19)) return 'one';
+        return 'other';
+      },
+      '11': function (n) {
+        if (isBetween(n % 10, 2, 4) && !isBetween(n % 100, 12, 14)) return 'few';
+        if (n % 10 === 0 || isBetween(n % 10, 5, 9) || isBetween(n % 100, 11, 14)) return 'many';
+        if (n % 10 == 1 && n % 100 != 11) return 'one';
+        return 'other';
+      },
+      '12': function (n) {
+        if (isBetween(n, 2, 4)) return 'few';
+        if (n == 1) return 'one';
+        return 'other';
+      },
+      '13': function (n) {
+        if (isBetween(n % 10, 2, 4) && !isBetween(n % 100, 12, 14)) return 'few';
+        if (n != 1 && isBetween(n % 10, 0, 1) || isBetween(n % 10, 5, 9) || isBetween(n % 100, 12, 14)) return 'many';
+        if (n == 1) return 'one';
+        return 'other';
+      },
+      '14': function (n) {
+        if (isBetween(n % 100, 3, 4)) return 'few';
+        if (n % 100 == 2) return 'two';
+        if (n % 100 == 1) return 'one';
+        return 'other';
+      },
+      '15': function (n) {
+        if (n === 0 || isBetween(n % 100, 2, 10)) return 'few';
+        if (isBetween(n % 100, 11, 19)) return 'many';
+        if (n == 1) return 'one';
+        return 'other';
+      },
+      '16': function (n) {
+        if (n % 10 == 1 && n != 11) return 'one';
+        return 'other';
+      },
+      '17': function (n) {
+        if (n == 3) return 'few';
+        if (n === 0) return 'zero';
+        if (n == 6) return 'many';
+        if (n == 2) return 'two';
+        if (n == 1) return 'one';
+        return 'other';
+      },
+      '18': function (n) {
+        if (n === 0) return 'zero';
+        if (isBetween(n, 0, 2) && n !== 0 && n != 2) return 'one';
+        return 'other';
+      },
+      '19': function (n) {
+        if (isBetween(n, 2, 10)) return 'few';
+        if (isBetween(n, 0, 1)) return 'one';
+        return 'other';
+      },
+      '20': function (n) {
+        if ((isBetween(n % 10, 3, 4) || n % 10 == 9) && !(isBetween(n % 100, 10, 19) || isBetween(n % 100, 70, 79) || isBetween(n % 100, 90, 99))) return 'few';
+        if (n % 1000000 === 0 && n !== 0) return 'many';
+        if (n % 10 == 2 && !isIn(n % 100, [12, 72, 92])) return 'two';
+        if (n % 10 == 1 && !isIn(n % 100, [11, 71, 91])) return 'one';
+        return 'other';
+      },
+      '21': function (n) {
+        if (n === 0) return 'zero';
+        if (n == 1) return 'one';
+        return 'other';
+      },
+      '22': function (n) {
+        if (isBetween(n, 0, 1) || isBetween(n, 11, 99)) return 'one';
+        return 'other';
+      },
+      '23': function (n) {
+        if (isBetween(n % 10, 1, 2) || n % 20 === 0) return 'one';
+        return 'other';
+      },
+      '24': function (n) {
+        if (isBetween(n, 3, 10) || isBetween(n, 13, 19)) return 'few';
+        if (isIn(n, [2, 12])) return 'two';
+        if (isIn(n, [1, 11])) return 'one';
+        return 'other';
+      }
+    };
+    var index = locales2rules[lang.replace(/-.*$/, '')];
+    if (!(index in pluralRules)) {
+      console.warn('plural form unknown for [' + lang + ']');
+      return function () {
+        return 'other';
+      };
+    }
+    return pluralRules[index];
+  }
+  gMacros.plural = function (str, param, key, prop) {
+    var n = parseFloat(param);
+    if (isNaN(n)) return str;
+    if (prop != gTextProp) return str;
+    if (!gMacros._pluralRules) {
+      gMacros._pluralRules = getPluralRules(gLanguage);
+    }
+    var index = '[' + gMacros._pluralRules(n) + ']';
+    if (n === 0 && key + '[zero]' in gL10nData) {
+      str = gL10nData[key + '[zero]'][prop];
+    } else if (n == 1 && key + '[one]' in gL10nData) {
+      str = gL10nData[key + '[one]'][prop];
+    } else if (n == 2 && key + '[two]' in gL10nData) {
+      str = gL10nData[key + '[two]'][prop];
+    } else if (key + index in gL10nData) {
+      str = gL10nData[key + index][prop];
+    } else if (key + '[other]' in gL10nData) {
+      str = gL10nData[key + '[other]'][prop];
+    }
+    return str;
+  };
+  function getL10nData(key, args, fallback) {
+    var data = gL10nData[key];
+    if (!data) {
+      console.warn('#' + key + ' is undefined.');
+      if (!fallback) {
+        return null;
+      }
+      data = fallback;
+    }
+    var rv = {};
+    for (var prop in data) {
+      var str = data[prop];
+      str = substIndexes(str, args, key, prop);
+      str = substArguments(str, args, key);
+      rv[prop] = str;
+    }
+    return rv;
+  }
+  function substIndexes(str, args, key, prop) {
+    var reIndex = /\{\[\s*([a-zA-Z]+)\(([a-zA-Z]+)\)\s*\]\}/;
+    var reMatch = reIndex.exec(str);
+    if (!reMatch || !reMatch.length) return str;
+    var macroName = reMatch[1];
+    var paramName = reMatch[2];
+    var param;
+    if (args && paramName in args) {
+      param = args[paramName];
+    } else if (paramName in gL10nData) {
+      param = gL10nData[paramName];
+    }
+    if (macroName in gMacros) {
+      var macro = gMacros[macroName];
+      str = macro(str, param, key, prop);
+    }
+    return str;
+  }
+  function substArguments(str, args, key) {
+    var reArgs = /\{\{\s*(.+?)\s*\}\}/g;
+    return str.replace(reArgs, function (matched_text, arg) {
+      if (args && arg in args) {
+        return args[arg];
+      }
+      if (arg in gL10nData) {
+        return gL10nData[arg];
+      }
+      console.log('argument {{' + arg + '}} for #' + key + ' is undefined.');
+      return matched_text;
+    });
+  }
+  function translateElement(element) {
+    var l10n = getL10nAttributes(element);
+    if (!l10n.id) return;
+    var data = getL10nData(l10n.id, l10n.args);
+    if (!data) {
+      console.warn('#' + l10n.id + ' is undefined.');
+      return;
+    }
+    if (data[gTextProp]) {
+      if (getChildElementCount(element) === 0) {
+        element[gTextProp] = data[gTextProp];
+      } else {
+        var children = element.childNodes;
+        var found = false;
+        for (var i = 0, l = children.length; i < l; i++) {
+          if (children[i].nodeType === 3 && /\S/.test(children[i].nodeValue)) {
+            if (found) {
+              children[i].nodeValue = '';
+            } else {
+              children[i].nodeValue = data[gTextProp];
+              found = true;
+            }
+          }
+        }
+        if (!found) {
+          var textNode = document.createTextNode(data[gTextProp]);
+          element.prepend(textNode);
+        }
+      }
+      delete data[gTextProp];
+    }
+    for (var k in data) {
+      element[k] = data[k];
+    }
+  }
+  function getChildElementCount(element) {
+    if (element.children) {
+      return element.children.length;
+    }
+    if (typeof element.childElementCount !== 'undefined') {
+      return element.childElementCount;
+    }
+    var count = 0;
+    for (var i = 0; i < element.childNodes.length; i++) {
+      count += element.nodeType === 1 ? 1 : 0;
+    }
+    return count;
+  }
+  function translateFragment(element) {
+    element = element || document.documentElement;
+    var children = getTranslatableChildren(element);
+    var elementCount = children.length;
+    for (var i = 0; i < elementCount; i++) {
+      translateElement(children[i]);
+    }
+    translateElement(element);
+  }
+  return {
+    get: function (key, args, fallbackString) {
+      var index = key.lastIndexOf('.');
+      var prop = gTextProp;
+      if (index > 0) {
+        prop = key.substring(index + 1);
+        key = key.substring(0, index);
+      }
+      var fallback;
+      if (fallbackString) {
+        fallback = {};
+        fallback[prop] = fallbackString;
+      }
+      var data = getL10nData(key, args, fallback);
+      if (data && prop in data) {
+        return data[prop];
+      }
+      return '{{' + key + '}}';
+    },
+    getData: function () {
+      return gL10nData;
+    },
+    getText: function () {
+      return gTextData;
+    },
+    getLanguage: function () {
+      return gLanguage;
+    },
+    setLanguage: function (lang, callback) {
+      loadLocale(lang, function () {
+        if (callback) callback();
+      });
+    },
+    getDirection: function () {
+      var rtlList = ['ar', 'he', 'fa', 'ps', 'ur'];
+      var shortCode = gLanguage.split('-', 1)[0];
+      return rtlList.indexOf(shortCode) >= 0 ? 'rtl' : 'ltr';
+    },
+    translate: translateFragment,
+    getReadyState: function () {
+      return gReadyState;
+    },
+    ready: function (callback) {
+      if (!callback) {
+        return;
+      } else if (gReadyState == 'complete' || gReadyState == 'interactive') {
+        window.setTimeout(function () {
+          callback();
+        });
+      } else if (document.addEventListener) {
+        document.addEventListener('localized', function once() {
+          document.removeEventListener('localized', once);
+          callback();
+        });
+      }
+    }
+  };
+}(window, document);
+
+/***/ }),
+/* 12 */
+/***/ ((__unused_webpack_module, exports, __w_pdfjs_require__) => {
+
+
+
+Object.defineProperty(exports, "__esModule", ({
+  value: true
+}));
+exports.PDFHistory = void 0;
+exports.isDestArraysEqual = isDestArraysEqual;
+exports.isDestHashesEqual = isDestHashesEqual;
+var _ui_utils = __w_pdfjs_require__(2);
+var _event_utils = __w_pdfjs_require__(9);
+const HASH_CHANGE_TIMEOUT = 1000;
+const POSITION_UPDATED_THRESHOLD = 50;
+const UPDATE_VIEWAREA_TIMEOUT = 1000;
+function getCurrentHash() {
+  return document.location.hash;
+}
+class PDFHistory {
+  constructor({
+    linkService,
+    eventBus
+  }) {
+    this.linkService = linkService;
+    this.eventBus = eventBus;
+    this._initialized = false;
+    this._fingerprint = "";
+    this.reset();
+    this._boundEvents = null;
+    this.eventBus._on("pagesinit", () => {
+      this._isPagesLoaded = false;
+      this.eventBus._on("pagesloaded", evt => {
+        this._isPagesLoaded = !!evt.pagesCount;
+      }, {
+        once: true
+      });
+    });
+  }
+  initialize({
+    fingerprint,
+    resetHistory = false,
+    updateUrl = false
+  }) {
+    if (!fingerprint || typeof fingerprint !== "string") {
+      console.error('PDFHistory.initialize: The "fingerprint" must be a non-empty string.');
+      return;
+    }
+    if (this._initialized) {
+      this.reset();
+    }
+    const reInitialized = this._fingerprint !== "" && this._fingerprint !== fingerprint;
+    this._fingerprint = fingerprint;
+    this._updateUrl = updateUrl === true;
+    this._initialized = true;
+    this._bindEvents();
+    const state = window.history.state;
+    this._popStateInProgress = false;
+    this._blockHashChange = 0;
+    this._currentHash = getCurrentHash();
+    this._numPositionUpdates = 0;
+    this._uid = this._maxUid = 0;
+    this._destination = null;
+    this._position = null;
+    if (!this._isValidState(state, true) || resetHistory) {
+      const {
+        hash,
+        page,
+        rotation
+      } = this._parseCurrentHash(true);
+      if (!hash || reInitialized || resetHistory) {
+        this._pushOrReplaceState(null, true);
+        return;
+      }
+      this._pushOrReplaceState({
+        hash,
+        page,
+        rotation
+      }, true);
+      return;
+    }
+    const destination = state.destination;
+    this._updateInternalState(destination, state.uid, true);
+    if (destination.rotation !== undefined) {
+      this._initialRotation = destination.rotation;
+    }
+    if (destination.dest) {
+      this._initialBookmark = JSON.stringify(destination.dest);
+      this._destination.page = null;
+    } else if (destination.hash) {
+      this._initialBookmark = destination.hash;
+    } else if (destination.page) {
+      this._initialBookmark = `page=${destination.page}`;
+    }
+  }
+  reset() {
+    if (this._initialized) {
+      this._pageHide();
+      this._initialized = false;
+      this._unbindEvents();
+    }
+    if (this._updateViewareaTimeout) {
+      clearTimeout(this._updateViewareaTimeout);
+      this._updateViewareaTimeout = null;
+    }
+    this._initialBookmark = null;
+    this._initialRotation = null;
+  }
+  push({
+    namedDest = null,
+    explicitDest,
+    pageNumber
+  }) {
+    if (!this._initialized) {
+      return;
+    }
+    if (namedDest && typeof namedDest !== "string") {
+      console.error("PDFHistory.push: " + `"${namedDest}" is not a valid namedDest parameter.`);
+      return;
+    } else if (!Array.isArray(explicitDest)) {
+      console.error("PDFHistory.push: " + `"${explicitDest}" is not a valid explicitDest parameter.`);
+      return;
+    } else if (!this._isValidPage(pageNumber)) {
+      if (pageNumber !== null || this._destination) {
+        console.error("PDFHistory.push: " + `"${pageNumber}" is not a valid pageNumber parameter.`);
+        return;
+      }
+    }
+    const hash = namedDest || JSON.stringify(explicitDest);
+    if (!hash) {
+      return;
+    }
+    let forceReplace = false;
+    if (this._destination && (isDestHashesEqual(this._destination.hash, hash) || isDestArraysEqual(this._destination.dest, explicitDest))) {
+      if (this._destination.page) {
+        return;
+      }
+      forceReplace = true;
+    }
+    if (this._popStateInProgress && !forceReplace) {
+      return;
+    }
+    this._pushOrReplaceState({
+      dest: explicitDest,
+      hash,
+      page: pageNumber,
+      rotation: this.linkService.rotation
+    }, forceReplace);
+    if (!this._popStateInProgress) {
+      this._popStateInProgress = true;
+      Promise.resolve().then(() => {
+        this._popStateInProgress = false;
+      });
+    }
+  }
+  pushPage(pageNumber) {
+    if (!this._initialized) {
+      return;
+    }
+    if (!this._isValidPage(pageNumber)) {
+      console.error(`PDFHistory.pushPage: "${pageNumber}" is not a valid page number.`);
+      return;
+    }
+    if (this._destination?.page === pageNumber) {
+      return;
+    }
+    if (this._popStateInProgress) {
+      return;
+    }
+    this._pushOrReplaceState({
+      dest: null,
+      hash: `page=${pageNumber}`,
+      page: pageNumber,
+      rotation: this.linkService.rotation
+    });
+    if (!this._popStateInProgress) {
+      this._popStateInProgress = true;
+      Promise.resolve().then(() => {
+        this._popStateInProgress = false;
+      });
+    }
+  }
+  pushCurrentPosition() {
+    if (!this._initialized || this._popStateInProgress) {
+      return;
+    }
+    this._tryPushCurrentPosition();
+  }
+  back() {
+    if (!this._initialized || this._popStateInProgress) {
+      return;
+    }
+    const state = window.history.state;
+    if (this._isValidState(state) && state.uid > 0) {
+      window.history.back();
+    }
+  }
+  forward() {
+    if (!this._initialized || this._popStateInProgress) {
+      return;
+    }
+    const state = window.history.state;
+    if (this._isValidState(state) && state.uid < this._maxUid) {
+      window.history.forward();
+    }
+  }
+  get popStateInProgress() {
+    return this._initialized && (this._popStateInProgress || this._blockHashChange > 0);
+  }
+  get initialBookmark() {
+    return this._initialized ? this._initialBookmark : null;
+  }
+  get initialRotation() {
+    return this._initialized ? this._initialRotation : null;
+  }
+  _pushOrReplaceState(destination, forceReplace = false) {
+    const shouldReplace = forceReplace || !this._destination;
+    const newState = {
+      fingerprint: this._fingerprint,
+      uid: shouldReplace ? this._uid : this._uid + 1,
+      destination
+    };
+    this._updateInternalState(destination, newState.uid);
+    let newUrl;
+    if (this._updateUrl && destination?.hash) {
+      const baseUrl = document.location.href.split("#")[0];
+      if (!baseUrl.startsWith("file://")) {
+        newUrl = `${baseUrl}#${destination.hash}`;
+      }
+    }
+    if (shouldReplace) {
+      window.history.replaceState(newState, "", newUrl);
+    } else {
+      window.history.pushState(newState, "", newUrl);
+    }
+  }
+  _tryPushCurrentPosition(temporary = false) {
+    if (!this._position) {
+      return;
+    }
+    let position = this._position;
+    if (temporary) {
+      position = Object.assign(Object.create(null), this._position);
+      position.temporary = true;
+    }
+    if (!this._destination) {
+      this._pushOrReplaceState(position);
+      return;
+    }
+    if (this._destination.temporary) {
+      this._pushOrReplaceState(position, true);
+      return;
+    }
+    if (this._destination.hash === position.hash) {
+      return;
+    }
+    if (!this._destination.page && (POSITION_UPDATED_THRESHOLD <= 0 || this._numPositionUpdates <= POSITION_UPDATED_THRESHOLD)) {
+      return;
+    }
+    let forceReplace = false;
+    if (this._destination.page >= position.first && this._destination.page <= position.page) {
+      if (this._destination.dest !== undefined || !this._destination.first) {
+        return;
+      }
+      forceReplace = true;
+    }
+    this._pushOrReplaceState(position, forceReplace);
+  }
+  _isValidPage(val) {
+    return Number.isInteger(val) && val > 0 && val <= this.linkService.pagesCount;
+  }
+  _isValidState(state, checkReload = false) {
+    if (!state) {
+      return false;
+    }
+    if (state.fingerprint !== this._fingerprint) {
+      if (checkReload) {
+        if (typeof state.fingerprint !== "string" || state.fingerprint.length !== this._fingerprint.length) {
+          return false;
+        }
+        const [perfEntry] = performance.getEntriesByType("navigation");
+        if (perfEntry?.type !== "reload") {
+          return false;
+        }
+      } else {
+        return false;
+      }
+    }
+    if (!Number.isInteger(state.uid) || state.uid < 0) {
+      return false;
+    }
+    if (state.destination === null || typeof state.destination !== "object") {
+      return false;
+    }
+    return true;
+  }
+  _updateInternalState(destination, uid, removeTemporary = false) {
+    if (this._updateViewareaTimeout) {
+      clearTimeout(this._updateViewareaTimeout);
+      this._updateViewareaTimeout = null;
+    }
+    if (removeTemporary && destination?.temporary) {
+      delete destination.temporary;
+    }
+    this._destination = destination;
+    this._uid = uid;
+    this._maxUid = Math.max(this._maxUid, uid);
+    this._numPositionUpdates = 0;
+  }
+  _parseCurrentHash(checkNameddest = false) {
+    const hash = unescape(getCurrentHash()).substring(1);
+    const params = (0, _ui_utils.parseQueryString)(hash);
+    const nameddest = params.get("nameddest") || "";
+    let page = params.get("page") | 0;
+    if (!this._isValidPage(page) || checkNameddest && nameddest.length > 0) {
+      page = null;
+    }
+    return {
+      hash,
+      page,
+      rotation: this.linkService.rotation
+    };
+  }
+  _updateViewarea({
+    location
+  }) {
+    if (this._updateViewareaTimeout) {
+      clearTimeout(this._updateViewareaTimeout);
+      this._updateViewareaTimeout = null;
+    }
+    this._position = {
+      hash: location.pdfOpenParams.substring(1),
+      page: this.linkService.page,
+      first: location.pageNumber,
+      rotation: location.rotation
+    };
+    if (this._popStateInProgress) {
+      return;
+    }
+    if (POSITION_UPDATED_THRESHOLD > 0 && this._isPagesLoaded && this._destination && !this._destination.page) {
+      this._numPositionUpdates++;
+    }
+    if (UPDATE_VIEWAREA_TIMEOUT > 0) {
+      this._updateViewareaTimeout = setTimeout(() => {
+        if (!this._popStateInProgress) {
+          this._tryPushCurrentPosition(true);
+        }
+        this._updateViewareaTimeout = null;
+      }, UPDATE_VIEWAREA_TIMEOUT);
+    }
+  }
+  _popState({
+    state
+  }) {
+    const newHash = getCurrentHash(),
+      hashChanged = this._currentHash !== newHash;
+    this._currentHash = newHash;
+    if (!state) {
+      this._uid++;
+      const {
+        hash,
+        page,
+        rotation
+      } = this._parseCurrentHash();
+      this._pushOrReplaceState({
+        hash,
+        page,
+        rotation
+      }, true);
+      return;
+    }
+    if (!this._isValidState(state)) {
+      return;
+    }
+    this._popStateInProgress = true;
+    if (hashChanged) {
+      this._blockHashChange++;
+      (0, _event_utils.waitOnEventOrTimeout)({
+        target: window,
+        name: "hashchange",
+        delay: HASH_CHANGE_TIMEOUT
+      }).then(() => {
+        this._blockHashChange--;
+      });
+    }
+    const destination = state.destination;
+    this._updateInternalState(destination, state.uid, true);
+    if ((0, _ui_utils.isValidRotation)(destination.rotation)) {
+      this.linkService.rotation = destination.rotation;
+    }
+    if (destination.dest) {
+      this.linkService.goToDestination(destination.dest);
+    } else if (destination.hash) {
+      this.linkService.setHash(destination.hash);
+    } else if (destination.page) {
+      this.linkService.page = destination.page;
+    }
+    Promise.resolve().then(() => {
+      this._popStateInProgress = false;
+    });
+  }
+  _pageHide() {
+    if (!this._destination || this._destination.temporary) {
+      this._tryPushCurrentPosition();
+    }
+  }
+  _bindEvents() {
+    if (this._boundEvents) {
+      return;
+    }
+    this._boundEvents = {
+      updateViewarea: this._updateViewarea.bind(this),
+      popState: this._popState.bind(this),
+      pageHide: this._pageHide.bind(this)
+    };
+    this.eventBus._on("updateviewarea", this._boundEvents.updateViewarea);
+    window.addEventListener("popstate", this._boundEvents.popState);
+    window.addEventListener("pagehide", this._boundEvents.pageHide);
+  }
+  _unbindEvents() {
+    if (!this._boundEvents) {
+      return;
+    }
+    this.eventBus._off("updateviewarea", this._boundEvents.updateViewarea);
+    window.removeEventListener("popstate", this._boundEvents.popState);
+    window.removeEventListener("pagehide", this._boundEvents.pageHide);
+    this._boundEvents = null;
+  }
+}
+exports.PDFHistory = PDFHistory;
+function isDestHashesEqual(destHash, pushHash) {
+  if (typeof destHash !== "string" || typeof pushHash !== "string") {
+    return false;
+  }
+  if (destHash === pushHash) {
+    return true;
+  }
+  const nameddest = (0, _ui_utils.parseQueryString)(destHash).get("nameddest");
+  if (nameddest === pushHash) {
+    return true;
+  }
+  return false;
+}
+function isDestArraysEqual(firstDest, secondDest) {
+  function isEntryEqual(first, second) {
+    if (typeof first !== typeof second) {
+      return false;
+    }
+    if (Array.isArray(first) || Array.isArray(second)) {
+      return false;
+    }
+    if (first !== null && typeof first === "object" && second !== null) {
+      if (Object.keys(first).length !== Object.keys(second).length) {
+        return false;
+      }
+      for (const key in first) {
+        if (!isEntryEqual(first[key], second[key])) {
+          return false;
+        }
+      }
+      return true;
+    }
+    return first === second || Number.isNaN(first) && Number.isNaN(second);
+  }
+  if (!(Array.isArray(firstDest) && Array.isArray(secondDest))) {
+    return false;
+  }
+  if (firstDest.length !== secondDest.length) {
+    return false;
+  }
+  for (let i = 0, ii = firstDest.length; i < ii; i++) {
+    if (!isEntryEqual(firstDest[i], secondDest[i])) {
+      return false;
+    }
+  }
+  return true;
+}
+
+/***/ }),
+/* 13 */
+/***/ ((__unused_webpack_module, exports, __w_pdfjs_require__) => {
+
+
+
+Object.defineProperty(exports, "__esModule", ({
+  value: true
+}));
+exports.PDFPageView = void 0;
+var _pdfjsLib = __w_pdfjs_require__(4);
+var _ui_utils = __w_pdfjs_require__(2);
+var _annotation_editor_layer_builder = __w_pdfjs_require__(14);
+var _annotation_layer_builder = __w_pdfjs_require__(6);
+var _app_options = __w_pdfjs_require__(15);
+var _l10n_utils = __w_pdfjs_require__(7);
+var _pdf_link_service = __w_pdfjs_require__(5);
+var _struct_tree_layer_builder = __w_pdfjs_require__(16);
+var _text_accessibility = __w_pdfjs_require__(17);
+var _text_highlighter = __w_pdfjs_require__(18);
+var _text_layer_builder = __w_pdfjs_require__(19);
+var _xfa_layer_builder = __w_pdfjs_require__(20);
+const MAX_CANVAS_PIXELS = _app_options.compatibilityParams.maxCanvasPixels || 16777216;
+const DEFAULT_LAYER_PROPERTIES = () => {
+  return {
+    annotationEditorUIManager: null,
+    annotationStorage: null,
+    downloadManager: null,
+    enableScripting: false,
+    fieldObjectsPromise: null,
+    findController: null,
+    hasJSActionsPromise: null,
+    get linkService() {
+      return new _pdf_link_service.SimpleLinkService();
+    }
+  };
+};
+class PDFPageView {
+  #annotationMode = _pdfjsLib.AnnotationMode.ENABLE_FORMS;
+  #hasRestrictedScaling = false;
+  #layerProperties = null;
+  #loadingId = null;
+  #previousRotation = null;
+  #renderError = null;
+  #renderingState = _ui_utils.RenderingStates.INITIAL;
+  #textLayerMode = _ui_utils.TextLayerMode.ENABLE;
+  #useThumbnailCanvas = {
+    directDrawing: true,
+    initialOptionalContent: true,
+    regularAnnotations: true
+  };
+  #viewportMap = new WeakMap();
+  constructor(options) {
+    const container = options.container;
+    const defaultViewport = options.defaultViewport;
+    this.id = options.id;
+    this.renderingId = "page" + this.id;
+    this.#layerProperties = options.layerProperties || DEFAULT_LAYER_PROPERTIES;
+    this.pdfPage = null;
+    this.pageLabel = null;
+    this.rotation = 0;
+    this.scale = options.scale || _ui_utils.DEFAULT_SCALE;
+    this.viewport = defaultViewport;
+    this.pdfPageRotate = defaultViewport.rotation;
+    this._optionalContentConfigPromise = options.optionalContentConfigPromise || null;
+    this.#textLayerMode = options.textLayerMode ?? _ui_utils.TextLayerMode.ENABLE;
+    this.#annotationMode = options.annotationMode ?? _pdfjsLib.AnnotationMode.ENABLE_FORMS;
+    this.imageResourcesPath = options.imageResourcesPath || "";
+    this.isOffscreenCanvasSupported = options.isOffscreenCanvasSupported ?? true;
+    this.maxCanvasPixels = options.maxCanvasPixels ?? MAX_CANVAS_PIXELS;
+    this.pageColors = options.pageColors || null;
+    this.eventBus = options.eventBus;
+    this.renderingQueue = options.renderingQueue;
+    this.l10n = options.l10n || _l10n_utils.NullL10n;
+    this.renderTask = null;
+    this.resume = null;
+    this._isStandalone = !this.renderingQueue?.hasViewer();
+    this._container = container;
+    if (options.useOnlyCssZoom) {
+      console.error("useOnlyCssZoom was removed, please use `maxCanvasPixels = 0` instead.");
+      this.maxCanvasPixels = 0;
+    }
+    this._annotationCanvasMap = null;
+    this.annotationLayer = null;
+    this.annotationEditorLayer = null;
+    this.textLayer = null;
+    this.zoomLayer = null;
+    this.xfaLayer = null;
+    this.structTreeLayer = null;
+    const div = document.createElement("div");
+    div.className = "page";
+    div.setAttribute("data-page-number", this.id);
+    div.setAttribute("role", "region");
+    this.l10n.get("page_landmark", {
+      page: this.id
+    }).then(msg => {
+      div.setAttribute("aria-label", msg);
+    });
+    this.div = div;
+    this.#setDimensions();
+    container?.append(div);
+    if (this._isStandalone) {
+      container?.style.setProperty("--scale-factor", this.scale * _pdfjsLib.PixelsPerInch.PDF_TO_CSS_UNITS);
+      const {
+        optionalContentConfigPromise
+      } = options;
+      if (optionalContentConfigPromise) {
+        optionalContentConfigPromise.then(optionalContentConfig => {
+          if (optionalContentConfigPromise !== this._optionalContentConfigPromise) {
+            return;
+          }
+          this.#useThumbnailCanvas.initialOptionalContent = optionalContentConfig.hasInitialVisibility;
+        });
+      }
+    }
+  }
+  get renderingState() {
+    return this.#renderingState;
+  }
+  set renderingState(state) {
+    if (state === this.#renderingState) {
+      return;
+    }
+    this.#renderingState = state;
+    if (this.#loadingId) {
+      clearTimeout(this.#loadingId);
+      this.#loadingId = null;
+    }
+    switch (state) {
+      case _ui_utils.RenderingStates.PAUSED:
+        this.div.classList.remove("loading");
+        break;
+      case _ui_utils.RenderingStates.RUNNING:
+        this.div.classList.add("loadingIcon");
+        this.#loadingId = setTimeout(() => {
+          this.div.classList.add("loading");
+          this.#loadingId = null;
+        }, 0);
+        break;
+      case _ui_utils.RenderingStates.INITIAL:
+      case _ui_utils.RenderingStates.FINISHED:
+        this.div.classList.remove("loadingIcon", "loading");
+        break;
+    }
+  }
+  #setDimensions() {
+    const {
+      viewport
+    } = this;
+    if (this.pdfPage) {
+      if (this.#previousRotation === viewport.rotation) {
+        return;
+      }
+      this.#previousRotation = viewport.rotation;
+    }
+    (0, _pdfjsLib.setLayerDimensions)(this.div, viewport, true, false);
+  }
+  setPdfPage(pdfPage) {
+    if (this._isStandalone && (this.pageColors?.foreground === "CanvasText" || this.pageColors?.background === "Canvas")) {
+      this._container?.style.setProperty("--hcm-highligh-filter", pdfPage.filterFactory.addHighlightHCMFilter("CanvasText", "Canvas", "HighlightText", "Highlight"));
+    }
+    this.pdfPage = pdfPage;
+    this.pdfPageRotate = pdfPage.rotate;
+    const totalRotation = (this.rotation + this.pdfPageRotate) % 360;
+    this.viewport = pdfPage.getViewport({
+      scale: this.scale * _pdfjsLib.PixelsPerInch.PDF_TO_CSS_UNITS,
+      rotation: totalRotation
+    });
+    this.#setDimensions();
+    this.reset();
+  }
+  destroy() {
+    this.reset();
+    this.pdfPage?.cleanup();
+  }
+  get _textHighlighter() {
+    return (0, _pdfjsLib.shadow)(this, "_textHighlighter", new _text_highlighter.TextHighlighter({
+      pageIndex: this.id - 1,
+      eventBus: this.eventBus,
+      findController: this.#layerProperties().findController
+    }));
+  }
+  async #renderAnnotationLayer() {
+    let error = null;
+    try {
+      await this.annotationLayer.render(this.viewport, "display");
+    } catch (ex) {
+      console.error(`#renderAnnotationLayer: "${ex}".`);
+      error = ex;
+    } finally {
+      this.eventBus.dispatch("annotationlayerrendered", {
+        source: this,
+        pageNumber: this.id,
+        error
+      });
+    }
+  }
+  async #renderAnnotationEditorLayer() {
+    let error = null;
+    try {
+      await this.annotationEditorLayer.render(this.viewport, "display");
+    } catch (ex) {
+      console.error(`#renderAnnotationEditorLayer: "${ex}".`);
+      error = ex;
+    } finally {
+      this.eventBus.dispatch("annotationeditorlayerrendered", {
+        source: this,
+        pageNumber: this.id,
+        error
+      });
+    }
+  }
+  async #renderXfaLayer() {
+    let error = null;
+    try {
+      const result = await this.xfaLayer.render(this.viewport, "display");
+      if (result?.textDivs && this._textHighlighter) {
+        this.#buildXfaTextContentItems(result.textDivs);
+      }
+    } catch (ex) {
+      console.error(`#renderXfaLayer: "${ex}".`);
+      error = ex;
+    } finally {
+      this.eventBus.dispatch("xfalayerrendered", {
+        source: this,
+        pageNumber: this.id,
+        error
+      });
+    }
+  }
+  async #renderTextLayer() {
+    const {
+      pdfPage,
+      textLayer,
+      viewport
+    } = this;
+    if (!textLayer) {
+      return;
+    }
+    let error = null;
+    try {
+      if (!textLayer.renderingDone) {
+        const readableStream = pdfPage.streamTextContent({
+          includeMarkedContent: true,
+          disableNormalization: true
+        });
+        textLayer.setTextContentSource(readableStream);
+      }
+      await textLayer.render(viewport);
+    } catch (ex) {
+      if (ex instanceof _pdfjsLib.AbortException) {
+        return;
+      }
+      console.error(`#renderTextLayer: "${ex}".`);
+      error = ex;
+    }
+    this.eventBus.dispatch("textlayerrendered", {
+      source: this,
+      pageNumber: this.id,
+      numTextDivs: textLayer.numTextDivs,
+      error
+    });
+    this.#renderStructTreeLayer();
+  }
+  async #renderStructTreeLayer() {
+    if (!this.textLayer) {
+      return;
+    }
+    this.structTreeLayer ||= new _struct_tree_layer_builder.StructTreeLayerBuilder();
+    const tree = await (!this.structTreeLayer.renderingDone ? this.pdfPage.getStructTree() : null);
+    const treeDom = this.structTreeLayer?.render(tree);
+    if (treeDom) {
+      this.canvas?.append(treeDom);
+    }
+    this.structTreeLayer?.show();
+  }
+  async #buildXfaTextContentItems(textDivs) {
+    const text = await this.pdfPage.getTextContent();
+    const items = [];
+    for (const item of text.items) {
+      items.push(item.str);
+    }
+    this._textHighlighter.setTextMapping(textDivs, items);
+    this._textHighlighter.enable();
+  }
+  _resetZoomLayer(removeFromDOM = false) {
+    if (!this.zoomLayer) {
+      return;
+    }
+    const zoomLayerCanvas = this.zoomLayer.firstChild;
+    this.#viewportMap.delete(zoomLayerCanvas);
+    zoomLayerCanvas.width = 0;
+    zoomLayerCanvas.height = 0;
+    if (removeFromDOM) {
+      this.zoomLayer.remove();
+    }
+    this.zoomLayer = null;
+  }
+  reset({
+    keepZoomLayer = false,
+    keepAnnotationLayer = false,
+    keepAnnotationEditorLayer = false,
+    keepXfaLayer = false,
+    keepTextLayer = false
+  } = {}) {
+    this.cancelRendering({
+      keepAnnotationLayer,
+      keepAnnotationEditorLayer,
+      keepXfaLayer,
+      keepTextLayer
+    });
+    this.renderingState = _ui_utils.RenderingStates.INITIAL;
+    const div = this.div;
+    const childNodes = div.childNodes,
+      zoomLayerNode = keepZoomLayer && this.zoomLayer || null,
+      annotationLayerNode = keepAnnotationLayer && this.annotationLayer?.div || null,
+      annotationEditorLayerNode = keepAnnotationEditorLayer && this.annotationEditorLayer?.div || null,
+      xfaLayerNode = keepXfaLayer && this.xfaLayer?.div || null,
+      textLayerNode = keepTextLayer && this.textLayer?.div || null;
+    for (let i = childNodes.length - 1; i >= 0; i--) {
+      const node = childNodes[i];
+      switch (node) {
+        case zoomLayerNode:
+        case annotationLayerNode:
+        case annotationEditorLayerNode:
+        case xfaLayerNode:
+        case textLayerNode:
+          continue;
+      }
+      node.remove();
+    }
+    div.removeAttribute("data-loaded");
+    if (annotationLayerNode) {
+      this.annotationLayer.hide();
+    }
+    if (annotationEditorLayerNode) {
+      this.annotationEditorLayer.hide();
+    }
+    if (xfaLayerNode) {
+      this.xfaLayer.hide();
+    }
+    if (textLayerNode) {
+      this.textLayer.hide();
+    }
+    this.structTreeLayer?.hide();
+    if (!zoomLayerNode) {
+      if (this.canvas) {
+        this.#viewportMap.delete(this.canvas);
+        this.canvas.width = 0;
+        this.canvas.height = 0;
+        delete this.canvas;
+      }
+      this._resetZoomLayer();
+    }
+  }
+  update({
+    scale = 0,
+    rotation = null,
+    optionalContentConfigPromise = null,
+    drawingDelay = -1
+  }) {
+    this.scale = scale || this.scale;
+    if (typeof rotation === "number") {
+      this.rotation = rotation;
+    }
+    if (optionalContentConfigPromise instanceof Promise) {
+      this._optionalContentConfigPromise = optionalContentConfigPromise;
+      optionalContentConfigPromise.then(optionalContentConfig => {
+        if (optionalContentConfigPromise !== this._optionalContentConfigPromise) {
+          return;
+        }
+        this.#useThumbnailCanvas.initialOptionalContent = optionalContentConfig.hasInitialVisibility;
+      });
+    }
+    this.#useThumbnailCanvas.directDrawing = true;
+    const totalRotation = (this.rotation + this.pdfPageRotate) % 360;
+    this.viewport = this.viewport.clone({
+      scale: this.scale * _pdfjsLib.PixelsPerInch.PDF_TO_CSS_UNITS,
+      rotation: totalRotation
+    });
+    this.#setDimensions();
+    if (this._isStandalone) {
+      this._container?.style.setProperty("--scale-factor", this.viewport.scale);
+    }
+    if (this.canvas) {
+      let onlyCssZoom = false;
+      if (this.#hasRestrictedScaling) {
+        if (this.maxCanvasPixels === 0) {
+          onlyCssZoom = true;
+        } else if (this.maxCanvasPixels > 0) {
+          const {
+            width,
+            height
+          } = this.viewport;
+          const {
+            sx,
+            sy
+          } = this.outputScale;
+          onlyCssZoom = (Math.floor(width) * sx | 0) * (Math.floor(height) * sy | 0) > this.maxCanvasPixels;
+        }
+      }
+      const postponeDrawing = !onlyCssZoom && drawingDelay >= 0 && drawingDelay < 1000;
+      if (postponeDrawing || onlyCssZoom) {
+        if (postponeDrawing && this.renderingState !== _ui_utils.RenderingStates.FINISHED) {
+          this.cancelRendering({
+            keepZoomLayer: true,
+            keepAnnotationLayer: true,
+            keepAnnotationEditorLayer: true,
+            keepXfaLayer: true,
+            keepTextLayer: true,
+            cancelExtraDelay: drawingDelay
+          });
+          this.renderingState = _ui_utils.RenderingStates.FINISHED;
+          this.#useThumbnailCanvas.directDrawing = false;
+        }
+        this.cssTransform({
+          target: this.canvas,
+          redrawAnnotationLayer: true,
+          redrawAnnotationEditorLayer: true,
+          redrawXfaLayer: true,
+          redrawTextLayer: !postponeDrawing,
+          hideTextLayer: postponeDrawing
+        });
+        if (postponeDrawing) {
+          return;
+        }
+        this.eventBus.dispatch("pagerendered", {
+          source: this,
+          pageNumber: this.id,
+          cssTransform: true,
+          timestamp: performance.now(),
+          error: this.#renderError
+        });
+        return;
+      }
+      if (!this.zoomLayer && !this.canvas.hidden) {
+        this.zoomLayer = this.canvas.parentNode;
+        this.zoomLayer.style.position = "absolute";
+      }
+    }
+    if (this.zoomLayer) {
+      this.cssTransform({
+        target: this.zoomLayer.firstChild
+      });
+    }
+    this.reset({
+      keepZoomLayer: true,
+      keepAnnotationLayer: true,
+      keepAnnotationEditorLayer: true,
+      keepXfaLayer: true,
+      keepTextLayer: true
+    });
+  }
+  cancelRendering({
+    keepAnnotationLayer = false,
+    keepAnnotationEditorLayer = false,
+    keepXfaLayer = false,
+    keepTextLayer = false,
+    cancelExtraDelay = 0
+  } = {}) {
+    if (this.renderTask) {
+      this.renderTask.cancel(cancelExtraDelay);
+      this.renderTask = null;
+    }
+    this.resume = null;
+    if (this.textLayer && (!keepTextLayer || !this.textLayer.div)) {
+      this.textLayer.cancel();
+      this.textLayer = null;
+    }
+    if (this.structTreeLayer && !this.textLayer) {
+      this.structTreeLayer = null;
+    }
+    if (this.annotationLayer && (!keepAnnotationLayer || !this.annotationLayer.div)) {
+      this.annotationLayer.cancel();
+      this.annotationLayer = null;
+      this._annotationCanvasMap = null;
+    }
+    if (this.annotationEditorLayer && (!keepAnnotationEditorLayer || !this.annotationEditorLayer.div)) {
+      this.annotationEditorLayer.cancel();
+      this.annotationEditorLayer = null;
+    }
+    if (this.xfaLayer && (!keepXfaLayer || !this.xfaLayer.div)) {
+      this.xfaLayer.cancel();
+      this.xfaLayer = null;
+      this._textHighlighter?.disable();
+    }
+  }
+  cssTransform({
+    target,
+    redrawAnnotationLayer = false,
+    redrawAnnotationEditorLayer = false,
+    redrawXfaLayer = false,
+    redrawTextLayer = false,
+    hideTextLayer = false
+  }) {
+    if (!target.hasAttribute("zooming")) {
+      target.setAttribute("zooming", true);
+      const {
+        style
+      } = target;
+      style.width = style.height = "";
+    }
+    const originalViewport = this.#viewportMap.get(target);
+    if (this.viewport !== originalViewport) {
+      const relativeRotation = this.viewport.rotation - originalViewport.rotation;
+      const absRotation = Math.abs(relativeRotation);
+      let scaleX = 1,
+        scaleY = 1;
+      if (absRotation === 90 || absRotation === 270) {
+        const {
+          width,
+          height
+        } = this.viewport;
+        scaleX = height / width;
+        scaleY = width / height;
+      }
+      target.style.transform = `rotate(${relativeRotation}deg) scale(${scaleX}, ${scaleY})`;
+    }
+    if (redrawAnnotationLayer && this.annotationLayer) {
+      this.#renderAnnotationLayer();
+    }
+    if (redrawAnnotationEditorLayer && this.annotationEditorLayer) {
+      this.#renderAnnotationEditorLayer();
+    }
+    if (redrawXfaLayer && this.xfaLayer) {
+      this.#renderXfaLayer();
+    }
+    if (this.textLayer) {
+      if (hideTextLayer) {
+        this.textLayer.hide();
+        this.structTreeLayer?.hide();
+      } else if (redrawTextLayer) {
+        this.#renderTextLayer();
+      }
+    }
+  }
+  get width() {
+    return this.viewport.width;
+  }
+  get height() {
+    return this.viewport.height;
+  }
+  getPagePoint(x, y) {
+    return this.viewport.convertToPdfPoint(x, y);
+  }
+  async #finishRenderTask(renderTask, error = null) {
+    if (renderTask === this.renderTask) {
+      this.renderTask = null;
+    }
+    if (error instanceof _pdfjsLib.RenderingCancelledException) {
+      this.#renderError = null;
+      return;
+    }
+    this.#renderError = error;
+    this.renderingState = _ui_utils.RenderingStates.FINISHED;
+    this._resetZoomLayer(true);
+    this.#useThumbnailCanvas.regularAnnotations = !renderTask.separateAnnots;
+    this.eventBus.dispatch("pagerendered", {
+      source: this,
+      pageNumber: this.id,
+      cssTransform: false,
+      timestamp: performance.now(),
+      error: this.#renderError
+    });
+    if (error) {
+      throw error;
+    }
+  }
+  async draw() {
+    if (this.renderingState !== _ui_utils.RenderingStates.INITIAL) {
+      console.error("Must be in new state before drawing");
+      this.reset();
+    }
+    const {
+      div,
+      l10n,
+      pageColors,
+      pdfPage,
+      viewport
+    } = this;
+    if (!pdfPage) {
+      this.renderingState = _ui_utils.RenderingStates.FINISHED;
+      throw new Error("pdfPage is not loaded");
+    }
+    this.renderingState = _ui_utils.RenderingStates.RUNNING;
+    const canvasWrapper = document.createElement("div");
+    canvasWrapper.classList.add("canvasWrapper");
+    div.append(canvasWrapper);
+    if (!this.textLayer && this.#textLayerMode !== _ui_utils.TextLayerMode.DISABLE && !pdfPage.isPureXfa) {
+      this._accessibilityManager ||= new _text_accessibility.TextAccessibilityManager();
+      this.textLayer = new _text_layer_builder.TextLayerBuilder({
+        highlighter: this._textHighlighter,
+        accessibilityManager: this._accessibilityManager,
+        isOffscreenCanvasSupported: this.isOffscreenCanvasSupported,
+        enablePermissions: this.#textLayerMode === _ui_utils.TextLayerMode.ENABLE_PERMISSIONS
+      });
+      div.append(this.textLayer.div);
+    }
+    if (!this.annotationLayer && this.#annotationMode !== _pdfjsLib.AnnotationMode.DISABLE) {
+      const {
+        annotationStorage,
+        downloadManager,
+        enableScripting,
+        fieldObjectsPromise,
+        hasJSActionsPromise,
+        linkService
+      } = this.#layerProperties();
+      this._annotationCanvasMap ||= new Map();
+      this.annotationLayer = new _annotation_layer_builder.AnnotationLayerBuilder({
+        pageDiv: div,
+        pdfPage,
+        annotationStorage,
+        imageResourcesPath: this.imageResourcesPath,
+        renderForms: this.#annotationMode === _pdfjsLib.AnnotationMode.ENABLE_FORMS,
+        linkService,
+        downloadManager,
+        l10n,
+        enableScripting,
+        hasJSActionsPromise,
+        fieldObjectsPromise,
+        annotationCanvasMap: this._annotationCanvasMap,
+        accessibilityManager: this._accessibilityManager
+      });
+    }
+    const renderContinueCallback = cont => {
+      showCanvas?.(false);
+      if (this.renderingQueue && !this.renderingQueue.isHighestPriority(this)) {
+        this.renderingState = _ui_utils.RenderingStates.PAUSED;
+        this.resume = () => {
+          this.renderingState = _ui_utils.RenderingStates.RUNNING;
+          cont();
+        };
+        return;
+      }
+      cont();
+    };
+    const {
+      width,
+      height
+    } = viewport;
+    const canvas = document.createElement("canvas");
+    canvas.setAttribute("role", "presentation");
+    canvas.hidden = true;
+    const hasHCM = !!(pageColors?.background && pageColors?.foreground);
+    let showCanvas = isLastShow => {
+      if (!hasHCM || isLastShow) {
+        canvas.hidden = false;
+        showCanvas = null;
+      }
+    };
+    canvasWrapper.append(canvas);
+    this.canvas = canvas;
+    const ctx = canvas.getContext("2d", {
+      alpha: false
+    });
+    const outputScale = this.outputScale = new _ui_utils.OutputScale();
+    if (this.maxCanvasPixels === 0) {
+      const invScale = 1 / this.scale;
+      outputScale.sx *= invScale;
+      outputScale.sy *= invScale;
+      this.#hasRestrictedScaling = true;
+    } else if (this.maxCanvasPixels > 0) {
+      const pixelsInViewport = width * height;
+      const maxScale = Math.sqrt(this.maxCanvasPixels / pixelsInViewport);
+      if (outputScale.sx > maxScale || outputScale.sy > maxScale) {
+        outputScale.sx = maxScale;
+        outputScale.sy = maxScale;
+        this.#hasRestrictedScaling = true;
+      } else {
+        this.#hasRestrictedScaling = false;
+      }
+    }
+    const sfx = (0, _ui_utils.approximateFraction)(outputScale.sx);
+    const sfy = (0, _ui_utils.approximateFraction)(outputScale.sy);
+    canvas.width = (0, _ui_utils.roundToDivide)(width * outputScale.sx, sfx[0]);
+    canvas.height = (0, _ui_utils.roundToDivide)(height * outputScale.sy, sfy[0]);
+    const {
+      style
+    } = canvas;
+    style.width = (0, _ui_utils.roundToDivide)(width, sfx[1]) + "px";
+    style.height = (0, _ui_utils.roundToDivide)(height, sfy[1]) + "px";
+    this.#viewportMap.set(canvas, viewport);
+    const transform = outputScale.scaled ? [outputScale.sx, 0, 0, outputScale.sy, 0, 0] : null;
+    const renderContext = {
+      canvasContext: ctx,
+      transform,
+      viewport,
+      annotationMode: this.#annotationMode,
+      optionalContentConfigPromise: this._optionalContentConfigPromise,
+      annotationCanvasMap: this._annotationCanvasMap,
+      pageColors
+    };
+    const renderTask = this.renderTask = this.pdfPage.render(renderContext);
+    renderTask.onContinue = renderContinueCallback;
+    const resultPromise = renderTask.promise.then(async () => {
+      showCanvas?.(true);
+      await this.#finishRenderTask(renderTask);
+      this.#renderTextLayer();
+      if (this.annotationLayer) {
+        await this.#renderAnnotationLayer();
+      }
+      if (!this.annotationEditorLayer) {
+        const {
+          annotationEditorUIManager
+        } = this.#layerProperties();
+        if (!annotationEditorUIManager) {
+          return;
+        }
+        this.annotationEditorLayer = new _annotation_editor_layer_builder.AnnotationEditorLayerBuilder({
+          uiManager: annotationEditorUIManager,
+          pageDiv: div,
+          pdfPage,
+          l10n,
+          accessibilityManager: this._accessibilityManager,
+          annotationLayer: this.annotationLayer?.annotationLayer
+        });
+      }
+      this.#renderAnnotationEditorLayer();
+    }, error => {
+      if (!(error instanceof _pdfjsLib.RenderingCancelledException)) {
+        showCanvas?.(true);
+      }
+      return this.#finishRenderTask(renderTask, error);
+    });
+    if (pdfPage.isPureXfa) {
+      if (!this.xfaLayer) {
+        const {
+          annotationStorage,
+          linkService
+        } = this.#layerProperties();
+        this.xfaLayer = new _xfa_layer_builder.XfaLayerBuilder({
+          pageDiv: div,
+          pdfPage,
+          annotationStorage,
+          linkService
+        });
+      } else if (this.xfaLayer.div) {
+        div.append(this.xfaLayer.div);
+      }
+      this.#renderXfaLayer();
+    }
+    div.setAttribute("data-loaded", true);
+    this.eventBus.dispatch("pagerender", {
+      source: this,
+      pageNumber: this.id
+    });
+    return resultPromise;
+  }
+  setPageLabel(label) {
+    this.pageLabel = typeof label === "string" ? label : null;
+    if (this.pageLabel !== null) {
+      this.div.setAttribute("data-page-label", this.pageLabel);
+    } else {
+      this.div.removeAttribute("data-page-label");
+    }
+  }
+  get thumbnailCanvas() {
+    const {
+      directDrawing,
+      initialOptionalContent,
+      regularAnnotations
+    } = this.#useThumbnailCanvas;
+    return directDrawing && initialOptionalContent && regularAnnotations ? this.canvas : null;
+  }
+}
+exports.PDFPageView = PDFPageView;
+
+/***/ }),
+/* 14 */
+/***/ ((__unused_webpack_module, exports, __w_pdfjs_require__) => {
+
+
+
+Object.defineProperty(exports, "__esModule", ({
+  value: true
+}));
+exports.AnnotationEditorLayerBuilder = void 0;
+var _pdfjsLib = __w_pdfjs_require__(4);
+var _l10n_utils = __w_pdfjs_require__(7);
+class AnnotationEditorLayerBuilder {
+  #annotationLayer = null;
+  #uiManager;
+  constructor(options) {
+    this.pageDiv = options.pageDiv;
+    this.pdfPage = options.pdfPage;
+    this.accessibilityManager = options.accessibilityManager;
+    this.l10n = options.l10n || _l10n_utils.NullL10n;
+    this.annotationEditorLayer = null;
+    this.div = null;
+    this._cancelled = false;
+    this.#uiManager = options.uiManager;
+    this.#annotationLayer = options.annotationLayer || null;
+  }
+  async render(viewport, intent = "display") {
+    if (intent !== "display") {
+      return;
+    }
+    if (this._cancelled) {
+      return;
+    }
+    const clonedViewport = viewport.clone({
+      dontFlip: true
+    });
+    if (this.div) {
+      this.annotationEditorLayer.update({
+        viewport: clonedViewport
+      });
+      this.show();
+      return;
+    }
+    const div = this.div = document.createElement("div");
+    div.className = "annotationEditorLayer";
+    div.tabIndex = 0;
+    div.hidden = true;
+    div.dir = this.#uiManager.direction;
+    this.pageDiv.append(div);
+    this.annotationEditorLayer = new _pdfjsLib.AnnotationEditorLayer({
+      uiManager: this.#uiManager,
+      div,
+      accessibilityManager: this.accessibilityManager,
+      pageIndex: this.pdfPage.pageNumber - 1,
+      l10n: this.l10n,
+      viewport: clonedViewport,
+      annotationLayer: this.#annotationLayer
+    });
+    const parameters = {
+      viewport: clonedViewport,
+      div,
+      annotations: null,
+      intent
+    };
+    this.annotationEditorLayer.render(parameters);
+    this.show();
+  }
+  cancel() {
+    this._cancelled = true;
+    if (!this.div) {
+      return;
+    }
+    this.pageDiv = null;
+    this.annotationEditorLayer.destroy();
+    this.div.remove();
+  }
+  hide() {
+    if (!this.div) {
+      return;
+    }
+    this.div.hidden = true;
+  }
+  show() {
+    if (!this.div || this.annotationEditorLayer.isEmpty) {
+      return;
+    }
+    this.div.hidden = false;
+  }
+}
+exports.AnnotationEditorLayerBuilder = AnnotationEditorLayerBuilder;
+
+/***/ }),
+/* 15 */
+/***/ ((__unused_webpack_module, exports) => {
+
+
+
+Object.defineProperty(exports, "__esModule", ({
+  value: true
+}));
+exports.compatibilityParams = exports.OptionKind = exports.AppOptions = void 0;
+const compatibilityParams = Object.create(null);
+exports.compatibilityParams = compatibilityParams;
+{
+  const userAgent = navigator.userAgent || "";
+  const platform = navigator.platform || "";
+  const maxTouchPoints = navigator.maxTouchPoints || 1;
+  const isAndroid = /Android/.test(userAgent);
+  const isIOS = /\b(iPad|iPhone|iPod)(?=;)/.test(userAgent) || platform === "MacIntel" && maxTouchPoints > 1;
+  (function checkCanvasSizeLimitation() {
+    if (isIOS || isAndroid) {
+      compatibilityParams.maxCanvasPixels = 5242880;
+    }
+  })();
+}
+const OptionKind = {
+  VIEWER: 0x02,
+  API: 0x04,
+  WORKER: 0x08,
+  PREFERENCE: 0x80
+};
+exports.OptionKind = OptionKind;
+const defaultOptions = {
+  annotationEditorMode: {
+    value: 0,
+    kind: OptionKind.VIEWER + OptionKind.PREFERENCE
+  },
+  annotationMode: {
+    value: 2,
+    kind: OptionKind.VIEWER + OptionKind.PREFERENCE
+  },
+  cursorToolOnLoad: {
+    value: 0,
+    kind: OptionKind.VIEWER + OptionKind.PREFERENCE
+  },
+  defaultZoomDelay: {
+    value: 400,
+    kind: OptionKind.VIEWER + OptionKind.PREFERENCE
+  },
+  defaultZoomValue: {
+    value: "",
+    kind: OptionKind.VIEWER + OptionKind.PREFERENCE
+  },
+  disableHistory: {
+    value: false,
+    kind: OptionKind.VIEWER
+  },
+  disablePageLabels: {
+    value: false,
+    kind: OptionKind.VIEWER + OptionKind.PREFERENCE
+  },
+  enablePermissions: {
+    value: false,
+    kind: OptionKind.VIEWER + OptionKind.PREFERENCE
+  },
+  enablePrintAutoRotate: {
+    value: true,
+    kind: OptionKind.VIEWER + OptionKind.PREFERENCE
+  },
+  enableScripting: {
+    value: true,
+    kind: OptionKind.VIEWER + OptionKind.PREFERENCE
+  },
+  enableStampEditor: {
+    value: true,
+    kind: OptionKind.VIEWER + OptionKind.PREFERENCE
+  },
+  externalLinkRel: {
+    value: "noopener noreferrer nofollow",
+    kind: OptionKind.VIEWER
+  },
+  externalLinkTarget: {
+    value: 0,
+    kind: OptionKind.VIEWER + OptionKind.PREFERENCE
+  },
+  historyUpdateUrl: {
+    value: false,
+    kind: OptionKind.VIEWER + OptionKind.PREFERENCE
+  },
+  ignoreDestinationZoom: {
+    value: false,
+    kind: OptionKind.VIEWER + OptionKind.PREFERENCE
+  },
+  imageResourcesPath: {
+    value: "./images/",
+    kind: OptionKind.VIEWER
+  },
+  maxCanvasPixels: {
+    value: 16777216,
+    kind: OptionKind.VIEWER
+  },
+  forcePageColors: {
+    value: false,
+    kind: OptionKind.VIEWER + OptionKind.PREFERENCE
+  },
+  pageColorsBackground: {
+    value: "Canvas",
+    kind: OptionKind.VIEWER + OptionKind.PREFERENCE
+  },
+  pageColorsForeground: {
+    value: "CanvasText",
+    kind: OptionKind.VIEWER + OptionKind.PREFERENCE
+  },
+  pdfBugEnabled: {
+    value: false,
+    kind: OptionKind.VIEWER + OptionKind.PREFERENCE
+  },
+  printResolution: {
+    value: 150,
+    kind: OptionKind.VIEWER
+  },
+  sidebarViewOnLoad: {
+    value: -1,
+    kind: OptionKind.VIEWER + OptionKind.PREFERENCE
+  },
+  scrollModeOnLoad: {
+    value: -1,
+    kind: OptionKind.VIEWER + OptionKind.PREFERENCE
+  },
+  spreadModeOnLoad: {
+    value: -1,
+    kind: OptionKind.VIEWER + OptionKind.PREFERENCE
+  },
+  textLayerMode: {
+    value: 1,
+    kind: OptionKind.VIEWER + OptionKind.PREFERENCE
+  },
+  viewerCssTheme: {
+    value: 0,
+    kind: OptionKind.VIEWER + OptionKind.PREFERENCE
+  },
+  viewOnLoad: {
+    value: 0,
+    kind: OptionKind.VIEWER + OptionKind.PREFERENCE
+  },
+  cMapPacked: {
+    value: true,
+    kind: OptionKind.API
+  },
+  cMapUrl: {
+    value: "../web/cmaps/",
+    kind: OptionKind.API
+  },
+  disableAutoFetch: {
+    value: false,
+    kind: OptionKind.API + OptionKind.PREFERENCE
+  },
+  disableFontFace: {
+    value: false,
+    kind: OptionKind.API + OptionKind.PREFERENCE
+  },
+  disableRange: {
+    value: false,
+    kind: OptionKind.API + OptionKind.PREFERENCE
+  },
+  disableStream: {
+    value: false,
+    kind: OptionKind.API + OptionKind.PREFERENCE
+  },
+  docBaseUrl: {
+    value: "",
+    kind: OptionKind.API
+  },
+  enableXfa: {
+    value: true,
+    kind: OptionKind.API + OptionKind.PREFERENCE
+  },
+  fontExtraProperties: {
+    value: false,
+    kind: OptionKind.API
+  },
+  isEvalSupported: {
+    value: true,
+    kind: OptionKind.API
+  },
+  isOffscreenCanvasSupported: {
+    value: true,
+    kind: OptionKind.API
+  },
+  maxImageSize: {
+    value: -1,
+    kind: OptionKind.API
+  },
+  pdfBug: {
+    value: false,
+    kind: OptionKind.API
+  },
+  standardFontDataUrl: {
+    value: "../web/standard_fonts/",
+    kind: OptionKind.API
+  },
+  verbosity: {
+    value: 1,
+    kind: OptionKind.API
+  },
+  workerPort: {
+    value: null,
+    kind: OptionKind.WORKER
+  },
+  workerSrc: {
+    value: "../build/pdf.worker.js",
+    kind: OptionKind.WORKER
+  }
+};
+{
+  defaultOptions.defaultUrl = {
+    value: "compressed.tracemonkey-pldi-09.pdf",
+    kind: OptionKind.VIEWER
+  };
+  defaultOptions.disablePreferences = {
+    value: false,
+    kind: OptionKind.VIEWER
+  };
+  defaultOptions.locale = {
+    value: navigator.language || "en-US",
+    kind: OptionKind.VIEWER
+  };
+  defaultOptions.sandboxBundleSrc = {
+    value: "../build/pdf.sandbox.js",
+    kind: OptionKind.VIEWER
+  };
+}
+const userOptions = Object.create(null);
+class AppOptions {
+  constructor() {
+    throw new Error("Cannot initialize AppOptions.");
+  }
+  static get(name) {
+    const userOption = userOptions[name];
+    if (userOption !== undefined) {
+      return userOption;
+    }
+    const defaultOption = defaultOptions[name];
+    if (defaultOption !== undefined) {
+      return compatibilityParams[name] ?? defaultOption.value;
+    }
+    return undefined;
+  }
+  static getAll(kind = null) {
+    const options = Object.create(null);
+    for (const name in defaultOptions) {
+      const defaultOption = defaultOptions[name];
+      if (kind) {
+        if ((kind & defaultOption.kind) === 0) {
+          continue;
+        }
+        if (kind === OptionKind.PREFERENCE) {
+          const value = defaultOption.value,
+            valueType = typeof value;
+          if (valueType === "boolean" || valueType === "string" || valueType === "number" && Number.isInteger(value)) {
+            options[name] = value;
+            continue;
+          }
+          throw new Error(`Invalid type for preference: ${name}`);
+        }
+      }
+      const userOption = userOptions[name];
+      options[name] = userOption !== undefined ? userOption : compatibilityParams[name] ?? defaultOption.value;
+    }
+    return options;
+  }
+  static set(name, value) {
+    userOptions[name] = value;
+  }
+  static setAll(options) {
+    for (const name in options) {
+      userOptions[name] = options[name];
+    }
+  }
+  static remove(name) {
+    delete userOptions[name];
+  }
+}
+exports.AppOptions = AppOptions;
+{
+  AppOptions._hasUserOptions = function () {
+    return Object.keys(userOptions).length > 0;
+  };
+}
+
+/***/ }),
+/* 16 */
+/***/ ((__unused_webpack_module, exports, __w_pdfjs_require__) => {
+
+
+
+Object.defineProperty(exports, "__esModule", ({
+  value: true
+}));
+exports.StructTreeLayerBuilder = void 0;
+var _ui_utils = __w_pdfjs_require__(2);
+const PDF_ROLE_TO_HTML_ROLE = {
+  Document: null,
+  DocumentFragment: null,
+  Part: "group",
+  Sect: "group",
+  Div: "group",
+  Aside: "note",
+  NonStruct: "none",
+  P: null,
+  H: "heading",
+  Title: null,
+  FENote: "note",
+  Sub: "group",
+  Lbl: null,
+  Span: null,
+  Em: null,
+  Strong: null,
+  Link: "link",
+  Annot: "note",
+  Form: "form",
+  Ruby: null,
+  RB: null,
+  RT: null,
+  RP: null,
+  Warichu: null,
+  WT: null,
+  WP: null,
+  L: "list",
+  LI: "listitem",
+  LBody: null,
+  Table: "table",
+  TR: "row",
+  TH: "columnheader",
+  TD: "cell",
+  THead: "columnheader",
+  TBody: null,
+  TFoot: null,
+  Caption: null,
+  Figure: "figure",
+  Formula: null,
+  Artifact: null
+};
+const HEADING_PATTERN = /^H(\d+)$/;
+class StructTreeLayerBuilder {
+  #treeDom = undefined;
+  get renderingDone() {
+    return this.#treeDom !== undefined;
+  }
+  render(structTree) {
+    if (this.#treeDom !== undefined) {
+      return this.#treeDom;
+    }
+    const treeDom = this.#walk(structTree);
+    treeDom?.classList.add("structTree");
+    return this.#treeDom = treeDom;
+  }
+  hide() {
+    if (this.#treeDom && !this.#treeDom.hidden) {
+      this.#treeDom.hidden = true;
+    }
+  }
+  show() {
+    if (this.#treeDom?.hidden) {
+      this.#treeDom.hidden = false;
+    }
+  }
+  #setAttributes(structElement, htmlElement) {
+    const {
+      alt,
+      id,
+      lang
+    } = structElement;
+    if (alt !== undefined) {
+      htmlElement.setAttribute("aria-label", (0, _ui_utils.removeNullCharacters)(alt));
+    }
+    if (id !== undefined) {
+      htmlElement.setAttribute("aria-owns", id);
+    }
+    if (lang !== undefined) {
+      htmlElement.setAttribute("lang", (0, _ui_utils.removeNullCharacters)(lang, true));
+    }
+  }
+  #walk(node) {
+    if (!node) {
+      return null;
+    }
+    const element = document.createElement("span");
+    if ("role" in node) {
+      const {
+        role
+      } = node;
+      const match = role.match(HEADING_PATTERN);
+      if (match) {
+        element.setAttribute("role", "heading");
+        element.setAttribute("aria-level", match[1]);
+      } else if (PDF_ROLE_TO_HTML_ROLE[role]) {
+        element.setAttribute("role", PDF_ROLE_TO_HTML_ROLE[role]);
+      }
+    }
+    this.#setAttributes(node, element);
+    if (node.children) {
+      if (node.children.length === 1 && "id" in node.children[0]) {
+        this.#setAttributes(node.children[0], element);
+      } else {
+        for (const kid of node.children) {
+          element.append(this.#walk(kid));
+        }
+      }
+    }
+    return element;
+  }
+}
+exports.StructTreeLayerBuilder = StructTreeLayerBuilder;
+
+/***/ }),
+/* 17 */
+/***/ ((__unused_webpack_module, exports, __w_pdfjs_require__) => {
+
+
+
+Object.defineProperty(exports, "__esModule", ({
+  value: true
+}));
+exports.TextAccessibilityManager = void 0;
+var _ui_utils = __w_pdfjs_require__(2);
+class TextAccessibilityManager {
+  #enabled = false;
+  #textChildren = null;
+  #textNodes = new Map();
+  #waitingElements = new Map();
+  setTextMapping(textDivs) {
+    this.#textChildren = textDivs;
+  }
+  static #compareElementPositions(e1, e2) {
+    const rect1 = e1.getBoundingClientRect();
+    const rect2 = e2.getBoundingClientRect();
+    if (rect1.width === 0 && rect1.height === 0) {
+      return +1;
+    }
+    if (rect2.width === 0 && rect2.height === 0) {
+      return -1;
+    }
+    const top1 = rect1.y;
+    const bot1 = rect1.y + rect1.height;
+    const mid1 = rect1.y + rect1.height / 2;
+    const top2 = rect2.y;
+    const bot2 = rect2.y + rect2.height;
+    const mid2 = rect2.y + rect2.height / 2;
+    if (mid1 <= top2 && mid2 >= bot1) {
+      return -1;
+    }
+    if (mid2 <= top1 && mid1 >= bot2) {
+      return +1;
+    }
+    const centerX1 = rect1.x + rect1.width / 2;
+    const centerX2 = rect2.x + rect2.width / 2;
+    return centerX1 - centerX2;
+  }
+  enable() {
+    if (this.#enabled) {
+      throw new Error("TextAccessibilityManager is already enabled.");
+    }
+    if (!this.#textChildren) {
+      throw new Error("Text divs and strings have not been set.");
+    }
+    this.#enabled = true;
+    this.#textChildren = this.#textChildren.slice();
+    this.#textChildren.sort(TextAccessibilityManager.#compareElementPositions);
+    if (this.#textNodes.size > 0) {
+      const textChildren = this.#textChildren;
+      for (const [id, nodeIndex] of this.#textNodes) {
+        const element = document.getElementById(id);
+        if (!element) {
+          this.#textNodes.delete(id);
+          continue;
+        }
+        this.#addIdToAriaOwns(id, textChildren[nodeIndex]);
+      }
+    }
+    for (const [element, isRemovable] of this.#waitingElements) {
+      this.addPointerInTextLayer(element, isRemovable);
+    }
+    this.#waitingElements.clear();
+  }
+  disable() {
+    if (!this.#enabled) {
+      return;
+    }
+    this.#waitingElements.clear();
+    this.#textChildren = null;
+    this.#enabled = false;
+  }
+  removePointerInTextLayer(element) {
+    if (!this.#enabled) {
+      this.#waitingElements.delete(element);
+      return;
+    }
+    const children = this.#textChildren;
+    if (!children || children.length === 0) {
+      return;
+    }
+    const {
+      id
+    } = element;
+    const nodeIndex = this.#textNodes.get(id);
+    if (nodeIndex === undefined) {
+      return;
+    }
+    const node = children[nodeIndex];
+    this.#textNodes.delete(id);
+    let owns = node.getAttribute("aria-owns");
+    if (owns?.includes(id)) {
+      owns = owns.split(" ").filter(x => x !== id).join(" ");
+      if (owns) {
+        node.setAttribute("aria-owns", owns);
+      } else {
+        node.removeAttribute("aria-owns");
+        node.setAttribute("role", "presentation");
+      }
+    }
+  }
+  #addIdToAriaOwns(id, node) {
+    const owns = node.getAttribute("aria-owns");
+    if (!owns?.includes(id)) {
+      node.setAttribute("aria-owns", owns ? `${owns} ${id}` : id);
+    }
+    node.removeAttribute("role");
+  }
+  addPointerInTextLayer(element, isRemovable) {
+    const {
+      id
+    } = element;
+    if (!id) {
+      return null;
+    }
+    if (!this.#enabled) {
+      this.#waitingElements.set(element, isRemovable);
+      return null;
+    }
+    if (isRemovable) {
+      this.removePointerInTextLayer(element);
+    }
+    const children = this.#textChildren;
+    if (!children || children.length === 0) {
+      return null;
+    }
+    const index = (0, _ui_utils.binarySearchFirstItem)(children, node => TextAccessibilityManager.#compareElementPositions(element, node) < 0);
+    const nodeIndex = Math.max(0, index - 1);
+    const child = children[nodeIndex];
+    this.#addIdToAriaOwns(id, child);
+    this.#textNodes.set(id, nodeIndex);
+    const parent = child.parentNode;
+    return parent?.classList.contains("markedContent") ? parent.id : null;
+  }
+  moveElementInDOM(container, element, contentElement, isRemovable) {
+    const id = this.addPointerInTextLayer(contentElement, isRemovable);
+    if (!container.hasChildNodes()) {
+      container.append(element);
+      return id;
+    }
+    const children = Array.from(container.childNodes).filter(node => node !== element);
+    if (children.length === 0) {
+      return id;
+    }
+    const elementToCompare = contentElement || element;
+    const index = (0, _ui_utils.binarySearchFirstItem)(children, node => TextAccessibilityManager.#compareElementPositions(elementToCompare, node) < 0);
+    if (index === 0) {
+      children[0].before(element);
+    } else {
+      children[index - 1].after(element);
+    }
+    return id;
+  }
+}
+exports.TextAccessibilityManager = TextAccessibilityManager;
+
+/***/ }),
+/* 18 */
+/***/ ((__unused_webpack_module, exports) => {
+
+
+
+Object.defineProperty(exports, "__esModule", ({
+  value: true
+}));
+exports.TextHighlighter = void 0;
+class TextHighlighter {
+  constructor({
+    findController,
+    eventBus,
+    pageIndex
+  }) {
+    this.findController = findController;
+    this.matches = [];
+    this.eventBus = eventBus;
+    this.pageIdx = pageIndex;
+    this._onUpdateTextLayerMatches = null;
+    this.textDivs = null;
+    this.textContentItemsStr = null;
+    this.enabled = false;
+  }
+  setTextMapping(divs, texts) {
+    this.textDivs = divs;
+    this.textContentItemsStr = texts;
+  }
+  enable() {
+    if (!this.textDivs || !this.textContentItemsStr) {
+      throw new Error("Text divs and strings have not been set.");
+    }
+    if (this.enabled) {
+      throw new Error("TextHighlighter is already enabled.");
+    }
+    this.enabled = true;
+    if (!this._onUpdateTextLayerMatches) {
+      this._onUpdateTextLayerMatches = evt => {
+        if (evt.pageIndex === this.pageIdx || evt.pageIndex === -1) {
+          this._updateMatches();
+        }
+      };
+      this.eventBus._on("updatetextlayermatches", this._onUpdateTextLayerMatches);
+    }
+    this._updateMatches();
+  }
+  disable() {
+    if (!this.enabled) {
+      return;
+    }
+    this.enabled = false;
+    if (this._onUpdateTextLayerMatches) {
+      this.eventBus._off("updatetextlayermatches", this._onUpdateTextLayerMatches);
+      this._onUpdateTextLayerMatches = null;
+    }
+    this._updateMatches(true);
+  }
+  _convertMatches(matches, matchesLength) {
+    if (!matches) {
+      return [];
+    }
+    const {
+      textContentItemsStr
+    } = this;
+    let i = 0,
+      iIndex = 0;
+    const end = textContentItemsStr.length - 1;
+    const result = [];
+    for (let m = 0, mm = matches.length; m < mm; m++) {
+      let matchIdx = matches[m];
+      while (i !== end && matchIdx >= iIndex + textContentItemsStr[i].length) {
+        iIndex += textContentItemsStr[i].length;
+        i++;
+      }
+      if (i === textContentItemsStr.length) {
+        console.error("Could not find a matching mapping");
+      }
+      const match = {
+        begin: {
+          divIdx: i,
+          offset: matchIdx - iIndex
+        }
+      };
+      matchIdx += matchesLength[m];
+      while (i !== end && matchIdx > iIndex + textContentItemsStr[i].length) {
+        iIndex += textContentItemsStr[i].length;
+        i++;
+      }
+      match.end = {
+        divIdx: i,
+        offset: matchIdx - iIndex
+      };
+      result.push(match);
+    }
+    return result;
+  }
+  _renderMatches(matches) {
+    if (matches.length === 0) {
+      return;
+    }
+    const {
+      findController,
+      pageIdx
+    } = this;
+    const {
+      textContentItemsStr,
+      textDivs
+    } = this;
+    const isSelectedPage = pageIdx === findController.selected.pageIdx;
+    const selectedMatchIdx = findController.selected.matchIdx;
+    const highlightAll = findController.state.highlightAll;
+    let prevEnd = null;
+    const infinity = {
+      divIdx: -1,
+      offset: undefined
+    };
+    function beginText(begin, className) {
+      const divIdx = begin.divIdx;
+      textDivs[divIdx].textContent = "";
+      return appendTextToDiv(divIdx, 0, begin.offset, className);
+    }
+    function appendTextToDiv(divIdx, fromOffset, toOffset, className) {
+      let div = textDivs[divIdx];
+      if (div.nodeType === Node.TEXT_NODE) {
+        const span = document.createElement("span");
+        div.before(span);
+        span.append(div);
+        textDivs[divIdx] = span;
+        div = span;
+      }
+      const content = textContentItemsStr[divIdx].substring(fromOffset, toOffset);
+      const node = document.createTextNode(content);
+      if (className) {
+        const span = document.createElement("span");
+        span.className = `${className} appended`;
+        span.append(node);
+        div.append(span);
+        return className.includes("selected") ? span.offsetLeft : 0;
+      }
+      div.append(node);
+      return 0;
+    }
+    let i0 = selectedMatchIdx,
+      i1 = i0 + 1;
+    if (highlightAll) {
+      i0 = 0;
+      i1 = matches.length;
+    } else if (!isSelectedPage) {
+      return;
+    }
+    let lastDivIdx = -1;
+    let lastOffset = -1;
+    for (let i = i0; i < i1; i++) {
+      const match = matches[i];
+      const begin = match.begin;
+      if (begin.divIdx === lastDivIdx && begin.offset === lastOffset) {
+        continue;
+      }
+      lastDivIdx = begin.divIdx;
+      lastOffset = begin.offset;
+      const end = match.end;
+      const isSelected = isSelectedPage && i === selectedMatchIdx;
+      const highlightSuffix = isSelected ? " selected" : "";
+      let selectedLeft = 0;
+      if (!prevEnd || begin.divIdx !== prevEnd.divIdx) {
+        if (prevEnd !== null) {
+          appendTextToDiv(prevEnd.divIdx, prevEnd.offset, infinity.offset);
+        }
+        beginText(begin);
+      } else {
+        appendTextToDiv(prevEnd.divIdx, prevEnd.offset, begin.offset);
+      }
+      if (begin.divIdx === end.divIdx) {
+        selectedLeft = appendTextToDiv(begin.divIdx, begin.offset, end.offset, "highlight" + highlightSuffix);
+      } else {
+        selectedLeft = appendTextToDiv(begin.divIdx, begin.offset, infinity.offset, "highlight begin" + highlightSuffix);
+        for (let n0 = begin.divIdx + 1, n1 = end.divIdx; n0 < n1; n0++) {
+          textDivs[n0].className = "highlight middle" + highlightSuffix;
+        }
+        beginText(end, "highlight end" + highlightSuffix);
+      }
+      prevEnd = end;
+      if (isSelected) {
+        findController.scrollMatchIntoView({
+          element: textDivs[begin.divIdx],
+          selectedLeft,
+          pageIndex: pageIdx,
+          matchIndex: selectedMatchIdx
+        });
+      }
+    }
+    if (prevEnd) {
+      appendTextToDiv(prevEnd.divIdx, prevEnd.offset, infinity.offset);
+    }
+  }
+  _updateMatches(reset = false) {
+    if (!this.enabled && !reset) {
+      return;
+    }
+    const {
+      findController,
+      matches,
+      pageIdx
+    } = this;
+    const {
+      textContentItemsStr,
+      textDivs
+    } = this;
+    let clearedUntilDivIdx = -1;
+    for (const match of matches) {
+      const begin = Math.max(clearedUntilDivIdx, match.begin.divIdx);
+      for (let n = begin, end = match.end.divIdx; n <= end; n++) {
+        const div = textDivs[n];
+        div.textContent = textContentItemsStr[n];
+        div.className = "";
+      }
+      clearedUntilDivIdx = match.end.divIdx + 1;
+    }
+    if (!findController?.highlightMatches || reset) {
+      return;
+    }
+    const pageMatches = findController.pageMatches[pageIdx] || null;
+    const pageMatchesLength = findController.pageMatchesLength[pageIdx] || null;
+    this.matches = this._convertMatches(pageMatches, pageMatchesLength);
+    this._renderMatches(this.matches);
+  }
+}
+exports.TextHighlighter = TextHighlighter;
+
+/***/ }),
+/* 19 */
+/***/ ((__unused_webpack_module, exports, __w_pdfjs_require__) => {
+
+
+
+Object.defineProperty(exports, "__esModule", ({
+  value: true
+}));
+exports.TextLayerBuilder = void 0;
+var _pdfjsLib = __w_pdfjs_require__(4);
+var _ui_utils = __w_pdfjs_require__(2);
+class TextLayerBuilder {
+  #enablePermissions = false;
+  #rotation = 0;
+  #scale = 0;
+  #textContentSource = null;
+  constructor({
+    highlighter = null,
+    accessibilityManager = null,
+    isOffscreenCanvasSupported = true,
+    enablePermissions = false
+  }) {
+    this.textContentItemsStr = [];
+    this.renderingDone = false;
+    this.textDivs = [];
+    this.textDivProperties = new WeakMap();
+    this.textLayerRenderTask = null;
+    this.highlighter = highlighter;
+    this.accessibilityManager = accessibilityManager;
+    this.isOffscreenCanvasSupported = isOffscreenCanvasSupported;
+    this.#enablePermissions = enablePermissions === true;
+    this.div = document.createElement("div");
+    this.div.className = "textLayer";
+    this.hide();
+  }
+  #finishRendering() {
+    this.renderingDone = true;
+    const endOfContent = document.createElement("div");
+    endOfContent.className = "endOfContent";
+    this.div.append(endOfContent);
+    this.#bindMouse();
+  }
+  get numTextDivs() {
+    return this.textDivs.length;
+  }
+  async render(viewport) {
+    if (!this.#textContentSource) {
+      throw new Error('No "textContentSource" parameter specified.');
+    }
+    const scale = viewport.scale * (globalThis.devicePixelRatio || 1);
+    const {
+      rotation
+    } = viewport;
+    if (this.renderingDone) {
+      const mustRotate = rotation !== this.#rotation;
+      const mustRescale = scale !== this.#scale;
+      if (mustRotate || mustRescale) {
+        this.hide();
+        (0, _pdfjsLib.updateTextLayer)({
+          container: this.div,
+          viewport,
+          textDivs: this.textDivs,
+          textDivProperties: this.textDivProperties,
+          isOffscreenCanvasSupported: this.isOffscreenCanvasSupported,
+          mustRescale,
+          mustRotate
+        });
+        this.#scale = scale;
+        this.#rotation = rotation;
+      }
+      this.show();
+      return;
+    }
+    this.cancel();
+    this.highlighter?.setTextMapping(this.textDivs, this.textContentItemsStr);
+    this.accessibilityManager?.setTextMapping(this.textDivs);
+    this.textLayerRenderTask = (0, _pdfjsLib.renderTextLayer)({
+      textContentSource: this.#textContentSource,
+      container: this.div,
+      viewport,
+      textDivs: this.textDivs,
+      textDivProperties: this.textDivProperties,
+      textContentItemsStr: this.textContentItemsStr,
+      isOffscreenCanvasSupported: this.isOffscreenCanvasSupported
+    });
+    await this.textLayerRenderTask.promise;
+    this.#finishRendering();
+    this.#scale = scale;
+    this.#rotation = rotation;
+    this.show();
+    this.accessibilityManager?.enable();
+  }
+  hide() {
+    if (!this.div.hidden) {
+      this.highlighter?.disable();
+      this.div.hidden = true;
+    }
+  }
+  show() {
+    if (this.div.hidden && this.renderingDone) {
+      this.div.hidden = false;
+      this.highlighter?.enable();
+    }
+  }
+  cancel() {
+    if (this.textLayerRenderTask) {
+      this.textLayerRenderTask.cancel();
+      this.textLayerRenderTask = null;
+    }
+    this.highlighter?.disable();
+    this.accessibilityManager?.disable();
+    this.textContentItemsStr.length = 0;
+    this.textDivs.length = 0;
+    this.textDivProperties = new WeakMap();
+  }
+  setTextContentSource(source) {
+    this.cancel();
+    this.#textContentSource = source;
+  }
+  #bindMouse() {
+    const {
+      div
+    } = this;
+    div.addEventListener("mousedown", evt => {
+      const end = div.querySelector(".endOfContent");
+      if (!end) {
+        return;
+      }
+      let adjustTop = evt.target !== div;
+      adjustTop &&= getComputedStyle(end).getPropertyValue("-moz-user-select") !== "none";
+      if (adjustTop) {
+        const divBounds = div.getBoundingClientRect();
+        const r = Math.max(0, (evt.pageY - divBounds.top) / divBounds.height);
+        end.style.top = (r * 100).toFixed(2) + "%";
+      }
+      end.classList.add("active");
+    });
+    div.addEventListener("mouseup", () => {
+      const end = div.querySelector(".endOfContent");
+      if (!end) {
+        return;
+      }
+      end.style.top = "";
+      end.classList.remove("active");
+    });
+    div.addEventListener("copy", event => {
+      if (!this.#enablePermissions) {
+        const selection = document.getSelection();
+        event.clipboardData.setData("text/plain", (0, _ui_utils.removeNullCharacters)((0, _pdfjsLib.normalizeUnicode)(selection.toString())));
+      }
+      event.preventDefault();
+      event.stopPropagation();
+    });
+  }
+}
+exports.TextLayerBuilder = TextLayerBuilder;
+
+/***/ }),
+/* 20 */
+/***/ ((__unused_webpack_module, exports, __w_pdfjs_require__) => {
+
+
+
+Object.defineProperty(exports, "__esModule", ({
+  value: true
+}));
+exports.XfaLayerBuilder = void 0;
+var _pdfjsLib = __w_pdfjs_require__(4);
+class XfaLayerBuilder {
+  constructor({
+    pageDiv,
+    pdfPage,
+    annotationStorage = null,
+    linkService,
+    xfaHtml = null
+  }) {
+    this.pageDiv = pageDiv;
+    this.pdfPage = pdfPage;
+    this.annotationStorage = annotationStorage;
+    this.linkService = linkService;
+    this.xfaHtml = xfaHtml;
+    this.div = null;
+    this._cancelled = false;
+  }
+  async render(viewport, intent = "display") {
+    if (intent === "print") {
+      const parameters = {
+        viewport: viewport.clone({
+          dontFlip: true
+        }),
+        div: this.div,
+        xfaHtml: this.xfaHtml,
+        annotationStorage: this.annotationStorage,
+        linkService: this.linkService,
+        intent
+      };
+      const div = document.createElement("div");
+      this.pageDiv.append(div);
+      parameters.div = div;
+      return _pdfjsLib.XfaLayer.render(parameters);
+    }
+    const xfaHtml = await this.pdfPage.getXfa();
+    if (this._cancelled || !xfaHtml) {
+      return {
+        textDivs: []
+      };
+    }
+    const parameters = {
+      viewport: viewport.clone({
+        dontFlip: true
+      }),
+      div: this.div,
+      xfaHtml,
+      annotationStorage: this.annotationStorage,
+      linkService: this.linkService,
+      intent
+    };
+    if (this.div) {
+      return _pdfjsLib.XfaLayer.update(parameters);
+    }
+    this.div = document.createElement("div");
+    this.pageDiv.append(this.div);
+    parameters.div = this.div;
+    return _pdfjsLib.XfaLayer.render(parameters);
+  }
+  cancel() {
+    this._cancelled = true;
+  }
+  hide() {
+    if (!this.div) {
+      return;
+    }
+    this.div.hidden = true;
+  }
+}
+exports.XfaLayerBuilder = XfaLayerBuilder;
+
+/***/ }),
+/* 21 */
+/***/ ((__unused_webpack_module, exports, __w_pdfjs_require__) => {
+
+
+
+Object.defineProperty(exports, "__esModule", ({
+  value: true
+}));
+exports.PDFScriptingManager = void 0;
+var _generic_scripting = __w_pdfjs_require__(22);
+var _pdf_scripting_manager = __w_pdfjs_require__(23);
+class PDFScriptingManagerComponents extends _pdf_scripting_manager.PDFScriptingManager {
+  constructor(options) {
+    if (!options.externalServices) {
+      window.addEventListener("updatefromsandbox", event => {
+        options.eventBus.dispatch("updatefromsandbox", {
+          source: window,
+          detail: event.detail
+        });
+      });
+    }
+    options.externalServices ||= {
+      createScripting: ({
+        sandboxBundleSrc
+      }) => {
+        return new _generic_scripting.GenericScripting(sandboxBundleSrc);
+      }
+    };
+    options.docProperties ||= pdfDocument => {
+      return (0, _generic_scripting.docProperties)(pdfDocument);
+    };
+    super(options);
+  }
+}
+exports.PDFScriptingManager = PDFScriptingManagerComponents;
+
+/***/ }),
+/* 22 */
+/***/ ((__unused_webpack_module, exports, __w_pdfjs_require__) => {
+
+
+
+Object.defineProperty(exports, "__esModule", ({
+  value: true
+}));
+exports.GenericScripting = void 0;
+exports.docProperties = docProperties;
+var _pdfjsLib = __w_pdfjs_require__(4);
+async function docProperties(pdfDocument) {
+  const url = "",
+    baseUrl = url.split("#")[0];
+  let {
+    info,
+    metadata,
+    contentDispositionFilename,
+    contentLength
+  } = await pdfDocument.getMetadata();
+  if (!contentLength) {
+    const {
+      length
+    } = await pdfDocument.getDownloadInfo();
+    contentLength = length;
+  }
+  return {
+    ...info,
+    baseURL: baseUrl,
+    filesize: contentLength,
+    filename: contentDispositionFilename || (0, _pdfjsLib.getPdfFilenameFromUrl)(url),
+    metadata: metadata?.getRaw(),
+    authors: metadata?.get("dc:creator"),
+    numPages: pdfDocument.numPages,
+    URL: url
+  };
+}
+class GenericScripting {
+  constructor(sandboxBundleSrc) {
+    this._ready = (0, _pdfjsLib.loadScript)(sandboxBundleSrc, true).then(() => {
+      return window.pdfjsSandbox.QuickJSSandbox();
+    });
+  }
+  async createSandbox(data) {
+    const sandbox = await this._ready;
+    sandbox.create(data);
+  }
+  async dispatchEventInSandbox(event) {
+    const sandbox = await this._ready;
+    setTimeout(() => sandbox.dispatchEvent(event), 0);
+  }
+  async destroySandbox() {
+    const sandbox = await this._ready;
+    sandbox.nukeSandbox();
+  }
+}
+exports.GenericScripting = GenericScripting;
+
+/***/ }),
+/* 23 */
+/***/ ((__unused_webpack_module, exports, __w_pdfjs_require__) => {
+
+
+
+Object.defineProperty(exports, "__esModule", ({
+  value: true
+}));
+exports.PDFScriptingManager = void 0;
+var _ui_utils = __w_pdfjs_require__(2);
+var _pdfjsLib = __w_pdfjs_require__(4);
+class PDFScriptingManager {
+  #closeCapability = null;
+  #destroyCapability = null;
+  #docProperties = null;
+  #eventBus = null;
+  #externalServices = null;
+  #pdfDocument = null;
+  #pdfViewer = null;
+  #ready = false;
+  #sandboxBundleSrc = null;
+  #scripting = null;
+  #willPrintCapability = null;
+  constructor({
+    eventBus,
+    sandboxBundleSrc = null,
+    externalServices = null,
+    docProperties = null
+  }) {
+    this.#eventBus = eventBus;
+    this.#sandboxBundleSrc = sandboxBundleSrc;
+    this.#externalServices = externalServices;
+    this.#docProperties = docProperties;
+  }
+  setViewer(pdfViewer) {
+    this.#pdfViewer = pdfViewer;
+  }
+  async setDocument(pdfDocument) {
+    if (this.#pdfDocument) {
+      await this.#destroyScripting();
+    }
+    this.#pdfDocument = pdfDocument;
+    if (!pdfDocument) {
+      return;
+    }
+    const [objects, calculationOrder, docActions] = await Promise.all([pdfDocument.getFieldObjects(), pdfDocument.getCalculationOrderIds(), pdfDocument.getJSActions()]);
+    if (!objects && !docActions) {
+      await this.#destroyScripting();
+      return;
+    }
+    if (pdfDocument !== this.#pdfDocument) {
+      return;
+    }
+    try {
+      this.#scripting = this.#initScripting();
+    } catch (error) {
+      console.error(`setDocument: "${error.message}".`);
+      await this.#destroyScripting();
+      return;
+    }
+    this._internalEvents.set("updatefromsandbox", event => {
+      if (event?.source === window) {
+        this.#updateFromSandbox(event.detail);
+      }
+    });
+    this._internalEvents.set("dispatcheventinsandbox", event => {
+      this.#scripting?.dispatchEventInSandbox(event.detail);
+    });
+    this._internalEvents.set("pagechanging", ({
+      pageNumber,
+      previous
+    }) => {
+      if (pageNumber === previous) {
+        return;
+      }
+      this.#dispatchPageClose(previous);
+      this.#dispatchPageOpen(pageNumber);
+    });
+    this._internalEvents.set("pagerendered", ({
+      pageNumber
+    }) => {
+      if (!this._pageOpenPending.has(pageNumber)) {
+        return;
+      }
+      if (pageNumber !== this.#pdfViewer.currentPageNumber) {
+        return;
+      }
+      this.#dispatchPageOpen(pageNumber);
+    });
+    this._internalEvents.set("pagesdestroy", async () => {
+      await this.#dispatchPageClose(this.#pdfViewer.currentPageNumber);
+      await this.#scripting?.dispatchEventInSandbox({
+        id: "doc",
+        name: "WillClose"
+      });
+      this.#closeCapability?.resolve();
+    });
+    for (const [name, listener] of this._internalEvents) {
+      this.#eventBus._on(name, listener);
+    }
+    try {
+      const docProperties = await this.#docProperties(pdfDocument);
+      if (pdfDocument !== this.#pdfDocument) {
+        return;
+      }
+      await this.#scripting.createSandbox({
+        objects,
+        calculationOrder,
+        appInfo: {
+          platform: navigator.platform,
+          language: navigator.language
+        },
+        docInfo: {
+          ...docProperties,
+          actions: docActions
+        }
+      });
+      this.#eventBus.dispatch("sandboxcreated", {
+        source: this
+      });
+    } catch (error) {
+      console.error(`setDocument: "${error.message}".`);
+      await this.#destroyScripting();
+      return;
+    }
+    await this.#scripting?.dispatchEventInSandbox({
+      id: "doc",
+      name: "Open"
+    });
+    await this.#dispatchPageOpen(this.#pdfViewer.currentPageNumber, true);
+    Promise.resolve().then(() => {
+      if (pdfDocument === this.#pdfDocument) {
+        this.#ready = true;
+      }
+    });
+  }
+  async dispatchWillSave() {
+    return this.#scripting?.dispatchEventInSandbox({
+      id: "doc",
+      name: "WillSave"
+    });
+  }
+  async dispatchDidSave() {
+    return this.#scripting?.dispatchEventInSandbox({
+      id: "doc",
+      name: "DidSave"
+    });
+  }
+  async dispatchWillPrint() {
+    if (!this.#scripting) {
+      return;
+    }
+    await this.#willPrintCapability?.promise;
+    this.#willPrintCapability = new _pdfjsLib.PromiseCapability();
+    try {
+      await this.#scripting.dispatchEventInSandbox({
+        id: "doc",
+        name: "WillPrint"
+      });
+    } catch (ex) {
+      this.#willPrintCapability.resolve();
+      this.#willPrintCapability = null;
+      throw ex;
+    }
+    await this.#willPrintCapability.promise;
+  }
+  async dispatchDidPrint() {
+    return this.#scripting?.dispatchEventInSandbox({
+      id: "doc",
+      name: "DidPrint"
+    });
+  }
+  get destroyPromise() {
+    return this.#destroyCapability?.promise || null;
+  }
+  get ready() {
+    return this.#ready;
+  }
+  get _internalEvents() {
+    return (0, _pdfjsLib.shadow)(this, "_internalEvents", new Map());
+  }
+  get _pageOpenPending() {
+    return (0, _pdfjsLib.shadow)(this, "_pageOpenPending", new Set());
+  }
+  get _visitedPages() {
+    return (0, _pdfjsLib.shadow)(this, "_visitedPages", new Map());
+  }
+  async #updateFromSandbox(detail) {
+    const pdfViewer = this.#pdfViewer;
+    const isInPresentationMode = pdfViewer.isInPresentationMode || pdfViewer.isChangingPresentationMode;
+    const {
+      id,
+      siblings,
+      command,
+      value
+    } = detail;
+    if (!id) {
+      switch (command) {
+        case "clear":
+          console.clear();
+          break;
+        case "error":
+          console.error(value);
+          break;
+        case "layout":
+          if (!isInPresentationMode) {
+            const modes = (0, _ui_utils.apiPageLayoutToViewerModes)(value);
+            pdfViewer.spreadMode = modes.spreadMode;
+          }
+          break;
+        case "page-num":
+          pdfViewer.currentPageNumber = value + 1;
+          break;
+        case "print":
+          await pdfViewer.pagesPromise;
+          this.#eventBus.dispatch("print", {
+            source: this
+          });
+          break;
+        case "println":
+          console.log(value);
+          break;
+        case "zoom":
+          if (!isInPresentationMode) {
+            pdfViewer.currentScaleValue = value;
+          }
+          break;
+        case "SaveAs":
+          this.#eventBus.dispatch("download", {
+            source: this
+          });
+          break;
+        case "FirstPage":
+          pdfViewer.currentPageNumber = 1;
+          break;
+        case "LastPage":
+          pdfViewer.currentPageNumber = pdfViewer.pagesCount;
+          break;
+        case "NextPage":
+          pdfViewer.nextPage();
+          break;
+        case "PrevPage":
+          pdfViewer.previousPage();
+          break;
+        case "ZoomViewIn":
+          if (!isInPresentationMode) {
+            pdfViewer.increaseScale();
+          }
+          break;
+        case "ZoomViewOut":
+          if (!isInPresentationMode) {
+            pdfViewer.decreaseScale();
+          }
+          break;
+        case "WillPrintFinished":
+          this.#willPrintCapability?.resolve();
+          this.#willPrintCapability = null;
+          break;
+      }
+      return;
+    }
+    if (isInPresentationMode && detail.focus) {
+      return;
+    }
+    delete detail.id;
+    delete detail.siblings;
+    const ids = siblings ? [id, ...siblings] : [id];
+    for (const elementId of ids) {
+      const element = document.querySelector(`[data-element-id="${elementId}"]`);
+      if (element) {
+        element.dispatchEvent(new CustomEvent("updatefromsandbox", {
+          detail
+        }));
+      } else {
+        this.#pdfDocument?.annotationStorage.setValue(elementId, detail);
+      }
+    }
+  }
+  async #dispatchPageOpen(pageNumber, initialize = false) {
+    const pdfDocument = this.#pdfDocument,
+      visitedPages = this._visitedPages;
+    if (initialize) {
+      this.#closeCapability = new _pdfjsLib.PromiseCapability();
+    }
+    if (!this.#closeCapability) {
+      return;
+    }
+    const pageView = this.#pdfViewer.getPageView(pageNumber - 1);
+    if (pageView?.renderingState !== _ui_utils.RenderingStates.FINISHED) {
+      this._pageOpenPending.add(pageNumber);
+      return;
+    }
+    this._pageOpenPending.delete(pageNumber);
+    const actionsPromise = (async () => {
+      const actions = await (!visitedPages.has(pageNumber) ? pageView.pdfPage?.getJSActions() : null);
+      if (pdfDocument !== this.#pdfDocument) {
+        return;
+      }
+      await this.#scripting?.dispatchEventInSandbox({
+        id: "page",
+        name: "PageOpen",
+        pageNumber,
+        actions
+      });
+    })();
+    visitedPages.set(pageNumber, actionsPromise);
+  }
+  async #dispatchPageClose(pageNumber) {
+    const pdfDocument = this.#pdfDocument,
+      visitedPages = this._visitedPages;
+    if (!this.#closeCapability) {
+      return;
+    }
+    if (this._pageOpenPending.has(pageNumber)) {
+      return;
+    }
+    const actionsPromise = visitedPages.get(pageNumber);
+    if (!actionsPromise) {
+      return;
+    }
+    visitedPages.set(pageNumber, null);
+    await actionsPromise;
+    if (pdfDocument !== this.#pdfDocument) {
+      return;
+    }
+    await this.#scripting?.dispatchEventInSandbox({
+      id: "page",
+      name: "PageClose",
+      pageNumber
+    });
+  }
+  #initScripting() {
+    this.#destroyCapability = new _pdfjsLib.PromiseCapability();
+    if (this.#scripting) {
+      throw new Error("#initScripting: Scripting already exists.");
+    }
+    return this.#externalServices.createScripting({
+      sandboxBundleSrc: this.#sandboxBundleSrc
+    });
+  }
+  async #destroyScripting() {
+    if (!this.#scripting) {
+      this.#pdfDocument = null;
+      this.#destroyCapability?.resolve();
+      return;
+    }
+    if (this.#closeCapability) {
+      await Promise.race([this.#closeCapability.promise, new Promise(resolve => {
+        setTimeout(resolve, 1000);
+      })]).catch(() => {});
+      this.#closeCapability = null;
+    }
+    this.#pdfDocument = null;
+    try {
+      await this.#scripting.destroySandbox();
+    } catch {}
+    this.#willPrintCapability?.reject(new Error("Scripting destroyed."));
+    this.#willPrintCapability = null;
+    for (const [name, listener] of this._internalEvents) {
+      this.#eventBus._off(name, listener);
+    }
+    this._internalEvents.clear();
+    this._pageOpenPending.clear();
+    this._visitedPages.clear();
+    this.#scripting = null;
+    this.#ready = false;
+    this.#destroyCapability?.resolve();
+  }
+}
+exports.PDFScriptingManager = PDFScriptingManager;
+
+/***/ }),
+/* 24 */
+/***/ ((__unused_webpack_module, exports, __w_pdfjs_require__) => {
+
+
+
+Object.defineProperty(exports, "__esModule", ({
+  value: true
+}));
+exports.PDFSinglePageViewer = void 0;
+var _ui_utils = __w_pdfjs_require__(2);
+var _pdf_viewer = __w_pdfjs_require__(25);
+class PDFSinglePageViewer extends _pdf_viewer.PDFViewer {
+  _resetView() {
+    super._resetView();
+    this._scrollMode = _ui_utils.ScrollMode.PAGE;
+    this._spreadMode = _ui_utils.SpreadMode.NONE;
+  }
+  set scrollMode(mode) {}
+  _updateScrollMode() {}
+  set spreadMode(mode) {}
+  _updateSpreadMode() {}
+}
+exports.PDFSinglePageViewer = PDFSinglePageViewer;
+
+/***/ }),
+/* 25 */
+/***/ ((__unused_webpack_module, exports, __w_pdfjs_require__) => {
+
+
+
+Object.defineProperty(exports, "__esModule", ({
+  value: true
+}));
+exports.PagesCountLimit = exports.PDFViewer = exports.PDFPageViewBuffer = void 0;
+var _pdfjsLib = __w_pdfjs_require__(4);
+var _ui_utils = __w_pdfjs_require__(2);
+var _l10n_utils = __w_pdfjs_require__(7);
+var _pdf_page_view = __w_pdfjs_require__(13);
+var _pdf_rendering_queue = __w_pdfjs_require__(26);
+var _pdf_link_service = __w_pdfjs_require__(5);
+const DEFAULT_CACHE_SIZE = 10;
+const PagesCountLimit = {
+  FORCE_SCROLL_MODE_PAGE: 15000,
+  FORCE_LAZY_PAGE_INIT: 7500,
+  PAUSE_EAGER_PAGE_INIT: 250
+};
+exports.PagesCountLimit = PagesCountLimit;
+function isValidAnnotationEditorMode(mode) {
+  return Object.values(_pdfjsLib.AnnotationEditorType).includes(mode) && mode !== _pdfjsLib.AnnotationEditorType.DISABLE;
+}
+class PDFPageViewBuffer {
+  #buf = new Set();
+  #size = 0;
+  constructor(size) {
+    this.#size = size;
+  }
+  push(view) {
+    const buf = this.#buf;
+    if (buf.has(view)) {
+      buf.delete(view);
+    }
+    buf.add(view);
+    if (buf.size > this.#size) {
+      this.#destroyFirstView();
+    }
+  }
+  resize(newSize, idsToKeep = null) {
+    this.#size = newSize;
+    const buf = this.#buf;
+    if (idsToKeep) {
+      const ii = buf.size;
+      let i = 1;
+      for (const view of buf) {
+        if (idsToKeep.has(view.id)) {
+          buf.delete(view);
+          buf.add(view);
+        }
+        if (++i > ii) {
+          break;
+        }
+      }
+    }
+    while (buf.size > this.#size) {
+      this.#destroyFirstView();
+    }
+  }
+  has(view) {
+    return this.#buf.has(view);
+  }
+  [Symbol.iterator]() {
+    return this.#buf.keys();
+  }
+  #destroyFirstView() {
+    const firstView = this.#buf.keys().next().value;
+    firstView?.destroy();
+    this.#buf.delete(firstView);
+  }
+}
+exports.PDFPageViewBuffer = PDFPageViewBuffer;
+
+// in variables so we can remove them in a node context
+const cancelAnimationFrame = window.cancelAnimationFrame
+const requestAnimationFrame = window.requestAnimationFrame
+
+const debounceRaf = function (fn) {
+  var queued
+  return function (...args) {
+    if (queued) cancelAnimationFrame(queued)
+
+    queued = requestAnimationFrame(fn.bind(fn, ...args))
+  }
+}
+
+class PDFViewer {
+  #buffer = null;
+  #altTextManager = null;
+  #annotationEditorMode = _pdfjsLib.AnnotationEditorType.NONE;
+  #annotationEditorUIManager = null;
+  #annotationMode = _pdfjsLib.AnnotationMode.ENABLE_FORMS;
+  #containerTopLeft = null;
+  #copyCallbackBound = null;
+  #enablePermissions = false;
+  #getAllTextInProgress = false;
+  #hiddenCopyElement = null;
+  #interruptCopyCondition = false;
+  #previousContainerHeight = 0;
+  #resizeObserver = new ResizeObserver(debounceRaf(this.#resizeObserverCallback.bind(this)));
+  #scrollModePageState = null;
+  #onVisibilityChange = null;
+  #scaleTimeoutId = null;
+  #textLayerMode = _ui_utils.TextLayerMode.ENABLE;
+  constructor(options) {
+    const viewerVersion = '3.11.174';
+    if (_pdfjsLib.version !== viewerVersion) {
+      throw new Error(`The API version "${_pdfjsLib.version}" does not match the Viewer version "${viewerVersion}".`);
+    }
+    this.container = options.container;
+    this.viewer = options.viewer || options.container.firstElementChild;
+    if (this.container?.tagName !== "DIV" || this.viewer?.tagName !== "DIV") {
+      throw new Error("Invalid `container` and/or `viewer` option.");
+    }
+    if (this.container.offsetParent && getComputedStyle(this.container).position !== "absolute") {
+      throw new Error("The `container` must be absolutely positioned.");
+    }
+
+    // Bug: ResizeObserver loop completed with undelivered notifications
+    // TODO: disable this function temporarily
+    // this.#resizeObserver.observe(this.container);
+
+    this.eventBus = options.eventBus;
+    this.linkService = options.linkService || new _pdf_link_service.SimpleLinkService();
+    this.downloadManager = options.downloadManager || null;
+    this.findController = options.findController || null;
+    this.#altTextManager = options.altTextManager || null;
+    if (this.findController) {
+      this.findController.onIsPageVisible = pageNumber => this._getVisiblePages().ids.has(pageNumber);
+    }
+    this._scriptingManager = options.scriptingManager || null;
+    this.#textLayerMode = options.textLayerMode ?? _ui_utils.TextLayerMode.ENABLE;
+    this.#annotationMode = options.annotationMode ?? _pdfjsLib.AnnotationMode.ENABLE_FORMS;
+    this.#annotationEditorMode = options.annotationEditorMode ?? _pdfjsLib.AnnotationEditorType.NONE;
+    this.imageResourcesPath = options.imageResourcesPath || "";
+    this.enablePrintAutoRotate = options.enablePrintAutoRotate || false;
+    this.removePageBorders = options.removePageBorders || false;
+    if (options.useOnlyCssZoom) {
+      console.error("useOnlyCssZoom was removed, please use `maxCanvasPixels = 0` instead.");
+      options.maxCanvasPixels = 0;
+    }
+    this.isOffscreenCanvasSupported = options.isOffscreenCanvasSupported ?? true;
+    this.maxCanvasPixels = options.maxCanvasPixels;
+    this.l10n = options.l10n || _l10n_utils.NullL10n;
+    this.#enablePermissions = options.enablePermissions || false;
+    this.pageColors = options.pageColors || null;
+    this.defaultRenderingQueue = !options.renderingQueue;
+    if (this.defaultRenderingQueue) {
+      this.renderingQueue = new _pdf_rendering_queue.PDFRenderingQueue();
+      this.renderingQueue.setViewer(this);
+    } else {
+      this.renderingQueue = options.renderingQueue;
+    }
+    this.scroll = (0, _ui_utils.watchScroll)(this.container, this._scrollUpdate.bind(this));
+    this.presentationModeState = _ui_utils.PresentationModeState.UNKNOWN;
+    this._onBeforeDraw = this._onAfterDraw = null;
+    this._resetView();
+    if (this.removePageBorders) {
+      this.viewer.classList.add("removePageBorders");
+    }
+    this.#updateContainerHeightCss();
+    this.eventBus._on("thumbnailrendered", ({
+      pageNumber,
+      pdfPage
+    }) => {
+      const pageView = this._pages[pageNumber - 1];
+      if (!this.#buffer.has(pageView)) {
+        pdfPage?.cleanup();
+      }
+    });
+  }
+  get pagesCount() {
+    return this._pages.length;
+  }
+  getPageView(index) {
+    return this._pages[index];
+  }
+  getCachedPageViews() {
+    return new Set(this.#buffer);
+  }
+  get pageViewsReady() {
+    return this._pagesCapability.settled && this._pages.every(pageView => pageView?.pdfPage);
+  }
+  get renderForms() {
+    return this.#annotationMode === _pdfjsLib.AnnotationMode.ENABLE_FORMS;
+  }
+  get enableScripting() {
+    return !!this._scriptingManager;
+  }
+  get currentPageNumber() {
+    return this._currentPageNumber;
+  }
+  set currentPageNumber(val) {
+    if (!Number.isInteger(val)) {
+      throw new Error("Invalid page number.");
+    }
+    if (!this.pdfDocument) {
+      return;
+    }
+    if (!this._setCurrentPageNumber(val, true)) {
+      console.error(`currentPageNumber: "${val}" is not a valid page.`);
+    }
+  }
+  _setCurrentPageNumber(val, resetCurrentPageView = false) {
+    if (this._currentPageNumber === val) {
+      if (resetCurrentPageView) {
+        this.#resetCurrentPageView();
+      }
+      return true;
+    }
+    if (!(0 < val && val <= this.pagesCount)) {
+      return false;
+    }
+    const previous = this._currentPageNumber;
+    this._currentPageNumber = val;
+    this.eventBus.dispatch("pagechanging", {
+      source: this,
+      pageNumber: val,
+      pageLabel: this._pageLabels?.[val - 1] ?? null,
+      previous
+    });
+    if (resetCurrentPageView) {
+      this.#resetCurrentPageView();
+    }
+    return true;
+  }
+  get currentPageLabel() {
+    return this._pageLabels?.[this._currentPageNumber - 1] ?? null;
+  }
+  set currentPageLabel(val) {
+    if (!this.pdfDocument) {
+      return;
+    }
+    let page = val | 0;
+    if (this._pageLabels) {
+      const i = this._pageLabels.indexOf(val);
+      if (i >= 0) {
+        page = i + 1;
+      }
+    }
+    if (!this._setCurrentPageNumber(page, true)) {
+      console.error(`currentPageLabel: "${val}" is not a valid page.`);
+    }
+  }
+  get currentScale() {
+    return this._currentScale !== _ui_utils.UNKNOWN_SCALE ? this._currentScale : _ui_utils.DEFAULT_SCALE;
+  }
+  set currentScale(val) {
+    if (isNaN(val)) {
+      throw new Error("Invalid numeric scale.");
+    }
+    if (!this.pdfDocument) {
+      return;
+    }
+    this.#setScale(val, {
+      noScroll: false
+    });
+  }
+  get currentScaleValue() {
+    return this._currentScaleValue;
+  }
+  set currentScaleValue(val) {
+    if (!this.pdfDocument) {
+      return;
+    }
+    this.#setScale(val, {
+      noScroll: false
+    });
+  }
+  get pagesRotation() {
+    return this._pagesRotation;
+  }
+  set pagesRotation(rotation) {
+    if (!(0, _ui_utils.isValidRotation)(rotation)) {
+      throw new Error("Invalid pages rotation angle.");
+    }
+    if (!this.pdfDocument) {
+      return;
+    }
+    rotation %= 360;
+    if (rotation < 0) {
+      rotation += 360;
+    }
+    if (this._pagesRotation === rotation) {
+      return;
+    }
+    this._pagesRotation = rotation;
+    const pageNumber = this._currentPageNumber;
+    this.refresh(true, {
+      rotation
+    });
+    if (this._currentScaleValue) {
+      this.#setScale(this._currentScaleValue, {
+        noScroll: true
+      });
+    }
+    this.eventBus.dispatch("rotationchanging", {
+      source: this,
+      pagesRotation: rotation,
+      pageNumber
+    });
+    if (this.defaultRenderingQueue) {
+      this.update();
+    }
+  }
+  get firstPagePromise() {
+    return this.pdfDocument ? this._firstPageCapability.promise : null;
+  }
+  get onePageRendered() {
+    return this.pdfDocument ? this._onePageRenderedCapability.promise : null;
+  }
+  get pagesPromise() {
+    return this.pdfDocument ? this._pagesCapability.promise : null;
+  }
+  #layerProperties() {
+    const self = this;
+    return {
+      get annotationEditorUIManager() {
+        return self.#annotationEditorUIManager;
+      },
+      get annotationStorage() {
+        return self.pdfDocument?.annotationStorage;
+      },
+      get downloadManager() {
+        return self.downloadManager;
+      },
+      get enableScripting() {
+        return !!self._scriptingManager;
+      },
+      get fieldObjectsPromise() {
+        return self.pdfDocument?.getFieldObjects();
+      },
+      get findController() {
+        return self.findController;
+      },
+      get hasJSActionsPromise() {
+        return self.pdfDocument?.hasJSActions();
+      },
+      get linkService() {
+        return self.linkService;
+      }
+    };
+  }
+  #initializePermissions(permissions) {
+    const params = {
+      annotationEditorMode: this.#annotationEditorMode,
+      annotationMode: this.#annotationMode,
+      textLayerMode: this.#textLayerMode
+    };
+    if (!permissions) {
+      return params;
+    }
+    if (!permissions.includes(_pdfjsLib.PermissionFlag.COPY) && this.#textLayerMode === _ui_utils.TextLayerMode.ENABLE) {
+      params.textLayerMode = _ui_utils.TextLayerMode.ENABLE_PERMISSIONS;
+    }
+    if (!permissions.includes(_pdfjsLib.PermissionFlag.MODIFY_CONTENTS)) {
+      params.annotationEditorMode = _pdfjsLib.AnnotationEditorType.DISABLE;
+    }
+    if (!permissions.includes(_pdfjsLib.PermissionFlag.MODIFY_ANNOTATIONS) && !permissions.includes(_pdfjsLib.PermissionFlag.FILL_INTERACTIVE_FORMS) && this.#annotationMode === _pdfjsLib.AnnotationMode.ENABLE_FORMS) {
+      params.annotationMode = _pdfjsLib.AnnotationMode.ENABLE;
+    }
+    return params;
+  }
+  #onePageRenderedOrForceFetch() {
+    if (document.visibilityState === "hidden" || !this.container.offsetParent || this._getVisiblePages().views.length === 0) {
+      return Promise.resolve();
+    }
+    const visibilityChangePromise = new Promise(resolve => {
+      this.#onVisibilityChange = () => {
+        if (document.visibilityState !== "hidden") {
+          return;
+        }
+        resolve();
+        document.removeEventListener("visibilitychange", this.#onVisibilityChange);
+        this.#onVisibilityChange = null;
+      };
+      document.addEventListener("visibilitychange", this.#onVisibilityChange);
+    });
+    return Promise.race([this._onePageRenderedCapability.promise, visibilityChangePromise]);
+  }
+  async getAllText() {
+    const texts = [];
+    const buffer = [];
+    for (let pageNum = 1, pagesCount = this.pdfDocument.numPages; pageNum <= pagesCount; ++pageNum) {
+      if (this.#interruptCopyCondition) {
+        return null;
+      }
+      buffer.length = 0;
+      const page = await this.pdfDocument.getPage(pageNum);
+      const {
+        items
+      } = await page.getTextContent();
+      for (const item of items) {
+        if (item.str) {
+          buffer.push(item.str);
+        }
+        if (item.hasEOL) {
+          buffer.push("\n");
+        }
+      }
+      texts.push((0, _ui_utils.removeNullCharacters)(buffer.join("")));
+    }
+    return texts.join("\n");
+  }
+  #copyCallback(textLayerMode, event) {
+    const selection = document.getSelection();
+    const {
+      focusNode,
+      anchorNode
+    } = selection;
+    if (anchorNode && focusNode && selection.containsNode(this.#hiddenCopyElement)) {
+      if (this.#getAllTextInProgress || textLayerMode === _ui_utils.TextLayerMode.ENABLE_PERMISSIONS) {
+        event.preventDefault();
+        event.stopPropagation();
+        return;
+      }
+      this.#getAllTextInProgress = true;
+      const savedCursor = this.container.style.cursor;
+      this.container.style.cursor = "wait";
+      const interruptCopy = ev => this.#interruptCopyCondition = ev.key === "Escape";
+      window.addEventListener("keydown", interruptCopy);
+      this.getAllText().then(async text => {
+        if (text !== null) {
+          await navigator.clipboard.writeText(text);
+        }
+      }).catch(reason => {
+        console.warn(`Something goes wrong when extracting the text: ${reason.message}`);
+      }).finally(() => {
+        this.#getAllTextInProgress = false;
+        this.#interruptCopyCondition = false;
+        window.removeEventListener("keydown", interruptCopy);
+        this.container.style.cursor = savedCursor;
+      });
+      event.preventDefault();
+      event.stopPropagation();
+    }
+  }
+  setDocument(pdfDocument) {
+    if (this.pdfDocument) {
+      this.eventBus.dispatch("pagesdestroy", {
+        source: this
+      });
+      this._cancelRendering();
+      this._resetView();
+      this.findController?.setDocument(null);
+      this._scriptingManager?.setDocument(null);
+      if (this.#annotationEditorUIManager) {
+        this.#annotationEditorUIManager.destroy();
+        this.#annotationEditorUIManager = null;
+      }
+    }
+    this.pdfDocument = pdfDocument;
+    if (!pdfDocument) {
+      return;
+    }
+    const pagesCount = pdfDocument.numPages;
+    const firstPagePromise = pdfDocument.getPage(1);
+    const optionalContentConfigPromise = pdfDocument.getOptionalContentConfig();
+    const permissionsPromise = this.#enablePermissions ? pdfDocument.getPermissions() : Promise.resolve();
+    if (pagesCount > PagesCountLimit.FORCE_SCROLL_MODE_PAGE) {
+      console.warn("Forcing PAGE-scrolling for performance reasons, given the length of the document.");
+      const mode = this._scrollMode = _ui_utils.ScrollMode.PAGE;
+      this.eventBus.dispatch("scrollmodechanged", {
+        source: this,
+        mode
+      });
+    }
+    this._pagesCapability.promise.then(() => {
+      this.eventBus.dispatch("pagesloaded", {
+        source: this,
+        pagesCount
+      });
+    }, () => {});
+    this._onBeforeDraw = evt => {
+      const pageView = this._pages[evt.pageNumber - 1];
+      if (!pageView) {
+        return;
+      }
+      this.#buffer.push(pageView);
+    };
+    this.eventBus._on("pagerender", this._onBeforeDraw);
+    this._onAfterDraw = evt => {
+      if (evt.cssTransform || this._onePageRenderedCapability.settled) {
+        return;
+      }
+      this._onePageRenderedCapability.resolve({
+        timestamp: evt.timestamp
+      });
+      this.eventBus._off("pagerendered", this._onAfterDraw);
+      this._onAfterDraw = null;
+      if (this.#onVisibilityChange) {
+        document.removeEventListener("visibilitychange", this.#onVisibilityChange);
+        this.#onVisibilityChange = null;
+      }
+    };
+    this.eventBus._on("pagerendered", this._onAfterDraw);
+    Promise.all([firstPagePromise, permissionsPromise]).then(([firstPdfPage, permissions]) => {
+      if (pdfDocument !== this.pdfDocument) {
+        return;
+      }
+      this._firstPageCapability.resolve(firstPdfPage);
+      this._optionalContentConfigPromise = optionalContentConfigPromise;
+      const {
+        annotationEditorMode,
+        annotationMode,
+        textLayerMode
+      } = this.#initializePermissions(permissions);
+      if (textLayerMode !== _ui_utils.TextLayerMode.DISABLE) {
+        const element = this.#hiddenCopyElement = document.createElement("div");
+        element.id = "hiddenCopyElement";
+        this.viewer.before(element);
+      }
+      if (annotationEditorMode !== _pdfjsLib.AnnotationEditorType.DISABLE) {
+        const mode = annotationEditorMode;
+        if (pdfDocument.isPureXfa) {
+          console.warn("Warning: XFA-editing is not implemented.");
+        } else if (isValidAnnotationEditorMode(mode)) {
+          this.#annotationEditorUIManager = new _pdfjsLib.AnnotationEditorUIManager(this.container, this.viewer, this.#altTextManager, this.eventBus, pdfDocument, this.pageColors);
+          if (mode !== _pdfjsLib.AnnotationEditorType.NONE) {
+            this.#annotationEditorUIManager.updateMode(mode);
+          }
+        } else {
+          console.error(`Invalid AnnotationEditor mode: ${mode}`);
+        }
+      }
+      const layerProperties = this.#layerProperties.bind(this);
+      const viewerElement = this._scrollMode === _ui_utils.ScrollMode.PAGE ? null : this.viewer;
+      const scale = this.currentScale;
+      const viewport = firstPdfPage.getViewport({
+        scale: scale * _pdfjsLib.PixelsPerInch.PDF_TO_CSS_UNITS
+      });
+      this.viewer.style.setProperty("--scale-factor", viewport.scale);
+      if (this.pageColors?.foreground === "CanvasText" || this.pageColors?.background === "Canvas") {
+        this.viewer.style.setProperty("--hcm-highligh-filter", pdfDocument.filterFactory.addHighlightHCMFilter("CanvasText", "Canvas", "HighlightText", "Highlight"));
+      }
+      for (let pageNum = 1; pageNum <= pagesCount; ++pageNum) {
+        const pageView = new _pdf_page_view.PDFPageView({
+          container: viewerElement,
+          eventBus: this.eventBus,
+          id: pageNum,
+          scale,
+          defaultViewport: viewport.clone(),
+          optionalContentConfigPromise,
+          renderingQueue: this.renderingQueue,
+          textLayerMode,
+          annotationMode,
+          imageResourcesPath: this.imageResourcesPath,
+          isOffscreenCanvasSupported: this.isOffscreenCanvasSupported,
+          maxCanvasPixels: this.maxCanvasPixels,
+          pageColors: this.pageColors,
+          l10n: this.l10n,
+          layerProperties
+        });
+        this._pages.push(pageView);
+      }
+      const firstPageView = this._pages[0];
+      if (firstPageView) {
+        firstPageView.setPdfPage(firstPdfPage);
+        this.linkService.cachePageRef(1, firstPdfPage.ref);
+      }
+      if (this._scrollMode === _ui_utils.ScrollMode.PAGE) {
+        this.#ensurePageViewVisible();
+      } else if (this._spreadMode !== _ui_utils.SpreadMode.NONE) {
+        this._updateSpreadMode();
+      }
+      this.#onePageRenderedOrForceFetch().then(async () => {
+        this.findController?.setDocument(pdfDocument);
+        this._scriptingManager?.setDocument(pdfDocument);
+        if (this.#hiddenCopyElement) {
+          this.#copyCallbackBound = this.#copyCallback.bind(this, textLayerMode);
+          document.addEventListener("copy", this.#copyCallbackBound);
+        }
+        if (this.#annotationEditorUIManager) {
+          this.eventBus.dispatch("annotationeditormodechanged", {
+            source: this,
+            mode: this.#annotationEditorMode
+          });
+        }
+        if (pdfDocument.loadingParams.disableAutoFetch || pagesCount > PagesCountLimit.FORCE_LAZY_PAGE_INIT) {
+          this._pagesCapability.resolve();
+          return;
+        }
+        let getPagesLeft = pagesCount - 1;
+        if (getPagesLeft <= 0) {
+          this._pagesCapability.resolve();
+          return;
+        }
+        for (let pageNum = 2; pageNum <= pagesCount; ++pageNum) {
+          const promise = pdfDocument.getPage(pageNum).then(pdfPage => {
+            const pageView = this._pages[pageNum - 1];
+            if (!pageView.pdfPage) {
+              pageView.setPdfPage(pdfPage);
+            }
+            this.linkService.cachePageRef(pageNum, pdfPage.ref);
+            if (--getPagesLeft === 0) {
+              this._pagesCapability.resolve();
+            }
+          }, reason => {
+            console.error(`Unable to get page ${pageNum} to initialize viewer`, reason);
+            if (--getPagesLeft === 0) {
+              this._pagesCapability.resolve();
+            }
+          });
+          if (pageNum % PagesCountLimit.PAUSE_EAGER_PAGE_INIT === 0) {
+            await promise;
+          }
+        }
+      });
+      this.eventBus.dispatch("pagesinit", {
+        source: this
+      });
+      pdfDocument.getMetadata().then(({
+        info
+      }) => {
+        if (pdfDocument !== this.pdfDocument) {
+          return;
+        }
+        if (info.Language) {
+          this.viewer.lang = info.Language;
+        }
+      });
+      if (this.defaultRenderingQueue) {
+        this.update();
+      }
+    }).catch(reason => {
+      console.error("Unable to initialize viewer", reason);
+      this._pagesCapability.reject(reason);
+    });
+  }
+  setPageLabels(labels) {
+    if (!this.pdfDocument) {
+      return;
+    }
+    if (!labels) {
+      this._pageLabels = null;
+    } else if (!(Array.isArray(labels) && this.pdfDocument.numPages === labels.length)) {
+      this._pageLabels = null;
+      console.error(`setPageLabels: Invalid page labels.`);
+    } else {
+      this._pageLabels = labels;
+    }
+    for (let i = 0, ii = this._pages.length; i < ii; i++) {
+      this._pages[i].setPageLabel(this._pageLabels?.[i] ?? null);
+    }
+  }
+  _resetView() {
+    this._pages = [];
+    this._currentPageNumber = 1;
+    this._currentScale = _ui_utils.UNKNOWN_SCALE;
+    this._currentScaleValue = null;
+    this._pageLabels = null;
+    this.#buffer = new PDFPageViewBuffer(DEFAULT_CACHE_SIZE);
+    this._location = null;
+    this._pagesRotation = 0;
+    this._optionalContentConfigPromise = null;
+    this._firstPageCapability = new _pdfjsLib.PromiseCapability();
+    this._onePageRenderedCapability = new _pdfjsLib.PromiseCapability();
+    this._pagesCapability = new _pdfjsLib.PromiseCapability();
+    this._scrollMode = _ui_utils.ScrollMode.VERTICAL;
+    this._previousScrollMode = _ui_utils.ScrollMode.UNKNOWN;
+    this._spreadMode = _ui_utils.SpreadMode.NONE;
+    this.#scrollModePageState = {
+      previousPageNumber: 1,
+      scrollDown: true,
+      pages: []
+    };
+    if (this._onBeforeDraw) {
+      this.eventBus._off("pagerender", this._onBeforeDraw);
+      this._onBeforeDraw = null;
+    }
+    if (this._onAfterDraw) {
+      this.eventBus._off("pagerendered", this._onAfterDraw);
+      this._onAfterDraw = null;
+    }
+    if (this.#onVisibilityChange) {
+      document.removeEventListener("visibilitychange", this.#onVisibilityChange);
+      this.#onVisibilityChange = null;
+    }
+    this.viewer.textContent = "";
+    this._updateScrollMode();
+    this.viewer.removeAttribute("lang");
+    if (this.#hiddenCopyElement) {
+      document.removeEventListener("copy", this.#copyCallbackBound);
+      this.#copyCallbackBound = null;
+      this.#hiddenCopyElement.remove();
+      this.#hiddenCopyElement = null;
+    }
+  }
+  #ensurePageViewVisible() {
+    if (this._scrollMode !== _ui_utils.ScrollMode.PAGE) {
+      throw new Error("#ensurePageViewVisible: Invalid scrollMode value.");
+    }
+    const pageNumber = this._currentPageNumber,
+      state = this.#scrollModePageState,
+      viewer = this.viewer;
+    viewer.textContent = "";
+    state.pages.length = 0;
+    if (this._spreadMode === _ui_utils.SpreadMode.NONE && !this.isInPresentationMode) {
+      const pageView = this._pages[pageNumber - 1];
+      viewer.append(pageView.div);
+      state.pages.push(pageView);
+    } else {
+      const pageIndexSet = new Set(),
+        parity = this._spreadMode - 1;
+      if (parity === -1) {
+        pageIndexSet.add(pageNumber - 1);
+      } else if (pageNumber % 2 !== parity) {
+        pageIndexSet.add(pageNumber - 1);
+        pageIndexSet.add(pageNumber);
+      } else {
+        pageIndexSet.add(pageNumber - 2);
+        pageIndexSet.add(pageNumber - 1);
+      }
+      const spread = document.createElement("div");
+      spread.className = "spread";
+      if (this.isInPresentationMode) {
+        const dummyPage = document.createElement("div");
+        dummyPage.className = "dummyPage";
+        spread.append(dummyPage);
+      }
+      for (const i of pageIndexSet) {
+        const pageView = this._pages[i];
+        if (!pageView) {
+          continue;
+        }
+        spread.append(pageView.div);
+        state.pages.push(pageView);
+      }
+      viewer.append(spread);
+    }
+    state.scrollDown = pageNumber >= state.previousPageNumber;
+    state.previousPageNumber = pageNumber;
+  }
+  _scrollUpdate() {
+    if (this.pagesCount === 0) {
+      return;
+    }
+    this.update();
+  }
+  #scrollIntoView(pageView, pageSpot = null) {
+    const {
+      div,
+      id
+    } = pageView;
+    if (this._currentPageNumber !== id) {
+      this._setCurrentPageNumber(id);
+    }
+    if (this._scrollMode === _ui_utils.ScrollMode.PAGE) {
+      this.#ensurePageViewVisible();
+      this.update();
+    }
+    if (!pageSpot && !this.isInPresentationMode) {
+      const left = div.offsetLeft + div.clientLeft,
+        right = left + div.clientWidth;
+      const {
+        scrollLeft,
+        clientWidth
+      } = this.container;
+      if (this._scrollMode === _ui_utils.ScrollMode.HORIZONTAL || left < scrollLeft || right > scrollLeft + clientWidth) {
+        pageSpot = {
+          left: 0,
+          top: 0
+        };
+      }
+    }
+    (0, _ui_utils.scrollIntoView)(div, pageSpot);
+    if (!this._currentScaleValue && this._location) {
+      this._location = null;
+    }
+  }
+  #isSameScale(newScale) {
+    return newScale === this._currentScale || Math.abs(newScale - this._currentScale) < 1e-15;
+  }
+  #setScaleUpdatePages(newScale, newValue, {
+    noScroll = false,
+    preset = false,
+    drawingDelay = -1
+  }) {
+    this._currentScaleValue = newValue.toString();
+    if (this.#isSameScale(newScale)) {
+      if (preset) {
+        this.eventBus.dispatch("scalechanging", {
+          source: this,
+          scale: newScale,
+          presetValue: newValue
+        });
+      }
+      return;
+    }
+    this.viewer.style.setProperty("--scale-factor", newScale * _pdfjsLib.PixelsPerInch.PDF_TO_CSS_UNITS);
+    const postponeDrawing = drawingDelay >= 0 && drawingDelay < 1000;
+    this.refresh(true, {
+      scale: newScale,
+      drawingDelay: postponeDrawing ? drawingDelay : -1
+    });
+    if (postponeDrawing) {
+      this.#scaleTimeoutId = setTimeout(() => {
+        this.#scaleTimeoutId = null;
+        this.refresh();
+      }, drawingDelay);
+    }
+    this._currentScale = newScale;
+    if (!noScroll) {
+      let page = this._currentPageNumber,
+        dest;
+      if (this._location && !(this.isInPresentationMode || this.isChangingPresentationMode)) {
+        page = this._location.pageNumber;
+        dest = [null, {
+          name: "XYZ"
+        }, this._location.left, this._location.top, null];
+      }
+      this.scrollPageIntoView({
+        pageNumber: page,
+        destArray: dest,
+        allowNegativeOffset: true
+      });
+    }
+    this.eventBus.dispatch("scalechanging", {
+      source: this,
+      scale: newScale,
+      presetValue: preset ? newValue : undefined
+    });
+    if (this.defaultRenderingQueue) {
+      this.update();
+    }
+  }
+  get #pageWidthScaleFactor() {
+    if (this._spreadMode !== _ui_utils.SpreadMode.NONE && this._scrollMode !== _ui_utils.ScrollMode.HORIZONTAL) {
+      return 2;
+    }
+    return 1;
+  }
+  #setScale(value, options) {
+    let scale = parseFloat(value);
+    if (scale > 0) {
+      options.preset = false;
+      this.#setScaleUpdatePages(scale, value, options);
+    } else {
+      const currentPage = this._pages[this._currentPageNumber - 1];
+      if (!currentPage) {
+        return;
+      }
+      let hPadding = _ui_utils.SCROLLBAR_PADDING,
+        vPadding = _ui_utils.VERTICAL_PADDING;
+      if (this.isInPresentationMode) {
+        hPadding = vPadding = 4;
+        if (this._spreadMode !== _ui_utils.SpreadMode.NONE) {
+          hPadding *= 2;
+        }
+      } else if (this.removePageBorders) {
+        hPadding = vPadding = 0;
+      } else if (this._scrollMode === _ui_utils.ScrollMode.HORIZONTAL) {
+        [hPadding, vPadding] = [vPadding, hPadding];
+      }
+      const pageWidthScale = (this.container.clientWidth - hPadding) / currentPage.width * currentPage.scale / this.#pageWidthScaleFactor;
+      const pageHeightScale = (this.container.clientHeight - vPadding) / currentPage.height * currentPage.scale;
+      switch (value) {
+        case "page-actual":
+          scale = 1;
+          break;
+        case "page-width":
+          scale = pageWidthScale;
+          break;
+        case "page-height":
+          scale = pageHeightScale;
+          break;
+        case "page-fit":
+          scale = Math.min(pageWidthScale, pageHeightScale);
+          break;
+        case "auto":
+          const horizontalScale = (0, _ui_utils.isPortraitOrientation)(currentPage) ? pageWidthScale : Math.min(pageHeightScale, pageWidthScale);
+          scale = Math.min(_ui_utils.MAX_AUTO_SCALE, horizontalScale);
+          break;
+        default:
+          console.error(`#setScale: "${value}" is an unknown zoom value.`);
+          return;
+      }
+      options.preset = true;
+      this.#setScaleUpdatePages(scale, value, options);
+    }
+  }
+  #resetCurrentPageView() {
+    const pageView = this._pages[this._currentPageNumber - 1];
+    if (this.isInPresentationMode) {
+      this.#setScale(this._currentScaleValue, {
+        noScroll: true
+      });
+    }
+    this.#scrollIntoView(pageView);
+  }
+  pageLabelToPageNumber(label) {
+    if (!this._pageLabels) {
+      return null;
+    }
+    const i = this._pageLabels.indexOf(label);
+    if (i < 0) {
+      return null;
+    }
+    return i + 1;
+  }
+  scrollPageIntoView({
+    pageNumber,
+    destArray = null,
+    allowNegativeOffset = false,
+    ignoreDestinationZoom = false
+  }) {
+    if (!this.pdfDocument) {
+      return;
+    }
+    const pageView = Number.isInteger(pageNumber) && this._pages[pageNumber - 1];
+    if (!pageView) {
+      console.error(`scrollPageIntoView: "${pageNumber}" is not a valid pageNumber parameter.`);
+      return;
+    }
+    if (this.isInPresentationMode || !destArray) {
+      this._setCurrentPageNumber(pageNumber, true);
+      return;
+    }
+    let x = 0,
+      y = 0;
+    let width = 0,
+      height = 0,
+      widthScale,
+      heightScale;
+    const changeOrientation = pageView.rotation % 180 !== 0;
+    const pageWidth = (changeOrientation ? pageView.height : pageView.width) / pageView.scale / _pdfjsLib.PixelsPerInch.PDF_TO_CSS_UNITS;
+    const pageHeight = (changeOrientation ? pageView.width : pageView.height) / pageView.scale / _pdfjsLib.PixelsPerInch.PDF_TO_CSS_UNITS;
+    let scale = 0;
+    switch (destArray[1].name) {
+      case "XYZ":
+        x = destArray[2];
+        y = destArray[3];
+        scale = destArray[4];
+        x = x !== null ? x : 0;
+        y = y !== null ? y : pageHeight;
+        break;
+      case "Fit":
+      case "FitB":
+        scale = "page-fit";
+        break;
+      case "FitH":
+      case "FitBH":
+        y = destArray[2];
+        scale = "page-width";
+        if (y === null && this._location) {
+          x = this._location.left;
+          y = this._location.top;
+        } else if (typeof y !== "number" || y < 0) {
+          y = pageHeight;
+        }
+        break;
+      case "FitV":
+      case "FitBV":
+        x = destArray[2];
+        width = pageWidth;
+        height = pageHeight;
+        scale = "page-height";
+        break;
+      case "FitR":
+        x = destArray[2];
+        y = destArray[3];
+        width = destArray[4] - x;
+        height = destArray[5] - y;
+        let hPadding = _ui_utils.SCROLLBAR_PADDING,
+          vPadding = _ui_utils.VERTICAL_PADDING;
+        if (this.removePageBorders) {
+          hPadding = vPadding = 0;
+        }
+        widthScale = (this.container.clientWidth - hPadding) / width / _pdfjsLib.PixelsPerInch.PDF_TO_CSS_UNITS;
+        heightScale = (this.container.clientHeight - vPadding) / height / _pdfjsLib.PixelsPerInch.PDF_TO_CSS_UNITS;
+        scale = Math.min(Math.abs(widthScale), Math.abs(heightScale));
+        break;
+      default:
+        console.error(`scrollPageIntoView: "${destArray[1].name}" is not a valid destination type.`);
+        return;
+    }
+    if (!ignoreDestinationZoom) {
+      if (scale && scale !== this._currentScale) {
+        this.currentScaleValue = scale;
+      } else if (this._currentScale === _ui_utils.UNKNOWN_SCALE) {
+        this.currentScaleValue = _ui_utils.DEFAULT_SCALE_VALUE;
+      }
+    }
+    if (scale === "page-fit" && !destArray[4]) {
+      this.#scrollIntoView(pageView);
+      return;
+    }
+    const boundingRect = [pageView.viewport.convertToViewportPoint(x, y), pageView.viewport.convertToViewportPoint(x + width, y + height)];
+    let left = Math.min(boundingRect[0][0], boundingRect[1][0]);
+    let top = Math.min(boundingRect[0][1], boundingRect[1][1]);
+    if (!allowNegativeOffset) {
+      left = Math.max(left, 0);
+      top = Math.max(top, 0);
+    }
+    this.#scrollIntoView(pageView, {
+      left,
+      top
+    });
+  }
+  _updateLocation(firstPage) {
+    const currentScale = this._currentScale;
+    const currentScaleValue = this._currentScaleValue;
+    const normalizedScaleValue = parseFloat(currentScaleValue) === currentScale ? Math.round(currentScale * 10000) / 100 : currentScaleValue;
+    const pageNumber = firstPage.id;
+    const currentPageView = this._pages[pageNumber - 1];
+    const container = this.container;
+    const topLeft = currentPageView.getPagePoint(container.scrollLeft - firstPage.x, container.scrollTop - firstPage.y);
+    const intLeft = Math.round(topLeft[0]);
+    const intTop = Math.round(topLeft[1]);
+    let pdfOpenParams = `#page=${pageNumber}`;
+    if (!this.isInPresentationMode) {
+      pdfOpenParams += `&zoom=${normalizedScaleValue},${intLeft},${intTop}`;
+    }
+    this._location = {
+      pageNumber,
+      scale: normalizedScaleValue,
+      top: intTop,
+      left: intLeft,
+      rotation: this._pagesRotation,
+      pdfOpenParams
+    };
+  }
+  update() {
+    const visible = this._getVisiblePages();
+    const visiblePages = visible.views,
+      numVisiblePages = visiblePages.length;
+    if (numVisiblePages === 0) {
+      return;
+    }
+    const newCacheSize = Math.max(DEFAULT_CACHE_SIZE, 2 * numVisiblePages + 1);
+    this.#buffer.resize(newCacheSize, visible.ids);
+    this.renderingQueue.renderHighestPriority(visible);
+    const isSimpleLayout = this._spreadMode === _ui_utils.SpreadMode.NONE && (this._scrollMode === _ui_utils.ScrollMode.PAGE || this._scrollMode === _ui_utils.ScrollMode.VERTICAL);
+    const currentId = this._currentPageNumber;
+    let stillFullyVisible = false;
+    for (const page of visiblePages) {
+      if (page.percent < 100) {
+        break;
+      }
+      if (page.id === currentId && isSimpleLayout) {
+        stillFullyVisible = true;
+        break;
+      }
+    }
+    this._setCurrentPageNumber(stillFullyVisible ? currentId : visiblePages[0].id);
+    this._updateLocation(visible.first);
+    this.eventBus.dispatch("updateviewarea", {
+      source: this,
+      location: this._location
+    });
+  }
+  containsElement(element) {
+    return this.container.contains(element);
+  }
+  focus() {
+    this.container.focus();
+  }
+  get _isContainerRtl() {
+    return getComputedStyle(this.container).direction === "rtl";
+  }
+  get isInPresentationMode() {
+    return this.presentationModeState === _ui_utils.PresentationModeState.FULLSCREEN;
+  }
+  get isChangingPresentationMode() {
+    return this.presentationModeState === _ui_utils.PresentationModeState.CHANGING;
+  }
+  get isHorizontalScrollbarEnabled() {
+    return this.isInPresentationMode ? false : this.container.scrollWidth > this.container.clientWidth;
+  }
+  get isVerticalScrollbarEnabled() {
+    return this.isInPresentationMode ? false : this.container.scrollHeight > this.container.clientHeight;
+  }
+  _getVisiblePages() {
+    const views = this._scrollMode === _ui_utils.ScrollMode.PAGE ? this.#scrollModePageState.pages : this._pages,
+      horizontal = this._scrollMode === _ui_utils.ScrollMode.HORIZONTAL,
+      rtl = horizontal && this._isContainerRtl;
+    return (0, _ui_utils.getVisibleElements)({
+      scrollEl: this.container,
+      views,
+      sortByVisibility: true,
+      horizontal,
+      rtl
+    });
+  }
+  cleanup() {
+    this.#resizeObserver.unobserve(this.container)
+    this.#resizeObserver.disconnect()
+
+    for (const pageView of this._pages) {
+      if (pageView.renderingState !== _ui_utils.RenderingStates.FINISHED) {
+        pageView.reset();
+      }
+    }
+  }
+  _cancelRendering() {
+    for (const pageView of this._pages) {
+      pageView.cancelRendering();
+    }
+  }
+  async #ensurePdfPageLoaded(pageView) {
+    if (pageView.pdfPage) {
+      return pageView.pdfPage;
+    }
+    try {
+      const pdfPage = await this.pdfDocument.getPage(pageView.id);
+      if (!pageView.pdfPage) {
+        pageView.setPdfPage(pdfPage);
+      }
+      if (!this.linkService._cachedPageNumber?.(pdfPage.ref)) {
+        this.linkService.cachePageRef(pageView.id, pdfPage.ref);
+      }
+      return pdfPage;
+    } catch (reason) {
+      console.error("Unable to get page for page view", reason);
+      return null;
+    }
+  }
+  #getScrollAhead(visible) {
+    if (visible.first?.id === 1) {
+      return true;
+    } else if (visible.last?.id === this.pagesCount) {
+      return false;
+    }
+    switch (this._scrollMode) {
+      case _ui_utils.ScrollMode.PAGE:
+        return this.#scrollModePageState.scrollDown;
+      case _ui_utils.ScrollMode.HORIZONTAL:
+        return this.scroll.right;
+    }
+    return this.scroll.down;
+  }
+  forceRendering(currentlyVisiblePages) {
+    const visiblePages = currentlyVisiblePages || this._getVisiblePages();
+    const scrollAhead = this.#getScrollAhead(visiblePages);
+    const preRenderExtra = this._spreadMode !== _ui_utils.SpreadMode.NONE && this._scrollMode !== _ui_utils.ScrollMode.HORIZONTAL;
+    const pageView = this.renderingQueue.getHighestPriority(visiblePages, this._pages, scrollAhead, preRenderExtra);
+    if (pageView) {
+      this.#ensurePdfPageLoaded(pageView).then(() => {
+        this.renderingQueue.renderView(pageView);
+      });
+      return true;
+    }
+    return false;
+  }
+  get hasEqualPageSizes() {
+    const firstPageView = this._pages[0];
+    for (let i = 1, ii = this._pages.length; i < ii; ++i) {
+      const pageView = this._pages[i];
+      if (pageView.width !== firstPageView.width || pageView.height !== firstPageView.height) {
+        return false;
+      }
+    }
+    return true;
+  }
+  getPagesOverview() {
+    let initialOrientation;
+    return this._pages.map(pageView => {
+      const viewport = pageView.pdfPage.getViewport({
+        scale: 1
+      });
+      const orientation = (0, _ui_utils.isPortraitOrientation)(viewport);
+      if (initialOrientation === undefined) {
+        initialOrientation = orientation;
+      } else if (this.enablePrintAutoRotate && orientation !== initialOrientation) {
+        return {
+          width: viewport.height,
+          height: viewport.width,
+          rotation: (viewport.rotation - 90) % 360
+        };
+      }
+      return {
+        width: viewport.width,
+        height: viewport.height,
+        rotation: viewport.rotation
+      };
+    });
+  }
+  get optionalContentConfigPromise() {
+    if (!this.pdfDocument) {
+      return Promise.resolve(null);
+    }
+    if (!this._optionalContentConfigPromise) {
+      console.error("optionalContentConfigPromise: Not initialized yet.");
+      return this.pdfDocument.getOptionalContentConfig();
+    }
+    return this._optionalContentConfigPromise;
+  }
+  set optionalContentConfigPromise(promise) {
+    if (!(promise instanceof Promise)) {
+      throw new Error(`Invalid optionalContentConfigPromise: ${promise}`);
+    }
+    if (!this.pdfDocument) {
+      return;
+    }
+    if (!this._optionalContentConfigPromise) {
+      return;
+    }
+    this._optionalContentConfigPromise = promise;
+    this.refresh(false, {
+      optionalContentConfigPromise: promise
+    });
+    this.eventBus.dispatch("optionalcontentconfigchanged", {
+      source: this,
+      promise
+    });
+  }
+  get scrollMode() {
+    return this._scrollMode;
+  }
+  set scrollMode(mode) {
+    if (this._scrollMode === mode) {
+      return;
+    }
+    if (!(0, _ui_utils.isValidScrollMode)(mode)) {
+      throw new Error(`Invalid scroll mode: ${mode}`);
+    }
+    if (this.pagesCount > PagesCountLimit.FORCE_SCROLL_MODE_PAGE) {
+      return;
+    }
+    this._previousScrollMode = this._scrollMode;
+    this._scrollMode = mode;
+    this.eventBus.dispatch("scrollmodechanged", {
+      source: this,
+      mode
+    });
+    this._updateScrollMode(this._currentPageNumber);
+  }
+  _updateScrollMode(pageNumber = null) {
+    const scrollMode = this._scrollMode,
+      viewer = this.viewer;
+    viewer.classList.toggle("scrollHorizontal", scrollMode === _ui_utils.ScrollMode.HORIZONTAL);
+    viewer.classList.toggle("scrollWrapped", scrollMode === _ui_utils.ScrollMode.WRAPPED);
+    if (!this.pdfDocument || !pageNumber) {
+      return;
+    }
+    if (scrollMode === _ui_utils.ScrollMode.PAGE) {
+      this.#ensurePageViewVisible();
+    } else if (this._previousScrollMode === _ui_utils.ScrollMode.PAGE) {
+      this._updateSpreadMode();
+    }
+    if (this._currentScaleValue && isNaN(this._currentScaleValue)) {
+      this.#setScale(this._currentScaleValue, {
+        noScroll: true
+      });
+    }
+    this._setCurrentPageNumber(pageNumber, true);
+    this.update();
+  }
+  get spreadMode() {
+    return this._spreadMode;
+  }
+  set spreadMode(mode) {
+    if (this._spreadMode === mode) {
+      return;
+    }
+    if (!(0, _ui_utils.isValidSpreadMode)(mode)) {
+      throw new Error(`Invalid spread mode: ${mode}`);
+    }
+    this._spreadMode = mode;
+    this.eventBus.dispatch("spreadmodechanged", {
+      source: this,
+      mode
+    });
+    this._updateSpreadMode(this._currentPageNumber);
+  }
+  _updateSpreadMode(pageNumber = null) {
+    if (!this.pdfDocument) {
+      return;
+    }
+    const viewer = this.viewer,
+      pages = this._pages;
+    if (this._scrollMode === _ui_utils.ScrollMode.PAGE) {
+      this.#ensurePageViewVisible();
+    } else {
+      viewer.textContent = "";
+      if (this._spreadMode === _ui_utils.SpreadMode.NONE) {
+        for (const pageView of this._pages) {
+          viewer.append(pageView.div);
+        }
+      } else {
+        const parity = this._spreadMode - 1;
+        let spread = null;
+        for (let i = 0, ii = pages.length; i < ii; ++i) {
+          if (spread === null) {
+            spread = document.createElement("div");
+            spread.className = "spread";
+            viewer.append(spread);
+          } else if (i % 2 === parity) {
+            spread = spread.cloneNode(false);
+            viewer.append(spread);
+          }
+          spread.append(pages[i].div);
+        }
+      }
+    }
+    if (!pageNumber) {
+      return;
+    }
+    if (this._currentScaleValue && isNaN(this._currentScaleValue)) {
+      this.#setScale(this._currentScaleValue, {
+        noScroll: true
+      });
+    }
+    this._setCurrentPageNumber(pageNumber, true);
+    this.update();
+  }
+  _getPageAdvance(currentPageNumber, previous = false) {
+    switch (this._scrollMode) {
+      case _ui_utils.ScrollMode.WRAPPED:
+        {
+          const {
+              views
+            } = this._getVisiblePages(),
+            pageLayout = new Map();
+          for (const {
+            id,
+            y,
+            percent,
+            widthPercent
+          } of views) {
+            if (percent === 0 || widthPercent < 100) {
+              continue;
+            }
+            let yArray = pageLayout.get(y);
+            if (!yArray) {
+              pageLayout.set(y, yArray ||= []);
+            }
+            yArray.push(id);
+          }
+          for (const yArray of pageLayout.values()) {
+            const currentIndex = yArray.indexOf(currentPageNumber);
+            if (currentIndex === -1) {
+              continue;
+            }
+            const numPages = yArray.length;
+            if (numPages === 1) {
+              break;
+            }
+            if (previous) {
+              for (let i = currentIndex - 1, ii = 0; i >= ii; i--) {
+                const currentId = yArray[i],
+                  expectedId = yArray[i + 1] - 1;
+                if (currentId < expectedId) {
+                  return currentPageNumber - expectedId;
+                }
+              }
+            } else {
+              for (let i = currentIndex + 1, ii = numPages; i < ii; i++) {
+                const currentId = yArray[i],
+                  expectedId = yArray[i - 1] + 1;
+                if (currentId > expectedId) {
+                  return expectedId - currentPageNumber;
+                }
+              }
+            }
+            if (previous) {
+              const firstId = yArray[0];
+              if (firstId < currentPageNumber) {
+                return currentPageNumber - firstId + 1;
+              }
+            } else {
+              const lastId = yArray[numPages - 1];
+              if (lastId > currentPageNumber) {
+                return lastId - currentPageNumber + 1;
+              }
+            }
+            break;
+          }
+          break;
+        }
+      case _ui_utils.ScrollMode.HORIZONTAL:
+        {
+          break;
+        }
+      case _ui_utils.ScrollMode.PAGE:
+      case _ui_utils.ScrollMode.VERTICAL:
+        {
+          if (this._spreadMode === _ui_utils.SpreadMode.NONE) {
+            break;
+          }
+          const parity = this._spreadMode - 1;
+          if (previous && currentPageNumber % 2 !== parity) {
+            break;
+          } else if (!previous && currentPageNumber % 2 === parity) {
+            break;
+          }
+          const {
+              views
+            } = this._getVisiblePages(),
+            expectedId = previous ? currentPageNumber - 1 : currentPageNumber + 1;
+          for (const {
+            id,
+            percent,
+            widthPercent
+          } of views) {
+            if (id !== expectedId) {
+              continue;
+            }
+            if (percent > 0 && widthPercent === 100) {
+              return 2;
+            }
+            break;
+          }
+          break;
+        }
+    }
+    return 1;
+  }
+  nextPage() {
+    const currentPageNumber = this._currentPageNumber,
+      pagesCount = this.pagesCount;
+    if (currentPageNumber >= pagesCount) {
+      return false;
+    }
+    const advance = this._getPageAdvance(currentPageNumber, false) || 1;
+    this.currentPageNumber = Math.min(currentPageNumber + advance, pagesCount);
+    return true;
+  }
+  previousPage() {
+    const currentPageNumber = this._currentPageNumber;
+    if (currentPageNumber <= 1) {
+      return false;
+    }
+    const advance = this._getPageAdvance(currentPageNumber, true) || 1;
+    this.currentPageNumber = Math.max(currentPageNumber - advance, 1);
+    return true;
+  }
+  increaseScale({
+    drawingDelay,
+    scaleFactor,
+    steps
+  } = {}) {
+    if (!this.pdfDocument) {
+      return;
+    }
+    let newScale = this._currentScale;
+    if (scaleFactor > 1) {
+      newScale = Math.round(newScale * scaleFactor * 100) / 100;
+    } else {
+      steps ??= 1;
+      do {
+        newScale = Math.ceil((newScale * _ui_utils.DEFAULT_SCALE_DELTA).toFixed(2) * 10) / 10;
+      } while (--steps > 0 && newScale < _ui_utils.MAX_SCALE);
+    }
+    this.#setScale(Math.min(_ui_utils.MAX_SCALE, newScale), {
+      noScroll: false,
+      drawingDelay
+    });
+  }
+  decreaseScale({
+    drawingDelay,
+    scaleFactor,
+    steps
+  } = {}) {
+    if (!this.pdfDocument) {
+      return;
+    }
+    let newScale = this._currentScale;
+    if (scaleFactor > 0 && scaleFactor < 1) {
+      newScale = Math.round(newScale * scaleFactor * 100) / 100;
+    } else {
+      steps ??= 1;
+      do {
+        newScale = Math.floor((newScale / _ui_utils.DEFAULT_SCALE_DELTA).toFixed(2) * 10) / 10;
+      } while (--steps > 0 && newScale > _ui_utils.MIN_SCALE);
+    }
+    this.#setScale(Math.max(_ui_utils.MIN_SCALE, newScale), {
+      noScroll: false,
+      drawingDelay
+    });
+  }
+  #updateContainerHeightCss(height = this.container.clientHeight) {
+    if (height !== this.#previousContainerHeight) {
+      this.#previousContainerHeight = height;
+      _ui_utils.docStyle.setProperty("--viewer-container-height", `${height}px`);
+    }
+  }
+  #resizeObserverCallback(entries) {
+    for (const entry of entries) {
+      if (entry.target === this.container) {
+        this.#updateContainerHeightCss(Math.floor(entry.borderBoxSize[0].blockSize));
+        this.#containerTopLeft = null;
+        break;
+      }
+    }
+  }
+  get containerTopLeft() {
+    return this.#containerTopLeft ||= [this.container.offsetTop, this.container.offsetLeft];
+  }
+  get annotationEditorMode() {
+    return this.#annotationEditorUIManager ? this.#annotationEditorMode : _pdfjsLib.AnnotationEditorType.DISABLE;
+  }
+  set annotationEditorMode({
+    mode,
+    editId = null
+  }) {
+    if (!this.#annotationEditorUIManager) {
+      throw new Error(`The AnnotationEditor is not enabled.`);
+    }
+    if (this.#annotationEditorMode === mode) {
+      return;
+    }
+    if (!isValidAnnotationEditorMode(mode)) {
+      throw new Error(`Invalid AnnotationEditor mode: ${mode}`);
+    }
+    if (!this.pdfDocument) {
+      return;
+    }
+    this.#annotationEditorMode = mode;
+    this.eventBus.dispatch("annotationeditormodechanged", {
+      source: this,
+      mode
+    });
+    this.#annotationEditorUIManager.updateMode(mode, editId);
+  }
+  set annotationEditorParams({
+    type,
+    value
+  }) {
+    if (!this.#annotationEditorUIManager) {
+      throw new Error(`The AnnotationEditor is not enabled.`);
+    }
+    this.#annotationEditorUIManager.updateParams(type, value);
+  }
+  refresh(noUpdate = false, updateArgs = Object.create(null)) {
+    if (!this.pdfDocument) {
+      return;
+    }
+    for (const pageView of this._pages) {
+      pageView.update(updateArgs);
+    }
+    if (this.#scaleTimeoutId !== null) {
+      clearTimeout(this.#scaleTimeoutId);
+      this.#scaleTimeoutId = null;
+    }
+    if (!noUpdate) {
+      this.update();
+    }
+  }
+}
+exports.PDFViewer = PDFViewer;
+
+/***/ }),
+/* 26 */
+/***/ ((__unused_webpack_module, exports, __w_pdfjs_require__) => {
+
+
+
+Object.defineProperty(exports, "__esModule", ({
+  value: true
+}));
+exports.PDFRenderingQueue = void 0;
+var _pdfjsLib = __w_pdfjs_require__(4);
+var _ui_utils = __w_pdfjs_require__(2);
+const CLEANUP_TIMEOUT = 30000;
+class PDFRenderingQueue {
+  constructor() {
+    this.pdfViewer = null;
+    this.pdfThumbnailViewer = null;
+    this.onIdle = null;
+    this.highestPriorityPage = null;
+    this.idleTimeout = null;
+    this.printing = false;
+    this.isThumbnailViewEnabled = false;
+    Object.defineProperty(this, "hasViewer", {
+      value: () => !!this.pdfViewer
+    });
+  }
+  setViewer(pdfViewer) {
+    this.pdfViewer = pdfViewer;
+  }
+  setThumbnailViewer(pdfThumbnailViewer) {
+    this.pdfThumbnailViewer = pdfThumbnailViewer;
+  }
+  isHighestPriority(view) {
+    return this.highestPriorityPage === view.renderingId;
+  }
+  renderHighestPriority(currentlyVisiblePages) {
+    if (this.idleTimeout) {
+      clearTimeout(this.idleTimeout);
+      this.idleTimeout = null;
+    }
+    if (this.pdfViewer.forceRendering(currentlyVisiblePages)) {
+      return;
+    }
+    if (this.isThumbnailViewEnabled && this.pdfThumbnailViewer?.forceRendering()) {
+      return;
+    }
+    if (this.printing) {
+      return;
+    }
+    if (this.onIdle) {
+      this.idleTimeout = setTimeout(this.onIdle.bind(this), CLEANUP_TIMEOUT);
+    }
+  }
+  getHighestPriority(visible, views, scrolledDown, preRenderExtra = false) {
+    const visibleViews = visible.views,
+      numVisible = visibleViews.length;
+    if (numVisible === 0) {
+      return null;
+    }
+    for (let i = 0; i < numVisible; i++) {
+      const view = visibleViews[i].view;
+      if (!this.isViewFinished(view)) {
+        return view;
+      }
+    }
+    const firstId = visible.first.id,
+      lastId = visible.last.id;
+    if (lastId - firstId + 1 > numVisible) {
+      const visibleIds = visible.ids;
+      for (let i = 1, ii = lastId - firstId; i < ii; i++) {
+        const holeId = scrolledDown ? firstId + i : lastId - i;
+        if (visibleIds.has(holeId)) {
+          continue;
+        }
+        const holeView = views[holeId - 1];
+        if (!this.isViewFinished(holeView)) {
+          return holeView;
+        }
+      }
+    }
+    let preRenderIndex = scrolledDown ? lastId : firstId - 2;
+    let preRenderView = views[preRenderIndex];
+    if (preRenderView && !this.isViewFinished(preRenderView)) {
+      return preRenderView;
+    }
+    if (preRenderExtra) {
+      preRenderIndex += scrolledDown ? 1 : -1;
+      preRenderView = views[preRenderIndex];
+      if (preRenderView && !this.isViewFinished(preRenderView)) {
+        return preRenderView;
+      }
+    }
+    return null;
+  }
+  isViewFinished(view) {
+    return view.renderingState === _ui_utils.RenderingStates.FINISHED;
+  }
+  renderView(view) {
+    switch (view.renderingState) {
+      case _ui_utils.RenderingStates.FINISHED:
+        return false;
+      case _ui_utils.RenderingStates.PAUSED:
+        this.highestPriorityPage = view.renderingId;
+        view.resume();
+        break;
+      case _ui_utils.RenderingStates.RUNNING:
+        this.highestPriorityPage = view.renderingId;
+        break;
+      case _ui_utils.RenderingStates.INITIAL:
+        this.highestPriorityPage = view.renderingId;
+        view.draw().finally(() => {
+          this.renderHighestPriority();
+        }).catch(reason => {
+          if (reason instanceof _pdfjsLib.RenderingCancelledException) {
+            return;
+          }
+          console.error(`renderView: "${reason}"`);
+        });
+        break;
+    }
+    return true;
+  }
+}
+exports.PDFRenderingQueue = PDFRenderingQueue;
+
+/***/ })
+/******/ 	]);
+/************************************************************************/
+/******/ 	// The module cache
+/******/ 	var __webpack_module_cache__ = {};
+/******/ 	
+/******/ 	// The require function
+/******/ 	function __w_pdfjs_require__(moduleId) {
+/******/ 		// Check if module is in cache
+/******/ 		var cachedModule = __webpack_module_cache__[moduleId];
+/******/ 		if (cachedModule !== undefined) {
+/******/ 			return cachedModule.exports;
+/******/ 		}
+/******/ 		// Create a new module (and put it into the cache)
+/******/ 		var module = __webpack_module_cache__[moduleId] = {
+/******/ 			// no module.id needed
+/******/ 			// no module.loaded needed
+/******/ 			exports: {}
+/******/ 		};
+/******/ 	
+/******/ 		// Execute the module function
+/******/ 		__webpack_modules__[moduleId](module, module.exports, __w_pdfjs_require__);
+/******/ 	
+/******/ 		// Return the exports of the module
+/******/ 		return module.exports;
+/******/ 	}
+/******/ 	
+/************************************************************************/
+var __webpack_exports__ = {};
+// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
+(() => {
+var exports = __webpack_exports__;
+
+
+Object.defineProperty(exports, "__esModule", ({
+  value: true
+}));
+Object.defineProperty(exports, "AnnotationLayerBuilder", ({
+  enumerable: true,
+  get: function () {
+    return _annotation_layer_builder.AnnotationLayerBuilder;
+  }
+}));
+Object.defineProperty(exports, "DownloadManager", ({
+  enumerable: true,
+  get: function () {
+    return _download_manager.DownloadManager;
+  }
+}));
+Object.defineProperty(exports, "EventBus", ({
+  enumerable: true,
+  get: function () {
+    return _event_utils.EventBus;
+  }
+}));
+Object.defineProperty(exports, "FindState", ({
+  enumerable: true,
+  get: function () {
+    return _pdf_find_controller.FindState;
+  }
+}));
+Object.defineProperty(exports, "GenericL10n", ({
+  enumerable: true,
+  get: function () {
+    return _genericl10n.GenericL10n;
+  }
+}));
+Object.defineProperty(exports, "LinkTarget", ({
+  enumerable: true,
+  get: function () {
+    return _pdf_link_service.LinkTarget;
+  }
+}));
+Object.defineProperty(exports, "NullL10n", ({
+  enumerable: true,
+  get: function () {
+    return _l10n_utils.NullL10n;
+  }
+}));
+Object.defineProperty(exports, "PDFFindController", ({
+  enumerable: true,
+  get: function () {
+    return _pdf_find_controller.PDFFindController;
+  }
+}));
+Object.defineProperty(exports, "PDFHistory", ({
+  enumerable: true,
+  get: function () {
+    return _pdf_history.PDFHistory;
+  }
+}));
+Object.defineProperty(exports, "PDFLinkService", ({
+  enumerable: true,
+  get: function () {
+    return _pdf_link_service.PDFLinkService;
+  }
+}));
+Object.defineProperty(exports, "PDFPageView", ({
+  enumerable: true,
+  get: function () {
+    return _pdf_page_view.PDFPageView;
+  }
+}));
+Object.defineProperty(exports, "PDFScriptingManager", ({
+  enumerable: true,
+  get: function () {
+    return _pdf_scripting_managerComponent.PDFScriptingManager;
+  }
+}));
+Object.defineProperty(exports, "PDFSinglePageViewer", ({
+  enumerable: true,
+  get: function () {
+    return _pdf_single_page_viewer.PDFSinglePageViewer;
+  }
+}));
+Object.defineProperty(exports, "PDFViewer", ({
+  enumerable: true,
+  get: function () {
+    return _pdf_viewer.PDFViewer;
+  }
+}));
+Object.defineProperty(exports, "ProgressBar", ({
+  enumerable: true,
+  get: function () {
+    return _ui_utils.ProgressBar;
+  }
+}));
+Object.defineProperty(exports, "RenderingStates", ({
+  enumerable: true,
+  get: function () {
+    return _ui_utils.RenderingStates;
+  }
+}));
+Object.defineProperty(exports, "ScrollMode", ({
+  enumerable: true,
+  get: function () {
+    return _ui_utils.ScrollMode;
+  }
+}));
+Object.defineProperty(exports, "SimpleLinkService", ({
+  enumerable: true,
+  get: function () {
+    return _pdf_link_service.SimpleLinkService;
+  }
+}));
+Object.defineProperty(exports, "SpreadMode", ({
+  enumerable: true,
+  get: function () {
+    return _ui_utils.SpreadMode;
+  }
+}));
+Object.defineProperty(exports, "StructTreeLayerBuilder", ({
+  enumerable: true,
+  get: function () {
+    return _struct_tree_layer_builder.StructTreeLayerBuilder;
+  }
+}));
+Object.defineProperty(exports, "TextLayerBuilder", ({
+  enumerable: true,
+  get: function () {
+    return _text_layer_builder.TextLayerBuilder;
+  }
+}));
+Object.defineProperty(exports, "XfaLayerBuilder", ({
+  enumerable: true,
+  get: function () {
+    return _xfa_layer_builder.XfaLayerBuilder;
+  }
+}));
+Object.defineProperty(exports, "parseQueryString", ({
+  enumerable: true,
+  get: function () {
+    return _ui_utils.parseQueryString;
+  }
+}));
+var _pdf_find_controller = __w_pdfjs_require__(1);
+var _pdf_link_service = __w_pdfjs_require__(5);
+var _ui_utils = __w_pdfjs_require__(2);
+var _annotation_layer_builder = __w_pdfjs_require__(6);
+var _download_manager = __w_pdfjs_require__(8);
+var _event_utils = __w_pdfjs_require__(9);
+var _genericl10n = __w_pdfjs_require__(10);
+var _l10n_utils = __w_pdfjs_require__(7);
+var _pdf_history = __w_pdfjs_require__(12);
+var _pdf_page_view = __w_pdfjs_require__(13);
+var _pdf_scripting_managerComponent = __w_pdfjs_require__(21);
+var _pdf_single_page_viewer = __w_pdfjs_require__(24);
+var _pdf_viewer = __w_pdfjs_require__(25);
+var _struct_tree_layer_builder = __w_pdfjs_require__(16);
+var _text_layer_builder = __w_pdfjs_require__(19);
+var _xfa_layer_builder = __w_pdfjs_require__(20);
+const pdfjsVersion = '3.11.174';
+const pdfjsBuild = 'ce8716743';
+})();
+
+/******/ 	return __webpack_exports__;
+/******/ })()
+;
+});
+//# sourceMappingURL=pdf_viewer.js.map

+ 2 - 2
scripts/src/logseq/tasks/dev.clj

@@ -80,8 +80,8 @@
 (defn publishing-backend
   "Builds publishing backend and copies over supporting frontend assets"
   [& args]
-  (apply shell {:dir "scripts"}
-         "yarn -s nbb-logseq -cp src -m logseq.tasks.dev.publishing"
+  (apply shell {:dir "deps/publishing" :extra-env {"ORIGINAL_PWD" (fs/cwd)}}
+         "yarn -s nbb-logseq -cp src:../graph-parser/src script/publishing.cljs"
          (into ["static"] args)))
 
 (defn watch-publishing-frontend

+ 23 - 5
scripts/src/logseq/tasks/lang.clj

@@ -74,12 +74,23 @@
            ;; Shorten values
            (map #(update % :string-to-translate shorten 50) sorted-missing)))))))
 
+(defn- delete-invalid-non-default-languages
+  [invalid-keys-by-lang]
+  (doseq [[lang invalid-keys] invalid-keys-by-lang]
+    (let [path (fs/path "src/resources/dicts" (str (name lang) ".edn"))
+          result (r/parse-string (String. (fs/read-all-bytes path)))
+          new-content (str (reduce
+                            (fn [result k]
+                              (r/dissoc result k))
+                            result invalid-keys))]
+      (spit (fs/file path) new-content))))
+
 (defn- validate-non-default-languages
   "This validation finds any translation keys that don't exist in the default
   language English. Logseq needs to work out of the box with its default
   language. This catches mistakes where another language has accidentally typoed
   keys or added ones without updating :en"
-  []
+  [{:keys [fix?]}]
   (let [dicts (get-dicts)
         ;; For now defined as :en but clj-kondo analysis could be more thorough
         valid-keys (set (keys (dicts :en)))
@@ -95,6 +106,10 @@
       (do
         (println "\nThese translation keys are invalid because they don't exist in English:")
         (task-util/print-table invalid-dicts)
+        (when fix?
+          (delete-invalid-non-default-languages
+           (update-vals (group-by :language invalid-dicts) #(map :invalid-key %)))
+          (println "These invalid non-language keys have been removed."))
         (System/exit 1)))))
 
 ;; Command to check for manual entries:
@@ -173,7 +188,7 @@
           (task-util/print-table (map #(hash-map :invalid-key %) expected-only))
           (when fix?
             (delete-not-used-key-from-dict-file expected-only)
-            (println "These invalid keys have been removed.")))
+            (println "These invalid ui keys have been removed.")))
         (System/exit 1)))))
 
 (def allowed-duplicates
@@ -184,11 +199,13 @@
    :de #{:graph :host :plugins :port :right-side-bar/whiteboards
          :settings-of-plugins :search-item/whiteboard :shortcut.category/navigating
          :settings-page/enable-tooltip :settings-page/enable-whiteboards :settings-page/plugin-system}
+   :ca #{:port :settings-page/tab-editor :settings-page/tab-general
+         :whiteboard/color :whiteboard/connector :whiteboard/text :whiteboard/triangle}
    :es #{:settings-page/tab-general :settings-page/tab-editor :whiteboard/color}
    :it #{:home :handbook/home :host :help/awesome-logseq
          :settings-page/tab-account :settings-page/tab-editor :whiteboard/link}
    :nl #{:plugins :type :left-side-bar/nav-recent-pages :plugin/update}
-   :pl #{:port}
+   :pl #{:port :home :host :plugin/marketplace :whiteboard/link}
    :pt-BR #{:plugins :right-side-bar/flashcards :settings-page/enable-flashcards :page/backlinks
             :host :settings-page/tab-editor :shortcut.category/plugins :whiteboard/link :settings-of-plugins :whiteboard
             :whiteboards :on-boarding/quick-tour-journal-page-desc-2 :plugin/downloads
@@ -201,7 +218,8 @@
             :settings-page/tab-editor :shortcut.category/whiteboard :whiteboard/medium
             :whiteboard/twitter-url :whiteboard/youtube-url :linked-references/filter-heading}
    :tr #{:help/awesome-logseq}
-   :id #{:host :port}})
+   :id #{:host :port}
+   :cs #{:host :port :help/blog :settings-page/tab-editor :whiteboard/text}})
 
 (defn- validate-languages-dont-have-duplicates
   "Looks up duplicates for all languages"
@@ -229,6 +247,6 @@
 (defn validate-translations
   "Runs multiple translation validations that fail fast if one of them is invalid"
   [& args]
-  (validate-non-default-languages)
+  (validate-non-default-languages {:fix? (contains? (set args) "--fix")})
   (validate-ui-translations-are-used {:fix? (contains? (set args) "--fix")})
   (validate-languages-dont-have-duplicates))

+ 24 - 6
src/main/frontend/common.css

@@ -425,7 +425,7 @@ p.warning {
 }
 
 .text-warning {
-  @apply text-yellow-500 dark:text-yellow-500/70;
+  @apply text-yellow-600 dark:text-yellow-500/70;
 }
 
 .bg-warning {
@@ -446,12 +446,30 @@ span.error {
   @apply bg-red-rx-04-alpha;
 }
 
+a.error,
+span.error {
+  @apply bg-red-rx-04-alpha text-red-rx-09 dark:bg-red-rx-06-alpha
+  dark:text-red-rx-10 rounded px-1.5 py-[2px] leading-none;
+}
+
+.text-error {
+  @apply text-red-rx-09;
+}
+
+.bg-error {
+  @apply bg-red-rx-04-alpha;
+}
+
+.text-success {
+  @apply text-green-rx-09;
+}
+
+.bg-success {
+  @apply bg-green-rx-04-alpha;
+}
+
 img.small {
-  display: inline;
-  width: 20px;
-  height: 20px;
-  margin-top: 0;
-  margin-bottom: 0;
+  @apply inline w-5 h-5 m-0;
 }
 
 a.tag {

+ 3 - 3
src/main/frontend/common/missionary_util.clj

@@ -3,6 +3,6 @@
   (:require [missionary.core :as m]))
 
 (defmacro <?
-  "Like m/?, but async channel as arg"
-  [c]
-  `(m/? (<! ~c)))
+  "Like m/?, but async channel or promise as arg"
+  [chan-or-promise]
+  `(m/? (<! ~chan-or-promise)))

+ 40 - 32
src/main/frontend/common/missionary_util.cljs

@@ -1,7 +1,8 @@
 (ns frontend.common.missionary-util
   "Utils based on missionary. Used by frontend and worker namespaces"
   (:require-macros [frontend.common.missionary-util])
-  (:require [clojure.core.async :as a]
+  (:require [cljs.core.async.impl.channels]
+            [clojure.core.async :as a]
             [missionary.core :as m])
   ;; (:import [missionary Cancelled])
   )
@@ -13,19 +14,19 @@
   "Retry task when it throw exception `(get ex-data :missionary/retry)`"
   [delays-seq task]
   (m/sp
-    (loop [[delay & rest-delays] (seq delays-seq)]
-      (let [r (try
-                (m/? task)
-                (catch :default e
-                  (if (and (some-> e ex-data :missionary/retry)
-                           (pos-int? delay))
-                    (do (m/? (m/sleep delay))
-                        (println :missionary/retry "after" delay "ms (" (ex-message e) ")")
-                        retry-sentinel)
-                    (throw e))))]
-        (if (identical? r retry-sentinel)
-          (recur rest-delays)
-          r)))))
+   (loop [[delay & rest-delays] (seq delays-seq)]
+     (let [r (try
+               (m/? task)
+               (catch :default e
+                 (if (and (some-> e ex-data :missionary/retry)
+                          (pos-int? delay))
+                   (do (m/? (m/sleep delay))
+                       (println :missionary/retry "after" delay "ms (" (ex-message e) ")")
+                       retry-sentinel)
+                   (throw e))))]
+       (if (identical? r retry-sentinel)
+         (recur rest-delays)
+         r)))))
 
 (defn mix
   "Return a flow which is mixed by `flows`"
@@ -39,10 +40,10 @@
   ([interval-ms value]
    (->>
     (m/ap
-      (loop []
-        (m/amb
-         (m/? (m/sleep interval-ms value))
-         (recur))))
+     (loop []
+       (m/amb
+        (m/? (m/sleep interval-ms value))
+        (recur))))
     (m/reductions {} value)
     (m/latest identity))))
 
@@ -50,10 +51,10 @@
   (defn debounce
     [duration-ms flow]
     (m/ap
-      (let [x (m/?< flow)]
-        (try (m/? (m/sleep duration-ms x))
-             (catch Cancelled _
-               (m/amb)))))))
+     (let [x (m/?< flow)]
+       (try (m/? (m/sleep duration-ms x))
+            (catch Cancelled _
+              (m/amb)))))))
 
 (defn run-task
   "Return the canceler"
@@ -71,14 +72,21 @@
   completing with true when put is accepted, or false if port was closed."
     [c x] (doto (m/dfv) (->> (a/put! c x)))))
 
-(defn <!
-  "Return a task that takes from given channel,
-  completing with value when take is accepted, or nil if port was closed."
-  [c] (doto (m/dfv) (->> (a/take! c))))
+(comment
+  (defn await-promise
+    "Returns a task completing with the result of given promise"
+    [p]
+    (let [v (m/dfv)]
+      (.then p #(v (fn [] %)) #(v (fn [] (throw %))))
+      (m/absolve v))))
 
-(defn await-promise
-  "Returns a task completing with the result of given promise"
-  [p]
-  (let [v (m/dfv)]
-    (.then p #(v (fn [] %)) #(v (fn [] (throw %))))
-    (m/absolve v)))
+(defn <!
+  "Return a task.
+  if arg is a channel, takes from given channel, completing with value when take is accepted, or nil if port was closed.
+  if arg is a promise, completing with the result of given promise."
+  [chan-or-promise]
+  (if (instance? cljs.core.async.impl.channels/ManyToManyChannel chan-or-promise)
+    (doto (m/dfv) (->> (a/take! chan-or-promise)))
+    (let [v (m/dfv)]
+      (.then chan-or-promise #(v (fn [] %)) #(v (fn [] (throw %))))
+      (m/absolve v))))

+ 1 - 4
src/main/frontend/components/all_pages.cljs

@@ -2,7 +2,6 @@
   "All pages"
   (:require [frontend.components.block :as component-block]
             [frontend.components.page :as component-page]
-            [frontend.components.icon :as icon-component]
             [frontend.components.views :as views]
             [frontend.context.i18n :refer [t]]
             [frontend.db :as db]
@@ -19,9 +18,7 @@
   (->> [{:id :block/title
          :name (t :block/name)
          :cell (fn [_table row _column]
-                 [:div.flex.flex-row.items-center.gap-1
-                  [:span.opacity-50 (icon-component/get-node-icon-cp row {:color? true})]
-                  (component-block/page-cp {} row)])
+                 (component-block/page-cp {} row))
          :type :string}
         {:id :block/type
          :name "Type"

+ 137 - 88
src/main/frontend/components/block.cljs

@@ -266,6 +266,34 @@
     (when (seq images)
       (lightbox/preview-images! images))))
 
+(rum/defc resize-image-handles
+  [dx-fn]
+  (let [handle-props {}
+        add-resizing-class! #(dom/add-class! js/document.documentElement "is-resizing-buf")
+        remove-resizing-class! #(dom/remove-class! js/document.documentElement "is-resizing-buf")
+        *handle-left (rum/use-ref nil)
+        *handle-right (rum/use-ref nil)]
+
+    (rum/use-effect!
+     (fn []
+       (doseq [el [(rum/deref *handle-left)
+                   (rum/deref *handle-right)]]
+         (-> (js/interact el)
+             (.draggable
+              (bean/->js
+               {:listeners
+                {:start (fn [e] (dx-fn :start e))
+                 :move (fn [e] (dx-fn :move e))
+                 :end (fn [e] (dx-fn :end e))}}))
+             (.styleCursor false)
+             (.on "dragstart" add-resizing-class!)
+             (.on "dragend" remove-resizing-class!))))
+     [])
+
+    [:<>
+     [:span.handle-left.image-resize (assoc handle-props :ref *handle-left)]
+     [:span.handle-right.image-resize (assoc handle-props :ref *handle-right)]]))
+
 (defonce *resizing-image? (atom false))
 (rum/defcs ^:large-vars/cleanup-todo resizable-image <
   (rum/local nil ::size)
@@ -273,8 +301,8 @@
                    (reset! *resizing-image? false)
                    state)}
   [state config title src metadata full-text local?]
-  (let [size (get state ::size)
-        breadcrumb? (:breadcrumb? config)
+  (let [breadcrumb? (:breadcrumb? config)
+        positioned? (:property-position config)
         asset-block (:asset-block config)
         asset-container [:div.asset-container {:key "resize-asset-container"}
                          [:img.rounded-sm.relative
@@ -284,7 +312,8 @@
                             :src src
                             :title title}
                            metadata)]
-                         (when-not breadcrumb?
+                         (when (and (not breadcrumb?)
+                                    (not positioned?))
                            [:<>
                             (let [image-src (fs/asset-path-normalize src)]
                               [:.asset-action-bar {:aria-hidden "true"}
@@ -353,46 +382,45 @@
                                    (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))
+        *width (get state ::size)
+        width (or @*width width)
         style (when-not (util/mobile?)
-                (cond (and width height)
-                      {:width width :height height}
-                      width
-                      {:width width}
-                      height
-                      {:height height}
+                (cond width
+                      {(if (:sidebar? config)
+                         :max-width :width) width}
                       :else
-                      {}))]
-    (if (:disable-resize? config)
+                      {}))
+        resizable? (and (not (mobile-util/native-platform?))
+                        (not breadcrumb?)
+                        (not positioned?))]
+    (if (or (:disable-resize? config)
+            (not resizable?))
       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 (or (:block/uuid config)
-                                                     (some-> config :block (:block/uuid)))]
-                               (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)))))
+      [:div.ls-resize-image.rounded-md
+       (when style {:style style})
+       asset-container
+       (resize-image-handles
+        (fn [k ^js event]
+          (let [dx (.-dx event)
+                ^js target (.-target event)]
+
+            (case k
+              :start
+              (let [c (.closest target ".ls-resize-image")]
+                (reset! *width (.-offsetWidth c))
+                (reset! *resizing-image? true))
+              :move
+              (let [width' (+ @*width dx)]
+                (when (or (> width' 60)
+                          (not (neg? dx)))
+                  (reset! *width width')))
+              :end
+              (let [width' @*width]
+                (when (and width' @*resizing-image?)
+                  (when-let [block-id (or (:block/uuid config)
+                                          (some-> config :block (:block/uuid)))]
+                    (editor-handler/resize-image! config block-id metadata full-text {:width width'})))
+                (reset! *resizing-image? false))))))])))
 
 (rum/defc audio-cp [src]
   ;; Change protocol to allow media fragment uris to play
@@ -786,6 +814,8 @@
   (let [*timer (rum/use-ref nil)                            ;; show
         *timer1 (rum/use-ref nil)                           ;; hide
         *el-popup (rum/use-ref nil)
+        *el-wrap (rum/use-ref nil)
+        [in-popup? set-in-popup!] (rum/use-state nil)
         [visible? set-visible!] (rum/use-state nil)
         ;; set-visible! (fn debug-visible [v] (js/console.warn "debug: visible" v) (set-visible! v))
         _  #_:clj-kondo/ignore (rum/defc preview-render []
@@ -820,13 +850,24 @@
                                                 :sidebar? sidebar?
                                                 :preview? true}))]))]
 
-    (if (and (not (:preview? config))
-             (or (not manual?) open?))
-      (popup-preview-impl children
-                          {:visible? visible? :set-visible! set-visible!
-                           :*timer *timer :*timer1 *timer1
-                           :render preview-render :*el-popup *el-popup})
-      children)))
+    (rum/use-effect!
+     (fn []
+       (if (some-> (rum/deref *el-wrap) (.closest "[data-radix-popper-content-wrapper]"))
+         (set-in-popup! true)
+         (set-in-popup! false)))
+     [])
+
+    [:span {:ref *el-wrap}
+     (if (boolean? in-popup?)
+       (if (and (not (:preview? config))
+                (not in-popup?)
+                (or (not manual?) open?))
+         (popup-preview-impl children
+                             {:visible? visible? :set-visible! set-visible!
+                              :*timer *timer :*timer1 *timer1
+                              :render preview-render :*el-popup *el-popup})
+         children)
+       children)]))
 
 (declare block-reference)
 (declare block-reference-preview)
@@ -838,6 +879,14 @@
     [:span.warning.mr-1 {:title "Node ref invalid"}
      (->ref id)]))
 
+(defn inline-text
+  ([format v]
+   (inline-text {} format v))
+  ([config format v]
+   (when (string? v)
+     (let [inline-list (gp-mldoc/inline->edn v (mldoc/get-default-config format))]
+       [:div.inline.mr-1 (map-inline config inline-list)]))))
+
 (rum/defcs page-cp-inner < db-mixins/query rum/reactive
   {:init (fn [state]
            (let [page (last (:rum/args state))
@@ -881,7 +930,10 @@
                                                 :config config
                                                 :id (:block/uuid entity)}))
               inner))
-          (block-reference config (:block/uuid entity) nil))
+          (block-reference config (:block/uuid entity)
+                           (if (string? label)
+                             (gp-mldoc/inline->edn label (mldoc/get-default-config :markdown))
+                             label)))
 
         (and (:block/name page) (util/uuid-string? (:block/name page)))
         (invalid-node-ref (:block/name page))
@@ -1148,7 +1200,7 @@
               block (when db-id (db/sub-block db-id))
               properties (:block/properties block)
               block-type (keyword (pu/lookup properties :logseq.property/ls-type))
-              hl-type (pu/lookup properties :logseq.property/hl-type)
+              hl-type (pu/lookup properties :logseq.property.pdf/hl-type)
               repo (state/get-current-repo)
               stop-inner-events? (= block-type :whiteboard-shape)]
           (if (and block (:block/title block))
@@ -1213,14 +1265,6 @@
             (invalid-node-ref id))))
       (invalid-node-ref id))))
 
-(defn inline-text
-  ([format v]
-   (inline-text {} format v))
-  ([config format v]
-   (when (string? v)
-     (let [inline-list (gp-mldoc/inline->edn v (mldoc/get-default-config format))]
-       [:div.inline.mr-1 (map-inline config inline-list)]))))
-
 (defn- render-macro
   [config name arguments macro-content format]
   [:div.macro {:data-macro-name name}
@@ -2145,7 +2189,7 @@
     (->elem
      elem
      (merge
-      {:data-hl-type (pu/lookup properties :logseq.property/hl-type)}
+      {:data-hl-type (pu/lookup properties :logseq.property.pdf/hl-type)}
       (when (and marker
                  (not (string/blank? marker))
                  (not= "nil" marker))
@@ -2159,7 +2203,7 @@
            :class "px-1 with-bg-color"})))
 
      ;; children
-     (let [area?  (= :area (keyword (pu/lookup properties :logseq.property/hl-type)))
+     (let [area?  (= :area (keyword (pu/lookup properties :logseq.property.pdf/hl-type)))
            hl-ref #(when (not (#{:default :whiteboard-shape} block-type))
                      [:div.prefix-link
                       {:on-pointer-down
@@ -2356,8 +2400,7 @@
                                         :block-cp blocks-container
                                         :editor-box (state/get-component :editor/box)
                                         :container-id (or (:container-id config)
-                                                          (::initial-container-id state))
-                                        :id (:id config)}
+                                                          (::initial-container-id state))}
                                        opts)))
 
 (rum/defc invalid-properties-cp
@@ -2601,7 +2644,8 @@
                      :page-cp page-cp
                      :block-cp blocks-container
                      :inline-text inline-text
-                     :other-position? true})]
+                     :other-position? true
+                     :property-position position})]
     (when (seq properties)
       (case position
         :block-below
@@ -2609,8 +2653,8 @@
          (for [pid properties]
            (let [property (db/entity pid)
                  v (get block pid)]
-             [:div.flex.flex-row.items-center.gap-2.hover:bg-secondary.rounded
-              [:div.flex.flex-row.opacity-50.hover:opacity-100.items-center
+             [:div.flex.flex-row.items-center.opacity-50.hover:opacity-100.transition-opacity.duration-300.ease-in.gap-1
+              [:div.flex.flex-row.items-center
                (property-component/property-key-cp block property opts)
                [:div.select-none ":"]]
               (pv/property-value block property v opts)]))]
@@ -2652,9 +2696,9 @@
                         :pointer-events (when stop-events? "none")}}
 
                 (not (string/blank?
-                      (pu/lookup properties :logseq.property/hl-color)))
+                      (pu/lookup properties :logseq.property.pdf/hl-color)))
                 (assoc :data-hl-color
-                       (pu/lookup properties :logseq.property/hl-color))
+                       (pu/lookup properties :logseq.property.pdf/hl-color))
 
                 (not block-ref?)
                 (assoc mouse-down-key (fn [e]
@@ -2683,7 +2727,6 @@
               :key (str "block-content-" uuid)
               :on-pointer-up (fn [e]
                                (when (and
-                                      (state/in-selection-mode?)
                                       (not (string/includes? content "```"))
                                       (not (gobj/get e "shiftKey"))
                                       (not (util/meta-key? e)))
@@ -2835,8 +2878,6 @@
                                                                                               :container-id (:container-id config)}))}})
            (block-content config block edit-input-id block-id slide?))
 
-          (when (and db-based? (not table?)) (block-positioned-properties config block :block-right))
-
           (when (and (not hide-block-refs-count?)
                      (not named?))
             [:div.flex.flex-row.items-center
@@ -2856,6 +2897,8 @@
                                     (editor-handler/edit-block! block :max))}
                 svg/edit])])])
 
+       (when (and db-based? (not table?)) (block-positioned-properties config block :block-right))
+
        (when-not (or (:table? config) (:property? config) (:page-title? config))
          (block-refs-count block refs-count *hide-block-refs?))
 
@@ -3079,7 +3122,7 @@
                 (-> (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))]
+                       (when-let [[asset-file-name file-obj asset-file-fpath matched-alias] (first res)]
                          (let [image? (config/ext-of-image? asset-file-name)
                                link-content (assets-handler/get-asset-file-link format
                                                                                 (if matched-alias
@@ -3095,7 +3138,8 @@
                              :edit-block? false
                              :replace-empty-target? true
                              :sibling?   true
-                             :before?    false}))))))))
+                             :before?    false}))
+                         (recur (rest res))))))))
 
             :else
             (prn ::unhandled-drop-data-transfer-type transfer-types))))))
@@ -3110,23 +3154,18 @@
       (let [node (.querySelector parent ".bullet-container")]
         (when doc-mode?
           (dom/remove-class! node "hide-inner-bullet"))))
-    (when (and
-           (state/in-selection-mode?)
-           (non-dragging? e))
+    (when (non-dragging? e)
       (when-let [container (gdom/getElement "app-container-wrapper")]
         (dom/add-class! container "blocks-selection-mode"))
       (editor-handler/highlight-selection-area! block-id {:append? true}))))
 
 (defn- block-mouse-leave
-  [e *control-show? block-id doc-mode?]
+  [*control-show? block-id doc-mode?]
   (reset! *control-show? false)
   (when doc-mode?
     (when-let [parent (gdom/getElement block-id)]
       (when-let [node (.querySelector parent ".bullet-container")]
-        (dom/add-class! node "hide-inner-bullet"))))
-  (when (and (non-dragging? e)
-             (not @*resizing-image?))
-    (state/into-selection-mode!)))
+        (dom/add-class! node "hide-inner-bullet")))))
 
 (defn- on-drag-and-mouse-attrs
   [block original-block uuid top? block-id *move-to']
@@ -3245,8 +3284,9 @@
         *control-show? (get container-state ::control-show?)
         db-collapsed? (util/collapsed? block)
         collapsed? (cond
-                     (or ref-or-custom-query? (root-block? config block))
-                     (state/sub-collapsed uuid)
+                     (or ref-or-custom-query? (root-block? config block)
+                         (and (or (ldb/class? block) (ldb/property? block)) (:page-title? config)))
+                     (state/sub-block-collapsed uuid)
 
                      :else
                      db-collapsed?)
@@ -3257,6 +3297,7 @@
         slide? (boolean (:slide? config))
         doc-mode? (:document/mode? config)
         embed? (:embed? config)
+        page-embed? (:page-embed? config)
         reference? (:reference? config)
         whiteboard-block? (pu/shape-block? block)
         block-id (str "ls-block-" uuid)
@@ -3296,7 +3337,7 @@
        (not slide?)
        (merge attrs)
 
-       (or reference? embed?)
+       (or reference? (and embed? (not page-embed?)))
        (assoc :data-transclude true)
 
        embed?
@@ -3315,7 +3356,7 @@
        (dnd-separator-wrapper block children block-id slide? true false))
 
      (when-not (:hide-title? config)
-       [:div.block-main-container.flex.flex-row.pr-2.gap-1
+       [:div.block-main-container.flex.flex-row.gap-1
         {:style (when (and db-based? (:page-title? config))
                   {:margin-left -30})
          :data-has-heading (some-> block :block/properties (pu/lookup :logseq.property/heading))
@@ -3328,8 +3369,8 @@
                             (block-handler/on-touch-cancel *show-left-menu? *show-right-menu?))
          :on-mouse-enter (fn [e]
                            (block-mouse-over e *control-show? block-id doc-mode?))
-         :on-mouse-leave (fn [e]
-                           (block-mouse-leave e *control-show? block-id doc-mode?))}
+         :on-mouse-leave (fn [_e]
+                           (block-mouse-leave *control-show? block-id doc-mode?))}
 
         (when (and (not slide?) (not in-whiteboard?) (not property?))
           (let [edit? (or editing?
@@ -3358,7 +3399,7 @@
                                        (when (ldb/property? block)
                                          {:type :tabler-icon
                                           :id "letter-p"})))]
-                [:div.ls-page-icon.flex.self-start3
+                [:div.ls-page-icon.flex.self-start
                  (icon-component/icon-picker icon
                                              {:on-chosen (fn [_e icon]
                                                            (if icon
@@ -3437,6 +3478,10 @@
                  linked-block? (or (:block/link block)
                                    (:original-block config))]
              (cond
+               (and (:page-title? config) (or (ldb/class? block) (ldb/property? block)))
+               (let [collapsed? (state/get-block-collapsed block-id)]
+                 (state/set-collapsed-block! block-id (if (some? collapsed?) collapsed? true)))
+
                (root-block? config block)
                (state/set-collapsed-block! block-id false)
 
@@ -3980,11 +4025,15 @@
                                                       {:top? top?
                                                        :bottom? bottom?})))})
         *wrap-ref (rum/use-ref nil)]
-
     (rum/use-effect!
      (fn []
-        ;; Try to fix virtuoso scrollable container blink for the block insertion at bottom
        (when virtualized?
+         (when (:current-page? config)
+           (let [ref (.-current *virtualized-ref)]
+             (ui-handler/scroll-to-anchor-block ref blocks false)
+             (state/set-state! :editor/virtualized-scroll-fn
+                               #(ui-handler/scroll-to-anchor-block ref blocks false))))
+         ;; Try to fix virtuoso scrollable container blink for the block insertion at bottom
          (let [^js *ob (volatile! nil)]
            (js/setTimeout
             (fn []

+ 40 - 11
src/main/frontend/components/block.css

@@ -27,7 +27,7 @@
   }
 
   .asset-container {
-    @apply relative inline-block mt-2;
+    @apply relative inline-block mt-2 w-full;
 
     .asset-action-bar {
       @apply top-0.5 right-0.5 absolute flex items-center
@@ -44,16 +44,11 @@
         @apply opacity-100;
       }
     }
-  }
-
-  .resize {
-    display: inline-flex;
-    /* Fix chrome missing resize handle issue https://bugs.chromium.org/p/chromium/issues/detail?id=1135676&q=css%20resize%20type%3DBug&can=2.*/
-    transform: translate3d(0, 0, 0);
-  }
 
-  .image-resize {
-    display: flex;
+    > img {
+      max-width: unset;
+      width: 100%;
+    }
   }
 
   .draw [aria-labelledby="shapes-title"] {
@@ -874,6 +869,20 @@ html.is-mac {
   opacity: 1;
 }
 
+.positioned-properties {
+  @apply text-sm;
+}
+
+.positioned-properties.block-left {
+  .asset-container {
+    @apply max-w-[120px];
+  }
+
+  .block-title-wrap {
+    @apply hidden;
+  }
+}
+
 .positioned-properties.block-right {
   button {
     @apply whitespace-nowrap mr-0.5;
@@ -894,7 +903,7 @@ html.is-mac {
   @apply flex flex-row flex-wrap self-start items-center;
   min-height: 24px;
   max-width: 256px;
-  margin-right: -28px;
+  margin-right: -16px;
 }
 
 .block-tag {
@@ -960,3 +969,23 @@ html.is-mac {
     }
   }
 }
+
+.ls-resize-image {
+  @apply flex relative;
+
+  .handle-left , .handle-right {
+    @apply absolute w-[6px] h-[15%] min-h-[30px] bg-black/30 hover:bg-black/70
+    top-[50%] left-[5px] rounded-full cursor-col-resize select-none
+    translate-y-[-20%] opacity-0 border border-gray-200;
+  }
+
+  .handle-right {
+    @apply left-auto right-[5px]
+  }
+
+  &:hover {
+    .handle-left, .handle-right {
+      @apply opacity-100;
+    }
+  }
+}

+ 13 - 3
src/main/frontend/components/block/views.css

@@ -1,7 +1,17 @@
 .ls-cards {
-  @apply grid grid-cols-[repeat(auto-fill,18rem)] gap-4 content;
-
   .ls-card-item {
-    @apply dark:shadow-gray-700 shadow-md rounded-md p-2 overflow-y-scroll h-[15rem];
+    @apply dark:shadow-gray-700 shadow-md rounded-md p-4 h-[15rem] w-full overflow-auto;
+  }
+
+  > div[data-virtuoso-scroller] {
+    @apply w-full;
+  }
+
+  .virtuoso-grid-list {
+    @apply flex flex-wrap gap-3;
+  }
+
+  .virtuoso-grid-item {
+    @apply flex w-[290px];
   }
 }

+ 10 - 0
src/main/frontend/components/container.cljs

@@ -987,6 +987,16 @@
       (app-context-menu-observer)
 
       [:a#download.hidden]
+      [:a#download-as-edn-v2.hidden]
+      [:a#download-as-json-v2.hidden]
+      [:a#download-as-json-debug.hidden]
+      [:a#download-as-sqlite-db.hidden]
+      [:a#download-as-roam-json.hidden]
+      [:a#download-as-html.hidden]
+      [:a#download-as-zip.hidden]
+      [:a#export-as-markdown.hidden]
+      [:a#export-as-opml.hidden]
+      [:a#convert-markdown-to-unordered-list-or-heading.hidden]
       (when (and (not config/mobile?)
                  (not config/publishing?))
         (help-button))])))

+ 5 - 17
src/main/frontend/components/export.cljs

@@ -98,11 +98,10 @@
             "Export debug JSON"]
            [:p.text-sm.opacity-70 "Any sensitive data will be removed in the exported json file, you can send it to us for debugging."]])
 
-        (when-not db-based?
-          (when (util/electron?)
-            [:div
-             [:a.font-medium {:on-click #(export/download-repo-as-html! current-repo)}
-              (t :export-public-pages)]]))
+        (when (util/electron?)
+          [:div
+           [:a.font-medium {:on-click #(export/download-repo-as-html! current-repo)}
+            (t :export-public-pages)]])
         (when-not (or (mobile-util/native-platform?) db-based?)
           [:div
            [:a.font-medium {:on-click #(export-text/export-repo-as-markdown! current-repo)}
@@ -119,18 +118,7 @@
         (when (and db-based? util/web-platform? (utils/nfsSupported))
           [:div
            [:hr]
-           (auto-backup)])]
-
-       [:a#download-as-edn-v2.hidden]
-       [:a#download-as-json-v2.hidden]
-       [:a#download-as-json-debug.hidden]
-       [:a#download-as-sqlite-db.hidden]
-       [:a#download-as-roam-json.hidden]
-       [:a#download-as-html.hidden]
-       [:a#download-as-zip.hidden]
-       [:a#export-as-markdown.hidden]
-       [:a#export-as-opml.hidden]
-       [:a#convert-markdown-to-unordered-list-or-heading.hidden]])))
+           (auto-backup)])]])))
 
 (def *export-block-type (atom :text))
 

+ 15 - 7
src/main/frontend/components/objects.cljs

@@ -117,7 +117,10 @@
         [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 {:add-tags-column? (= (:db/ident class) :logseq.class/Root)})
+        ;; Properties can be nil for published private graphs
+        properties' (remove nil? properties)
+        columns* (views/build-columns config properties' {:add-tags-column? (or (= (:db/ident class) :logseq.class/Root)
+                                                                                (> (count (distinct (mapcat :block/tags objects))) 1))})
         columns (cond
                   (= (:db/ident class) :logseq.class/Pdf-annotation)
                   (remove #(contains? #{:logseq.property/ls-type} (:id %)) columns*)
@@ -156,7 +159,8 @@
        (ui/foldable
         [:div.font-medium.opacity-50 "Tagged Nodes"]
         [:div.mt-2
-         (views/view view-entity {:data data
+         (views/view view-entity {:config config
+                                  :data data
                                   :set-data! set-data!
                                   :views-title (class-views class views view-entity {:set-view-entity! set-view-entity!
                                                                                      :set-views! set-views!})
@@ -171,6 +175,7 @@
                                                             {:on-change (fn [_e files]
                                                                           (p/do!
                                                                            (editor-handler/upload-asset! nil files :markdown editor-handler/*asset-uploading? true)
+                                                                           (set-data! (get-class-objects class))
                                                                            (shui/dialog-close!)))})])))
                                                      #(add-new-class-object! class set-data!))
                                   :show-add-property? true
@@ -195,10 +200,11 @@
         {:disable-on-pointer-down? true})])))
 
 (rum/defcs class-objects < rum/reactive db-mixins/query mixins/container-id
-  [state class]
+  [state class current-page?]
   (when class
     (let [class (db/sub-block (:db/id class))
-          config {:container-id (:container-id state)}
+          config {:container-id (:container-id state)
+                  :current-page? current-page?}
           properties (outliner-property/get-class-properties class)
           repo (state/get-current-repo)
           objects (->> (db-model/sub-class-objects repo (:db/id class))
@@ -244,7 +250,8 @@
       (ui/foldable
        [:div.font-medium.opacity-50 "Nodes with Property"]
        [:div.mt-2
-        (views/view view-entity {:data data
+        (views/view view-entity {:config config
+                                 :data data
                                  :set-data! set-data!
                                  :title-key :views.table/property-nodes
                                  :columns columns
@@ -272,10 +279,11 @@
 
 ;; Show all nodes containing the given property
 (rum/defcs property-related-objects < rum/reactive db-mixins/query mixins/container-id
-  [state property]
+  [state property current-page?]
   (when property
     (let [property' (db/sub-block (:db/id property))
-          config {:container-id (:container-id state)}
+          config {:container-id (:container-id state)
+                  :current-page? current-page?}
           ;; Show tags to help differentiate property rows
           properties [property' (db/entity :block/tags)]
           repo (state/get-current-repo)

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

@@ -87,11 +87,12 @@
            (not sidebar?))
       (when (and (string/blank? (:block/title block))
                  (not preview?))
-        (editor-handler/edit-block! block :max))))
-  state)
+        (editor-handler/edit-block! block :max)))))
 
 (rum/defc page-blocks-inner <
-  {:did-mount open-root-block!}
+  {:did-mount (fn [state]
+                (open-root-block! state)
+                state)}
   [page-e blocks config sidebar? whiteboard? _block-uuid]
   (when page-e
     (let [hiccup (component-block/->hiccup blocks config {})]
@@ -220,8 +221,6 @@
 
                      :else
                      children)
-          *loading? (:*loading? config)
-          loading? (when *loading? (rum/react *loading?))
           db-based? (config/db-based-graph? repo)]
       [:<>
        (when (and db-based? (or (ldb/class? block) (ldb/property? block)))
@@ -230,11 +229,7 @@
 
        [:div.ml-1
         (cond
-          loading?
-          nil
-
           (and
-           (not loading?)
            (not block?)
            (empty? children) page-e)
           (dummy-block page-e)
@@ -433,17 +428,25 @@
   [state page whiteboard-page? sidebar? container-id]
   (let [*hover? (::hover? state)
         hover? (rum/react *hover?)]
-    [:div.ls-page-title.flex.flex-1.w-full.content.items-start
+    [:div.ls-page-title.flex.flex-1.w-full.content.items-start.title
      {:class (when-not whiteboard-page? "title")
       :on-pointer-down (fn [e]
                          (when (util/right-click? e)
                            (state/set-state! :page-title/context {:page (:block/title page)
-                                                                  :page-entity page})))}
+                                                                  :page-entity page})))
+      :on-click (fn [e]
+                  (when-not (= (.-nodeName (.-target e)) "INPUT")
+                    (.preventDefault e)
+                    (when (gobj/get e "shiftKey")
+                      (state/sidebar-add-block!
+                       (state/get-current-repo)
+                       (:db/id page)
+                       :page))))}
 
      [:div.w-full.relative {:on-mouse-over #(reset! *hover? true)
                             :on-mouse-leave (fn []
                                               (reset! *hover? false))}
-      (when hover?
+      (when (and hover? (not= (:db/id (state/get-edit-block)) (:db/id page)))
         [:div.absolute.-top-3.left-0.fade-in
          [:div.flex.flex-row.items-center.gap-2
           (when-not (:logseq.property/icon (db/entity (:db/id page)))
@@ -464,7 +467,7 @@
             :on-click (fn [e]
                         (state/pub-event! [:editor/new-property {:block page
                                                                  :target (.-target e)}]))}
-           "Set property")]])
+           "Set node property")]])
       (component-block/block-container {:page-title? true
                                         :hide-title? sidebar?
                                         :hide-children? true
@@ -591,7 +594,7 @@
                    :on-mouse-leave (fn [e]
                                      (page-mouse-leave e *control-show?))}
                   (page-blocks-collapse-control title *control-show? *all-collapsed?)])
-               (when (and (not whiteboard?) (ldb/page? page))
+               (when (and (not whiteboard?) (not sidebar?) (ldb/page? page))
                  (if db-based?
                    (db-page-title page whiteboard-page? sidebar? (:container-id state))
                    (page-title-cp page {:journal? journal?
@@ -603,10 +606,10 @@
               (db-page/configure-property page))
 
             (when (and db-based? class-page?)
-              (objects/class-objects page))
+              (objects/class-objects page (:current-page? option)))
 
             (when (and db-based? (ldb/property? page))
-              (objects/property-related-objects page))
+              (objects/property-related-objects page (:current-page? option)))
 
             (when (and block? (not sidebar?) (not whiteboard?))
               (let [config (merge config {:id "block-parent"
@@ -644,7 +647,11 @@
               (when (and (not journal?) (not db-based?))
                 (hierarchy/structures (:block/title page))))
 
-            (when-not (or whiteboard? unlinked-refs? sidebar? home? (and block? (not db-based?)))
+            (when-not (or whiteboard? unlinked-refs?
+                          sidebar?
+                          home?
+                          (or class-page? property-page?)
+                          (and block? (not db-based?)))
               [:div {:key "page-unlinked-references"}
                (reference/unlinked-references page)])])]))))
 
@@ -666,9 +673,13 @@
                      (route-handler/update-page-title-and-label! (state/get-route-match))))))
              (assoc state
                     ::page-name page-name'
-                    ::loading? *loading?)))}
+                    ::loading? *loading?)))
+   :will-unmount (fn [state]
+                   (state/set-state! :editor/virtualized-scroll-fn nil)
+                   state)}
   [state option]
-  (page-inner (assoc option :*loading? (::loading? state))))
+  (when-not (rum/react (::loading? state))
+    (page-inner option)))
 
 (rum/defcs page-cp
   [state option]

+ 32 - 25
src/main/frontend/components/property.cljs

@@ -78,7 +78,7 @@
                           (map (fn [type]
                                  {:label (property-config/property-type-label type)
                                   :value type})))]
-    [:div {:class "flex items-center col-span-1"}
+    [:div {:class "flex items-center"}
      (shui/select
       (cond->
        {:default-open (boolean default-open?)
@@ -266,17 +266,18 @@
                             :icon-value icon
                             :del-btn? (boolean icon)}))]
 
-         (shui/trigger-as
-          :button.property-m
-          (-> (when-not config/publishing?
-                {:on-click (fn [^js e]
-                             (shui/popup-show! (.-target e) content-fn
-                                               {:as-dropdown? true :auto-focus? true
-                                                :content-props {:onEscapeKeyDown #(.preventDefault %)}}))})
-              (assoc :class "flex items-center"))
-          (if icon
-            (icon-component/icon icon {:size 15 :color? true})
-            (property-icon property nil)))))
+         [:div.property-icon
+          (shui/trigger-as
+           :button.property-m
+           (-> (when-not config/publishing?
+                 {:on-click (fn [^js e]
+                              (shui/popup-show! (.-target e) content-fn
+                                                {:as-dropdown? true :auto-focus? true
+                                                 :content-props {:onEscapeKeyDown #(.preventDefault %)}}))})
+               (assoc :class "flex items-center"))
+           (if icon
+             (icon-component/icon icon {:size 15 :color? true})
+             (property-icon property nil)))]))
 
      (if config/publishing?
        [:a.property-k.flex.select-none.jtrigger
@@ -338,13 +339,13 @@
     [:div.ls-property-input.flex.flex-1.flex-row.items-center.flex-wrap.gap-1
      {:ref #(reset! *ref %)}
      (if property-key
-       [:div.ls-property-add.grid.grid-cols-4.gap-1.flex.flex-1.flex-row.items-center
-        [:div.flex.flex-row.items-center.col-span-1.property-key.gap-1
+       [:div.ls-property-add.gap-1.flex.flex-1.flex-row.items-center
+        [:div.flex.flex-row.items-center.property-key.gap-1
          (when-not (:db/id property) (property-icon property (:type @*property-schema)))
          (if (:db/id property)                              ; property exists already
            (property-key-cp block property opts)
            [:div property-key])]
-        [:div.col-span-3.flex.flex-row {:on-pointer-down (fn [e] (util/stop-propagation e))}
+        [:div.flex.flex-row {:on-pointer-down (fn [e] (util/stop-propagation e))}
          (when (not= @*show-new-property-config? :adding-property)
            (cond
              @*show-new-property-config?
@@ -388,9 +389,12 @@
        :on-click (fn [e]
                    (state/pub-event! [:editor/new-property (merge opts {:block block
                                                                         :target (.-target e)})]))}
-      [:div.flex.flex-row.items-center
+      [:div.flex.flex-row.items-center.shrink-0
        (ui/icon "plus" {:size 16})
-       [:div.ml-1 "Add property"]]]]))
+       [:div.ml-1
+        (if (:class-schema? opts)
+          "Add tag property"
+          "Add property")]]]]))
 
 (defn- resolve-linked-block-if-exists
   "Properties will be updated for the linked page instead of the refed block.
@@ -430,27 +434,27 @@
                                                                     :page-cp page-cp))]
         [:div {:key (str "property-pair-" (:db/id block) "-" (:db/id property))
                :class (cond
-                        (and (= (:db/ident property) :logseq.property.class/properties) (seq v))
-                        "property-pair !flex flex-col"
+                        (= (:db/ident property) :logseq.property.class/properties)
+                        "property-pair !flex !flex-col"
                         (or date? datetime? checkbox?)
                         "property-pair items-center"
                         :else
                         "property-pair items-start")}
          (if (seq sortable-opts)
-           (dnd/sortable-item (assoc sortable-opts :class "property-key col-span-1") property-key-cp')
-           [:div.property-key.col-span-1 property-key-cp'])
+           (dnd/sortable-item (assoc sortable-opts :class "property-key") property-key-cp')
+           [:div.property-key property-key-cp'])
 
          (let [class-properties? (= (:db/ident property) :logseq.property.class/properties)
                property-desc (when-not (= (:db/ident property) :logseq.property/description)
                                (:logseq.property/description property))]
-           [:div.property-value-container.col-span-3.flex.flex-row.gap-1.items-center
+           [:div.property-value-container.flex.flex-row.gap-1.items-center
             (cond-> {}
               class-properties? (assoc :class (if (:logseq.property.class/properties block)
                                                 "ml-2 -mt-1"
                                                 "-ml-1")))
             (when-not (or block? class-properties? property-desc)
-              [:div.opacity-30 {:style {:margin-left 5}}
-               [:span.bullet-container.cursor [:span.bullet]]])
+              [:div {:class "pl-1.5 -mr-[3px] opacity-60"}
+               [:span.bullet-container [:span.bullet]]])
             [:div.flex.flex-1
              [:div.property-value.flex.flex-1
               (cond-> {}
@@ -575,7 +579,10 @@
                           ;; other position
                           (when-not (or (and (:sidebar? opts) (= (:id opts) (str (:block/uuid block))))
                                         show-empty-and-hidden-properties?)
-                            (outliner-property/property-with-other-position? ent)))))))
+                            (outliner-property/property-with-other-position? ent))
+
+                          (and (:gallery-view? opts)
+                               (contains? #{:logseq.property.class/properties} (:db/ident ent))))))))
                   properties))
         {:keys [all-classes classes-properties]} (outliner-property/get-block-classes-properties (db/get-db) (:db/id block))
         classes-properties-set (set classes-properties)

+ 25 - 6
src/main/frontend/components/property.css

@@ -44,10 +44,8 @@
 }
 
 .ls-properties-area {
-  @apply grid;
-
   .property-pair {
-    @apply grid grid-cols-4 gap-1;
+    @apply flex flex-row flex-wrap space-x-1;
 
     .jtrigger {
       @apply relative;
@@ -179,7 +177,7 @@
 
 .positioned-properties, .property-value-inner {
   .select-item {
-    @apply flex items-center;
+    @apply flex items-center shrink-0;
   }
 
   .ls-icon-priorityLvlUrgent {
@@ -262,6 +260,8 @@ a.control-link {
 }
 
 .property-value-container {
+  @apply flex-1 shrink-0;
+  min-width: 100px;
   min-height: 29px;
 }
 
@@ -271,6 +271,7 @@ a.control-link {
 
 .property-key {
   /* Same height with one-line block container */
+  min-width: 160px;
   min-height: 29px;
 
   h1.title, h2.title {
@@ -287,8 +288,26 @@ a.control-link {
   }
 }
 
+.ls-card-item {
+    .property-key, .property-value-container {
+        min-width: initial;
+    }
+}
+
 .property-key-inner {
-  @apply flex flex-row items-center gap-1 relative;
+  @apply flex flex-row items-start gap-1 relative;
+
+  .property-icon {
+    @apply flex items-center;
+    height: 28px;
+
+    button {
+      margin-top: -1px;
+    }
+  }
+  .editor-inner {
+    width: 120px;
+  }
 }
 
 .property-select {
@@ -324,7 +343,7 @@ a.control-link {
   }
 
   .inner-wrap {
-    @apply flex items-center w-full justify-between;
+    @apply flex items-center w-full justify-between gap-1 flex-wrap;
 
     > strong {
       @apply flex items-center gap-1 font-normal opacity-90;

+ 13 - 7
src/main/frontend/components/property/value.cljs

@@ -50,9 +50,14 @@
                    text))))
 
 (rum/defc property-empty-text-value
-  [& {:as opts}]
+  [property {:keys [property-position]}]
   [:span.inline-flex.items-center.cursor-pointer
-   (merge {:class "empty-text-btn" :variant :text} opts) "Empty"])
+   (merge {:class "empty-text-btn" :variant :text})
+   (if property-position
+     (if-let [icon (:logseq.property/icon property)]
+       (icon-component/icon icon {:color? true})
+       (ui/icon "line-dashed"))
+     "Empty")])
 
 (rum/defc icon-row
   [block editing?]
@@ -769,7 +774,7 @@
             (inline-text {} :markdown (str value'))))))))
 
 (rum/defc select-item
-  [property type value {:keys [page-cp inline-text other-position? _icon?] :as opts}]
+  [property type value {:keys [page-cp inline-text other-position? property-position _icon?] :as opts}]
   (let [closed-values? (seq (:property/closed-values property))
         tag? (or (:tag? opts) (= (:db/ident property) :block/tags))
         inline-text-cp (fn [content]
@@ -794,6 +799,7 @@
          (rum/with-key
            (page-cp {:disable-preview? true
                      :tag? tag?
+                     :property-position property-position
                      :meta-click? other-position?} value)
            (:db/id value)))
 
@@ -851,13 +857,13 @@
                                          :auto-focus? true
                                          :trigger-id trigger-id}))))]
       (shui/trigger-as
-       (if (:other-position? opts) :div :div.jtrigger.flex.flex-1.w-full)
+       (if (:other-position? opts) :div.jtrigger :div.jtrigger.flex.flex-1.w-full)
        {:ref *el
         :id trigger-id
         :tabIndex 0
         :on-click show!}
        (if (string/blank? value)
-         (property-empty-text-value)
+         (property-empty-text-value property opts)
          (value-f))))))
 
 (defn- property-value-inner
@@ -981,7 +987,7 @@
                 [(property-value-date-picker block property nil {:toggle-fn toggle-fn})]))
              (if date?
                (property-value-date-picker block property nil {:toggle-fn toggle-fn})
-               (property-empty-text-value))))]))))
+               (property-empty-text-value property opts))))]))))
 
 (rum/defc multiple-values < rum/reactive db-mixins/query
   [block property opts schema]
@@ -1056,7 +1062,7 @@
                           (interpose [:span.opacity-50.text-sm " > "]
                                      (concat
                                       (map (fn [{title :block/title :as ancestor}]
-                                             [:a {:on-click #(route-handler/redirect-to-page! (:block/uuid ancestor))} title])
+                                             [:a.whitespace-nowrap {:on-click #(route-handler/redirect-to-page! (:block/uuid ancestor))} title])
                                            page-ancestors)
                                       [value-cp]))]
                          value-cp)))]]

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

@@ -117,7 +117,7 @@
 
 (defn- calculate-collapsed?
   [current-block current-block-uuid {:keys [collapsed?]}]
-  (let [temp-collapsed? (state/sub-collapsed current-block-uuid)
+  (let [temp-collapsed? (state/sub-block-collapsed current-block-uuid)
         collapsed?' (if (some? temp-collapsed?)
                       temp-collapsed?
                       (or collapsed?

+ 78 - 72
src/main/frontend/components/repo.cljs

@@ -47,10 +47,10 @@
   [repos]
   (if-let [m (and (seq repos) (graph/get-metadata-local))]
     (->> repos
-      (map (fn [r] (merge r (get m (:url r)))))
-      (sort (fn [r1 r2]
-              (compare (or (:last-seen-at r2) (:created-at r2))
-                (or (:last-seen-at r1) (:created-at r1))))))
+         (map (fn [r] (merge r (get m (:url r)))))
+         (sort (fn [r1 r2]
+                 (compare (or (:last-seen-at r2) (:created-at r2))
+                          (or (:last-seen-at r1) (:created-at r1))))))
     repos))
 
 (defn- safe-locale-date
@@ -71,17 +71,17 @@
      [:div
       [:span.flex.items-center.gap-1
        (normalized-graph-label repo
-         (fn []
-           (when-not (state/sub :rtc/downloading-graph-uuid)
-             (cond
-               root                                         ; exists locally
-               (state/pub-event! [:graph/switch url])
+                               (fn []
+                                 (when-not (state/sub :rtc/downloading-graph-uuid)
+                                   (cond
+                                     root                                         ; exists locally
+                                     (state/pub-event! [:graph/switch url])
 
-               (and db-based? remote?)
-               (state/pub-event! [:rtc/download-remote-graph GraphName GraphUUID])
+                                     (and db-based? remote?)
+                                     (state/pub-event! [:rtc/download-remote-graph GraphName GraphUUID])
 
-               :else
-               (state/pub-event! [:graph/pull-down-remote-graph repo])))))]
+                                     :else
+                                     (state/pub-event! [:graph/pull-down-remote-graph repo])))))]
       (when-let [time (some-> (or last-seen-at created-at) (safe-locale-date))]
         [:small.text-gray-400.opacity-50 (str "Last opened at: " time)])]
 
@@ -109,11 +109,11 @@
              :on-click (fn []
                          (let [has-prompt? true
                                prompt-str (cond only-cloud?
-                                            (str "Are you sure to permanently delete the graph \"" GraphName "\" from our server?")
-                                            db-based?
-                                            (str "Are you sure to permanently delete the graph \"" url "\" from Logseq?")
-                                            :else
-                                            (str "Are you sure to unlink the graph \"" url "\" from local folder?"))
+                                                (str "Are you sure to permanently delete the graph \"" GraphName "\" from our server?")
+                                                db-based?
+                                                (str "Are you sure to permanently delete the graph \"" url "\" from Logseq?")
+                                                :else
+                                                (str "Are you sure to unlink the graph \"" url "\" from local folder?"))
                                unlink-or-remote-fn! (fn []
                                                       (repo-handler/remove-repo! repo)
                                                       (state/pub-event! [:graph/unlinked repo (state/get-current-repo)]))
@@ -125,19 +125,19 @@
                                                                              file-sync/<delete-graph)]
                                                           (state/set-state! [:file-sync/remote-graphs :loading] true)
                                                           (go (<! (delete-graph GraphUUID))
-                                                            (state/delete-repo! repo)
-                                                            (state/delete-remote-graph! repo)
-                                                            (state/set-state! [:file-sync/remote-graphs :loading] false)))))
+                                                              (state/delete-repo! repo)
+                                                              (state/delete-remote-graph! repo)
+                                                              (state/set-state! [:file-sync/remote-graphs :loading] false)))))
                                                     unlink-or-remote-fn!)
                                confirm-fn!
                                (fn []
                                  (-> (shui/dialog-confirm!
-                                       [:p.font-medium.-my-4 prompt-str
-                                        [:span.mt-1.flex.font-normal.opacity-70
-                                         (if (or db-based? only-cloud?)
-                                           [:small.text-red-rx-11 "⚠️ Notice that we can't recover this graph after being deleted. Make sure you have backups before deleting it."]
-                                           [:small.opacity-70 "⚠️ It won't remove your local files!"])]])
-                                   (p/then #(action-confirm-fn!))))]
+                                      [:p.font-medium.-my-4 prompt-str
+                                       [:span.mt-1.flex.font-normal.opacity-70
+                                        (if (or db-based? only-cloud?)
+                                          [:small.text-red-rx-11 "⚠️ Notice that we can't recover this graph after being deleted. Make sure you have backups before deleting it."]
+                                          [:small.opacity-70 "⚠️ It won't remove your local files!"])]])
+                                     (p/then #(action-confirm-fn!))))]
 
                            (if has-prompt?
                              (confirm-fn!)
@@ -272,18 +272,24 @@
                   (shui/tabler-icon "folder-plus")
                   [:span (t :new-graph)]))
 
-   (shui/button {:size :sm :variant :ghost
-                 :on-click #(state/pub-event! [:graph/new-db-graph])}
-     (shui/tabler-icon "database-plus") [:span (if util/electron? "Create db graph" "Create new graph")])
+   (when-not config/publishing?
+     (shui/button
+      {:size :sm :variant :ghost
+       :on-click #(state/pub-event! [:graph/new-db-graph])}
+      (shui/tabler-icon "database-plus")
+      [:span (if util/electron? "Create db graph" "Create new graph")]))
 
-   (shui/button {:size :sm :variant :ghost
-                 :on-click (fn [] (route-handler/redirect! {:to :import}))}
-                (shui/tabler-icon "database-import")
-                [:span (t :import-notes)])
+   (when-not config/publishing?
+     (shui/button
+      {:size :sm :variant :ghost
+       :on-click (fn [] (route-handler/redirect! {:to :import}))}
+      (shui/tabler-icon "database-import")
+      [:span (t :import-notes)]))
 
-   (shui/button {:size :sm :variant :ghost
-                 :on-click #(route-handler/redirect-to-all-graphs)}
-                (shui/tabler-icon "layout-2") [:span (t :all-graphs)])])
+   (when-not config/publishing?
+     (shui/button {:size :sm :variant :ghost
+                   :on-click #(route-handler/redirect-to-all-graphs)}
+                  (shui/tabler-icon "layout-2") [:span (t :all-graphs)]))])
 
 (rum/defcs repos-dropdown < rum/reactive
   (rum/local false ::electron-multiple-windows?)
@@ -375,40 +381,40 @@
 (defn invalid-graph-name-warning
   []
   (notification/show!
-    [:div
-     [:p "Graph name can't contain following reserved characters:"]
-     [:ul
-      [:li "< (less than)"]
-      [:li "> (greater than)"]
-      [:li ": (colon)"]
-      [:li "\" (double quote)"]
-      [:li "/ (forward slash)"]
-      [:li "\\ (backslash)"]
-      [:li "| (vertical bar or pipe)"]
-      [:li "? (question mark)"]
-      [:li "* (asterisk)"]
-      [:li "# (hash)"]
+   [:div
+    [:p "Graph name can't contain following reserved characters:"]
+    [:ul
+     [:li "< (less than)"]
+     [:li "> (greater than)"]
+     [:li ": (colon)"]
+     [:li "\" (double quote)"]
+     [:li "/ (forward slash)"]
+     [:li "\\ (backslash)"]
+     [:li "| (vertical bar or pipe)"]
+     [:li "? (question mark)"]
+     [:li "* (asterisk)"]
+     [:li "# (hash)"]
       ;; `+` is used to encode path that includes `:` or `/`
-      [:li "+ (plus)"]]]
-    :warning false))
+     [:li "+ (plus)"]]]
+   :warning false))
 
 (defn invalid-graph-name?
   "Returns boolean indicating if DB graph name is invalid. Must be kept in sync with invalid-graph-name-warning"
   [graph-name]
   (or (fs-util/include-reserved-chars? graph-name)
-    (string/includes? graph-name "+")
-    (string/includes? graph-name "/")))
+      (string/includes? graph-name "+")
+      (string/includes? graph-name "/")))
 
 (rum/defcs new-db-graph < rum/reactive
-                          (rum/local "" ::graph-name)
-                          (rum/local false ::cloud?)
-                          (rum/local false ::creating-db?)
-                          (rum/local (rum/create-ref) ::input-ref)
-                          {:did-mount (fn [s]
-                                        (when-let [^js input (some-> @(::input-ref s)
-                                                               (rum/deref))]
-                                          (js/setTimeout #(.focus input) 32))
-                                        s)}
+  (rum/local "" ::graph-name)
+  (rum/local false ::cloud?)
+  (rum/local false ::creating-db?)
+  (rum/local (rum/create-ref) ::input-ref)
+  {:did-mount (fn [s]
+                (when-let [^js input (some-> @(::input-ref s)
+                                             (rum/deref))]
+                  (js/setTimeout #(.focus input) 32))
+                s)}
   [state]
   (let [*creating-db? (::creating-db? state)
         *graph-name (::graph-name state)
@@ -440,22 +446,22 @@
                            (shui/dialog-close!))))))
         submit! (fn [^js e click?]
                   (when-let [value (and (or click? (= (gobj/get e "key") "Enter"))
-                                     (util/trim-safe (.-value (rum/deref input-ref))))]
+                                        (util/trim-safe (.-value (rum/deref input-ref))))]
                     (reset! *graph-name value)
                     (new-db-f)))]
     [:div.new-graph.flex.flex-col.gap-4.p-1.pt-2
      (shui/input
-       {:default-value @*graph-name
-        :disabled @*creating-db?
-        :ref input-ref
-        :placeholder "your graph name"
-        :on-key-down submit!})
+      {:default-value @*graph-name
+       :disabled @*creating-db?
+       :ref input-ref
+       :placeholder "your graph name"
+       :on-key-down submit!})
      (when (user-handler/logged-in?)
        [:div.flex.flex-row.items-center.gap-1
         (shui/checkbox
-          {:id "rtc-sync"
-           :value @*cloud?
-           :on-checked-change #(swap! *cloud? not)})
+         {:id "rtc-sync"
+          :value @*cloud?
+          :on-checked-change #(swap! *cloud? not)})
         [:label.opacity-70.text-sm
          {:for "rtc-sync"}
          "Use Logseq Sync?"]])

+ 7 - 8
src/main/frontend/components/right_sidebar.cljs

@@ -37,9 +37,9 @@
   [repo idx block]
   (let [id (:block/uuid block)]
     (page/page-cp {:parameters  {:path {:name (str id)}}
-                :sidebar?    true
-                :sidebar/idx idx
-                :repo        repo})))
+                   :sidebar?    true
+                   :sidebar/idx idx
+                   :repo        repo})))
 
 (rum/defc page-cp < rum/reactive
   [repo page-name]
@@ -56,7 +56,6 @@
   [repo block idx sidebar-key ref?]
   (when-let [block-id (:block/uuid block)]
     [[:.flex.items-center {:class (when ref? "ml-2")}
-      (ui/icon "letter-n" {:class "text-md mr-2"})
       (block/breadcrumb {:id     "block-parent"
                          :block? true
                          :sidebar-key sidebar-key} repo block-id {:indent? false})]
@@ -349,11 +348,11 @@
      [])
 
     (rum/use-effect!
-      (fn []
+     (fn []
         ;; sidebar animation duration
-        (js/setTimeout
-          #(reset! ui-handler/*right-sidebar-resized-at (js/Date.now)) 300))
-      [sidebar-open?])
+       (js/setTimeout
+        #(reset! ui-handler/*right-sidebar-resized-at (js/Date.now)) 300))
+     [sidebar-open?])
 
     [:.resizer
      {:ref              el-ref

+ 87 - 46
src/main/frontend/components/views.cljs

@@ -13,6 +13,7 @@
             [frontend.date :as date]
             [frontend.db :as db]
             [frontend.handler.property :as property-handler]
+            [frontend.handler.ui :as ui-handler]
             [frontend.state :as state]
             [frontend.ui :as ui]
             [frontend.util :as util]
@@ -24,13 +25,18 @@
             [rum.core :as rum]
             [frontend.mixins :as mixins]
             [logseq.shui.table.core :as table-core]
-            [logseq.db :as ldb]))
+            [logseq.db :as ldb]
+            [frontend.config :as config]
+            [frontend.db-mixins :as db-mixins]))
 
 (defn- get-latest-entity
   [e]
-  (assoc (db/entity (:db/id e))
-         :id (:id e)
-         :block.temp/refs-count (:block.temp/refs-count e)))
+  (let [transacted-ids (:updated-ids @(:db/latest-transacted-entity-uuids @state/state))]
+    (if (and transacted-ids (contains? transacted-ids (:block/uuid e)))
+      (assoc (db/entity (:db/id e))
+             :id (:id e)
+             :block.temp/refs-count (:block.temp/refs-count e))
+      e)))
 
 (rum/defc header-checkbox < rum/static
   [{:keys [selected-all? selected-some? toggle-selected-all!]}]
@@ -111,19 +117,30 @@
       (reduce + (filter number? col))
       (string/join ", " col))))
 
-(rum/defc block-container < rum/static
-  [config row]
-  (let [container (state/get-component :block/container)]
-    [:div.relative.w-full
-     (container config row)]))
+(rum/defcs block-container < rum/reactive db-mixins/query
+  (rum/local false ::deleted?)
+  [state config row table]
+  (let [*deleted? (::deleted? state)
+        container (state/get-component :block/container)
+        row' (db/sub-block (:db/id row))]
+    (if (nil? row')                    ; this row has been deleted
+      (when-not @*deleted?
+        (when-let [f (get-in table [:data-fns :set-data!])]
+          (f (remove (fn [r] (= (:id r) (:id row))) (:data table)))
+          (reset! *deleted? true)
+          nil))
+      [:div.relative.w-full
+       (container config row')])))
 
 (defn build-columns
   [config properties & {:keys [with-object-name? add-tags-column?]
                         :or {with-object-name? true
                              add-tags-column? true}}]
-  (let [properties (if (or (some #(= (:db/ident %) :block/tags) properties) (not add-tags-column?))
-                     properties
-                     (conj properties (db/entity :block/tags)))]
+  (let [;; FIXME: Shouldn't file graphs have :block/tags?
+        add-tags-column?' (and (config/db-based-graph? (state/get-current-repo)) add-tags-column?)
+        properties' (if (or (some #(= (:db/ident %) :block/tags) properties) (not add-tags-column?'))
+                      properties
+                      (conj properties (db/entity :block/tags)))]
     (->> (concat
           [{:id :select
             :name "Select"
@@ -137,10 +154,12 @@
               :name "Name"
               :type :string
               :header header-cp
-              :cell (fn [_table row _column]
+              :cell (fn [table row _column]
                       (block-container (assoc config
                                               :raw-title? (ldb/asset? row)
-                                              :table? true) row))
+                                              :table? true)
+                                       row
+                                       table))
               :disable-hide? true})]
           (keep
            (fn [column]
@@ -157,24 +176,23 @@
                                   (or (db/entity ident) column))
                        get-value (if-let [f (:get-value property)]
                                    (fn [row]
-                                     (f (get-latest-entity row)))
+                                     (f row))
                                    (when (de/entity? property)
-                                     (fn [row] (get-property-value-for-search (get-latest-entity row) 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]
-                                            (let [row (get-latest-entity 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)))))]
+                                            (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 column)
                               (:block/title property))
@@ -187,7 +205,7 @@
                     :get-value get-value
                     :get-value-for-sort get-value-for-sort
                     :type (:type property)}))))
-           properties)
+           properties')
 
           [{:id :block/created-at
             :name (t :page/created-at)
@@ -1033,7 +1051,7 @@
        (property-handler/set-block-property! repo (:db/id entity) :logseq.property.table/sized-columns sized-columns))}))
 
 (rum/defc table-view < rum/static
-  [table option row-selection add-new-object!]
+  [table option row-selection add-new-object! *scroller-ref]
   (let [selected-rows (shui/table-get-selection-rows row-selection (:rows table))]
     (shui/table
      (let [columns' (:columns table)
@@ -1043,7 +1061,8 @@
          (table-header table columns' option selected-rows)
 
          (ui/virtualized-list
-          {:custom-scroll-parent (gdom/getElement "main-content-container")
+          {:ref #(reset! *scroller-ref %)
+           :custom-scroll-parent (gdom/getElement "main-content-container")
            :increase-viewport-by 128
            :overscan 128
            :compute-item-key (fn [idx]
@@ -1073,18 +1092,32 @@
                        :group-by-page? group-by-page?
                        :ref? true)))))
 
+(rum/defc gallery-card-item
+  [table view-entity block config]
+  [:div.ls-card-item.content
+   {:key (str "view-card-" (:db/id view-entity) "-" (:db/id block))}
+   [:div.-ml-4
+    (block-container (assoc config
+                            :id (str (:block/uuid block))
+                            :gallery-view? true)
+                     block
+                     table)]])
+
 (rum/defcs gallery-view < rum/static mixins/container-id
-  [state config view-entity result]
+  [state config table view-entity blocks *scroller-ref]
   (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)]])]))
+     (when (seq blocks)
+       (ui/virtualized-grid
+        {:ref #(reset! *scroller-ref %)
+         :total-count (count blocks)
+         :custom-scroll-parent (gdom/getElement "main-content-container")
+         :item-content (fn [idx]
+                         (when-let [block (nth blocks idx)]
+                           (gallery-card-item table view-entity block config')))}))]))
 
 (defn- run-effects!
-  [{:keys [data columns state data-fns]} input input-filters set-input-filters!]
+  [option {:keys [data columns state data-fns]} input input-filters set-input-filters! *scroller-ref gallery?]
   (let [{:keys [filters sorting]} state
         {:keys [set-row-filter! set-data!]} data-fns]
     (rum/use-effect!
@@ -1101,14 +1134,20 @@
     (rum/use-effect!
      (fn []
        ;; Entities might be outdated
-       (let [new-data (map get-latest-entity data)
+       (let [;; TODO: should avoid this for better performance, 300ms for 40k pages
+             new-data (map get-latest-entity data)
+             ;; TODO: db support native order-by, limit, offset, 350ms for 40k pages
              data' (table-core/table-sort-rows new-data sorting columns)]
-         (set-data! data')))
+         (set-data! data')
+         (when (and (:current-page? (:config option)) (seq data) (map? (first data)) (:block/uuid (first data)))
+           (ui-handler/scroll-to-anchor-block @*scroller-ref data' gallery?)
+           (state/set-state! :editor/virtualized-scroll-fn #(ui-handler/scroll-to-anchor-block @*scroller-ref data' gallery?)))))
      [sorting])))
 
 (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}}]
+                :or {render-empty-title? false}}
+   *scroller-ref]
   (let [[input set-input!] (rum/use-state "")
         sorting (:logseq.property.table/sorting view-entity)
         [sorting set-sorting!] (rum/use-state (or sorting [{:id :block/updated-at, :asc? false}]))
@@ -1154,9 +1193,10 @@
         table (shui/table-option table-map)
         *view-ref (rum/use-ref nil)
         display-type (or (:db/ident (get view-entity :logseq.property.view/type))
-                         :logseq.property.view/type.table)]
+                         :logseq.property.view/type.table)
+        gallery? (= display-type :logseq.property.view/type.gallery)]
 
-    (run-effects! table-map input input-filters set-input-filters!)
+    (run-effects! option table-map input input-filters set-input-filters! *scroller-ref gallery?)
 
     [:div.flex.flex-col.gap-2.grid
      {:ref *view-ref}
@@ -1190,11 +1230,11 @@
        (list-view (:config option) view-entity (:rows table))
 
        :logseq.property.view/type.gallery
-       (gallery-view (:config option) view-entity (:rows table))
+       (gallery-view (:config option) table view-entity (:rows table) *scroller-ref)
 
-       (table-view table option row-selection add-new-object!))]))
+       (table-view table option row-selection add-new-object! *scroller-ref))]))
 
-(rum/defc view
+(rum/defcs view
   "Provides a view for data like query results and tagged objects, multiple
    layouts such as table and list are supported. Args:
    * view-entity: a db Entity
@@ -1208,7 +1248,8 @@
      * add-property!: `fn` to add a new property (or column)
      * on-delete-rows: `fn` to trigger when deleting selected objects"
   < rum/reactive
-  [view-entity option]
+  (rum/local nil ::scroller-ref)
+  [state view-entity option]
   (let [view-entity' (db/sub-block (:db/id view-entity))]
-    (rum/with-key (view-inner view-entity' option)
+    (rum/with-key (view-inner view-entity' option (::scroller-ref state))
       (str "view-" (:db/id view-entity')))))

+ 4 - 3
src/main/frontend/db/async.cljs

@@ -119,10 +119,11 @@
 (defn <get-block
   [graph name-or-uuid & {:keys [children? nested-children?]
                          :or {children? true
-                              nested-children? false}}]
+                              nested-children? false}
+                         :as opts}]
   (let [name' (str name-or-uuid)
         *async-queries (:db/async-queries @state/state)
-        async-requested? (get @*async-queries name')
+        async-requested? (get @*async-queries [name' opts])
         e (cond
             (number? name-or-uuid)
             (db/entity name-or-uuid)
@@ -136,7 +137,7 @@
     (if (or (:block.temp/fully-loaded? e) async-requested?)
       e
       (when-let [^Object sqlite @db-browser/*worker]
-        (swap! *async-queries assoc name' true)
+        (swap! *async-queries assoc [name' opts] true)
         (state/update-state! :db/async-query-loading (fn [s] (conj s name')))
         (p/let [result (.get-block-and-children sqlite graph id (ldb/write-transit-str
                                                                  {:children? children?

+ 33 - 26
src/main/frontend/db/async/util.cljs

@@ -8,28 +8,35 @@
 
 (defn <q
   [graph {:keys [transact-db?]
-          :or {transact-db? true}} & inputs]
+          :or {transact-db? true}
+          :as opts} & inputs]
   (assert (not-any? fn? inputs) "Async query inputs can't include fns because fn can't be serialized")
   (when-let [^Object sqlite @state/*db-worker]
-    (p/let [result (.q sqlite graph (ldb/write-transit-str inputs))]
-      (when result
-        (let [result' (ldb/read-transit-str result)]
-          (when (and transact-db? (seq result') (coll? result'))
-            (when-let [conn (db-conn/get-db graph false)]
-              (let [tx-data (->>
-                             (if (and (coll? (first result'))
-                                      (not (map? (first result'))))
-                               (apply concat result')
-                               result')
-                             (remove nil?))]
-                (if (every? map? tx-data)
-                  (try
-                    (d/transact! conn tx-data)
-                    (catch :default e
-                      (js/console.error "<q failed with:" e)
-                      nil))
-                  (js/console.log "<q skipped tx for inputs:" inputs)))))
-          result')))))
+    (let [*async-queries (:db/async-queries @state/state)
+          async-requested? (get @*async-queries [inputs opts])]
+      (if async-requested?
+        (let [db (db-conn/get-db graph)]
+          (apply d/q (first inputs) db (rest inputs)))
+        (p/let [result (.q sqlite graph (ldb/write-transit-str inputs))]
+          (swap! *async-queries assoc [inputs opts] true)
+          (when result
+            (let [result' (ldb/read-transit-str result)]
+              (when (and transact-db? (seq result') (coll? result'))
+                (when-let [conn (db-conn/get-db graph false)]
+                  (let [tx-data (->>
+                                 (if (and (coll? (first result'))
+                                          (not (map? (first result'))))
+                                   (apply concat result')
+                                   result')
+                                 (remove nil?))]
+                    (if (every? map? tx-data)
+                      (try
+                        (d/transact! conn tx-data)
+                        (catch :default e
+                          (js/console.error "<q failed with:" e)
+                          nil))
+                      (js/console.log "<q skipped tx for inputs:" inputs)))))
+              result')))))))
 
 (defn <pull
   ([graph id]
@@ -45,9 +52,9 @@
 
 (comment
   (defn <pull-many
-   [graph selector ids]
-   (assert (seq ids))
-   (when-let [^Object sqlite @state/*db-worker]
-     (p/let [result (.pull-many sqlite graph (ldb/write-transit-str selector) (ldb/write-transit-str ids))]
-       (when result
-         (ldb/read-transit-str result))))))
+    [graph selector ids]
+    (assert (seq ids))
+    (when-let [^Object sqlite @state/*db-worker]
+      (p/let [result (.pull-many sqlite graph (ldb/write-transit-str selector) (ldb/write-transit-str ids))]
+        (when result
+          (ldb/read-transit-str result))))))

+ 75 - 1
src/main/frontend/db/rtc/debug_ui.cljs

@@ -26,6 +26,7 @@
 (rum/defcs ^:large-vars/cleanup-todo rtc-debug-ui < rum/reactive
   (rum/local nil ::logs)
   (rum/local nil ::sub-log-canceler)
+  (rum/local nil ::keys-state)
   {:will-mount (fn [state]
                  (let [canceler
                        (c.m/run-task
@@ -245,4 +246,77 @@
        (shui/select-content
         (shui/select-group
          (for [{:keys [graph-uuid graph-status]} (:remote-graphs debug-state*)]
-           (shui/select-item {:value graph-uuid :disabled (some? graph-status)} graph-uuid)))))]]))
+           (shui/select-item {:value graph-uuid :disabled (some? graph-status)} graph-uuid)))))]
+
+     [:hr.my-2]
+
+     (let [*keys-state (get state ::keys-state)
+           keys-state @*keys-state]
+       [:div
+        [:div.pb-2.flex.flex-row.items-center.gap-2
+         (shui/button
+          {:size :sm
+           :on-click (fn [_]
+                       (let [^object worker @db-browser/*worker]
+                         (p/let [result1 (.rtc-get-graph-keys worker (state/get-current-repo))
+                                 graph-keys (ldb/read-transit-str result1)
+                                 result2 (some->> (state/get-auth-id-token) (.device-list-devices worker))
+                                 devices (ldb/read-transit-str result2)]
+                           (swap! (get state ::keys-state) #(merge % graph-keys {:devices devices})))))}
+          (shui/tabler-icon "refresh") "keys-state")]
+        [:div.pb-4
+         [:pre.select-text
+          (-> {:devices (:devices keys-state)
+               :graph-aes-key-jwk (:aes-key-jwk keys-state)}
+              (fipp/pprint {:width 20})
+              with-out-str)]]
+        (shui/button
+         {:size :sm
+          :on-click (fn [_]
+                      (let [^object worker @db-browser/*worker]
+                        (when-let [device-uuid (not-empty (:remove-device-device-uuid keys-state))]
+                          (when-let [token (state/get-auth-id-token)]
+                            (.device-remove-device worker token device-uuid)))))}
+         "Remove device:")
+        [:input.form-input.my-2.py-1.w-32
+         {:on-change (fn [e] (swap! *keys-state assoc :remove-device-device-uuid (util/evalue e)))
+          :on-focus (fn [e] (let [v (.-value (.-target e))]
+                              (when (= v "device-uuid here")
+                                (set! (.-value (.-target e)) ""))))
+          :placeholder "device-uuid here"}]
+        (shui/button
+         {:size :sm
+          :on-click (fn [_]
+                      (let [^object worker @db-browser/*worker]
+                        (when-let [device-uuid (not-empty (:remove-public-key-device-uuid keys-state))]
+                          (when-let [key-name (not-empty (:remove-public-key-key-name keys-state))]
+                            (when-let [token (state/get-auth-id-token)]
+                              (.device-remove-device-public-key worker token device-uuid key-name))))))}
+         "Remove public-key:")
+        [:input.form-input.my-2.py-1.w-32
+         {:on-change (fn [e] (swap! *keys-state assoc :remove-public-key-device-uuid (util/evalue e)))
+          :on-focus (fn [e] (let [v (.-value (.-target e))]
+                              (when (= v "device-uuid here")
+                                (set! (.-value (.-target e)) ""))))
+          :placeholder "device-uuid here"}]
+        [:input.form-input.my-2.py-1.w-32
+         {:on-change (fn [e] (swap! *keys-state assoc :remove-public-key-key-name (util/evalue e)))
+          :on-focus (fn [e] (let [v (.-value (.-target e))]
+                              (when (= v "key-name here")
+                                (set! (.-value (.-target e)) ""))))
+          :placeholder "key-name here"}]
+        (shui/button
+         {:size :sm
+          :on-click (fn [_]
+                      (let [^object worker @db-browser/*worker]
+                        (when-let [token (state/get-auth-id-token)]
+                          (when-let [device-uuid (not-empty (:sync-private-key-device-uuid keys-state))]
+                            (.rtc-sync-current-graph-encrypted-aes-key
+                             worker token (ldb/write-transit-str [(parse-uuid device-uuid)]))))))}
+         "Sync CurrentGraph EncryptedAesKey")
+        [:input.form-input.my-2.py-1.w-32
+         {:on-change (fn [e] (swap! *keys-state assoc :sync-private-key-device-uuid (util/evalue e)))
+          :on-focus (fn [e] (let [v (.-value (.-target e))]
+                              (when (= v "device-uuid here")
+                                (set! (.-value (.-target e)) ""))))
+          :placeholder "device-uuid here"}]])]))

+ 6 - 2
src/main/frontend/dicts.cljc

@@ -47,6 +47,7 @@
    :zh-CN   (edn-resource "dicts/zh-cn.edn")
    :zh-Hant (edn-resource "dicts/zh-hant.edn")
    :af      (edn-resource "dicts/af.edn")
+   :ca      (edn-resource "dicts/ca.edn")
    :es      (edn-resource "dicts/es.edn")
    :nb-NO   (edn-resource "dicts/nb-no.edn")
    :pt-BR   (edn-resource "dicts/pt-br.edn")
@@ -60,7 +61,8 @@
    :sk      (edn-resource "dicts/sk.edn")
    :uk      (edn-resource "dicts/uk.edn")
    :fa      (edn-resource "dicts/fa.edn")
-   :id      (edn-resource "dicts/id.edn")})
+   :id      (edn-resource "dicts/id.edn")
+   :cs      (edn-resource "dicts/cs.edn")})
 
 (def languages
   "List of languages presented to user"
@@ -71,6 +73,7 @@
    {:label "简体中文" :value :zh-CN}
    {:label "繁體中文" :value :zh-Hant}
    {:label "Afrikaans" :value :af}
+   {:label "Català" :value :ca}
    {:label "Español" :value :es}
    {:label "Norsk (bokmål)" :value :nb-NO}
    {:label "Polski" :value :pl}
@@ -84,7 +87,8 @@
    {:label "한국어" :value :ko}
    {:label "Slovenčina" :value :sk}
    {:label "فارسی" :value :fa}
-   {:label "Bahasa Indonesia" :value :id}])
+   {:label "Bahasa Indonesia" :value :id}
+   {:label "Čeština" :value :cs}])
 
 (assert (= (set (keys dicts)) (set (map :value languages)))
         "List of user-facing languages must match list of dictionaries")

+ 9 - 6
src/main/frontend/extensions/pdf/assets.cljs

@@ -66,8 +66,11 @@
 (defn resolve-area-image-file
   [img-stamp current {:keys [page id] :as _hl}]
   (when-let [key (:key current)]
-    (-> (str common-config/local-assets-dir "/" key "/")
-        (str (util/format "%s_%s_%s.png" page id img-stamp)))))
+    (-> common-config/local-assets-dir
+        (str (if (config/db-based-graph?)
+               (let [image-id (some-> id (db-utils/entity) :logseq.property.pdf/hl-image :block/uuid)]
+                 (util/format "/%s.png" image-id))
+               (util/format "/%s/%s_%s_%s.png" key page id img-stamp))))))
 
 (defn file-based-ensure-ref-page!
   [pdf-current]
@@ -130,7 +133,7 @@
                 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)}
+                        (pu/get-pid :logseq.property.pdf/hl-color) (:color properties)}
 
                         db-base?
                         (assoc (pu/get-pid :logseq.property.pdf/hl-value) hl)
@@ -158,12 +161,12 @@
               properties (cond->
                           {:block/tags :logseq.class/Pdf-annotation
                            :logseq.property/ls-type  :annotation
-                           :logseq.property/hl-color (:color properties)
+                           :logseq.property.pdf/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
+                           (assoc :logseq.property.pdf/hl-type :area
                                   :logseq.property.pdf/hl-image (:image content)))]
           (when (string? text)
             (editor-handler/api-insert-new-block!
@@ -282,7 +285,7 @@
   [highlight]
   (when-let [block (db-model/get-block-by-uuid (:id highlight))]
     (when-let [color (get-in highlight [:properties :color])]
-      (let [k (pu/get-pid :logseq.property/hl-color)]
+      (let [k (pu/get-pid :logseq.property.pdf/hl-color)]
         (property-handler/set-block-property! (state/get-current-repo) (:block/uuid block) k color)))))
 
 (defn unlink-hl-area-image$

+ 1 - 1
src/main/frontend/extensions/pdf/core.cljs

@@ -821,7 +821,7 @@
     (let [^js viewer        (:viewer state)
           in-system-window? (some-> viewer (.-$inSystemWindow))]
       [:div.extensions__pdf-viewer-cnt.visible-scrollbar
-       [:div.extensions__pdf-viewer.overflow-x-auto
+       [:div.extensions__pdf-viewer.overflow-x-auto.absolute
         {:ref *el-ref :class (util/classnames [{:is-area-dashed area-dashed?}])}
         [:div.pdfViewer "viewer pdf"]
         [:div.pp-holder]

+ 1 - 1
src/main/frontend/extensions/pdf/utils.cljs

@@ -124,7 +124,7 @@
 (defn load-base-assets$
   []
   (p/let [_ (util/js-load$ (str util/JS_ROOT "/pdfjs/pdf.js"))
-          _ (util/js-load$ (str util/JS_ROOT "/pdfjs/pdf_viewer.js"))]))
+          _ (util/js-load$ (str util/JS_ROOT "/pdf_viewer2.js"))]))
 
 (defn get-page-from-el
   [^js/HTMLElement el]

+ 9 - 6
src/main/frontend/extensions/pdf/windows.cljs

@@ -9,11 +9,14 @@
 
 (defn resolve-styles!
   [^js doc]
-  (doseq [r ["./css/style.css"]]
-    (let [^js link (js/document.createElement "link")]
-      (set! (.-rel link) "stylesheet")
-      (set! (.-href link) r)
-      (.appendChild (.-head doc) link))))
+  (when-let [styles (keep #(when (some-> % (.-href) (.endsWith "style.css"))
+                             (.-href %))
+                          (seq js/document.styleSheets))]
+    (doseq [r styles]
+      (let [^js link (js/document.createElement "link")]
+        (set! (.-rel link) "stylesheet")
+        (set! (.-href link) r)
+        (.appendChild (.-head doc) link)))))
 
 (defn resolve-own-document
   [^js viewer]
@@ -107,7 +110,7 @@
                 (state/set-state! :pdf/system-win? true)
                 ;; NOTE: must do ipc in new window
                 (some-> (.-apis win)
-                  (.doAction (bean/->js [:window/open-blank-callback :pdf]))))))]
+                        (.doAction (bean/->js [:window/open-blank-callback :pdf]))))))]
 
       (js/setTimeout
        (fn []

+ 11 - 10
src/main/frontend/handler/editor.cljs

@@ -1501,7 +1501,7 @@
           ;; 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))]
+           (when-let [[asset-file-name file-obj asset-file-fpath matched-alias] (first res)]
              (let [image? (config/ext-of-image? asset-file-name)]
                (insert-command!
                 id
@@ -1516,7 +1516,8 @@
                 format
                 {:last-pattern (if drop-or-paste? "" commands/command-trigger)
                  :restore?     true
-                 :command      :insert-asset})))))
+                 :command      :insert-asset})
+               (recur (rest res))))))
         (p/catch (fn [e]
                    (js/console.error e)))
         (p/finally
@@ -1560,6 +1561,13 @@
                                :edit-block? false
                                :properties properties}
                   edit-block (state/get-edit-block)
+                  _ (when (util/electron?)
+                      (if-let [from (not-empty (.-path file))]
+                        (-> (js/window.apis.copyFileToAssets dir file-rpath from)
+                            (p/catch #(js/console.error "Debug: Copy Asset Error#" %)))
+                        (-> (p/let [buffer (.arrayBuffer file)]
+                              (fs/write-file! repo dir file-rpath buffer {:skip-compare? false}))
+                            (p/catch #(js/console.error "Debug: Writing Asset #" %)))))
                   insert-opts' (if (and (:block/uuid edit-block)
                                         (string/blank? (:block/title edit-block)))
                                  (assoc insert-opts
@@ -1570,14 +1578,7 @@
                   result (api-insert-new-block! file-name-without-ext insert-opts')
                   new-entity (db/entity [:block/uuid (:block/uuid result)])]
             (if (util/electron?)
-              (if-let [from (not-empty (.-path file))]
-                (-> (js/window.apis.copyFileToAssets dir file-rpath from)
-                    (p/then (fn [_dest] new-entity))
-                    (p/catch #(js/console.error "Debug: Copy Asset Error#" %)))
-                (-> (p/let [buffer (.arrayBuffer file)]
-                      (fs/write-file! repo dir file-rpath buffer {:skip-compare? false}))
-                    (p/then (fn [_] new-entity))
-                    (p/catch #(js/console.error "Debug: Writing Asset #" %))))
+              new-entity
               (->
                (p/do! (js/console.debug "Debug: Writing Asset #" dir file-rpath)
                       (cond

+ 4 - 0
src/main/frontend/handler/events.cljs

@@ -1050,6 +1050,10 @@
 
     nil))
 
+(defmethod handle :db/export-sqlite [_]
+  (export/export-repo-as-sqlite-db! (state/get-current-repo))
+  nil)
+
 (defmethod handle :editor/run-query-command [_]
   (editor-handler/run-query-command!))
 

+ 2 - 1
src/main/frontend/handler/export.cljs

@@ -34,7 +34,8 @@
   (when-let [db (db/get-db repo)]
     (let [{:keys [asset-filenames html]}
           (publish-html/build-html db
-                                   {:app-state (select-keys @state/state
+                                   {:repo repo
+                                    :app-state (select-keys @state/state
                                                             [:ui/theme
                                                              :ui/sidebar-collapsed-blocks])
                                     :repo-config (get-in @state/state [:config repo])

+ 19 - 18
src/main/frontend/handler/import.cljs

@@ -26,7 +26,8 @@
             [medley.core :as medley]
             [frontend.persist-db :as persist-db]
             [promesa.core :as p]
-            [frontend.db.async :as db-async]))
+            [frontend.db.async :as db-async]
+            [logseq.db.sqlite.util :as sqlite-util]))
 
 (defn index-files!
   "Create file structure, then parse into DB (client only)"
@@ -52,7 +53,7 @@
                                          ".md")]
                            {:file/path path
                             :file/content text}))))
-                files)
+                   files)
         files (remove nil? files)]
     (file-repo-handler/parse-files-and-load-to-db! repo files nil)
     (let [files (->> (map (fn [{:file/keys [path content]}] (when path [path content])) files)
@@ -63,15 +64,15 @@
                                             :finish-handler finish-handler}))
     (let [journal-pages-tx (let [titles (filter date/normalize-journal-title titles)]
                              (map
-                               (fn [title]
-                                 (let [day (date/journal-title->int title)
-                                       journal-title (date-time-util/int->journal-title day (state/get-date-formatter))]
-                                   (when journal-title
-                                     (let [page-name (util/page-name-sanity-lc journal-title)]
-                                       {:block/name page-name
-                                        :block/type "journal"
-                                        :block/journal-day day}))))
-                               titles))]
+                              (fn [title]
+                                (let [day (date/journal-title->int title)
+                                      journal-title (date-time-util/int->journal-title day (state/get-date-formatter))]
+                                  (when journal-title
+                                    (let [page-name (util/page-name-sanity-lc journal-title)]
+                                      {:block/name page-name
+                                       :block/type "journal"
+                                       :block/journal-day day}))))
+                              titles))]
       (when (seq journal-pages-tx)
         (db/transact! repo journal-pages-tx)))))
 
@@ -84,7 +85,6 @@
                     (fn []
                       (finished-ok-handler))))))
 
-
 ;;; import OPML files
 (defn import-from-opml!
   [data finished-ok-handler]
@@ -183,7 +183,7 @@
   (let [imported-chan (async/promise-chan)]
     (try
       (let [blocks (->> (:blocks data)
-                        (mapv tree-translator-fn )
+                        (mapv tree-translator-fn)
                         (sort-by :title)
                         (medley/indexed))
             job-chan (async/to-chan! blocks)]
@@ -198,8 +198,8 @@
               (create-page-with-exported-tree! block)
               (recur))
             (let [result (async/<! (p->c (db-async/<get-all-referenced-blocks-uuid (state/get-current-repo))))]
-                (editor/set-blocks-id! result)
-                (async/offer! imported-chan true)))))
+              (editor/set-blocks-id! result)
+              (async/offer! imported-chan true)))))
 
       (catch :default e
         (notification/show! (str "Error happens when importing:\n" e) :error)
@@ -236,6 +236,7 @@
       (repo-handler/restore-and-setup-repo! graph)
       (state/set-current-repo! graph)
       (persist-db/<export-db graph {})
+      (db/transact! graph (sqlite-util/import-tx :sqlite-db))
       (finished-ok-handler))
      (p/catch
       (fn [e]
@@ -248,9 +249,9 @@
   [raw finished-ok-handler]
   (try
     (let [data (edn/read-string raw)]
-     (async/go
-       (async/<! (import-from-tree! data tree-vec-translate-edn))
-       (finished-ok-handler nil)))
+      (async/go
+        (async/<! (import-from-tree! data tree-vec-translate-edn))
+        (finished-ok-handler nil)))
     (catch :default e
       (js/console.error e)
       (notification/show!

+ 8 - 9
src/main/frontend/handler/page.cljs

@@ -230,19 +230,18 @@
 
 (defn get-all-pages
   [repo]
-  (let [graph-specific-hidden?
-        (if (config/db-based-graph? repo)
+  (let [db-based? (config/db-based-graph? repo)
+        graph-specific-hidden?
+        (if db-based?
           (fn [p]
             (and (ldb/property? p) (ldb/built-in? p)))
           (fn [p]
             (gp-db/built-in-pages-names (string/upper-case (:block/name p)))))]
-    (->> (db/get-all-pages repo)
-         (remove (fn [p]
-                   (let [name (:block/name p)]
-                     (or (util/uuid-string? name)
-                         (common-config/draw? name)
-                         (graph-specific-hidden? p)))))
-         (common-handler/fix-pages-timestamps))))
+    (cond->>
+     (->> (db/get-all-pages repo)
+          (remove graph-specific-hidden?))
+      (not db-based?)
+      (common-handler/fix-pages-timestamps))))
 
 (defn get-filters
   [page]

+ 7 - 6
src/main/frontend/handler/paste.cljs

@@ -104,8 +104,8 @@
                                 (.getType ^js (first clipboard-items)
                                           "web application/logseq"))))
           blocks-str (when blocks-blob (.text blocks-blob))]
-         (when blocks-str
-           (common-util/safe-read-map-string blocks-str))))
+    (when blocks-str
+      (common-util/safe-read-map-string blocks-str))))
 
 (defn- markdown-blocks?
   [text]
@@ -232,10 +232,11 @@
   (when id
     (let [clipboard-data (gobj/get e "clipboardData")
           files (.-files clipboard-data)]
-      (when-let [file (first files)]
-        (when-let [block (state/get-edit-block)]
-          (editor-handler/upload-asset! id #js[file] (:block/format block)
-                                       editor-handler/*asset-uploading? true)))
+      (loop [files files]
+        (when-let [file (first files)]
+          (when-let [block (state/get-edit-block)]
+            (editor-handler/upload-asset! id #js[file] (:block/format block) editor-handler/*asset-uploading? true))
+          (recur (rest files))))
       (util/stop e))))
 
 (defn editor-on-paste!

+ 19 - 17
src/main/frontend/handler/repo.cljs

@@ -48,12 +48,12 @@
            (do
              (state/set-current-repo! nil)
              (when-let [graph (:url (first (state/get-repos)))]
-              (notification/show! (str "Removed graph "
-                                       (pr-str (text-util/get-graph-name-from-path url))
-                                       ". Redirecting to graph "
-                                       (pr-str (text-util/get-graph-name-from-path graph)))
-                                  :success)
-              (state/pub-event! [:graph/switch graph {:persist? false}])))
+               (notification/show! (str "Removed graph "
+                                        (pr-str (text-util/get-graph-name-from-path url))
+                                        ". Redirecting to graph "
+                                        (pr-str (text-util/get-graph-name-from-path graph)))
+                                   :success)
+               (state/pub-event! [:graph/switch graph {:persist? false}])))
            (notification/show! (str "Removed graph " (pr-str (text-util/get-graph-name-from-path url))) :success)))))))
 
 (defn start-repo-db-if-not-exists!
@@ -95,14 +95,14 @@
     (state/reset-parsing-state!)
     (let [dir (config/get-repo-dir repo)]
       (when-not (state/unlinked-dir? dir)
-       (route-handler/redirect-to-home!)
-       (let [local? (config/local-file-based-graph? repo)]
-         (if local?
-           (nfs-rebuild-index! repo ok-handler)
-           (rebuild-index! repo))
-         (js/setTimeout
-          (route-handler/redirect-to-home!)
-          500))))))
+        (route-handler/redirect-to-home!)
+        (let [local? (config/local-file-based-graph? repo)]
+          (if local?
+            (nfs-rebuild-index! repo ok-handler)
+            (rebuild-index! repo))
+          (js/setTimeout
+           (route-handler/redirect-to-home!)
+           500))))))
 
 (defn get-repos
   []
@@ -146,8 +146,8 @@
 (defn get-detail-graph-info
   [url]
   (when-let [graphs (seq (and url (combine-local-&-remote-graphs
-                                    (state/get-repos)
-                                    (state/get-remote-file-graphs))))]
+                                   (state/get-repos)
+                                   (state/get-remote-file-graphs))))]
     (first (filter #(when-let [url' (:url %)]
                       (= url url')) graphs))))
 
@@ -184,7 +184,9 @@
 (defn- create-db [full-graph-name {:keys [file-graph-import?]}]
   (->
    (p/let [config (migrate-db-config config/config-default-content)
-           _ (persist-db/<new full-graph-name {:config config})
+           _ (persist-db/<new full-graph-name
+                              (cond-> {:config config}
+                                file-graph-import? (assoc :import-type :file-graph)))
            _ (start-repo-db-if-not-exists! full-graph-name)
            _ (state/add-repo! {:url full-graph-name :root (config/get-local-dir full-graph-name)})
            _ (restore-and-setup-repo! full-graph-name)

+ 3 - 1
src/main/frontend/handler/route.cljs

@@ -184,7 +184,9 @@
 (defn jump-to-anchor!
   [anchor-text]
   (when anchor-text
-    (js/setTimeout #(ui-handler/highlight-element! anchor-text) 200)))
+    (js/setTimeout #(ui-handler/highlight-element! anchor-text) 200)
+    (when-let [f (:editor/virtualized-scroll-fn @state/state)]
+      (f))))
 
 (defn set-route-match!
   [route]

+ 38 - 11
src/main/frontend/handler/ui.cljs

@@ -243,8 +243,8 @@
   (when-not (= repo target-repo)        ; TODO: remove this once we support multi-tabs OPFS access
     (when target-repo
       (if (util/electron?)
-       (ipc/ipc "openNewWindow" target-repo)
-       (js/window.open (str config/app-website "#/?graph=" target-repo) "_blank")))))
+        (ipc/ipc "openNewWindow" target-repo)
+        (js/window.open (str config/app-website "#/?graph=" target-repo) "_blank")))))
 
 (defn toggle-show-empty-hidden-properties!
   []
@@ -258,17 +258,44 @@
     (if (seq block-ids)
       (let [block-ids' (set block-ids)]
         (reset! *state
-               {:mode :block
-                :ids block-ids'
-                :show? (cond
-                         (= mode :global)
-                         true
-                         (not= ids block-ids')
-                         true
-                         :else
-                         (not show?))}))
+                {:mode :block
+                 :ids block-ids'
+                 :show? (cond
+                          (= mode :global)
+                          true
+                          (not= ids block-ids')
+                          true
+                          :else
+                          (not show?))}))
       (reset! *state
               {:mode :global
                :show? (if (= mode :block)
                         true
                         (not show?))}))))
+
+(defn scroll-to-anchor-block
+  [ref blocks gallery?]
+  (when ref
+    (let [anchor (get-in (state/get-route-match) [:query-params :anchor])
+          anchor-id (when (and anchor (string/starts-with? anchor "ls-block-"))
+                      (let [id (subs anchor 9)]
+                        (when (util/uuid-string? id)
+                          (uuid id))))]
+      (when (and ref anchor-id)
+        (let [block-ids (map :block/uuid blocks)
+              find-idx (fn [anchor-id]
+                         (let [idx (.indexOf block-ids anchor-id)]
+                           (when (pos? idx) idx)))
+              idx (or (find-idx anchor-id)
+                      (let [block (db/entity [:block/uuid anchor-id])
+                            parents (map :block/uuid (db/get-block-parents (state/get-current-repo) (:block/uuid block) {}))]
+                        (some find-idx parents)))]
+          (when idx
+            (js/setTimeout
+             (fn []
+               (.scrollToIndex ref #js {:index idx})
+               ;; wait until this block has been rendered.
+               (js/setTimeout #(highlight-element! anchor) 200))
+             ;; BUG: grid scrollToIndex not working in useEffect on first render
+             ;; https://github.com/petyosi/react-virtuoso/issues/757
+             (if gallery? 100 0))))))))

+ 3 - 0
src/main/frontend/handler/worker.cljs

@@ -39,6 +39,9 @@
 (defmethod handle :rtc-log [_ _worker log]
   (state/pub-event! [:rtc/log log]))
 
+(defmethod handle :export-current-db [_]
+  (state/pub-event! [:db/export-sqlite]))
+
 (defmethod handle :default [_ _worker data]
   (prn :debug "Worker data not handled: " data))
 

+ 7 - 0
src/main/frontend/modules/outliner/pipeline.cljs

@@ -19,6 +19,10 @@
         tx-report {:tx-meta tx-meta
                    :tx-data tx-data}]
     (when (= repo (state/get-current-repo))
+      (when (seq deleted-block-uuids)
+        (let [ids (map (fn [id] (:db/id (db/entity [:block/uuid id]))) deleted-block-uuids)]
+          (state/sidebar-remove-deleted-block! ids)))
+
       (let [conn (db/get-db repo false)]
         (cond
           initial-pages?
@@ -35,6 +39,9 @@
 
           :else
           (do
+            (state/set-state! :db/latest-updated-entity-uuids
+                              {:updated-ids (set (map :block/uuid blocks))
+                               :deleted-ids (set deleted-block-uuids)})
             (let [tx-data' (concat
                             (map
                              (fn [id]

+ 17 - 21
src/main/frontend/publishing.cljs

@@ -1,31 +1,28 @@
 (ns frontend.publishing
   "Entry ns for publishing build. Provides frontend for publishing single page
   application"
-  (:require [frontend.state :as state]
-            [datascript.core :as d]
-            [datascript.transit :as dt]
-            [frontend.db :as db]
-            [rum.core :as rum]
-            [frontend.handler.route :as route-handler]
-            [frontend.page :as page]
+  (:require [cljs.reader :as reader]
             [clojure.string :as string]
-            [frontend.routes :as routes]
-            [frontend.context.i18n :as i18n]
-            [reitit.frontend :as rf]
-            [reitit.frontend.easy :as rfe]
-            [cljs.reader :as reader]
             [frontend.components.block :as block]
             [frontend.components.editor :as editor]
             [frontend.components.page :as page-component]
             [frontend.components.reference :as reference]
             [frontend.components.whiteboard :as whiteboard]
-            [frontend.modules.shortcut.core :as shortcut]
-            [frontend.handler.events :as events]
+            [frontend.context.i18n :as i18n]
             [frontend.handler.command-palette :as command-palette]
+            [frontend.handler.events :as events]
+            [frontend.handler.repo :as repo-handler]
+            [frontend.handler.route :as route-handler]
+            [frontend.handler.ui :as ui-handler]
+            [frontend.modules.shortcut.core :as shortcut]
+            [frontend.page :as page]
             [frontend.persist-db.browser :as db-browser]
+            [frontend.routes :as routes]
+            [frontend.state :as state]
             [promesa.core :as p]
-            [frontend.handler.repo :as repo-handler]
-            [frontend.handler.ui :as ui-handler]))
+            [reitit.frontend :as rf]
+            [reitit.frontend.easy :as rfe]
+            [rum.core :as rum]))
 
 ;; The publishing site should be as thin as possible.
 ;; Both files and git libraries can be removed.
@@ -58,11 +55,10 @@
     (let [repo (-> @state/state :config keys first)]
       (state/set-current-repo! repo)
       (p/let [_ (repo-handler/restore-and-setup-repo! repo)
-              _ (let [data (unescape-html data)
-                      db (dt/read-transit-str data)
-                      datoms (d/datoms db :eavt)]
-                  (db/transact! repo datoms {:init-db? true
-                                             :new-graph? true}))]
+              _ (let [db-transit-str (unescape-html data)]
+                  (when-let [^js worker @state/*db-worker]
+                    (.resetDB worker repo db-transit-str)))
+              _ (repo-handler/restore-and-setup-repo! repo)]
         (state/set-db-restoring! false)
         (ui-handler/re-render-root!)))))
 

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

@@ -39,7 +39,7 @@
                    whiteboard? (ldb/whiteboard? (db/get-page page-name))]
                (if whiteboard?
                  (whiteboard/whiteboard-route route-match)
-                 (page/page-cp route-match))))}]
+                 (page/page-cp (assoc route-match :current-page? true)))))}]
 
    ["/page/:name/block/:block-route-name"
     {:name :page-block

+ 30 - 25
src/main/frontend/state.cljs

@@ -157,8 +157,8 @@
       :editor/container-id                   (atom nil)
       :editor/next-edit-block                (atom nil)
       :editor/raw-mode-block                 (atom nil)
+      :editor/virtualized-scroll-fn          nil
 
-      :selection/mode                        (atom false)
       ;; Warning: blocks order is determined when setting this attribute
       :selection/blocks                      (atom [])
       :selection/start-block                 (atom nil)
@@ -319,7 +319,8 @@
       :ui/select-query-cache                 (atom {})
       :favorites/updated?                    (atom 0)
       :db/async-query-loading                (atom #{})
-      :db/async-queries                      (atom {})})))
+      :db/async-queries                      (atom {})
+      :db/latest-transacted-entity-uuids     (atom {})})))
 
 ;; Block ast state
 ;; ===============
@@ -1058,6 +1059,18 @@ Similar to re-frame subscriptions"
    (when block-id
      (sub :editor/content {:path-in-sub-atom block-id}))))
 
+(defn set-selection-start-block!
+  [start-block]
+  (set-state! :selection/start-block start-block))
+
+(defn get-selection-start-block
+  []
+  (or @(get @state :selection/start-block)
+      (when-let [edit-block (get-edit-block)]
+        (let [id (str "ls-block-" (:block/uuid edit-block))]
+          (set-selection-start-block! id)
+          id))))
+
 (defn get-cursor-range
   []
   @(:editor/cursor-range @state))
@@ -1126,14 +1139,6 @@ Similar to re-frame subscriptions"
   (when-let [input (get-input)]
     (util/get-selection-start input)))
 
-(defn get-selection-start-block
-  []
-  @(get @state :selection/start-block))
-
-(defn set-selection-start-block!
-  [start-block]
-  (set-state! :selection/start-block start-block))
-
 (defn get-selection-direction
   []
   @(:selection/direction @state))
@@ -1185,17 +1190,11 @@ Similar to re-frame subscriptions"
   ([blocks direction]
    (when (seq blocks)
      (let [blocks (vec (remove nil? blocks))]
-       (set-state! :selection/mode true)
        (set-selection-blocks-aux! blocks)
        (when direction (set-state! :selection/direction direction))))))
 
-(defn into-selection-mode!
-  []
-  (set-state! :selection/mode true))
-
 (defn state-clear-selection!
   []
-  (set-state! :selection/mode false)
   (set-state! :selection/blocks nil)
   (set-state! :selection/direction nil)
   (set-state! :selection/start-block nil)
@@ -1212,14 +1211,10 @@ Similar to re-frame subscriptions"
       (some-> (first (get-selection-blocks))
               (gobj/get "id"))))
 
-(defn in-selection-mode?
-  []
-  @(:selection/mode @state))
-
 (defn selection?
   "True sense of selection mode with valid selected block"
   []
-  (and (in-selection-mode?) (seq (get-selection-blocks))))
+  (seq (get-selection-blocks)))
 
 (defn conj-selection-block!
   [block-or-blocks direction]
@@ -1231,13 +1226,11 @@ Similar to re-frame subscriptions"
 
 (defn drop-selection-block!
   [block]
-  (set-state! :selection/mode true)
   (set-selection-blocks-aux! (-> (remove #(= block %) (get-unsorted-selection-blocks))
                                  vec)))
 
 (defn drop-selection-blocks-starts-with!
   [block]
-  (set-state! :selection/mode true)
   (let [blocks (get-unsorted-selection-blocks)
         blocks' (-> (take-while (fn [b] (not= (.-id b) (.-id block))) blocks)
                     vec
@@ -1248,7 +1241,6 @@ Similar to re-frame subscriptions"
   []
   (let [blocks @(:selection/blocks @state)
         blocks' (vec (butlast blocks))]
-    (set-state! :selection/mode true)
     (set-selection-blocks-aux! blocks')
     (last blocks)))
 
@@ -1295,6 +1287,15 @@ Similar to re-frame subscriptions"
   (when (empty? (:sidebar/blocks @state))
     (hide-right-sidebar!)))
 
+(defn sidebar-remove-deleted-block!
+  [ids]
+  (let [ids-set (set ids)]
+    (update-state! :sidebar/blocks (fn [items]
+                                     (remove (fn [[repo id _]]
+                                               (and (= repo (get-current-repo)) (contains? ids-set id))) items)))
+    (when (empty? (:sidebar/blocks @state))
+      (hide-right-sidebar!))))
+
 (defn sidebar-remove-rest!
   [db-id]
   (update-state! :sidebar/blocks (fn [blocks]
@@ -2101,10 +2102,14 @@ Similar to re-frame subscriptions"
   (let [current-repo (get-current-repo)]
     (set-state! [:ui/collapsed-blocks current-repo block-id] value)))
 
-(defn sub-collapsed
+(defn sub-block-collapsed
   [block-id]
   (sub [:ui/collapsed-blocks (get-current-repo) block-id]))
 
+(defn get-block-collapsed
+  [block-id]
+  (get-in @state [:ui/collapsed-blocks (get-current-repo) block-id]))
+
 (defn get-modal-id
   []
   (shui-dialog/get-last-modal-id))

+ 3 - 5
src/main/frontend/ui.cljs

@@ -2,11 +2,10 @@
   "Main ns for reusable components"
   (:require ["@logseq/react-tweet-embed" :as react-tweet-embed]
             ["react-intersection-observer" :as react-intersection-observer]
-            ["react-resize-context" :as Resize]
             ["react-textarea-autosize" :as TextareaAutosize]
             ["react-tippy" :as react-tippy]
             ["react-transition-group" :refer [CSSTransition TransitionGroup]]
-            ["react-virtuoso" :refer [Virtuoso]]
+            ["react-virtuoso" :refer [Virtuoso VirtuosoGrid]]
             ["@emoji-mart/data" :as emoji-data]
             ["emoji-mart" :as emoji-mart]
             [cljs-bean.core :as bean]
@@ -45,9 +44,8 @@
 (defonce css-transition (r/adapt-class CSSTransition))
 (defonce textarea (r/adapt-class (gobj/get TextareaAutosize "default")))
 (defonce virtualized-list (r/adapt-class Virtuoso))
+(defonce virtualized-grid (r/adapt-class VirtuosoGrid))
 
-(def resize-provider (r/adapt-class (gobj/get Resize "ResizeProvider")))
-(def resize-consumer (r/adapt-class (gobj/get Resize "ResizeConsumer")))
 (def Tippy (r/adapt-class (gobj/get react-tippy "Tooltip")))
 (def ReactTweetEmbed (r/adapt-class react-tweet-embed))
 (def useInView (gobj/get react-intersection-observer "useInView"))
@@ -321,7 +319,7 @@
             (button
              {:button-props {:aria-label "Close"}
               :variant :ghost
-              :class "hover:bg-transparent hover:text-foreground"
+              :class "hover:bg-transparent hover:text-foreground scale-90"
               :on-click (fn []
                           (notification/clear! uid))
               :icon "x"})]]]]]])))

+ 7 - 9
src/main/frontend/ui.css

@@ -53,22 +53,20 @@
 }
 
 .ui__notifications {
-  position: fixed;
+  @apply fixed top-12 pointer-events-none w-full;
+
   z-index: var(--ls-z-index-level-4);
-  width: 100%;
-  top: 3.2em;
-  pointer-events: none;
 
   &-content {
-    @apply inset-0 flex items-end justify-center px-4 py-3
-    pointer-events-none sm:px-6 sm:py-3 sm:items-start
+    @apply inset-0 flex items-end justify-center px-4 py-2
+    pointer-events-none sm:px-6 sm:py-2 sm:items-start
     sm:justify-end;
   }
 
   .notification-area {
     @apply border;
 
-    background-color: or(--ls-notification-background, --lx-gray-03, --ls-tertiary-background-color, --rx-gray-03);
+    background-color: or(--ls-notification-background, --lx-gray-02, --ls-tertiary-background-color, --rx-gray-03);
     color: or(--ls-notification-text-color, --lx-gray-11, --ls-primary-text-color, --rx-gray-11);
 
     &:has(.ls-wrap-widen) {
@@ -177,7 +175,7 @@
 @media (min-width: 820px) {
     .ui__dialog-content[label=flashcards__cp] {
         min-width: 800px;
-        max-width: 980px;
+        max-width: min(85%, 980px);
     }
 }
 
@@ -325,4 +323,4 @@ input[type='range'] {
   .item-results-wrap {
     @apply px-2;
   }
-}
+}

+ 124 - 0
src/main/frontend/worker/crypt.cljs

@@ -0,0 +1,124 @@
+(ns frontend.worker.crypt
+  "Fns to en/decrypt some block attrs"
+  (:require [datascript.core :as d]
+            [frontend.worker.state :as worker-state]
+            [promesa.core :as p]))
+
+(defonce ^:private encoder (new js/TextEncoder "utf-8"))
+(comment (defonce ^:private decoder (new js/TextDecoder "utf-8")))
+
+(defn string=>arraybuffer
+  [s]
+  (.encode encoder s))
+
+(defn <rsa-encrypt
+  "Return an arraybuffer"
+  [message public-key]
+  (assert (string? message))
+  (let [data (string=>arraybuffer message)]
+    (js/crypto.subtle.encrypt
+     #js{:name "RSA-OAEP"}
+     public-key
+     data)))
+
+(comment
+  (defn <decrypt
+    [cipher-text private-key]
+    (p/let [result (js/crypto.subtle.decrypt
+                    #js{:name "RSA-OAEP"}
+                    private-key
+                    cipher-text)]
+      (.decode decoder result))))
+
+(comment
+  (defn <aes-encrypt
+    [message aes-key]
+    (p/let [data (.encode encoder message)
+            iv (js/crypto.getRandomValues (js/Uint8Array. 12))
+            ciphertext (js/crypto.subtle.encrypt
+                        #js{:name "AES-GCM" :iv iv}
+                        aes-key
+                        data)]
+      {:ciphertext ciphertext
+       :iv iv})))
+
+(comment
+  (defn <aes-decrypt
+    [encrypted-data aes-key]
+    (p/let [{:keys [ciphertext iv]} encrypted-data
+            decrypted (js/crypto.subtle.decrypt
+                       #js{:name "AES-GCM" :iv iv}
+                       aes-key
+                       ciphertext)]
+      (.decode decoder decrypted))))
+
+(defonce ^:private key-algorithm
+  #js{:name "RSA-OAEP"
+      :modulusLength 4096
+      :publicExponent (new js/Uint8Array #js[1 0 1])
+      :hash "SHA-256"})
+
+(defn <gen-key-pair
+  []
+  (p/let [result (js/crypto.subtle.generateKey
+                  key-algorithm
+                  true
+                  #js["encrypt" "decrypt"])]
+    (js->clj result :keywordize-keys true)))
+
+(defonce ^:private aes-key-algorithm
+  #js{:name "AES-GCM"
+      :length 256})
+
+(defn <gen-aes-key
+  []
+  (p/let [result (js/crypto.subtle.generateKey
+                  aes-key-algorithm
+                  true
+                  #js["encrypt" "decrypt"])]
+    (js->clj result :keywordize-keys true)))
+
+(defn <export-key
+  [key']
+  (assert (instance? js/CryptoKey key') key')
+  (js/crypto.subtle.exportKey "jwk" key'))
+
+(defn <import-public-key
+  [jwk]
+  (assert (instance? js/Object jwk) jwk)
+  (js/crypto.subtle.importKey "jwk" jwk key-algorithm true #js["encrypt"]))
+
+(defn <import-private-key
+  [jwk]
+  (assert (instance? js/Object jwk) jwk)
+  (js/crypto.subtle.importKey "jwk" jwk key-algorithm true #js["decrypt"]))
+
+(comment
+  (p/let [{:keys [publicKey privateKey]} (<gen-key-pair)]
+    (p/doseq [msg (map #(str "message" %) (range 1000))]
+      (p/let [encrypted (<encrypt msg publicKey)
+              plaintxt (<decrypt encrypted privateKey)]
+        (prn :encrypted msg)
+        (prn :plaintxt plaintxt))))
+
+  (p/let [k (<gen-aes-key)
+          kk (<export-key k)
+          encrypted (<aes-encrypt (apply str (repeat 1000 "x")) k)
+          plaintxt (<aes-decrypt encrypted k)]
+    (prn :encrypted encrypted)
+    (prn :plaintxt plaintxt)))
+
+(defn store-graph-keys-jwk
+  [repo aes-key-jwk]
+  (let [conn (worker-state/get-client-ops-conn repo)]
+    (assert (some? conn) repo)
+    (let [aes-key-datom (first (d/datoms @conn :avet :aes-key-jwk))]
+      (assert (nil? aes-key-datom) aes-key-datom)
+      (d/transact! conn [[:db/add "e1" :aes-key-jwk aes-key-jwk]]))))
+
+(defn get-graph-keys-jwk
+  [repo]
+  (let [conn (worker-state/get-client-ops-conn repo)]
+    (assert (some? conn) repo)
+    (let [aes-key-datom (first (d/datoms @conn :avet :aes-key-jwk))]
+      {:aes-key-jwk (:v aes-key-datom)})))

+ 47 - 38
src/main/frontend/worker/db/migrate.cljs

@@ -271,7 +271,8 @@
     (when (ldb/db-based-graph? db)
       (let [property (d/entity db :logseq.property.node/display-type)
             ;; fix property
-            _ (when-not (ldb/property? property)
+            _ (when-not (and (ldb/property? property)
+                             (true? (:db/index property)))
                 (let [fix-tx-data (->>
                                    (select-keys db-property/built-in-properties [:logseq.property.node/display-type])
                                    (sqlite-create-graph/build-initial-properties*)
@@ -288,9 +289,10 @@
 (defn- rename-card-view-to-gallery-view
   [conn _search-db]
   (when (ldb/db-based-graph? @conn)
-    [{:db/id (:db/id (d/entity @conn :logseq.property.view/type.card))
-      :db/ident :logseq.property.view/type.gallery
-      :block/title "Gallery View"}]))
+    (let [card (d/entity @conn :logseq.property.view/type.card)]
+      [{:db/id (:db/id card)
+        :db/ident :logseq.property.view/type.gallery
+        :block/title "Gallery View"}])))
 
 (defn- add-pdf-annotation-class
   [conn _search-db]
@@ -347,24 +349,27 @@
    [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.1 {:fix add-card-view}]
+   [29.2 {:fix rename-card-view-to-gallery-view}]
+   ;; Asset relies on :logseq.property.view/type.gallery
    [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}]
    [37 {:classes [:logseq.class/Code-block :logseq.class/Quote-block :logseq.class/Math-block]
         :properties [:logseq.property.node/display-type :logseq.property.code/lang]}]
    [38 {:fix add-tags-for-typed-display-blocks}]
-   [39 {:fix rename-card-view-to-gallery-view}]
    [40 {:classes [:logseq.class/pdf-annotation]
         :properties [:logseq.property/ls-type :logseq.property/hl-color :logseq.property/asset
                      :logseq.property.pdf/hl-page :logseq.property.pdf/hl-value
                      :logseq.property/hl-type :logseq.property.pdf/hl-image]
         :fix add-pdf-annotation-class}]
    [41 {:fix (rename-classes {:logseq.class/pdf-annotation :logseq.class/Pdf-annotation})}]
-   [42 {:properties [:kv/value :block/type :block/schema :block/parent
+   [42 {:fix (rename-properties {:logseq.property/hl-color :logseq.property.pdf/hl-color
+                                 :logseq.property/hl-type :logseq.property.pdf/hl-type})}]
+   [43 {:properties [:kv/value :block/type :block/schema :block/parent
                      :block/order :block/collapsed? :block/page
                      :block/refs :block/path-refs :block/link
                      :block/title :block/closed-value-property
@@ -376,6 +381,34 @@
   (when (< db-schema/version max-schema-version)
     (js/console.warn (str "Current db schema-version is " db-schema/version ", max available schema-version is " max-schema-version))))
 
+(defn- upgrade-version!
+  [conn search-db db-based? version {:keys [properties classes fix]}]
+  (let [db @conn
+        new-properties (->> (select-keys db-property/built-in-properties properties)
+                                  ;; property already exists, this should never happen
+                            (remove (fn [[k _]]
+                                      (when (d/entity db k)
+                                        (assert (str "DB migration: property already exists " k)))))
+                            (into {})
+                            sqlite-create-graph/build-initial-properties*
+                            (map (fn [b] (assoc b :logseq.property/built-in? true))))
+        new-classes (->> (select-keys db-class/built-in-classes classes)
+                               ;; 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* % (zipmap properties properties)))
+                         (map (fn [b] (assoc b :logseq.property/built-in? true))))
+        fixes (when (fn? fix)
+                (fix conn search-db))
+        tx-data (if db-based? (concat new-properties new-classes fixes) fixes)
+        tx-data' (concat
+                  [(sqlite-util/kv :logseq.kv/schema-version version)]
+                  tx-data)]
+    (ldb/transact! conn tx-data' {:db-migrate? true})
+    (println "DB schema migrated to" version)))
+
 (defn migrate
   "Migrate 'frontend' datascript schema and data. To add a new migration,
   add an entry to schema-version->updates and bump db-schema/version"
@@ -395,39 +428,15 @@
         (let [db-based? (ldb/db-based-graph? @conn)
               updates (keep (fn [[v updates]]
                               (when (and (< version-in-db v) (<= v db-schema/version))
-                                updates))
-                            schema-version->updates)
-              properties (mapcat :properties updates)
-              new-properties (->> (select-keys db-property/built-in-properties properties)
-                                  ;; property already exists, this should never happen
-                                  (remove (fn [[k _]]
-                                            (when (d/entity db k)
-                                              (assert (str "DB migration: property already exists " k)))))
-                                  (into {})
-                                  sqlite-create-graph/build-initial-properties*
-                                  (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
-                               (remove (fn [[k _]]
-                                         (when (d/entity db k)
-                                           (assert (str "DB migration: class already exists " k)))))
-                               (into {})
-                               (#(sqlite-create-graph/build-initial-classes* % (zipmap properties properties)))
-                               (map (fn [b] (assoc b :logseq.property/built-in? true))))
-              fixes (mapcat
-                     (fn [update']
-                       (when-let [fix (:fix update')]
-                         (when (fn? fix)
-                           (fix conn search-db)))) updates)
-              tx-data' (if db-based? (concat new-properties new-classes fixes) fixes)]
-          (when (seq tx-data')
-            (let [tx-data' (concat tx-data' [(sqlite-util/kv :logseq.kv/schema-version db-schema/version)])]
-              (ldb/transact! conn tx-data' {:db-migrate? true}))
-            (println "DB schema migrated to " db-schema/version " from " version-in-db ".")))
+                                [v updates]))
+                            schema-version->updates)]
+          (println "DB schema migrated from" version-in-db)
+          (doseq [[v m] updates]
+            (upgrade-version! conn search-db db-based? v m)))
         (catch :default e
           (prn :error (str "DB migration failed to migrate to " db-schema/version " from " version-in-db ":"))
-          (js/console.error e))))))
+          (js/console.error e)
+          (throw e))))))
 
 ;; Backend migrations
 ;; ==================

+ 99 - 17
src/main/frontend/worker/db_worker.cljs

@@ -7,11 +7,13 @@
             [clojure.edn :as edn]
             [clojure.string :as string]
             [datascript.core :as d]
-            [datascript.storage :refer [IStorage]]
+            [datascript.storage :refer [IStorage] :as storage]
             [frontend.common.file.core :as common-file]
+            [frontend.worker.crypt :as worker-crypt]
             [frontend.worker.db-listener :as db-listener]
             [frontend.worker.db-metadata :as worker-db-metadata]
             [frontend.worker.db.migrate :as db-migrate]
+            [frontend.worker.device :as worker-device]
             [frontend.worker.export :as worker-export]
             [frontend.worker.file :as file]
             [frontend.worker.handler.page :as worker-page]
@@ -24,6 +26,7 @@
             [frontend.worker.state :as worker-state] ;; [frontend.worker.undo-redo :as undo-redo]
             [frontend.worker.undo-redo2 :as undo-redo]
             [frontend.worker.util :as worker-util]
+            [goog.object :as gobj]
             [logseq.common.config :as common-config]
             [logseq.common.util :as common-util]
             [logseq.db :as ldb]
@@ -32,9 +35,10 @@
             [logseq.db.sqlite.create-graph :as sqlite-create-graph]
             [logseq.db.sqlite.util :as sqlite-util]
             [logseq.outliner.op :as outliner-op]
-            [goog.object :as gobj]
             [promesa.core :as p]
-            [shadow.cljs.modern :refer [defclass]]))
+            [shadow.cljs.modern :refer [defclass]]
+            [logseq.db.frontend.schema :as db-schema]
+            [me.tonsky.persistent-sorted-set :as set :refer [BTSet]]))
 
 (defonce *sqlite worker-state/*sqlite)
 (defonce *sqlite-conns worker-state/*sqlite-conns)
@@ -46,7 +50,7 @@
 (defn- check-worker-scope!
   []
   (when (or (gobj/get js/self "React")
-          (gobj/get js/self "module$react"))
+            (gobj/get js/self "module$react"))
     (throw (js/Error. "[db-worker] React is forbidden in worker scope!"))))
 
 (defn- <get-opfs-pool
@@ -88,6 +92,37 @@
   [^js pool data]
   (.importDb ^js pool repo-path data))
 
+(defn- get-all-datoms-from-sqlite-db
+  [db]
+  (some->> (.exec db #js {:sql "select * from kvs"
+                          :rowMode "array"})
+           bean/->clj
+           (mapcat
+            (fn [[_addr content _addresses]]
+              (let [content' (sqlite-util/transit-read content)
+                    datoms (when (map? content')
+                             (:keys content'))]
+                datoms)))
+           distinct
+           (map (fn [[e a v t]]
+                  (d/datom e a v t)))))
+
+(defn- rebuild-db-from-datoms!
+  "Persistent-sorted-set has been broken, used addresses can't be found"
+  [datascript-conn sqlite-db]
+  (let [datoms (get-all-datoms-from-sqlite-db sqlite-db)
+        db (d/init-db [] db-schema/schema-for-db-based-graph
+                      {:storage (storage/storage @datascript-conn)})
+        db (d/db-with db
+                      (map (fn [d]
+                             [:db/add (:e d) (:a d) (:v d) (:t d)]) datoms))]
+    (prn :debug :rebuild-db-from-datoms :datoms-count (count datoms))
+    ;; export db first
+    (worker-util/post-message :notification ["The SQLite db will be exported to avoid any data-loss." :warning false])
+    (worker-util/post-message :export-current-db [])
+    (.exec sqlite-db #js {:sql "delete from kvs"})
+    (d/reset-conn! datascript-conn db)))
+
 (comment
   (defn- gc-kvs-table!
     [^Object db]
@@ -226,9 +261,20 @@
 
 (defn close-db!
   [repo]
-  (let [{:keys [db search client-ops]} (@*sqlite-conns repo)]
+  (let [{:keys [db search client-ops]} (get @*sqlite-conns repo)]
     (close-db-aux! repo db search client-ops)))
 
+(defn reset-db!
+  [repo db-transit-str]
+  (when-let [conn (get @*datascript-conns repo)]
+    (let [new-db (ldb/read-transit-str db-transit-str)
+          new-db' (update new-db :eavt (fn [^BTSet s]
+                                         (set! (.-storage s) (.-storage (:eavt @conn)))
+                                         s))]
+      (d/reset-conn! conn new-db' {:reset-conn! true})
+      (d/reset-schema! conn (:schema new-db))
+      nil)))
+
 (defn- get-dbs
   [repo]
   (if @*publishing?
@@ -246,30 +292,32 @@
       [db search-db client-ops-db])))
 
 (defn- create-or-open-db!
-  [repo {:keys [config]}]
+  [repo {:keys [config import-type]}]
   (when-not (worker-state/get-sqlite-conn repo)
     (p/let [[db search-db client-ops-db] (get-dbs repo)
             storage (new-sqlite-storage repo {})
-            client-ops-storage (new-sqlite-client-ops-storage repo)]
+            client-ops-storage (when-not @*publishing? (new-sqlite-client-ops-storage repo))
+            db-based? (sqlite-util/db-based-graph? repo)]
       (swap! *sqlite-conns assoc repo {:db db
                                        :search search-db
                                        :client-ops client-ops-db})
       (.exec db "PRAGMA locking_mode=exclusive")
       (sqlite-common-db/create-kvs-table! db)
-      (sqlite-common-db/create-kvs-table! client-ops-db)
+      (when-not @*publishing? (sqlite-common-db/create-kvs-table! client-ops-db))
       (db-migrate/migrate-sqlite-db db)
-      (db-migrate/migrate-sqlite-db client-ops-db)
+      (when-not @*publishing? (db-migrate/migrate-sqlite-db client-ops-db))
       (search/create-tables-and-triggers! search-db)
       (let [schema (sqlite-util/get-schema repo)
             conn (sqlite-common-db/get-storage-conn storage schema)
-            client-ops-conn (sqlite-common-db/get-storage-conn client-ops-storage client-op/schema-in-db)
+            client-ops-conn (when-not @*publishing? (sqlite-common-db/get-storage-conn client-ops-storage client-op/schema-in-db))
             initial-data-exists? (and (d/entity @conn :logseq.class/Root)
                                       (= "db" (:kv/value (d/entity @conn :logseq.kv/db-type))))]
         (swap! *datascript-conns assoc repo conn)
         (swap! *client-ops-conns assoc repo client-ops-conn)
-        (when (and (sqlite-util/db-based-graph? repo) (not initial-data-exists?))
+        (when (and db-based? (not initial-data-exists?))
           (let [config (or config {})
-                initial-data (sqlite-create-graph/build-db-initial-data config)]
+                initial-data (sqlite-create-graph/build-db-initial-data config
+                                                                        (when import-type {:import-type import-type}))]
             (d/transact! conn initial-data {:initial-db? true})))
 
         (try
@@ -278,9 +326,13 @@
           (catch :default _e))
 
         ;; (gc-kvs-table! db)
-        (db-migrate/migrate conn search-db)
+        (try
+          (db-migrate/migrate conn search-db)
+          (catch :default _e
+            (when db-based?
+              (rebuild-db-from-datoms! conn db))))
 
-        (db-listener/listen-db-changes! repo conn)))))
+        (db-listener/listen-db-changes! repo (get @*datascript-conns repo))))))
 
 (defn- iter->vec [iter']
   (when iter'
@@ -392,12 +444,11 @@
 
   (createOrOpenDB
    [_this repo opts-str]
-   (let [{:keys [close-other-db? config]
-          :or {close-other-db? true}} (ldb/read-transit-str opts-str)]
+   (let [{:keys [close-other-db?] :or {close-other-db? true} :as opts} (ldb/read-transit-str opts-str)]
      (p/do!
       (when close-other-db?
         (close-other-dbs! repo))
-      (create-or-open-db! repo {:config config}))))
+      (create-or-open-db! repo (dissoc opts :close-other-db?)))))
 
   (getMaxTx
    [_this repo]
@@ -551,6 +602,10 @@
    [_this repo]
    (close-db! repo))
 
+  (resetDB
+   [_this repo db-transit]
+   (reset-db! repo db-transit))
+
   (unsafeUnlinkDB
    [_this repo]
    (p/let [pool (<get-opfs-pool repo)
@@ -773,6 +828,33 @@
    (with-write-transit-str
      (js/Promise. (rtc-core/new-task--snapshot-list token graph-uuid))))
 
+  (rtc-get-graph-keys
+   [this repo]
+   (with-write-transit-str
+     (worker-crypt/get-graph-keys-jwk repo)))
+
+  (rtc-sync-current-graph-encrypted-aes-key
+   [this token device-uuids-transit-str]
+   (with-write-transit-str
+     (js/Promise.
+      (worker-device/new-task--sync-current-graph-encrypted-aes-key
+       token device-uuids-transit-str))))
+
+  (device-list-devices
+   [this token]
+   (with-write-transit-str
+     (js/Promise. (worker-device/new-task--list-devices token))))
+
+  (device-remove-device-public-key
+   [this token device-uuid key-name]
+   (with-write-transit-str
+     (js/Promise. (worker-device/new-task--remove-device-public-key token device-uuid key-name))))
+
+  (device-remove-device
+   [this token device-uuid]
+   (with-write-transit-str
+     (js/Promise. (worker-device/new-task--remove-device token device-uuid))))
+
   (undo
    [_this repo _page-block-uuid-str]
    (when-let [conn (worker-state/get-datascript-conn repo)]

+ 208 - 0
src/main/frontend/worker/device.cljs

@@ -0,0 +1,208 @@
+(ns frontend.worker.device
+  "Each device is assigned an id, and has some metadata(e.g. public&private-key for each device)"
+  (:require ["/frontend/idbkv" :as idb-keyval]
+            [cljs-time.coerce :as tc]
+            [cljs-time.core :as t]
+            [clojure.string :as string]
+            [frontend.common.missionary-util :as c.m]
+            [frontend.worker.crypt :as crypt]
+            [frontend.worker.rtc.client-op :as client-op]
+            [frontend.worker.rtc.ws-util :as ws-util]
+            [frontend.worker.state :as worker-state]
+            [goog.crypt.base64 :as base64]
+            [logseq.db :as ldb]
+            [missionary.core :as m]
+            [promesa.core :as p]))
+
+;;; TODO: move frontend.idb to deps/, then we can use it in both frontend and db-worker
+;;; now, I just direct use "/frontend/idbkv" here
+(defonce ^:private store (delay (idb-keyval/newStore "localforage" "keyvaluepairs" 2)))
+
+(defn- <get-item
+  [key']
+  (when (and key' @store)
+    (idb-keyval/get key' @store)))
+
+(defn- <set-item!
+  [key' value]
+  (when (and key' @store)
+    (idb-keyval/set key' value @store)))
+
+(defn- <remove-item!
+  [key']
+  (idb-keyval/del key' @store))
+
+(def ^:private item-key-device-id "device-id")
+(def ^:private item-key-device-name "device-name")
+(def ^:private item-key-device-created-at "device-created-at")
+(def ^:private item-key-device-updated-at "device-updated-at")
+(def ^:private item-key-device-public-key-jwk "device-public-key-jwk")
+(def ^:private item-key-device-private-key-jwk "device-private-key-jwk")
+
+(defonce *device-id (atom nil :validator uuid?))
+(defonce *device-name (atom nil))
+(defonce *device-public-key (atom nil :validator #(instance? js/CryptoKey %)))
+(defonce *device-private-key (atom nil :validator #(instance? js/CryptoKey %)))
+
+(defn- new-task--get-user-devices
+  [get-ws-create-task]
+  (m/join :devices (ws-util/send&recv get-ws-create-task {:action "get-user-devices"})))
+
+(defn- new-task--add-user-device
+  [get-ws-create-task device-name]
+  (m/join :device (ws-util/send&recv get-ws-create-task {:action "add-user-device"
+                                                         :device-name device-name})))
+
+(defn- new-task--remove-user-device*
+  [get-ws-create-task device-uuid]
+  (ws-util/send&recv get-ws-create-task {:action "remove-user-device"
+                                         :device-uuid device-uuid}))
+
+(comment
+  (defn- new-task--update-user-device-name
+    [get-ws-create-task device-uuid device-name]
+    (ws-util/send&recv get-ws-create-task {:action "update-user-device-name"
+                                           :device-uuid device-uuid
+                                           :device-name device-name})))
+
+(defn- new-task--add-device-public-key
+  [get-ws-create-task device-uuid key-name public-key-jwk]
+  (ws-util/send&recv get-ws-create-task {:action "add-device-public-key"
+                                         :device-uuid device-uuid
+                                         :key-name key-name
+                                         :public-key (ldb/write-transit-str public-key-jwk)}))
+
+(defn- new-task--remove-device-public-key*
+  [get-ws-create-task device-uuid key-name]
+  (ws-util/send&recv get-ws-create-task {:action "remove-device-public-key"
+                                         :device-uuid device-uuid
+                                         :key-name key-name}))
+
+(defn- new-task--sync-encrypted-aes-key*
+  [get-ws-create-task device-uuid->encrypted-aes-key graph-uuid]
+  (ws-util/send&recv get-ws-create-task
+                     {:action "sync-encrypted-aes-key"
+                      :device-uuid->encrypted-aes-key device-uuid->encrypted-aes-key
+                      :graph-uuid graph-uuid}))
+
+(defn- new-get-ws-create-task
+  [token]
+  (:get-ws-create-task (ws-util/gen-get-ws-create-map--memoized (ws-util/get-ws-url token))))
+
+(defn new-task--ensure-device-metadata!
+  "Generate new device items if not exists.
+  Store in indexeddb.
+  Import to `*device-id`, `*device-public-key`, `*device-private-key`"
+  [token]
+  (m/sp
+   (let [device-uuid (c.m/<? (<get-item item-key-device-id))]
+     (when-not device-uuid
+       (let [get-ws-create-task (new-get-ws-create-task token)
+             agent-data (js->clj (.toJSON js/navigator.userAgentData) :keywordize-keys true)
+             generated-device-name (string/join
+                                    "-"
+                                    [(:platform agent-data)
+                                     (when (:mobile agent-data) "mobile")
+                                     (:brand (first (:brands agent-data)))
+                                     (tc/to-epoch (t/now))])
+             {:keys [device-id device-name created-at updated-at]}
+             (m/? (new-task--add-user-device get-ws-create-task generated-device-name))
+             {:keys [publicKey privateKey]} (c.m/<? (crypt/<gen-key-pair))
+             public-key-jwk (c.m/<? (crypt/<export-key publicKey))
+             private-key-jwk (c.m/<? (crypt/<export-key privateKey))]
+         (c.m/<? (<set-item! item-key-device-id (str device-id)))
+         (c.m/<? (<set-item! item-key-device-name device-name))
+         (c.m/<? (<set-item! item-key-device-created-at created-at))
+         (c.m/<? (<set-item! item-key-device-updated-at updated-at))
+         (c.m/<? (<set-item! item-key-device-public-key-jwk public-key-jwk))
+         (c.m/<? (<set-item! item-key-device-private-key-jwk private-key-jwk))
+         (m/? (new-task--add-device-public-key
+               get-ws-create-task device-id "default-public-key" public-key-jwk))))
+     (c.m/<?
+      (p/let [device-uuid-str (<get-item item-key-device-id)
+              device-name (<get-item item-key-device-name)
+              device-public-key-jwk (<get-item item-key-device-public-key-jwk)
+              device-public-key (crypt/<import-public-key device-public-key-jwk)
+              device-private-key-jwk (<get-item item-key-device-private-key-jwk)
+              device-private-key (crypt/<import-private-key device-private-key-jwk)]
+        (reset! *device-id (uuid device-uuid-str))
+        (reset! *device-name device-name)
+        (reset! *device-public-key device-public-key)
+        (reset! *device-private-key device-private-key))))))
+
+(defn new-task--list-devices
+  "Return device list.
+  Also sync local device metadata to remote if not exists in remote side"
+  [token]
+  (m/sp
+   (let [get-ws-create-task (new-get-ws-create-task token)
+         devices (m/? (new-task--get-user-devices get-ws-create-task))]
+     (when
+          ;; check current device has been synced to remote
+          ;; if not exists in remote, remove local-metadata and recreate in local and remote
+      (and @*device-id @*device-name @*device-public-key
+           (not (some
+                 (fn [device]
+                   (let [{:keys [device-id]} device]
+                     (when (= device-id (str @*device-id))
+                       true)))
+                 devices)))
+       (c.m/<? (<remove-item! item-key-device-id))
+       (c.m/<? (<remove-item! item-key-device-name))
+       (c.m/<? (<remove-item! item-key-device-created-at))
+       (c.m/<? (<remove-item! item-key-device-updated-at))
+       (c.m/<? (<remove-item! item-key-device-public-key-jwk))
+       (c.m/<? (<remove-item! item-key-device-private-key-jwk))
+       (m/? (new-task--ensure-device-metadata! token)))
+     devices)))
+
+(defn new-task--remove-device-public-key
+  [token device-uuid key-name]
+  (assert (some? key-name))
+  (m/sp
+   (when-let [device-uuid* (cond-> device-uuid (string? device-uuid) parse-uuid)]
+     (let [get-ws-create-task (new-get-ws-create-task token)]
+       (m/? (new-task--remove-device-public-key* get-ws-create-task device-uuid* key-name))))))
+
+(defn new-task--remove-device
+  [token device-uuid]
+  (m/sp
+   (when-let [device-uuid* (cond-> device-uuid (string? device-uuid) parse-uuid)]
+     (let [get-ws-create-task (new-get-ws-create-task token)]
+       (m/? (new-task--remove-user-device* get-ws-create-task device-uuid*))))))
+
+(defn new-task--sync-current-graph-encrypted-aes-key
+  [token device-uuids-transit-str]
+  (let [repo (worker-state/get-current-repo)
+        device-uuids (ldb/read-transit-str device-uuids-transit-str)]
+    (assert (and (seq device-uuids) (every? uuid? device-uuids)) device-uuids)
+    (m/sp
+     (when-let [graph-uuid (client-op/get-graph-uuid repo)]
+       (when-let [{:keys [aes-key-jwk]} (crypt/get-graph-keys-jwk repo)]
+         (let [device-uuids (set device-uuids)
+               get-ws-create-task (new-get-ws-create-task token)
+               devices (m/? (new-task--get-user-devices get-ws-create-task))]
+           (when-let [devices* (not-empty
+                                (filter
+                                 (fn [device]
+                                   (and (contains? device-uuids (uuid (:device-id device)))
+                                        (some? (get-in device [:keys :default-public-key]))))
+                                 devices))]
+             (let [device-uuid->encrypted-aes-key
+                   (m/?
+                    (apply m/join (fn [& x] (into {} x))
+                           (map (fn [device]
+                                  (m/sp
+                                   (let [device-public-key
+                                         (c.m/<?
+                                          (crypt/<import-public-key
+                                           (clj->js
+                                            (ldb/read-transit-str
+                                             (get-in device [:keys :default-public-key :public-key])))))]
+                                     [(uuid (:device-id device))
+                                      (base64/encodeByteArray
+                                       (js/Uint8Array.
+                                        (c.m/<? (crypt/<rsa-encrypt aes-key-jwk device-public-key))))])))
+                                devices*)))]
+               (m/? (new-task--sync-encrypted-aes-key*
+                     get-ws-create-task device-uuid->encrypted-aes-key graph-uuid))))))))))

+ 10 - 3
src/main/frontend/worker/handler/page/db_based/page.cljs

@@ -26,9 +26,16 @@
                   (update :block/tags
                           (fnil into [])
                           (mapv (fn [tag]
-                                  (if (uuid? tag)
-                                    (d/entity @conn [:block/uuid tag])
-                                    tag))
+                                  (let [v (if (uuid? tag)
+                                            (d/entity @conn [:block/uuid tag])
+                                            tag)]
+                                    (cond
+                                      (de/entity? v)
+                                      (:db/id v)
+                                      (map? v)
+                                      (:db/id v)
+                                      :else
+                                      v)))
                                 tags)))
           property-vals-tx-m
           ;; Builds property values for built-in properties like logseq.property.pdf/file

+ 3 - 1
src/main/frontend/worker/pipeline.cljs

@@ -49,7 +49,9 @@
   (when (and (not (:skip-validate-db? tx-meta false))
              (:dev? context)
              (not (:importing? context)) (sqlite-util/db-based-graph? repo))
-    (let [valid? (db-validate/validate-tx-report! tx-report (:validate-db-options context))]
+    (let [valid? (if (get-in tx-report [:tx-meta :reset-conn!])
+                   true
+                   (db-validate/validate-tx-report! tx-report (:validate-db-options context)))]
       (when (and (get-in context [:validate-db-options :fail-invalid?]) (not valid?))
         (worker-util/post-message :notification
                                   [["Invalid DB!"] :error]))))

+ 26 - 12
src/main/frontend/worker/rtc/client_op.cljs

@@ -48,9 +48,17 @@
                                   (ma/-fail! ::ops-schema %))))
 
 (def schema-in-db
+  "TODO: rename this db-name from client-op to client-metadata+op.
+  and move it to its own namespace."
   {:block/uuid {:db/unique :db.unique/identity}
    :local-tx {:db/index true}
-   :graph-uuid {:db/index true}})
+   :graph-uuid {:db/index true}
+   :aes-key-jwk {:db/index true}
+
+   ;; device
+   :device/uuid {:db/unique :db.unique/identity}
+   :device/public-key-jwk {}
+   :device/private-key-jwk {}})
 
 (defn update-graph-uuid
   [repo graph-uuid]
@@ -59,6 +67,11 @@
     (assert (nil? (first (d/datoms @conn :avet :graph-uuid))))
     (d/transact! conn [[:db/add "e" :graph-uuid graph-uuid]])))
 
+(defn get-graph-uuid
+  [repo]
+  (when-let [conn (worker-state/get-client-ops-conn repo)]
+    (:v (first (d/datoms @conn :avet :graph-uuid)))))
+
 (defn update-local-tx
   [repo t]
   {:pre [(some? t)]}
@@ -148,24 +161,25 @@
 (defn- get-all-op-datoms
   [conn]
   (->> (d/datoms @conn :eavt)
-       (remove (fn [datom] (contains? #{:graph-uuid :local-tx} (:a datom))))
        (group-by :e)))
 
-(defn get-all-ops*
+(defn- get-all-ops*
+  "Return e->op-map"
   [conn]
   (let [e->datoms (get-all-op-datoms conn)]
-    (map (fn [same-ent-datoms]
-           (into {} (map (juxt :a :v)) same-ent-datoms))
-         (vals e->datoms))))
+    (into {}
+          (keep (fn [[e same-ent-datoms]]
+                  (let [op-map (into {} (map (juxt :a :v)) same-ent-datoms)]
+                    (when (:block/uuid op-map)
+                      [e op-map])))
+                e->datoms))))
 
 (defn get&remove-all-ops*
   [conn]
-  (let [e->datoms (get-all-op-datoms conn)
-        retract-all-tx-data (map (fn [e] [:db.fn/retractEntity e]) (keys e->datoms))]
+  (let [e->op-map (get-all-ops* conn)
+        retract-all-tx-data (map (fn [e] [:db.fn/retractEntity e]) (keys e->op-map))]
     (d/transact! conn retract-all-tx-data)
-    (map (fn [same-ent-datoms]
-           (into {} (map (juxt :a :v)) same-ent-datoms))
-         (vals e->datoms))))
+    (vals e->op-map)))
 
 (defn get-all-ops
   "Return coll of
@@ -180,7 +194,7 @@
        (keep (fn [[k v]]
                (when (not= :block/uuid k) v))
              m))
-     (get-all-ops* conn))))
+     (vals (get-all-ops* conn)))))
 
 (defn get&remove-all-ops
   "Return coll of

+ 41 - 1
src/main/frontend/worker/rtc/const.cljs

@@ -260,7 +260,47 @@
       [:req-id :string]
       [:action :string]
       [:graph-uuid :string]
-      [:asset-uuids [:sequential :uuid]]]]]))
+      [:asset-uuids [:sequential :uuid]]]]
+    ["get-user-devices"
+     [:map
+      [:req-id :string]
+      [:action :string]]]
+    ["add-user-device"
+     [:map
+      [:req-id :string]
+      [:action :string]
+      [:device-name :string]]]
+    ["remove-user-device"
+     [:map
+      [:req-id :string]
+      [:action :string]
+      [:device-uuid :uuid]]]
+    ["update-user-device-name"
+     [:map
+      [:req-id :string]
+      [:action :string]
+      [:device-uuid :uuid]
+      [:device-name :string]]]
+    ["add-device-public-key"
+     [:map
+      [:req-id :string]
+      [:action :string]
+      [:device-uuid :uuid]
+      [:key-name :string]
+      [:public-key :string]]]
+    ["remove-device-public-key"
+     [:map
+      [:req-id :string]
+      [:action :string]
+      [:device-uuid :uuid]
+      [:key-name :string]]]
+    ["sync-encrypted-aes-key"
+     [:map
+      [:req-id :string]
+      [:action :string]
+      [:device-uuid->encrypted-aes-key [:map-of :uuid :string]]
+      [:graph-uuid :uuid]]]]))
+
 (def data-to-ws-encoder (m/encoder data-to-ws-schema (mt/transformer
                                                       mt/string-transformer
                                                       (mt/key-transformer {:encode m/-keyword->string}))))

+ 143 - 162
src/main/frontend/worker/rtc/core.cljs

@@ -10,13 +10,14 @@
             [frontend.worker.rtc.remote-update :as r.remote-update]
             [frontend.worker.rtc.skeleton]
             [frontend.worker.rtc.ws :as ws]
-            [frontend.worker.rtc.ws-util :as ws-util]
+            [frontend.worker.rtc.ws-util :as ws-util :refer [gen-get-ws-create-map--memoized]]
             [frontend.worker.state :as worker-state]
             [frontend.worker.util :as worker-util]
             [logseq.common.config :as common-config]
             [logseq.db :as ldb]
             [malli.core :as ma]
-            [missionary.core :as m])
+            [missionary.core :as m]
+            [frontend.worker.device :as worker-device])
   (:import [missionary Cancelled]))
 
 (def ^:private rtc-state-schema
@@ -29,17 +30,17 @@
   "Return a flow: receive messages from ws, and filter messages with :req-id=`push-updates` or `online-users-updated`."
   [get-ws-create-task]
   (m/ap
-    (loop []
-      (let [ws (m/? get-ws-create-task)
-            x (try
-                (m/?> (m/eduction
-                       (filter (fn [data] (contains? #{"online-users-updated" "push-updates"} (:req-id data))))
-                       (ws/recv-flow ws)))
-                (catch js/CloseEvent _
-                  sentinel))]
-        (if (identical? x sentinel)
-          (recur)
-          x)))))
+   (loop []
+     (let [ws (m/? get-ws-create-task)
+           x (try
+               (m/?> (m/eduction
+                      (filter (fn [data] (contains? #{"online-users-updated" "push-updates"} (:req-id data))))
+                      (ws/recv-flow ws)))
+               (catch js/CloseEvent _
+                 sentinel))]
+       (if (identical? x sentinel)
+         (recur)
+         x)))))
 
 (defn- create-local-updates-check-flow
   "Return a flow: emit if need to push local-updates"
@@ -58,19 +59,19 @@
   [interval-ms flow]
   (let [v {:type :pull-remote-updates}
         clock-flow (m/ap
-                     (loop []
-                       (m/amb
-                        (m/? (m/sleep interval-ms v))
-                        (recur))))]
+                    (loop []
+                      (m/amb
+                       (m/? (m/sleep interval-ms v))
+                       (recur))))]
     (m/ap
-      (m/amb
-       v
-       (let [_ (m/?< (->> flow
-                          (m/reductions {} nil)
-                          (m/latest identity)))]
-         (try
-           (m/?< clock-flow)
-           (catch Cancelled _ (m/amb))))))))
+     (m/amb
+      v
+      (let [_ (m/?< (->> flow
+                         (m/reductions {} nil)
+                         (m/latest identity)))]
+        (try
+          (m/?< clock-flow)
+          (catch Cancelled _ (m/amb))))))))
 
 (defn- create-mixed-flow
   "Return a flow that emits all kinds of events:
@@ -91,39 +92,17 @@
         mix-flow (m/stream (c.m/mix remote-updates-flow local-updates-check-flow))]
     (c.m/mix mix-flow (create-pull-remote-updates-flow 60000 mix-flow))))
 
-(defn- new-task--get-ws-create
-  "Return a map with atom *current-ws and a task
-  that get current ws, create one if needed(closed or not created yet)"
-  [url & {:keys [retry-count open-ws-timeout]
-          :or {retry-count 10 open-ws-timeout 10000}}]
-  (let [*current-ws (atom nil)
-        ws-create-task (ws/mws-create url {:retry-count retry-count :open-ws-timeout open-ws-timeout})]
-    {:*current-ws *current-ws
-     :get-ws-create-task
-     (m/sp
-       (let [ws @*current-ws]
-         (if (and ws
-                  (not (ws/closed? ws)))
-           ws
-           (let [ws (m/? ws-create-task)]
-             (reset! *current-ws ws)
-             ws))))}))
-
-(def new-task--get-ws-create--memoized
-  "Return a memoized task to reuse the same websocket."
-  (memoize new-task--get-ws-create))
-
 (defn- create-ws-state-flow
   [*current-ws]
   (m/relieve
    (m/ap
-     (let [ws (m/?< (m/watch *current-ws))]
-       (try
-         (if ws
-           (m/?< (ws/create-mws-state-flow ws))
-           (m/amb))
-         (catch Cancelled _
-           (m/amb)))))))
+    (let [ws (m/?< (m/watch *current-ws))]
+      (try
+        (if ws
+          (m/?< (ws/create-mws-state-flow ws))
+          (m/amb))
+        (catch Cancelled _
+          (m/amb)))))))
 
 (defn- create-rtc-state-flow
   [ws-state-flow]
@@ -141,16 +120,16 @@
   we need to ensure that no two concurrent rtc-loop-tasks are modifying `conn` at the same time"
   [started-dfv task]
   (m/sp
-    (when-not (compare-and-set! *rtc-lock nil true)
-      (let [e (ex-info "Must not run multiple rtc-loops, try later"
-                       {:type :rtc.exception/lock-failed
-                        :missionary/retry true})]
-        (started-dfv e)
-        (throw e)))
-    (try
-      (m/? task)
-      (finally
-        (reset! *rtc-lock nil)))))
+   (when-not (compare-and-set! *rtc-lock nil true)
+     (let [e (ex-info "Must not run multiple rtc-loops, try later"
+                      {:type :rtc.exception/lock-failed
+                       :missionary/retry true})]
+       (started-dfv e)
+       (throw e)))
+   (try
+     (m/? task)
+     (finally
+       (reset! *rtc-lock nil)))))
 
 (defn- create-rtc-loop
   "Return a map with [:rtc-state-flow :rtc-loop-task :*rtc-auto-push? :onstarted-task]
@@ -167,7 +146,7 @@
                                      (assert (map? message) message)
                                      (rtc-log-and-state/rtc-log type (assoc message :graph-uuid graph-uuid)))
         {:keys [*current-ws get-ws-create-task]}
-        (new-task--get-ws-create--memoized ws-url)
+        (gen-get-ws-create-map--memoized ws-url)
         get-ws-create-task         (r.client/ensure-register-graph-updates
                                     get-ws-create-task graph-uuid repo conn *last-calibrate-t *online-users)
         {:keys [assets-sync-loop-task]}
@@ -182,40 +161,40 @@
      (holding-rtc-lock
       started-dfv
       (m/sp
-        (try
+       (try
           ;; init run to open a ws
-          (m/? get-ws-create-task)
-          (started-dfv true)
-          (reset! *assets-sync-loop-canceler
-                  (c.m/run-task assets-sync-loop-task :assets-sync-loop-task))
-          (->>
-           (let [event (m/?> mixed-flow)]
-             (case (:type event)
-               :remote-update
-               (try (r.remote-update/apply-remote-update graph-uuid repo conn date-formatter event add-log-fn)
-                    (catch :default e
-                      (when (= ::r.remote-update/need-pull-remote-data (:type (ex-data e)))
-                        (m/? (r.client/new-task--pull-remote-data
-                              repo conn graph-uuid date-formatter get-ws-create-task add-log-fn)))))
-
-               :local-update-check
-               (m/? (r.client/new-task--push-local-ops
-                     repo conn graph-uuid date-formatter
-                     get-ws-create-task add-log-fn))
-
-               :online-users-updated
-               (reset! *online-users (:online-users (:value event)))
-
-               :pull-remote-updates
-               (m/? (r.client/new-task--pull-remote-data
-                     repo conn graph-uuid date-formatter get-ws-create-task add-log-fn))))
-           (m/ap)
-           (m/reduce {} nil)
-           (m/?))
-          (catch Cancelled e
-            (when @*assets-sync-loop-canceler (@*assets-sync-loop-canceler))
-            (add-log-fn :rtc.log/cancelled {})
-            (throw e)))))}))
+         (m/? get-ws-create-task)
+         (started-dfv true)
+         (reset! *assets-sync-loop-canceler
+                 (c.m/run-task assets-sync-loop-task :assets-sync-loop-task))
+         (->>
+          (let [event (m/?> mixed-flow)]
+            (case (:type event)
+              :remote-update
+              (try (r.remote-update/apply-remote-update graph-uuid repo conn date-formatter event add-log-fn)
+                   (catch :default e
+                     (when (= ::r.remote-update/need-pull-remote-data (:type (ex-data e)))
+                       (m/? (r.client/new-task--pull-remote-data
+                             repo conn graph-uuid date-formatter get-ws-create-task add-log-fn)))))
+
+              :local-update-check
+              (m/? (r.client/new-task--push-local-ops
+                    repo conn graph-uuid date-formatter
+                    get-ws-create-task add-log-fn))
+
+              :online-users-updated
+              (reset! *online-users (:online-users (:value event)))
+
+              :pull-remote-updates
+              (m/? (r.client/new-task--pull-remote-data
+                    repo conn graph-uuid date-formatter get-ws-create-task add-log-fn))))
+          (m/ap)
+          (m/reduce {} nil)
+          (m/?))
+         (catch Cancelled e
+           (when @*assets-sync-loop-canceler (@*assets-sync-loop-canceler))
+           (add-log-fn :rtc.log/cancelled {})
+           (throw e)))))}))
 
 (def ^:private empty-rtc-loop-metadata
   {:graph-uuid nil
@@ -232,29 +211,31 @@
 (defn new-task--rtc-start
   [repo token]
   (m/sp
-    (if-let [conn (worker-state/get-datascript-conn repo)]
-      (if-let [graph-uuid (ldb/get-graph-rtc-uuid @conn)]
-        (let [user-uuid (:sub (worker-util/parse-jwt token))
-              config (worker-state/get-config repo)
-              date-formatter (common-config/get-date-formatter config)
-              {:keys [rtc-state-flow *rtc-auto-push? rtc-loop-task *online-users onstarted-task]}
-              (create-rtc-loop graph-uuid repo conn date-formatter token)
-              canceler (c.m/run-task rtc-loop-task :rtc-loop-task)
-              start-ex (m/? onstarted-task)]
-          (if-let [start-ex (:ex-data start-ex)]
-            (r.ex/->map start-ex)
-            (do (reset! *rtc-loop-metadata {:repo repo
-                                            :graph-uuid graph-uuid
-                                            :user-uuid user-uuid
-                                            :rtc-state-flow rtc-state-flow
-                                            :*rtc-auto-push? *rtc-auto-push?
-                                            :*online-users *online-users
-                                            :*rtc-lock *rtc-lock
-                                            :canceler canceler})
-                nil)))
-        (r.ex/->map r.ex/ex-local-not-rtc-graph))
-      (r.ex/->map (ex-info "Not found db-conn" {:type :rtc.exception/not-found-db-conn
-                                                :repo repo})))))
+   ;; ensure device metadata existing first
+   (m/? (worker-device/new-task--ensure-device-metadata! token))
+   (if-let [conn (worker-state/get-datascript-conn repo)]
+     (if-let [graph-uuid (ldb/get-graph-rtc-uuid @conn)]
+       (let [user-uuid (:sub (worker-util/parse-jwt token))
+             config (worker-state/get-config repo)
+             date-formatter (common-config/get-date-formatter config)
+             {:keys [rtc-state-flow *rtc-auto-push? rtc-loop-task *online-users onstarted-task]}
+             (create-rtc-loop graph-uuid repo conn date-formatter token)
+             canceler (c.m/run-task rtc-loop-task :rtc-loop-task)
+             start-ex (m/? onstarted-task)]
+         (if-let [start-ex (:ex-data start-ex)]
+           (r.ex/->map start-ex)
+           (do (reset! *rtc-loop-metadata {:repo repo
+                                           :graph-uuid graph-uuid
+                                           :user-uuid user-uuid
+                                           :rtc-state-flow rtc-state-flow
+                                           :*rtc-auto-push? *rtc-auto-push?
+                                           :*online-users *online-users
+                                           :*rtc-lock *rtc-lock
+                                           :canceler canceler})
+               nil)))
+       (r.ex/->map r.ex/ex-local-not-rtc-graph))
+     (r.ex/->map (ex-info "Not found db-conn" {:type :rtc.exception/not-found-db-conn
+                                               :repo repo})))))
 
 (defn rtc-stop
   []
@@ -269,32 +250,32 @@
 
 (defn new-task--get-graphs
   [token]
-  (let [{:keys [get-ws-create-task]} (new-task--get-ws-create--memoized (ws-util/get-ws-url token))]
+  (let [{:keys [get-ws-create-task]} (gen-get-ws-create-map--memoized (ws-util/get-ws-url token))]
     (m/join :graphs
             (ws-util/send&recv get-ws-create-task {:action "list-graphs"}))))
 
 (defn new-task--delete-graph
   "Return a task that return true if succeed"
   [token graph-uuid]
-  (let [{:keys [get-ws-create-task]} (new-task--get-ws-create--memoized (ws-util/get-ws-url token))]
+  (let [{:keys [get-ws-create-task]} (gen-get-ws-create-map--memoized (ws-util/get-ws-url token))]
     (m/sp
-      (let [{:keys [ex-data]}
-            (m/? (ws-util/send&recv get-ws-create-task
-                                    {:action "delete-graph" :graph-uuid graph-uuid}))]
-        (when ex-data (prn ::delete-graph-failed graph-uuid ex-data))
-        (boolean (nil? ex-data))))))
+     (let [{:keys [ex-data]}
+           (m/? (ws-util/send&recv get-ws-create-task
+                                   {:action "delete-graph" :graph-uuid graph-uuid}))]
+       (when ex-data (prn ::delete-graph-failed graph-uuid ex-data))
+       (boolean (nil? ex-data))))))
 
 (defn new-task--get-user-info
   "Return a task that return users-info about the graph."
   [token graph-uuid]
-  (let [{:keys [get-ws-create-task]} (new-task--get-ws-create--memoized (ws-util/get-ws-url token))]
+  (let [{:keys [get-ws-create-task]} (gen-get-ws-create-map--memoized (ws-util/get-ws-url token))]
     (m/join :users
             (ws-util/send&recv get-ws-create-task
                                {:action "get-users-info" :graph-uuid graph-uuid}))))
 
 (defn new-task--grant-access-to-others
   [token graph-uuid & {:keys [target-user-uuids target-user-emails]}]
-  (let [{:keys [get-ws-create-task]} (new-task--get-ws-create--memoized (ws-util/get-ws-url token))]
+  (let [{:keys [get-ws-create-task]} (gen-get-ws-create-map--memoized (ws-util/get-ws-url token))]
     (ws-util/send&recv get-ws-create-task
                        (cond-> {:action "grant-access"
                                 :graph-uuid graph-uuid}
@@ -304,7 +285,7 @@
 (defn new-task--get-block-content-versions
   "Return a task that return map [:ex-data :ex-message :versions]"
   [token graph-uuid block-uuid]
-  (let [{:keys [get-ws-create-task]} (new-task--get-ws-create--memoized (ws-util/get-ws-url token))]
+  (let [{:keys [get-ws-create-task]} (gen-get-ws-create-map--memoized (ws-util/get-ws-url token))]
     (m/join :versions (ws-util/send&recv get-ws-create-task
                                          {:action "query-block-content-versions"
                                           :block-uuids [block-uuid]
@@ -313,27 +294,27 @@
 (def ^:private create-get-state-flow
   (let [rtc-loop-metadata-flow (m/watch *rtc-loop-metadata)]
     (m/ap
-      (let [{rtc-lock :*rtc-lock :keys [repo graph-uuid user-uuid rtc-state-flow *rtc-auto-push? *online-users]}
-            (m/?< rtc-loop-metadata-flow)]
-        (try
-          (when (and repo rtc-state-flow *rtc-auto-push? rtc-lock)
-            (m/?<
-             (m/latest
-              (fn [rtc-state rtc-auto-push? rtc-lock online-users pending-local-ops-count local-tx remote-tx]
-                {:graph-uuid graph-uuid
-                 :user-uuid user-uuid
-                 :unpushed-block-update-count pending-local-ops-count
-                 :local-tx local-tx
-                 :remote-tx remote-tx
-                 :rtc-state rtc-state
-                 :rtc-lock rtc-lock
-                 :auto-push? rtc-auto-push?
-                 :online-users online-users})
-              rtc-state-flow (m/watch *rtc-auto-push?) (m/watch rtc-lock) (m/watch *online-users)
-              (client-op/create-pending-ops-count-flow repo)
-              (rtc-log-and-state/create-local-t-flow graph-uuid)
-              (rtc-log-and-state/create-remote-t-flow graph-uuid))))
-          (catch Cancelled _))))))
+     (let [{rtc-lock :*rtc-lock :keys [repo graph-uuid user-uuid rtc-state-flow *rtc-auto-push? *online-users]}
+           (m/?< rtc-loop-metadata-flow)]
+       (try
+         (when (and repo rtc-state-flow *rtc-auto-push? rtc-lock)
+           (m/?<
+            (m/latest
+             (fn [rtc-state rtc-auto-push? rtc-lock online-users pending-local-ops-count local-tx remote-tx]
+               {:graph-uuid graph-uuid
+                :user-uuid user-uuid
+                :unpushed-block-update-count pending-local-ops-count
+                :local-tx local-tx
+                :remote-tx remote-tx
+                :rtc-state rtc-state
+                :rtc-lock rtc-lock
+                :auto-push? rtc-auto-push?
+                :online-users online-users})
+             rtc-state-flow (m/watch *rtc-auto-push?) (m/watch rtc-lock) (m/watch *online-users)
+             (client-op/create-pending-ops-count-flow repo)
+             (rtc-log-and-state/create-local-t-flow graph-uuid)
+             (rtc-log-and-state/create-remote-t-flow graph-uuid))))
+         (catch Cancelled _))))))
 
 (defn new-task--get-debug-state
   []
@@ -341,13 +322,13 @@
 
 (defn new-task--snapshot-graph
   [token graph-uuid]
-  (let [{:keys [get-ws-create-task]} (new-task--get-ws-create--memoized (ws-util/get-ws-url token))]
+  (let [{:keys [get-ws-create-task]} (gen-get-ws-create-map--memoized (ws-util/get-ws-url token))]
     (m/join #(select-keys % [:snapshot-uuid :graph-uuid])
             (ws-util/send&recv get-ws-create-task {:action "snapshot-graph"
                                                    :graph-uuid graph-uuid}))))
 (defn new-task--snapshot-list
   [token graph-uuid]
-  (let [{:keys [get-ws-create-task]} (new-task--get-ws-create--memoized (ws-util/get-ws-url token))]
+  (let [{:keys [get-ws-create-task]} (gen-get-ws-create-map--memoized (ws-util/get-ws-url token))]
     (m/join :snapshot-list
             (ws-util/send&recv get-ws-create-task {:action "snapshot-list"
                                                    :graph-uuid graph-uuid}))))
@@ -355,25 +336,25 @@
 (defn new-task--upload-graph
   [token repo remote-graph-name]
   (m/sp
-    (if-let [conn (worker-state/get-datascript-conn repo)]
-      (let [{:keys [get-ws-create-task]} (new-task--get-ws-create--memoized (ws-util/get-ws-url token))]
-        (m/? (r.upload-download/new-task--upload-graph get-ws-create-task repo conn remote-graph-name)))
-      (r.ex/->map (ex-info "Not found db-conn" {:type :rtc.exception/not-found-db-conn
-                                                :repo repo})))))
+   (if-let [conn (worker-state/get-datascript-conn repo)]
+     (let [{:keys [get-ws-create-task]} (gen-get-ws-create-map--memoized (ws-util/get-ws-url token))]
+       (m/? (r.upload-download/new-task--upload-graph get-ws-create-task repo conn remote-graph-name)))
+     (r.ex/->map (ex-info "Not found db-conn" {:type :rtc.exception/not-found-db-conn
+                                               :repo repo})))))
 
 (defn new-task--request-download-graph
   [token graph-uuid]
-  (let [{:keys [get-ws-create-task]} (new-task--get-ws-create--memoized (ws-util/get-ws-url token))]
+  (let [{:keys [get-ws-create-task]} (gen-get-ws-create-map--memoized (ws-util/get-ws-url token))]
     (r.upload-download/new-task--request-download-graph get-ws-create-task graph-uuid)))
 
 (defn new-task--download-info-list
   [token graph-uuid]
-  (let [{:keys [get-ws-create-task]} (new-task--get-ws-create--memoized (ws-util/get-ws-url token))]
+  (let [{:keys [get-ws-create-task]} (gen-get-ws-create-map--memoized (ws-util/get-ws-url token))]
     (r.upload-download/new-task--download-info-list get-ws-create-task graph-uuid)))
 
 (defn new-task--wait-download-info-ready
   [token download-info-uuid graph-uuid timeout-ms]
-  (let [{:keys [get-ws-create-task]} (new-task--get-ws-create--memoized (ws-util/get-ws-url token))]
+  (let [{:keys [get-ws-create-task]} (gen-get-ws-create-map--memoized (ws-util/get-ws-url token))]
     (r.upload-download/new-task--wait-download-info-ready
      get-ws-create-task download-info-uuid graph-uuid timeout-ms)))
 

+ 96 - 94
src/main/frontend/worker/rtc/full_upload_download_graph.cljs

@@ -5,6 +5,7 @@
             [clojure.set :as set]
             [datascript.core :as d]
             [frontend.common.missionary-util :as c.m]
+            [frontend.worker.crypt :as crypt]
             [frontend.worker.db-listener :as db-listener]
             [frontend.worker.rtc.client-op :as client-op]
             [frontend.worker.rtc.const :as rtc-const]
@@ -113,38 +114,41 @@
 (defn new-task--upload-graph
   [get-ws-create-task repo conn remote-graph-name]
   (m/sp
-    (rtc-log-and-state/rtc-log :rtc.log/upload {:sub-type :fetch-presigned-put-url
-                                                :message "fetching presigned put-url"})
-    (let [[{:keys [url key]} all-blocks-str]
-          (m/?
-           (m/join
-            vector
-            (ws-util/send&recv get-ws-create-task {:action "presign-put-temp-s3-obj"})
-            (m/sp
-              (let [all-blocks (export-as-blocks @conn)]
-                (ldb/write-transit-str all-blocks)))))]
-      (rtc-log-and-state/rtc-log :rtc.log/upload {:sub-type :upload-data
-                                                  :message "uploading data"})
-      (c.m/<? (http/put url {:body all-blocks-str :with-credentials? false}))
-      (rtc-log-and-state/rtc-log :rtc.log/upload {:sub-type :request-upload-graph
-                                                  :message "requesting upload-graph"})
-      (let [upload-resp
-            (m/? (ws-util/send&recv get-ws-create-task {:action "upload-graph"
-                                                        :s3-key key
-                                                        :graph-name remote-graph-name}))]
-        (if-let [graph-uuid (:graph-uuid upload-resp)]
-          (do
-            (ldb/transact! conn
-                           [{:db/ident :logseq.kv/graph-uuid :kv/value graph-uuid}
-                            {:db/ident :logseq.kv/graph-local-tx :kv/value "0"}])
-            (client-op/update-graph-uuid repo graph-uuid)
-            (when-not rtc-const/RTC-E2E-TEST
-              (let [^js worker-obj (:worker/object @worker-state/*state)]
-                (m/? (c.m/await-promise (.storeMetadata worker-obj repo (pr-str {:kv/value graph-uuid}))))))
-            (rtc-log-and-state/rtc-log :rtc.log/upload {:sub-type :upload-completed
-                                                        :message "upload-graph completed"})
-            {:graph-uuid graph-uuid})
-          (throw (ex-info "upload-graph failed" {:upload-resp upload-resp})))))))
+   (rtc-log-and-state/rtc-log :rtc.log/upload {:sub-type :fetch-presigned-put-url
+                                               :message "fetching presigned put-url"})
+   (let [[{:keys [url key]} all-blocks-str]
+         (m/?
+          (m/join
+           vector
+           (ws-util/send&recv get-ws-create-task {:action "presign-put-temp-s3-obj"})
+           (m/sp
+            (let [all-blocks (export-as-blocks @conn)]
+              (ldb/write-transit-str all-blocks)))))]
+     (rtc-log-and-state/rtc-log :rtc.log/upload {:sub-type :upload-data
+                                                 :message "uploading data"})
+     (c.m/<? (http/put url {:body all-blocks-str :with-credentials? false}))
+     (rtc-log-and-state/rtc-log :rtc.log/upload {:sub-type :request-upload-graph
+                                                 :message "requesting upload-graph"})
+     (let [aes-key (c.m/<? (crypt/<gen-aes-key))
+           aes-key-jwk (ldb/write-transit-str (c.m/<? (crypt/<export-key aes-key)))
+           upload-resp
+           (m/? (ws-util/send&recv get-ws-create-task {:action "upload-graph"
+                                                       :s3-key key
+                                                       :graph-name remote-graph-name}))]
+       (if-let [graph-uuid (:graph-uuid upload-resp)]
+         (do
+           (ldb/transact! conn
+                          [{:db/ident :logseq.kv/graph-uuid :kv/value graph-uuid}
+                           {:db/ident :logseq.kv/graph-local-tx :kv/value "0"}])
+           (client-op/update-graph-uuid repo graph-uuid)
+           (crypt/store-graph-keys-jwk repo aes-key-jwk)
+           (when-not rtc-const/RTC-E2E-TEST
+             (let [^js worker-obj (:worker/object @worker-state/*state)]
+               (c.m/<? (.storeMetadata worker-obj repo (pr-str {:kv/value graph-uuid})))))
+           (rtc-log-and-state/rtc-log :rtc.log/upload {:sub-type :upload-completed
+                                                       :message "upload-graph completed"})
+           {:graph-uuid graph-uuid})
+         (throw (ex-info "upload-graph failed" {:upload-resp upload-resp})))))))
 
 (def page-of-block
   (memoize
@@ -230,7 +234,6 @@
        [schema-blocks (conj normal-blocks block)]))
    [[] []] blocks))
 
-
 (defn- create-graph-for-rtc-test
   "it's complex to setup db-worker related stuff, when I only want to test rtc related logic"
   [repo init-tx-data other-tx-data]
@@ -256,9 +259,9 @@
   (let [{:keys [t blocks]} all-blocks
         card-one-attrs (blocks->card-one-attrs blocks)
         blocks (worker-util/profile :convert-card-one-value-from-value-coll
-                 (map (partial convert-card-one-value-from-value-coll card-one-attrs) blocks))
+                                    (map (partial convert-card-one-value-from-value-coll card-one-attrs) blocks))
         blocks (worker-util/profile :normalize-remote-blocks
-                 (normalized-remote-blocks-coercer blocks))
+                                    (normalized-remote-blocks-coercer blocks))
         ;;TODO: remove this, client/schema already converted to :db/cardinality, :db/valueType by remote,
         ;; and :client/schema should be removed by remote too
         blocks (map #(dissoc % :client/schema) blocks)
@@ -271,26 +274,25 @@
                              schema-blocks)
         ^js worker-obj (:worker/object @worker-state/*state)]
     (m/sp
-      (client-op/update-local-tx repo t)
-      (rtc-log-and-state/update-local-t graph-uuid t)
-      (rtc-log-and-state/update-remote-t graph-uuid t)
-      (if rtc-const/RTC-E2E-TEST
-        (create-graph-for-rtc-test repo init-tx-data tx-data)
-        (m/?
-         (c.m/await-promise
-          (p/do!
-            (.createOrOpenDB worker-obj repo (ldb/write-transit-str {:close-other-db? false}))
-            (.exportDB worker-obj repo)
-            (.transact worker-obj repo init-tx-data {:rtc-download-graph? true
-                                                     :gen-undo-ops? false
+     (client-op/update-local-tx repo t)
+     (rtc-log-and-state/update-local-t graph-uuid t)
+     (rtc-log-and-state/update-remote-t graph-uuid t)
+     (if rtc-const/RTC-E2E-TEST
+       (create-graph-for-rtc-test repo init-tx-data tx-data)
+       (c.m/<?
+        (p/do!
+         (.createOrOpenDB worker-obj repo (ldb/write-transit-str {:close-other-db? false}))
+         (.exportDB worker-obj repo)
+         (.transact worker-obj repo init-tx-data {:rtc-download-graph? true
+                                                  :gen-undo-ops? false
                                                      ;; only transact db schema, skip validation to avoid warning
-                                                     :skip-validate-db? true
-                                                     :persist-op? false} (worker-state/get-context))
-            (.transact worker-obj repo tx-data {:rtc-download-graph? true
-                                                :gen-undo-ops? false
-                                                :persist-op? false} (worker-state/get-context))
-            (transact-block-refs! repo)))))
-      (worker-util/post-message :add-repo {:repo repo}))))
+                                                  :skip-validate-db? true
+                                                  :persist-op? false} (worker-state/get-context))
+         (.transact worker-obj repo tx-data {:rtc-download-graph? true
+                                             :gen-undo-ops? false
+                                             :persist-op? false} (worker-state/get-context))
+         (transact-block-refs! repo))))
+     (worker-util/post-message :add-repo {:repo repo}))))
 
 ;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;; async download-graph ;;
@@ -315,48 +317,48 @@
   [get-ws-create-task download-info-uuid graph-uuid timeout-ms]
   (->
    (m/sp
-     (rtc-log-and-state/rtc-log :rtc.log/download {:sub-type :wait-remote-graph-data-ready
-                                                   :message "waiting for the remote to prepare the data"
-                                                   :graph-uuid graph-uuid})
-     (loop []
-       (m/? (m/sleep 3000))
-       (let [{:keys [download-info-list]}
-             (m/? (ws-util/send&recv get-ws-create-task {:action "download-info-list"
-                                                         :graph-uuid graph-uuid}))]
-         (if-let [found-download-info
-                  (some
-                   (fn [download-info]
-                     (when (and (= download-info-uuid (:download-info-uuid download-info))
-                                (:download-info-s3-url download-info))
-                       download-info))
-                   download-info-list)]
-           found-download-info
-           (recur)))))
+    (rtc-log-and-state/rtc-log :rtc.log/download {:sub-type :wait-remote-graph-data-ready
+                                                  :message "waiting for the remote to prepare the data"
+                                                  :graph-uuid graph-uuid})
+    (loop []
+      (m/? (m/sleep 3000))
+      (let [{:keys [download-info-list]}
+            (m/? (ws-util/send&recv get-ws-create-task {:action "download-info-list"
+                                                        :graph-uuid graph-uuid}))]
+        (if-let [found-download-info
+                 (some
+                  (fn [download-info]
+                    (when (and (= download-info-uuid (:download-info-uuid download-info))
+                               (:download-info-s3-url download-info))
+                      download-info))
+                  download-info-list)]
+          found-download-info
+          (recur)))))
    (m/timeout timeout-ms :timeout)))
 
 (defn new-task--download-graph-from-s3
   [graph-uuid graph-name s3-url]
   (m/sp
-    (rtc-log-and-state/rtc-log :rtc.log/download {:sub-type :downloading-graph-data
-                                                  :message "downloading graph data"
-                                                  :graph-uuid graph-uuid})
-    (let [^js worker-obj              (:worker/object @worker-state/*state)
-          {:keys [status body] :as r} (c.m/<? (http/get s3-url {:with-credentials? false}))
-          repo                        (str sqlite-util/db-version-prefix graph-name)]
-      (if (not= 200 status)
-        (throw (ex-info "download-graph from s3 failed" {:resp r}))
-        (do
-          (rtc-log-and-state/rtc-log :rtc.log/download {:sub-type :transact-graph-data-to-db
-                                                        :message "transacting graph data to local db"
-                                                        :graph-uuid graph-uuid})
-          (let [all-blocks (ldb/read-transit-str body)]
-            (worker-state/set-rtc-downloading-graph! true)
-            (m/? (new-task--transact-remote-all-blocks all-blocks repo graph-uuid))
-            (client-op/update-graph-uuid repo graph-uuid)
-            (when-not rtc-const/RTC-E2E-TEST
-              (m/? (c.m/await-promise (.storeMetadata worker-obj repo (pr-str {:kv/value graph-uuid})))))
-            (worker-state/set-rtc-downloading-graph! false)
-            (rtc-log-and-state/rtc-log :rtc.log/download {:sub-type :download-completed
-                                                          :message "download completed"
-                                                          :graph-uuid graph-uuid})
-            nil))))))
+   (rtc-log-and-state/rtc-log :rtc.log/download {:sub-type :downloading-graph-data
+                                                 :message "downloading graph data"
+                                                 :graph-uuid graph-uuid})
+   (let [^js worker-obj              (:worker/object @worker-state/*state)
+         {:keys [status body] :as r} (c.m/<? (http/get s3-url {:with-credentials? false}))
+         repo                        (str sqlite-util/db-version-prefix graph-name)]
+     (if (not= 200 status)
+       (throw (ex-info "download-graph from s3 failed" {:resp r}))
+       (do
+         (rtc-log-and-state/rtc-log :rtc.log/download {:sub-type :transact-graph-data-to-db
+                                                       :message "transacting graph data to local db"
+                                                       :graph-uuid graph-uuid})
+         (let [all-blocks (ldb/read-transit-str body)]
+           (worker-state/set-rtc-downloading-graph! true)
+           (m/? (new-task--transact-remote-all-blocks all-blocks repo graph-uuid))
+           (client-op/update-graph-uuid repo graph-uuid)
+           (when-not rtc-const/RTC-E2E-TEST
+             (c.m/<? (.storeMetadata worker-obj repo (pr-str {:kv/value graph-uuid}))))
+           (worker-state/set-rtc-downloading-graph! false)
+           (rtc-log-and-state/rtc-log :rtc.log/download {:sub-type :download-completed
+                                                         :message "download completed"
+                                                         :graph-uuid graph-uuid})
+           nil))))))

+ 44 - 22
src/main/frontend/worker/rtc/ws_util.cljs

@@ -23,16 +23,16 @@
   [ws message]
   {:pre [(= "apply-ops" (:action message))]}
   (m/sp
-    (let [decoded-message (rtc-const/data-to-ws-coercer (assoc message :req-id "temp-id"))
-          message-str (js/JSON.stringify (clj->js (select-keys (rtc-const/data-to-ws-encoder decoded-message)
-                                                               ["graph-uuid" "ops" "t-before"])))
-          len (.-length (utf8/encode message-str))]
-      (when (< 100000 len)
-        (let [{:keys [url key]} (m/? (ws/send&recv ws {:action "presign-put-temp-s3-obj"}))
-              {:keys [status] :as resp} (c.m/<? (http/put url {:body message-str :with-credentials? false}))]
-          (when-not (http/unexceptional-status? status)
-            (throw (ex-info "failed to upload apply-ops message" {:resp resp})))
-          key)))))
+   (let [decoded-message (rtc-const/data-to-ws-coercer (assoc message :req-id "temp-id"))
+         message-str (js/JSON.stringify (clj->js (select-keys (rtc-const/data-to-ws-encoder decoded-message)
+                                                              ["graph-uuid" "ops" "t-before"])))
+         len (.-length (utf8/encode message-str))]
+     (when (< 100000 len)
+       (let [{:keys [url key]} (m/? (ws/send&recv ws {:action "presign-put-temp-s3-obj"}))
+             {:keys [status] :as resp} (c.m/<? (http/put url {:body message-str :with-credentials? false}))]
+         (when-not (http/unexceptional-status? status)
+           (throw (ex-info "failed to upload apply-ops message" {:resp resp})))
+         key)))))
 
 (defn send&recv
   "Return a task: throw exception if recv ex-data response.
@@ -43,19 +43,41 @@
   - adjust its timeout to 20s"
   [get-ws-create-task message]
   (m/sp
-    (let [ws (m/? get-ws-create-task)
-          opts (when (and (= "apply-ops" (:action message))
-                          (< 400 (count (:ops message))))
-                 {:timeout-ms 20000})
-          s3-key (when (= "apply-ops" (:action message))
-                   (m/? (put-apply-ops-message-on-s3-if-too-huge ws message)))
-          message* (if s3-key
-                     (-> message
-                         (assoc :s3-key s3-key)
-                         (dissoc :graph-uuid :ops :t-before))
-                     message)]
-      (handle-remote-ex (m/? (ws/send&recv ws message* opts))))))
+   (let [ws (m/? get-ws-create-task)
+         opts (when (and (= "apply-ops" (:action message))
+                         (< 400 (count (:ops message))))
+                {:timeout-ms 20000})
+         s3-key (when (= "apply-ops" (:action message))
+                  (m/? (put-apply-ops-message-on-s3-if-too-huge ws message)))
+         message* (if s3-key
+                    (-> message
+                        (assoc :s3-key s3-key)
+                        (dissoc :graph-uuid :ops :t-before))
+                    message)]
+     (handle-remote-ex (m/? (ws/send&recv ws message* opts))))))
 
 (defn get-ws-url
   [token]
   (gstring/format @worker-state/*rtc-ws-url token))
+
+(defn- gen-get-ws-create-map
+  "Return a map with atom *current-ws and a task
+  that get current ws, create one if needed(closed or not created yet)"
+  [url & {:keys [retry-count open-ws-timeout]
+          :or {retry-count 10 open-ws-timeout 10000}}]
+  (let [*current-ws (atom nil)
+        ws-create-task (ws/mws-create url {:retry-count retry-count :open-ws-timeout open-ws-timeout})]
+    {:*current-ws *current-ws
+     :get-ws-create-task
+     (m/sp
+      (let [ws @*current-ws]
+        (if (and ws
+                 (not (ws/closed? ws)))
+          ws
+          (let [ws (m/? ws-create-task)]
+            (reset! *current-ws ws)
+            ws))))}))
+
+(def gen-get-ws-create-map--memoized
+  "Return a memoized task to reuse the same websocket."
+  (memoize gen-get-ws-create-map))

+ 1 - 1
src/main/frontend/worker/search.cljs

@@ -420,7 +420,7 @@ DROP TRIGGER IF EXISTS blocks_au;
     (let [fuzzy-blocks-to-add (filter page-or-object? blocks-to-add)
           fuzzy-blocks-to-remove (filter page-or-object? blocks-to-remove)]
       (when (or (seq fuzzy-blocks-to-add) (seq fuzzy-blocks-to-remove))
-        (swap! fuzzy-search-indices update-in repo
+        (swap! fuzzy-search-indices update repo
                (fn [indice]
                  (when indice
                    (doseq [page-entity fuzzy-blocks-to-remove]

+ 2 - 3
src/main/frontend/worker/state.cljs

@@ -26,14 +26,13 @@
 
                        ;; new implementation
                        :undo/repo->ops (atom {})
-                       :redo/repo->ops (atom {})
-                       }))
+                       :redo/repo->ops (atom {})}))
 
 (defonce *rtc-ws-url (atom nil))
 
 (defonce *sqlite (atom nil))
 ;; repo -> {:db conn :search conn :client-ops conn}
-(defonce *sqlite-conns (atom nil))
+(defonce *sqlite-conns (atom {}))
 ;; repo -> conn
 (defonce *datascript-conns (atom nil))
 

+ 720 - 0
src/resources/dicts/ca.edn

@@ -0,0 +1,720 @@
+{:all-files                                         "Llista d'arxius"
+ :all-graphs                                        "Llista de grafs"
+ :all-journals                                      "Llista de diaris"
+ :all-pages                                         "Llista de pàgines"
+ :all-whiteboards                                   "Totes les pissarres"
+ :auto-heading                                      "Encapçalaments automàtics"
+ :bold                                              "Negreta"
+ :cancel                                            "Cancel·lar"
+ :close                                             "Tancar"
+ :code                                              "Codi"
+ :delete                                            "Eliminar"
+ :download                                          "Descarregar"
+ :export                                            "Exportar"
+ :export-copied-to-clipboard                        "¡Copiat al porta-retalls!"
+ :export-copy-to-clipboard                          "Copiar al porta-retalls"
+ :export-edn                                        "Exportar a EDN"
+ :export-graph                                      "Exportar graf"
+ :export-json                                       "Exportar a JSON"
+ :export-markdown                                   "Exportar com Markdown estàndard (sense propietats de bloc)"
+ :export-opml                                       "Exportar a OPML"
+ :export-page                                       "Exportar pàgina"
+ :export-public-pages                               "Exportar pàgines públiques"
+ :export-roam-json                                  "Exportar a Roam JSON"
+ :export-save-to-file                               "Desar a arxiu"
+ :export-transparent-background                     "Fons transparent"
+ :graph                                             "Graf"
+ :heading                                           "Encapçalament {1}"
+ :help-shortcut-title                               "Clic per veure dreceres i altres suggeriments"
+ :highlight                                         "Ressaltat"
+ :home                                              "Inici"
+ :host                                              "Servidor"
+ :import                                            "Importar"
+ :importing                                         "Important"
+ :italics                                           "Cursiva"
+ :language                                          "Idioma"
+ :loading                                           "Carregant..."
+ :loading-files                                     "Carregant arxius"
+ :login                                             "Iniciar sessió"
+ :logout                                            "Tancar sessió"
+ :new-graph                                         "Afegir nou graf"
+ :new-page                                          "Nova pàgina:"
+ :open-a-directory                                  "Obrir un directori local"
+ :parsing-files                                     "Analitzant arxius"
+ :plugins                                           "Extensions"
+ :port                                              "Port"
+ :re-index                                          "Reindexar"
+ :re-index-detail                                   "Reconstruir el graf"
+ :re-index-discard-unsaved-changes-warning          "Al reindexar es descartarà el graf actual i es processaran de nou tots els arxius segons com estan actualment emmagatzemats al disc. Perdrà els canvis no desats i pot tardar una mica. Continuar?"
+ :re-index-multiple-windows-warning                 "Ha de tancar les altres finestres abans de reindexar aquest graf."
+ :relaunch-confirm-to-work                          "Ha de reiniciar la aplicació per fer que funcioni. Desitja reiniciar-la ara?"
+ :remove-background                                 "Eliminar el fons"
+ :remove-heading                                    "Eliminar encapçalament"
+ :remove-orphaned-pages                             "Eliminar pàgines orfes?"
+ :save                                              "Desar"
+ :settings                                          "Opcions"
+ :settings-of-plugins                               "Opcions d'Extensions"
+ :strikethrough                                     "Ratllat"
+ :submit                                            "Enviar"
+ :sync-from-local-changes-detected                  "Refrescar detecta i processa els arxius modificats al seu disc que difereixen del contingut actual de la pàgina a Logseq. Continuar?"
+ :sync-from-local-files                             "Refrescar"
+ :sync-from-local-files-detail                      "Importar canvis dels arxius locals"
+ :themes                                            "Temes"
+ :toggle-theme                                      "Alternar tema"
+ :type                                              "Tipus"
+ :untitled                                          "Sense títol"
+ :whiteboards                                       "Pissarres"
+ :yes                                               "Si"
+ :accessibility/skip-to-main-content                "Anar a contingut principal"
+ :asset/confirm-delete                              "¿Està segur que desitja eliminar aquest {1}?"
+ :asset/copy                                        "Copiar imatge"
+ :asset/delete                                      "Eliminar imatge"
+ :asset/maximize                                    "Maximitzar imatge"
+ :asset/open-in-browser                             "Obrir imatge al navegador"
+ :asset/physical-delete                             "Eliminar també l'arxiu (aquesta acció no es pot desfer)"
+ :asset/show-in-folder                              "Mostrar imatge a la carpeta"
+ :block/name                                        "Nom de pàgina"
+ :bug-report/clipboard-inspector-title              "Inspector de dades del porta-retalls"
+ :bug-report/inspector-page-btn-back                "Retrocedir"
+ :bug-report/inspector-page-btn-copy                "Copiar el resultat"
+ :bug-report/inspector-page-btn-create-issue        "Crear un report"
+ :bug-report/inspector-page-copy-notif              "¡Copiat al porta-retalls!"
+ :bug-report/inspector-page-desc-1                  "Pressiona Ctrl+V / ⌘+V per inspeccionar el porta-retalls"
+ :bug-report/inspector-page-desc-2                  "o fes clic aquí per enganxar si estàs utilitzant la versió mòbil"
+ :bug-report/inspector-page-desc-clipboard          "Aquí estan les dades llegides del porta-retalls."
+ :bug-report/inspector-page-desc-copy               "Si això està be per compartir, fes clic al botó de copiar."
+ :bug-report/inspector-page-desc-create-issue       "Ara pots reportar el resultat enganxat al porta-retalls. Si us plau, enganxa el resultat a la secció 'Context addicional' i menciona d'on has copiat el contingut original. ¡Gràcies!"
+ :bug-report/inspector-page-placeholder             "Mantingui pressionat aquí per enganxar si està en un dispositiu mòbil"
+ :bug-report/inspector-page-tip                     "Hi ha alguna errada? Cap problema, fes clic per tornar al pas anterior."
+ :bug-report/main-desc                              "Podries ajudar-nos reportant un error? Nosaltres el resolem tan aviat com puguem."
+ :bug-report/main-title                             "Reportar una errada"
+ :bug-report/section-clipboard-btn-desc             "Inspeccionar i recol·lectar dades del porta-retalls"
+ :bug-report/section-clipboard-btn-title            "Ajudant del porta-retalls"
+ :bug-report/section-clipboard-desc                 "Pots utilitzar aquestes pràctiques eines per proporcionar-nos informació addicional."
+ :bug-report/section-clipboard-title                "La errada està relacionada amb aquestes característiques?"
+ :bug-report/section-issues-btn-desc                "¡Ajuda a millorar Logseq!"
+ :bug-report/section-issues-btn-title               "Enviar un report de errades"
+ :bug-report/section-issues-desc                    "Si no hi ha eines disponibles per recopilar informació addicional, reporta la errada directament."
+ :bug-report/section-issues-title                   "O ..."
+ :color/blue                                        "Blau"
+ :color/gray                                        "Gris"
+ :color/green                                       "Verd"
+ :color/pink                                        "Rosa"
+ :color/purple                                      "Púrpura"
+ :color/red                                         "Vermell"
+ :color/yellow                                      "Groc"
+ :command.auto-complete/complete                    "Auto-completar: Escollir elements seleccionats"
+ :command.auto-complete/next                        "Auto-completar: Seleccionar següent element"
+ :command.auto-complete/prev                        "Auto-completar: Seleccionar element anterior"
+ :command.auto-complete/shift-complete              "Auto-completar: Obrir element seleccionat al panell lateral"
+ :command.cards/forgotten                           "Targetes: oblidades "
+ :command.cards/next-card                           "Targetes: targeta següent"
+ :command.cards/recall                              "Targetes: esperat un moment per recordar"
+ :command.cards/remembered                          "Targetes: recordades"
+ :command.cards/toggle-answers                      "Targetes: mostrar/ocultar respostes/prova de completar frases "
+ :command.command-palette/toggle                    "Cercar ordres"
+ :command.command/run                               "Executar ordre git"
+ :command.command/toggle-favorite                   "Afegir/Eliminar de favorits"
+ :command.dev/show-block-ast                        "(Dev) Mostrar AST de bloc"
+ :command.dev/show-block-data                       "(Dev) Mostrar dades de bloc"
+ :command.dev/show-page-ast                         "(Dev) Mostrar AST de pàgina"
+ :command.dev/show-page-data                        "(Dev) Mostrar dades de pàgina"
+ :command.editor/backspace                          "Retrocedir / Eliminar endarrere"
+ :command.editor/backward-kill-word                 "Esborrar paraula anterior"
+ :command.editor/backward-word                      "Moure cursor una paraula endarrere"
+ :command.editor/beginning-of-block                 "Moure cursor al inici del bloc"
+ :command.editor/bold                               "Negreta"
+ :command.editor/clear-block                        "Esborrar el contingut del bloc"
+ :command.editor/collapse-block-children            "Col·lapsar"
+ :command.editor/copy                               "Copiar"
+ :command.editor/copy-current-file                  "Copiar l'arxiu actual"
+ :command.editor/copy-embed                         "Copiar bloc per incrustar"
+ :command.editor/copy-page-url                      "Copiar url de pàgina"
+ :command.editor/copy-text                          "Copiar selecció com text"
+ :command.editor/cut                                "Enganxar"
+ :command.editor/cycle-todo                         "Rotar estat TODO del element"
+ :command.editor/delete                             "Suprimir / Eliminar endavant"
+ :command.editor/delete-selection                   "Eliminar blocs seleccionats"
+ :command.editor/down                               "Moure cursor avall / Seleccionar avall"
+ :command.editor/end-of-block                       "Moure cursor al final del bloc"
+ :command.editor/escape-editing                     "Sortir de edició"
+ :command.editor/expand-block-children              "Expandir"
+ :command.editor/follow-link                        "Seguir enllaç sota el cursor"
+ :command.editor/forward-kill-word                  "Esborrar paraula següent"
+ :command.editor/forward-word                       "Moure cursor una paraula endavant"
+ :command.editor/highlight                          "Ressaltat"
+ :command.editor/indent                             "Augmentar sagnat"
+ :command.editor/insert-link                        "Enllaç html"
+ :command.editor/insert-youtube-timestamp           "Inserir marca de temps de YouTube"
+ :command.editor/italics                            "Cursiva"
+ :command.editor/kill-line-after                    "Esborrar línia posterior al cursor"
+ :command.editor/kill-line-before                   "Esborrar línia anterior al cursor"
+ :command.editor/left                               "Moure cursor a l'esquerra / Obrir bloc seleccionat al inici"
+ :command.editor/move-block-down                    "Moure bloc avall"
+ :command.editor/move-block-up                      "Moure bloc amunt"
+ :command.editor/new-block                          "Crear bloc nou"
+ :command.editor/new-line                           "Nova línia al bloc"
+ :command.editor/new-whiteboard                     "Nova pissarra"
+ :command.editor/open-edit                          "Editar bloc seleccionat"
+ :command.editor/open-file-in-default-app           "Obrir arxiu a la aplicació per defecte"
+ :command.editor/open-file-in-directory             "Obrir arxiu al directori pare"
+ :command.editor/open-link-in-sidebar               "Obrir enllaç al panell lateral"
+ :command.editor/outdent                            "Disminuir indentació"
+ :command.editor/paste-text-in-one-block-at-point   "Enganxar text en un bloc al punt"
+ :command.editor/redo                               "Refer"
+ :command.editor/replace-block-reference-at-point   "Reemplaçar referència de bloc amb el seu contingut al punt"
+ :command.editor/right                              "Moure cursor a la dreta / Obrir bloc seleccionat al final"
+ :command.editor/select-all-blocks                  "Seleccionar tots els blocs"
+ :command.editor/select-block-down                  "Seleccionar bloc de sota"
+ :command.editor/select-block-up                    "Seleccionar bloc de sobre"
+ :command.editor/select-down                        "Seleccionar el contingut a continuació"
+ :command.editor/select-parent                      "Seleccionar bloc pare"
+ :command.editor/select-up                          "Seleccionar contingut anterior"
+ :command.editor/strike-through                     "Ratllat"
+ :command.editor/toggle-number-list                 "Alternar llista de nombres"
+ :command.editor/toggle-open-blocks                 "Alternar blocs oberts, (col·lapsar o expandir tots)"
+ :command.editor/toggle-block-children              "Alternar Expandir/Col·lapsar"
+ :command.editor/undo                               "Desfer"
+ :command.editor/up                                 "Moure cursor amunt / Seleccionar amunt"
+ :command.editor/zoom-in                            "Apropar / Endavant"
+ :command.editor/zoom-out                           "Allunyar / Enrere"
+ :command.git/commit                                "Confirmar"
+ :command.go/all-graphs                             "Anar a tots els grafs"
+ :command.go/all-pages                              "Anar a totes les pàgines"
+ :command.go/backward                               "Enrere"
+ :command.go/electron-find-in-page                  "Trobar text a la pàgina"
+ :command.go/electron-jump-to-the-next              "Saltar a la següent coincidència de la cerca"
+ :command.go/electron-jump-to-the-previous          "Saltar a la coincidència anterior de la cerca"
+ :command.go/flashcards                             "Alternar targetes de memorització"
+ :command.go/forward                                "Endavant"
+ :command.go/graph-view                             "Anar a la vista de grafs"
+ :command.go/home                                   "Anar a la pàgina d'inici"
+ :command.go/journals                               "Anar al diari"
+ :command.go/keyboard-shortcuts                     "Anar a dreceres de teclat"
+ :command.go/next-journal                           "Anar al següent diari"
+ :command.go/prev-journal                           "Anar al diari anterior"
+ :command.go/search                                 "Cercar al graf"
+ :command.go/search-in-page                         "Cercar blocs a la pàgina"
+ :command.go/tomorrow                               "Anar a demà"
+ :command.go/whiteboards                            "Anar a pissarres"
+ :command.graph/add                                 "Afegir un graf"
+ :command.graph/export-as-html                      "Exportar un graf públic com  html "
+ :command.graph/open                                "Seleccionar graf a obrir"
+ :command.graph/re-index                            "Reindexar graf actual"
+ :command.graph/remove                              "Eliminar un graf"
+ :command.misc/copy                                 "Copiar"
+ :command.pdf/close                                 "Pdf: Tancar document pdf actual"
+ :command.pdf/find                                  "Pdf: Cercar text en el document pdf actual"
+ :command.pdf/next-page                             "Pdf: Pàgina següent del document pdf actual"
+ :command.pdf/previous-page                         "Pdf: Pàgina anterior del document pdf actual"
+ :command.search/re-index                           "Reconstruir índex de cerca"
+ :command.sidebar/clear                             "Tancar tot al panell lateral dret"
+ :command.sidebar/close-top                         "Tancar l'element superior del panell lateral dret"
+ :command.sidebar/open-today-page                   "Obrir diari al panell lateral dret"
+ :command.ui/clear-all-notifications                "Esborrar totes les notificacions"
+ :command.ui/goto-plugins                           "Anar al panell d'extensions"
+ :command.ui/install-plugins-from-file              "Instal·lar extensions de plugins.edn"
+ :command.ui/select-theme-color                     "Seleccionar temes de colors disponibles"
+ :command.ui/toggle-brackets                        "Alternar claudàtors"
+ :command.ui/toggle-contents                        "Alternar contingut al panell lateral"
+ :command.ui/toggle-document-mode                   "Alternar mode document"
+ :command.ui/toggle-help                            "Alternar ajuda"
+ :command.ui/toggle-left-sidebar                    "Alternar panell lateral esquerra"
+ :command.ui/toggle-right-sidebar                   "Alternar panell lateral dret"
+ :command.ui/toggle-settings                        "Alternar Opcions"
+ :command.ui/toggle-theme                           "Alternar entre tema clar/fosc"
+ :command.ui/toggle-wide-mode                       "Alternar mode ample"
+ :command.whiteboard/bring-forward                  "Avançar"
+ :command.whiteboard/bring-to-front                 "Moure a primer pla"
+ :command.whiteboard/clone-down                     "Clonar avall"
+ :command.whiteboard/clone-left                     "Clonar a la esquerra"
+ :command.whiteboard/clone-right                    "Clonar a la dreta"
+ :command.whiteboard/clone-up                       "Clonar amunt"
+ :command.whiteboard/connector                      "Eina connector"
+ :command.whiteboard/ellipse                        "Eina el·lipse"
+ :command.whiteboard/eraser                         "Eina esborrador"
+ :command.whiteboard/group                          "Agrupar selecció"
+ :command.whiteboard/highlighter                    "Eina ressaltar"
+ :command.whiteboard/lock                           "Bloquejar selecció"
+ :command.whiteboard/pan                            "Eina moure"
+ :command.whiteboard/pencil                         "Eina llapis"
+ :command.whiteboard/portal                         "Eina portal"
+ :command.whiteboard/rectangle                      "Eina rectangle"
+ :command.whiteboard/reset-zoom                     "Restablir zoom"
+ :command.whiteboard/select                         "Seleccionar eina"
+ :command.whiteboard/send-backward                  "Retrocedir"
+ :command.whiteboard/send-to-back                   "Moure enrere"
+ :command.whiteboard/text                           "Eina text"
+ :command.whiteboard/toggle-grid                    "Alternar la quadrícula del llenç"
+ :command.whiteboard/ungroup                        "Desagrupar selecció"
+ :command.whiteboard/unlock                         "Desbloquejar selecció"
+ :command.whiteboard/zoom-in                        "Apropar"
+ :command.whiteboard/zoom-out                       "Allunyar"
+ :command.whiteboard/zoom-to-fit                    "Zoom al dibuix"
+ :command.whiteboard/zoom-to-selection              "Zoom per ajustar a la selecció"
+ :command.window/close                              "Tancar finestra"
+ :content/click-to-edit                             "Clic per editar"
+ :content/copy-block-emebed                         "Copiar bloc a incrustar (embed)"
+ :content/copy-block-ref                            "Copiar referència de bloc"
+ :content/copy-block-url                            "Copiar URL del bloc"
+ :content/copy-export-as                            "Copiar / Exportar com..."
+ :content/copy-ref                                  "Copiar aquesta referència"
+ :content/delete-ref                                "Eliminar aquesta referència"
+ :content/open-in-sidebar                           "Obrir a panell lateral"
+ :content/replace-with-embed                        "Reemplaçar amb incrustació"
+ :content/replace-with-text                         "Reemplaçar amb text"
+ :context-menu/input-template-name                  "Quin es el nom de  la plantilla?"
+ :context-menu/make-a-flashcard                     "Crear una targeta de memorització"
+ :context-menu/make-a-template                      "Crear una plantilla"
+ :context-menu/preview-flashcard                    "Vista prèvia de targeta de memorització"
+ :context-menu/template-exists-warning              "¡La plantilla ja existeix!"
+ :context-menu/template-include-parent-block        "Incloure el bloc pare a la plantilla?"
+ :context-menu/toggle-number-list                   "Alternar llista de nombres"
+ :dev/show-block-ast                                "(Dev) Mostrar AST de bloc"
+ :dev/show-block-data                               "(Dev) Mostrar dades de bloc"
+ :dev/show-page-ast                                 "(Dev) Mostrar AST de pàgina"
+ :dev/show-page-data                                "(Dev) Mostrar dades de pàgina"
+ :editor/block-search                               "Cercar un bloc"
+ :editor/collapse-block-children                    "Col·lapsar tot"
+ :editor/copy                                       "Copiar"
+ :editor/cut                                        "Tallar"
+ :editor/cycle-todo                                 "Rotar l'estat de la TODO del element actual"
+ :editor/delete-selection                           "Eliminar els blocs seleccionats"
+ :editor/expand-block-children                      "Expandir tot"
+ :file/format-not-supported                         "Format .{1} no suportat."
+ :file/last-modified-at                             "Data de modificació"
+ :file/name                                         "Nom del arxiu"
+ :file/no-data                                      "No hi ha dades"
+ :file-sync/connectivity-testing-failed             "Han fallat les proves de connexió a la xarxa. Consulti la configuració de xarxa. URLs de prova: "
+ :file-sync/graph-deleted                           "El graf remot actual s'ha eliminat"
+ :file-sync/other-user-graph                        "El graf local actual està unit al graf remot de un altre usuari, per tant no es pot començar a sincronitzar"
+ :file-sync/rsapi-cannot-upload-err                 "No es possible començar la sincronització, comprovi si la hora local es correcta."
+ :file/validate-existing-file-error                 "La pàgina existeix en un altre arxiu: {1}, arxiu actual: {2}. Si us plau conservi només un arxiu i reindexi el graf. "
+ :flashcards/modal-btn-forgotten                    "Oblidat"
+ :flashcards/modal-btn-hide-answers                 "Ocultar resposta"
+ :flashcards/modal-btn-next-card                    "Següent"
+ :flashcards/modal-btn-recall                       "Necessito una estona per recordar"
+ :flashcards/modal-btn-remembered                   "Recordat"
+ :flashcards/modal-btn-reset                        "Reiniciar"
+ :flashcards/modal-btn-reset-tip                    "Reiniciar aquesta targeta per poder-la revisar immediatament."
+ :flashcards/modal-btn-show-answers                 "Veure respostes"
+ :flashcards/modal-btn-show-clozes                  "Veure clozes"
+ :flashcards/modal-current-total                    "Actual/Total"
+ :flashcards/modal-finished                         "¡Felicitats, has revisat totes les targetes a aquesta consulta, ene veiem la propera vegada! 💯"
+ :flashcards/modal-overdue-total                    "Endarrerit/Total"
+ :flashcards/modal-select-all                       "Tot"
+ :flashcards/modal-select-switch                    "Canviar a"
+ :flashcards/modal-toggle-preview-mode              "Alternar el mode de vista prèvia"
+ :flashcards/modal-toggle-random-mode               "Alternar el mode aleatori"
+ :flashcards/modal-welcome-desc-1                   "Pot afegir \"#card\" a qualsevol bloc per convertir-lo en una targeta o executar \"/cloze\" per afegir alguns clozes."
+ :flashcards/modal-welcome-desc-2                   "Pot "
+ :flashcards/modal-welcome-desc-3                   "fer clic en aquest enllaç"
+ :flashcards/modal-welcome-desc-4                   " per revisar la documentació."
+ :flashcards/modal-welcome-title                    "¡És hora de crear una targeta!"
+ :graph/all-graphs                                  "Tots els grafs"
+ :graph/local-graphs                                "Grafs locals:"
+ :graph/remote-graphs                               "Grafs remots:"
+ :handbook/close                                    "Tancar"
+ :handbook/help-categories                          "Categories d'ajuda"
+ :handbook/home                                     "Inici"
+ :handbook/popular-topics                           "Temes populars"
+ :handbook/search                                   "Cercar"
+ :handbook/settings                                 "Opcions"
+ :handbook/title                                    "Ajuda"
+ :handbook/topics                                   "Temes"
+ :header/go-back                                    "Retrocedir"
+ :header/go-forward                                 "Avançar"
+ :header/more                                       "Mes"
+ :header/search                                     "Cercar"
+ :header/toggle-left-sidebar                        "Alternar panell lateral esquerre"
+ :help/about                                        "Sobre Logseq"
+ :help/awesome-logseq                               "Increïble Logseq"
+ :help/block-reference                              "Referència de bloc"
+ :help/blog                                         "Blog de Logseq"
+ :help/bug                                          "Reportar un problema"
+ :help/changelog                                    "Registre de canvis"
+ :help/context-menu                                 "Menú contextual"
+ :help/docs                                         "Documentació"
+ :help/feature                                      "Sol·licitar una funció"
+ :help/forum-community                              "Fòrum de la comunitat"
+ :help/markdown-syntax                              "Sintaxi de Markdown"
+ :help/open-link-in-sidebar                         "Obrir enllaç al panell lateral"
+ :help/org-mode-syntax                              "Sintaxi del mode Org"
+ :help/privacy                                      "Política de privacitat"
+ :help/reference-autocomplete                       "Referència de pàgina"
+ :help/roadmap                                      "Full de ruta"
+ :help/search                                       "Cercar pàgines/blocs/ordres"
+ :help/shortcut                                     "Drecera"
+ :help/shortcut-page-title                          "Dreceres de teclat"
+ :help/shortcuts                                    "Dreceres de teclat"
+ :help/shortcuts-triggers                           "Iniciadors"
+ :help/slash-autocomplete                           "Autocompletat de barra '/'"
+ :help/start                                        "Començar"
+ :help/terms                                        "Termes"
+ :help/title-about                                  "Sobre la"
+ :help/title-community                              "Comunitat"
+ :help/title-development                            "Desenvolupament"
+ :help/title-terms                                  "Termes"
+ :help/title-usage                                  "Utilització"
+ :keymap/all                                        "Tot"
+ :keymap/conflicts-for-label                        "Conflicte per combinació de tecles per"
+ :keymap/custom                                     "Personalitzat"
+ :keymap/customize-for-label                        "Personalitzar dreceres"
+ :keymap/disabled                                   "Deshabilitat"
+ :keymap/keystroke-filter                           "Filtre de pulsació de tecles"
+ :keymap/keystroke-record-desc                      "Pressioni qualsevol seqüencia de tecles per filtrar dreceres"
+ :keymap/keystroke-record-setup-label               "Pressioni qualsevol seqüencia de tecles per establir una drecera"
+ :keymap/restore-to-default                         "Restablir a valor predeterminat del sistema"
+ :keymap/search                                     "Cercar"
+ :keymap/total                                      "Dreceres de teclat totals"
+ :keymap/unset                                      "Sense establir"
+ :left-side-bar/journals                            "Diaris"
+ :left-side-bar/nav-favorites                       "Preferits"
+ :left-side-bar/nav-recent-pages                    "Recents"
+ :left-side-bar/switch                              "Canviar a:"
+ :linked-references/filter-directions               "Faci clic per incloure i shift-clic per excloure. Faci clic un altre cop per eliminar."
+ :linked-references/filter-excludes                 "Excloure: "
+ :linked-references/filter-heading                  "Filtre"
+ :linked-references/filter-includes                 "Incloure: "
+ :linked-references/filter-search                   "Cercar a les pàgines vinculades"
+ :linked-references/reference-count (fn [filtered-count total] (str (when filtered-count (str filtered-count " de ")) total (if (= total 1) " Referència vinculada" " Referencies vinculades")))
+ :notification/clear-all                            "Netejar tot"
+ :on-boarding/command-palette-quick-tour            "Tour ràpid per acostumar-se"
+ :on-boarding/importing-desc                        "Si estan en format JSON, EDN o Markdown Logseq pot treballar amb ells."
+ :on-boarding/importing-lsq-desc                    "Importar un arxiu EDN o JSON exportat del teu graf de Logseq."
+ :on-boarding/importing-main-desc                   "També pot fer això després a la aplicació."
+ :on-boarding/importing-main-title                  "Importar notes existents"
+ :on-boarding/importing-opml-desc                   "Importar arxius OPML"
+ :on-boarding/importing-roam-desc                   "Importar un arxiu JSON exportat del teu graf de Roam"
+ :on-boarding/importing-title                       "Tens notes que vulguis importar?"
+ :on-boarding/main-desc                             "Primer tens que escollir una carpeta on Logseq desarà els teus pensaments, idees i notes."
+ :on-boarding/main-title (fn [] ["¡Benvingut a " [:strong "Logseq!"]])
+ :on-boarding/quick-tour-btn-back                   "Anterior"
+ :on-boarding/quick-tour-btn-finish                 "Finalitzar"
+ :on-boarding/quick-tour-btn-next                   "Següent"
+ :on-boarding/quick-tour-btn-skip                   "Saltar tour ràpid"
+ :on-boarding/quick-tour-favorites-desc-1           "Ancora les teves pàgines preferides a través del menú `... `  en qualsevol pàgina."
+ :on-boarding/quick-tour-favorites-desc-2           "També afegim algunes pàgines plantilla per ajudar-te a començar. Pots eliminar-les un cop comencis a escriure les teves pròpies notes."
+ :on-boarding/quick-tour-favorites-title            "⭐️ Preferides"
+ :on-boarding/quick-tour-help-desc                  "Sempre pots fer clic aquí per ajuda i altra informació sobre Logseq."
+ :on-boarding/quick-tour-help-title                 "❓ Ajuda"
+ :on-boarding/quick-tour-journal-page-desc-1        "Aquesta es la pàgina del diari d'avui. Aquí pots reflectir els teus pensaments, aprenentatge i idees. No et preocupis per organitzar-los. Només escriu i "
+ :on-boarding/quick-tour-journal-page-desc-2        "[[enllaça]]"
+ :on-boarding/quick-tour-journal-page-desc-3        "els teus pensaments."
+ :on-boarding/quick-tour-journal-page-title         "📆 Pàgina de Diari"
+ :on-boarding/quick-tour-left-sidebar-desc          "Obre el panell lateral esquerre per explorar elements importants del menú a Logseq."
+ :on-boarding/quick-tour-left-sidebar-title         "👀 Panell Lateral Esquerre"
+ :on-boarding/quick-tour-steps                      "PAS "
+ :on-boarding/tour-whiteboard-btn-back              "Anterior"
+ :on-boarding/tour-whiteboard-btn-finish            "Finalitzar"
+ :on-boarding/tour-whiteboard-btn-next              "Següent"
+ :on-boarding/tour-whiteboard-home                  "{1} Localització per les teves pissarres"
+ :on-boarding/tour-whiteboard-home-description      "Les pissarres tenen la seva pròpia secció a la aplicació on les hi pots fer una ullada, crear-de de noves o eliminar-les fàcilment."
+ :on-boarding/tour-whiteboard-new                   "{1} Crear una pissarra nova"
+ :on-boarding/tour-whiteboard-new-description       "Existeixen moltes maneres de crear una pissarra nova. Una d'elles està sempre aquí al panell."
+ :on-boarding/welcome-whiteboard-modal-description  "Les pissarres son una gran eina per fer pluja de idees i organització. Ara pots col·locar qualsevol pensament de la base de coneixement o nou un al costat de l'altre en un llenç especial per connectar-los, associar-los i entendre'ls de noves maneres."
+ :on-boarding/welcome-whiteboard-modal-skip         "Ometre"
+ :on-boarding/welcome-whiteboard-modal-start        "Començar a utilitzar la pissarra"
+ :on-boarding/welcome-whiteboard-modal-title        "Un nou llenç pels teus pensaments." 
+ :page/add-to-favorites                             "Afegir a Preferits"
+ :page/backlinks                                    "Enllaços per retrocedir"
+ :page/copy-page-url                                "Copiar URL de la pàgina"
+ :page/created-at                                   "Creada el"
+ :page/delete                                       "Eliminar pàgina"
+ :page/delete-confirmation                          "Està segur que desitja eliminar aquesta pàgina i el seu arxiu?"
+ :page/illegal-page-name                            "¡Nom de pàgina no permès!"
+ :page/logseq-is-having-a-problem                   "Logseq està tenint un problema. Per intentar tornar a un estat de treball, intenti el següent procediment en ordre:"
+ :page/make-private                                 "Fer privada"
+ :page/make-public                                  "Fer pública al publicar"
+ :page/open-backup-directory                        "Obrir el directori de còpia de seguretat de la pàgina"
+ :page/open-in-finder                               "Obrir ubicació del arxiu"
+ :page/open-with-default-app                        "Obrir amb la aplicació preestablerta"
+ :page/page-already-exists                          "¡La pàgina “{1}” ja existeix!"
+ :page/slide-view                                   "Veure com diapositives"
+ :page/slide-view-tip-go-fullscreen (fn [] [[:span.opacity-70 "Consell: pressiona "] [:code "f"] [:span.opacity-70 " per anar a pantalla completa"]])
+ :page/something-went-wrong                         "Alguna cosa ha sortit malament"
+ :page/step                                         "Pas {1}"
+ :page/try                                          "Intentar"
+ :page/unfavorite                                   "Treure pàgina de preferides"
+ :page/updated-at                                   "Actualitzada el"
+ :page/version-history                              "Revisar l'historial de la pàgina"
+ :page/whiteboard-to-journal-error                  "¡Les pàgines de pissarra no poden ser reanomenades amb títol de diari!"
+ :pdf/auto-open-context-menu                        "Obrir automàticament el menú contextual per a selecció."
+ :pdf/copy-ref                                      "Copiar referència"
+ :pdf/copy-text                                     "Copiar text"
+ :pdf/doc-metadata                                  "Metadades del document"
+ :pdf/hl-block-colored                              "Etiqueta de color para ressaltar bloc"
+ :pdf/linked-ref                                    "Referències vinculades"
+ :pdf/toggle-dashed                                 "Estil puntejat per les àrees ressaltades"
+ :plugin/all                                        "Tot"
+ :plugin/all-updated                                "¡Tot està actualitzat!"
+ :plugin/auto-check-for-updates                     "Comprovació automàtica d'actualitzacions"
+ :plugin/check-all-updates                          "Cercar actualitzacions"
+ :plugin/checking-for-updates                       "Comprovant actualitzacions d'extensions..."
+ :plugin/check-update                               "Cercar actualització"
+ :plugin/contribute                                 "✨ Escriure i publicar una nova extensió"
+ :plugin/custom-js-alert                            "S'ha trobat l'arxiu custom.js, desitja permetre que s'executi? (Si no entén el contingut d'aquest arxiu es recomana no permetre la seva execució ja que representa certs riscs de seguretat)."
+ :plugin/delete-alert                               "Està segur de desinstal·lar la extensió [{1}]?"
+ :plugin/disabled                                   "Inactiu"
+ :plugin/downloads                                  "Descàrregues"
+ :plugin/enabled                                    "Habilitat"
+ :plugin/found-n-updates                            "S'han trobat {1} actualitzacions"
+ :plugin/found-updates                              "Noves actualitzacions"
+ :plugin/install                                    "Instal·lar"
+ :plugin/installed                                  "Instal·lat"
+ :plugin/installed-plugin                           "Extensió instal·lada: {1}"
+ :plugin.install-from-file/menu-title               "Instal·lar des de plugins.edn"
+ :plugin.install-from-file/notice                   "Les següents extensions reemplaçaran les seves extensions: "
+ :plugin.install-from-file/success                  "¡Totes les extensions han estat instal·lades !"
+ :plugin.install-from-file/title                    "Instal·lar  extensions des de  plugins.edn"
+ :plugin/installing                                 "Instal·lant"
+ :plugin/list-of-updates                            "Actualitzacions d'extensions: "
+ :plugin/load-unpacked                              "Carregar extensió desempaquetada"
+ :plugin/marketplace                                "Catàleg"
+ :plugin/not-installed                              "No instal·lat"
+ :plugin/open-logseq-dir                            "Obrir"
+ :plugin/open-package                               "Obrir paquet"
+ :plugin/open-preferences                           "Obrir preferències"
+ :plugin/open-settings                              "Obrir opcions"
+ :plugin/refresh-lists                              "Actualitzar llistes"
+ :plugin/reload                                     "Recarregar"
+ :plugin/remote-error                               "Error remot: "
+ :plugin/restart                                    "Reiniciar la aplicació"
+ :plugin/search-plugin                              "Cercar extensions"
+ :plugin/security-warning                           "Les extensions poden accedir al teu graf, arxius locals i emetre sol·licituds de xarxa."
+ :plugin/stars                                      "Estrelles"
+ :plugin/title                                      "Títol ({1})"
+ :plugin/uninstall                                  "Desinstal·lar"
+ :plugin/unpacked                                   "Desempaquetat"
+ :plugin/unpacked-tips                              "Seleccionar el directori de la extensió"
+ :plugin/update                                     "Actualitzar"
+ :plugin/update-all-selected                        "Actualitzar tot lo seleccionat"
+ :plugin/update-available                           "Actualització disponible"
+ :plugin/update-plugin                              "Actualitzat extensió: {1} - {2}"
+ :plugin/updates-downloading                        "Descarregant totes les actualitzacions"
+ :plugin/updating                                   "Actualitzant"
+ :plugin/up-to-date                                 "Està actualitzat {1}"
+ :query/config-property-settings                    "Configuració de propietats per aquesta consulta:"
+ :right-side-bar/all-pages                          "Llista de pàgines"
+ :right-side-bar/block-ref                          "Referència de bloc"
+ :right-side-bar/contents                           "Contingut"
+ :right-side-bar/flashcards                         "Targetes de memorització"
+ :right-side-bar/graph-view                         "Vista de Graf"
+ :right-side-bar/help                               "Ajuda"
+ :right-side-bar/page-graph                         "Graf de pàgina"
+ :right-side-bar/pane-close                         "Tancar"
+ :right-side-bar/pane-close-all                     "Tancar tot"
+ :right-side-bar/pane-close-others                  "Tancar altres"
+ :right-side-bar/pane-collapse                      "Col·lapsar"
+ :right-side-bar/pane-collapse-all                  "Col·lapsar tot"
+ :right-side-bar/pane-collapse-others               "Col·lapsar altres"
+ :right-side-bar/pane-expand                        "Expandir"
+ :right-side-bar/pane-expand-all                    "Expandir tot"
+ :right-side-bar/pane-more                          "Mes"
+ :right-side-bar/pane-open-as-page                  "Obrir com pàgina"
+ :right-side-bar/separator                          "Eina per redimensionar panell dret "
+ :right-side-bar/show-journals                      "Mostrar Diaris"
+ :right-side-bar/switch-theme                       "Temes"
+ :right-side-bar/toggle-right-sidebar               "Alternar panell lateral dret"
+ :right-side-bar/whiteboards                        "Pissarres"
+ :search-item/no-result                             "Sense resultats coincidents"
+ :search-item/page                                  "Pàgina"
+ :search-item/whiteboard                            "Pissarra"
+ :select/default-prompt                             "Seleccioni un"
+ :select/default-select-multiple                    "Seleccioni un o varis"
+ :select.graph/add-graph                            "Si, afegir un altre graf"
+ :select.graph/empty-placeholder-description        "No trobem un graf. Vol afegir-ne un?"
+ :select.graph/prompt                               "Seleccioni un graf"
+ :settings-page/alpha-features                      "Característiques Alfa"
+ :settings-page/app-updated                         "La aplicació està actualitzada 🎉"
+ :settings-page/auto-chmod                          "Canviar automàticament els permisos dels arxius"
+ :settings-page/auto-chmod-desc                     "Desactivar els permisos de edició dels usuaris amb permisos atorgats per pertanyen-ça a un grup."
+ :settings-page/auto-expand-block-refs              "Expandir referències de bloc automàticament al fer un apropament"
+ :settings-page/auto-expand-block-refs-tip          "Aquesta opció controla si expandir el bloc de referències automàticament al fer un apropament."
+ :settings-page/auto-updater                        "Auto actualitzador"
+ :settings-page/beta-features                       "Característiques Beta"
+ :settings-page/changelog                           "Què hi ha de nou?"
+ :settings-page/check-for-updates                   "Cerca d'actualitzacions"
+ :settings-page/checking                            "Cercant ..."
+ :settings-page/clear                               "Netejar"
+ :settings-page/clear-cache                         "Netejar memòria cau"
+ :settings-page/clear-cache-warning                 "Netejar la memòria cau tancarà els grafs oberts. Es perdran en canvis no desats."
+ :settings-page/current-version                     "Versió actual"
+ :settings-page/custom-configuration                "Configuració personalitzada"
+ :settings-page/custom-date-format                  "Format de data preferida"
+ :settings-page/custom-date-format-notification     "Ha de reindexar el graf per que aquest canvi tingui efecte"
+ :settings-page/custom-date-format-warning          "¡Es requereix reindexar! ¡Les referències existents del diari podrien estar trencades!"
+ :settings-page/custom-global-configuration         "Configuració global personalitzada"
+ :settings-page/custom-theme                        "Tema personalitzat"
+ :settings-page/developer-mode                      "Mode desenvolupador"
+ :settings-page/developer-mode-desc                 "El mode desenvolupador permet als col·laboradors i desenvolupadors d'extensions provar les integracions amb Logseq de manera més eficient."
+ :settings-page/disable-sentry                      "Enviar dades d'ús i diagnòstics a Logseq"
+ :settings-page/disable-sentry-desc                 " Logseq mai recol·lectarà la seva base de dades de grafs local o vendrà les seves dades."
+ :settings-page/edit-config-edn                     "Editar config.edn (per aquest repositori)"
+ :settings-page/edit-custom-css                     "Editar custom.css"
+ :settings-page/edit-export-css                     "Editar export.css"
+ :settings-page/edit-global-config-edn              "Editar config.edn global"
+ :settings-page/enable-all-pages-public             "Fer totes les pàgines publiques al publicar"
+ :settings-page/enable-flashcards                   "Targetes de memorització"
+ :settings-page/enable-journals                     "Habilitar diaris"
+ :settings-page/enable-shortcut-tooltip             "Habilitar descripció emergent de dreceres de teclat"
+ :settings-page/enable-timetracking                 "Habilitar rastreig de temps"
+ :settings-page/enable-tooltip                      "Habilitar descripció emergent"
+ :settings-page/enable-whiteboards                  "Pissarres"
+ :settings-page/export-theme                        "Tema exportació"
+ :settings-page/git-commit-delay                    "Temps en segons para Git auto commit"
+ :settings-page/git-desc-1                          "Para veure l'historial de edició de la pàgina, fer clic als tres punts horitzontals a la cantonada superior dreta i seleccionar \"Veure historial de la pàgina\"."
+ :settings-page/git-desc-2                          "Per usuaris professionals, Logseq també es compatible amb"
+ :settings-page/git-desc-3                          " per control de versions. Utilitzi Git sota el seu propi risc ja que l'equip de Logseq no dona suport a problemes generals amb Git."
+ :settings-page/git-commit-on-close  "Git auto commit"
+ :settings-page/git-switcher-label                  "Habilitar Git auto commit"
+ :settings-page/git-tip                             "Si te Logseq Sync habilitat, pot veure l'historial de edició de la pàgina directament. Aquesta secció es només per coneixedors de tecnologia."
+ :settings-page/home-default-page                   "Establir pàgina d'inici"
+ :settings-page/login-prompt                        "Per accedir a noves característiques abans que ningú ha de ser patrocinador de Logseq en Open Collective y després iniciar sessió."
+ :settings-page/native-titlebar                     "Barra de títol nativa"
+ :settings-page/native-titlebar-desc                "Habilita la barra de títol nativa de la finestra de Windows i Linux."
+ :settings-page/network-proxy                       "Proxy de xarxa"
+ :settings-page/plugin-system                       "Habilitar extensions"
+ :settings-page/preferred-file-format               "Format d'arxiu preferit"
+ :settings-page/preferred-outdenting                "Disminució lògica de sagnat"
+ :settings-page/preferred-outdenting-tip            "El costat esquerra mostra el sagnat amb la configuració per defecte, i el dret mostra amb el sagnat lògic habilitat"
+ :settings-page/preferred-outdenting-tip-more       "→ conèixer més"
+ :settings-page/preferred-pasting-file              "Preferir enganxar arxiu"
+ :settings-page/preferred-pasting-file-hint         "Quan està habilitat, al enganxar una imatge de internet la imatge serà descarregada i inserida. Quan està deshabilitat, l'enllaç a la imatge serà enganxat."
+ :settings-page/preferred-workflow                  "Flux de treball preferit"
+ :settings-page/revision                            "Revisió: "
+ :settings-page/show-brackets                       "Mostrar claudàtors"
+ :settings-page/show-full-blocks                    "Mostrar totes les línies de una referència a bloc"
+ :settings-page/spell-checker                       "Corrector ortogràfic"
+ :settings-page/sync                                "Sincronitzar"
+ :settings-page/sync-desc-1                         "Clic"
+ :settings-page/sync-desc-2                         "aquí"
+ :settings-page/sync-desc-3                         "per instruccions de com configurar i utilitzar Sync."
+ :settings-page/sync-diff-merge                     "Habilitar unió intel·ligent al sincronitzar"
+ :settings-page/sync-diff-merge-desc                "Unir actualitzacions locals amb arxius remots automàticament quan existeixi un conflicte, en lloc de sobreescriure els arxius remots"
+ :settings-page/sync-diff-merge-warn                "La capacitat d'unió intel·ligent només s'activa en un client després de la primera sincronització amb èxit del graf amb el servidor remot a la nova versió de LogSeq. Habilitar això en tots els dispositius per millorar l'experiència."
+ :settings-page/tab-account                         "Compte"
+ :settings-page/tab-advanced                        "Avançat"
+ :settings-page/tab-assets                          "Recursos"
+ :settings-page/tab-editor                          "Editor"
+ :settings-page/tab-features                        "Característiques"
+ :settings-page/tab-general                         "General"
+ :settings-page/tab-keymap                          "Mapa de teclat"
+ :settings-page/tab-version-control                 "Control de versions"
+ :settings-page/theme-dark                          "fosc"
+ :settings-page/theme-light                         "clar"
+ :settings-page/theme-system                        "sistema"
+ :settings-page/update-available                    "S'ha trobat una nova versió"
+ :settings-page/update-error-1                      "¡⚠️ Ups, alguna cosa ha fallat!"
+ :settings-page/update-error-2                      " Si us plau comprovi"
+ :settings-permission/start-granting                "Permetre"
+ :shortcut.category/basics                          "Bàsic"
+ :shortcut.category/block-command-editing           "Ordres d'edició de bloc"
+ :shortcut.category/block-editing                   "Edició de bloc general"
+ :shortcut.category/block-selection                 "Selecció de blocs (prémer Esc para sortir)"
+ :shortcut.category/formatting                      "Format"
+ :shortcut.category/navigating                      "Navegació"
+ :shortcut.category/others                          "Altres"
+ :shortcut.category/plugins                         "Extensions"
+ :shortcut.category/toggle                          "Alternar"
+ :shortcut.category/whiteboard                      "Pissarra"
+ :text/image                                        "Imatge"
+ :tips/all-done                                     "¡Tot fet!"
+ :unlinked-references/reference-count (fn [total] (str total (if (= total 1) " Referència desvinculada" " Referències desvinculades")))
+ :updater/new-version-install                       "S'ha descarregat una nova versió."
+ :updater/quit-and-install                          "Reiniciar per instal·lar"
+ :whiteboard/add-block-or-page                      "Afegir bloc o pàgina"
+ :whiteboard/align-bottom                           "Alinear a baix"
+ :whiteboard/align-center-horizontally              "Alinear al centre horitzontalment"
+ :whiteboard/align-center-vertically                "Alinear al centre verticalment"
+ :whiteboard/align-left                             "Alinear a la esquerra"
+ :whiteboard/align-right                            "Alinear a la dreta"
+ :whiteboard/align-top                              "Alinear a dalt"
+ :whiteboard/arrow-head                             "Cap de fletxa"
+ :whiteboard/auto-resize                            "Canviar mida automàticament"
+ :whiteboard/bold                                   "Negreta"
+ :whiteboard/circle                                 "Cercle"
+ :whiteboard/collapse                               "Col·lapsar"
+ :whiteboard/color                                  "Color"
+ :whiteboard/connector                              "Connector"
+ :whiteboard/copy                                   "Copiar"
+ :whiteboard/cut                                    "Tallar"
+ :whiteboard/dashboard-card-created                 "Creat "
+ :whiteboard/dashboard-card-edited                  "Editat "
+ :whiteboard/dashboard-card-new-whiteboard          "Nova pissarra"
+ :whiteboard/delete                                 "Eliminar"
+ :whiteboard/deselect-all                           "Desseleccionar tot"
+ :whiteboard/dev-print-shape-props                  "(Dev) Imprimir propietats de forma"
+ :whiteboard/distribute-horizontally                "Distribuir horitzontalment"
+ :whiteboard/distribute-vertically                  "Distribuir verticalment"
+ :whiteboard/draw                                   "Dibuixar"
+ :whiteboard/edit-pdf                               "Editar PDF"
+ :whiteboard/eraser                                 "Esborrar"
+ :whiteboard/expand                                 "Expandir"
+ :whiteboard/export                                 "Exportar"
+ :whiteboard/extra-large                            "Extra gran"
+ :whiteboard/extra-small                            "Extra petit"
+ :whiteboard/fill                                   "Reomplir"
+ :whiteboard/flip-horizontally                      "voltejar horitzontalment"
+ :whiteboard/flip-vertically                        "voltejar verticalment"
+ :whiteboard/group                                  "Agrupar"
+ :whiteboard/highlight                              "Ressaltar"
+ :whiteboard/huge                                   "Enorme"
+ :whiteboard/italic                                 "Itàlica"
+ :whiteboard/large                                  "Gran"
+ :whiteboard/link                                   "Enllaç"
+ :whiteboard/link-to-any-page-or-block              "Enllaç a qualsevol pàgina o bloc"
+ :whiteboard/lock                                   "Bloquejar"
+ :whiteboard/medium                                 "Mitjà"
+ :whiteboard/move-to-back                           "Moure al fons"
+ :whiteboard/move-to-front                          "Moure al capdavant"
+ :whiteboard/new-block                              "Nou bloc:"
+ :whiteboard/new-block-no-colon                     "Nou bloc"
+ :whiteboard/new-page                               "Nova pàgina:"
+ :whiteboard/new-whiteboard                         "Nova pissarra"
+ :whiteboard/opacity                                "Opacitat"
+ :whiteboard/open-page                              "Obrir pàgina"
+ :whiteboard/open-page-in-sidebar                   "Obrir pàgina a panell lateral"
+ :whiteboard/open-twitter-url                       "Obrir url de Twitter"
+ :whiteboard/open-website-url                       "Obrir url de lloc web"
+ :whiteboard/open-youtube-url                       "Obrir url de YouTube"
+ :whiteboard/pack-into-rectangle                    "Empaquetar en un rectangle"
+ :whiteboard/pan                                    "Moure"
+ :whiteboard/paste                                  "Enganxar"
+ :whiteboard/paste-as-link                          "Enganxar com enllaç"
+ :whiteboard/rectangle                              "Recarregar"
+ :whiteboard/redo                                   "Refer"
+ :whiteboard/references                             "Referencies"
+ :whiteboard/reload                                 "Recarregar"
+ :whiteboard/remove-link                            "Eliminar enllaç"
+ :whiteboard/scale-level                            "Escalar nivell"
+ :whiteboard/search-only-blocks                     "Cercar només blocs"
+ :whiteboard/search-only-pages                      "Cercar només pàgines"
+ :whiteboard/select                                 "Seleccionar"
+ :whiteboard/select-all                             "Seleccionar tot"
+ :whiteboard/select-custom-color                    "Seleccionar color personalitzat"
+ :whiteboard/shape                                  "Forma"
+ :whiteboard/shape-quick-links                      "Enllaços ràpids de forma"
+ :whiteboard/small                                  "Petit"
+ :whiteboard/snap-to-grid                           "Ajustar a la quadrícula"
+ :whiteboard/start-typing-to-search                 "Comença a escriure per cercar..."
+ :whiteboard/stroke-type                            "Tipus de línia"
+ :whiteboard/text                                   "Text"
+ :whiteboard/toggle-grid                            "Alternar quadrícula"
+ :whiteboard/toggle-pen-mode                        "Alternar mode ploma"
+ :whiteboard/triangle                               "Triangle"
+ :whiteboard/twitter-url                            "url de Twitter"
+ :whiteboard/undo                                   "Desfer"
+ :whiteboard/ungroup                                "Desagrupar"
+ :whiteboard/unlock                                 "Desbloquejar"
+ :whiteboard/website-url                            "url de lloc web"
+ :whiteboard/youtube-url                            "url de YouTube"
+ :whiteboard/zoom-in                                "Apropar"
+ :whiteboard/zoom-out                               "Allunyar"
+ :whiteboard/zoom-to-fit                            "Zoom per ajustar"
+ :window/close                                      "Tancar"
+ :window/exit-fullscreen                            "Sortir de pantalla completa"
+ :window/maximize                                   "Maximitzar"
+ :window/minimize                                   "Minimitzar"
+ :window/restore                                    "Restaurar"
+}

+ 735 - 0
src/resources/dicts/cs.edn

@@ -0,0 +1,735 @@
+{:accessibility/skip-to-main-content               "Přeskočit na hlavní obsah"
+ :on-boarding/welcome-whiteboard-modal-title       "Nové plátno pro vaše myšlenky."
+ :on-boarding/welcome-whiteboard-modal-description "Tabule jsou skvělým nástrojem pro brainstorming a organizaci. Od nynějška můžete své libovolné nápady umístit na prostorové plátno, abyste je mohli propojit a pochopit novým způsobem."
+ :on-boarding/welcome-whiteboard-modal-skip        "Přeskočit"
+ :on-boarding/welcome-whiteboard-modal-start       "Začněte psát na tabuli"
+ :on-boarding/tour-whiteboard-home                 "{1} Domov pro vaše tabule"
+ :on-boarding/tour-whiteboard-home-description     "Tabule mají v aplikaci vlastní sekci, kde je můžete na první pohled vidět, vytvářet nové nebo je snadno mazat."
+ :on-boarding/tour-whiteboard-new                  "{1} Vytvořit novou tabuli"
+ :on-boarding/tour-whiteboard-new-description      "Novou tabuli můžete vytvořit několika způsoby. Jeden z nich je vždy přímo zde v hlavním panelu."
+ :handbook/title                                   "Nápověda"
+ :handbook/topics                                  "Témata"
+ :handbook/popular-topics                          "Oblíbená témata"
+ :handbook/help-categories                         "Kategorie nápovědy"
+ :handbook/search                                  "Vyhledávání"
+ :handbook/home                                    "Domů"
+ :handbook/settings                                "Nastavení"
+ :handbook/close                                   "Zavřít"
+ :on-boarding/tour-whiteboard-btn-next             "Další"
+ :on-boarding/tour-whiteboard-btn-back             "Zpět"
+ :on-boarding/tour-whiteboard-btn-finish           "Dokončit"
+ :on-boarding/quick-tour-btn-next                  "Další"
+ :on-boarding/quick-tour-btn-back                  "Zpět"
+ :on-boarding/quick-tour-btn-finish                "Dokončit"
+ :on-boarding/quick-tour-btn-skip                  "Přeskočit rychlou prohlídku"
+ :on-boarding/quick-tour-steps                     "KROK "
+ :on-boarding/quick-tour-help-title                "❓ Nápověda"
+ :on-boarding/quick-tour-help-desc                 "Nápovědu a další informace o systému Logseq najdete vždy zde."
+ :on-boarding/quick-tour-journal-page-title        "📆 Stránka denního deníku"
+ :on-boarding/quick-tour-journal-page-desc-1       "Toto je dnešní stránka denního deníku. Zde můžete vypisovat své myšlenky, poznatky a nápady. S uspořádáním si nedělejte starosti. Prostě jen pište a"
+ :on-boarding/quick-tour-journal-page-desc-2       "[[odkaz]]"
+ :on-boarding/quick-tour-journal-page-desc-3       "své myšlenky."
+ :on-boarding/quick-tour-left-sidebar-title        "👀 Levý postranní panel"
+ :on-boarding/quick-tour-left-sidebar-desc         "Otevřete levý postranní panel a prozkoumejte důležité položky nabídky Logseq."
+ :on-boarding/quick-tour-favorites-title           "⭐️ Oblíbené"
+ :on-boarding/quick-tour-favorites-desc-1          "Připněte si své oblíbené stránky prostřednictvím `... `menu na libovolné stránce."
+ :on-boarding/quick-tour-favorites-desc-2          "Přidali jsme sem také několik šablon stránek, které vám pomohou začít. Ty můžete odstranit, jakmile začnete psát vlastní poznámky."
+ :on-boarding/command-palette-quick-tour           "Rychlá prohlídka pro vstup na palubu"
+ :on-boarding/importing-main-title                 "Import stávajících poznámek"
+ :on-boarding/importing-main-desc                  "Tento postup můžete provést i později v aplikaci."
+ :on-boarding/importing-title                      "Máte již poznámky, které chcete importovat?"
+ :on-boarding/importing-desc                       "Pokud jsou ve formátu JSON, EDN nebo Markdown, Logseq s nimi dokáže pracovat."
+ :on-boarding/importing-roam-desc                  "Importujte JSON export vašeho Roam grafu"
+ :on-boarding/importing-lsq-desc                   "Importujte EDN nebo JSON export vašeho Logseq grafu"
+ :on-boarding/importing-opml-desc                  " Importujte soubory OPML"
+ :on-boarding/main-title                           (fn [] ["Vítejte v " [:strong "Logseq!"]])
+ :on-boarding/main-desc                            "Nejprve si musíte vybrat složku, do které bude Logseq ukládat vaše myšlenky, nápady a poznámky."
+ :query/config-property-settings                   "Nastavení vlastností pro tento dotaz:"
+ :bug-report/main-title                            "Nahlášení chyby"
+ :bug-report/clipboard-inspector-title             "Inspektor dat schránky"
+ :bug-report/main-desc                             "Můžete nám pomoci odesláním hlášení o chybě? Vyřešíme to, jakmile to bude možné."
+ :bug-report/section-clipboard-title               "Souvisí chyba, na kterou jste narazili, s těmito funkcemi?"
+ :bug-report/section-clipboard-desc                "Pomocí těchto praktických nástrojů nám můžete poskytnout další informace."
+ :bug-report/section-clipboard-btn-title           "Pomocník pro schránku"
+ :bug-report/section-clipboard-btn-desc            "Kontrola a shromažďování dat schránky"
+ :bug-report/section-issues-title                  "Nebo ..."
+ :bug-report/section-issues-desc                   "Pokud nemáte k dispozici žádné nástroje pro shromažďování dalších informací, nahlaste chybu přímo."
+ :bug-report/section-issues-btn-title              "Odeslání hlášení chyby"
+ :bug-report/section-issues-btn-desc               "Pomozte vylepšit Logseq!"
+ :bug-report/inspector-page-desc-1                 "Stisknutím kláves Ctrl+V / ⌘+V zkontrolujte data schránky."
+ :bug-report/inspector-page-desc-2                 "nebo klikněte zde pro vložení, pokud používáte mobilní verzi"
+ :bug-report/inspector-page-placeholder            "Dlouhým stisknutím zde vložíte, pokud používáte mobilní aplikaci"
+ :bug-report/inspector-page-tip                    "Je něco špatně? Žádný problém, kliknutím se vrátíte k předchozímu kroku."
+ :bug-report/inspector-page-btn-back               "Vraťte se zpět"
+ :bug-report/inspector-page-btn-copy               "Kopírovat výsledek"
+ :bug-report/inspector-page-copy-notif             "Zkopírováno do schránky!"
+ :bug-report/inspector-page-btn-create-issue       "Vytvoření problému"
+ :bug-report/inspector-page-desc-clipboard         "Zde jsou data načtená ze schránky."
+ :bug-report/inspector-page-desc-copy              "Pokud je možné je sdílet, klikněte na tlačítko Kopírovat."
+ :bug-report/inspector-page-desc-create-issue      "Nyní můžete nahlásit výsledek vložený do schránky. Výsledek vložte do části „Další kontext“ a uveďte, odkud jste původní obsah zkopírovali. Děkujeme!"
+ :help/title-usage                                 "Použití"
+ :help/title-community                             "Komunita"
+ :help/title-development                           "Vývoj"
+ :help/title-about                                 "O nás"
+ :help/title-terms                                 "Podmínky"
+ :help/start                                       "Začínáme"
+ :help/about                                       "O Logseq"
+ :help/roadmap                                     "Plán činnosti"
+ :help/bug                                         "Nahlásit chybu"
+ :help/feature                                     "Požadovat novou funkci"
+ :help/changelog                                   "Seznam změn"
+ :help/docs                                        "Dokumentace"
+ :help/privacy                                     "Zásady ochrany osobních údajů"
+ :help/terms                                       "Podmínky používání"
+ :help/forum-community                             "Komunitní fórum"
+ :help/shortcuts                                   "Klávesové zkratky"
+ :help/shortcuts-triggers                          "Akce"
+ :help/shortcut                                    "Zkratky"
+ :help/search                                      "Hledání stránek/bloků/příkazů"
+ :help/slash-autocomplete                          "Automatické dokončování lomítka"
+ :help/reference-autocomplete                      "Automatické dokončování odkazů na stránky"
+ :help/block-reference                             "Reference na blok"
+ :help/open-link-in-sidebar                        "Otevřít odkaz v postranním panelu"
+ :search-item/whiteboard                           "Tabule"
+ :search-item/page                                 "Stránka"
+ :search-item/no-result                            "Žádný odpovídající výsledek"
+ :help/context-menu                                "Kontextová nabídka bloku"
+ :bold                                             "Tučné"
+ :italics                                          "Kurzíva"
+ :highlight                                        "Zvýraznění"
+ :strikethrough                                    "Přeškrtnutí"
+ :code                                             "Kód"
+ :untitled                                         "Bez názvu"
+ :right-side-bar/help                              "Nápověda"
+ :right-side-bar/switch-theme                      "Motiv"
+ :right-side-bar/contents                          "Obsah"
+ :right-side-bar/page-graph                        "Graf stránky"
+ :right-side-bar/block-ref                         "Reference bloku"
+ :right-side-bar/graph-view                        "Zobrazení grafu"
+ :right-side-bar/all-pages                         "Všechny stránky"
+ :right-side-bar/whiteboards                       "Tabule"
+ :right-side-bar/flashcards                        "Kartičky"
+ :right-side-bar/show-journals                     "Zobrazit deníky"
+ :right-side-bar/separator                         "Nástroj na změnu velikosti pravého postranního panelu"
+ :right-side-bar/toggle-right-sidebar              "Přepínání pravého postranního panelu"
+ :right-side-bar/pane-close                        "Zavřít"
+ :right-side-bar/pane-close-others                 "Zavřít ostatní"
+ :right-side-bar/pane-close-all                    "Zavřít vše"
+ :right-side-bar/pane-collapse                     "Sbalit"
+ :right-side-bar/pane-collapse-others              "Sbalit ostatní"
+ :right-side-bar/pane-collapse-all                 "Sbalit vše"
+ :right-side-bar/pane-expand                       "Rozbalit"
+ :right-side-bar/pane-expand-all                   "Rozbalit vše"
+ :right-side-bar/pane-open-as-page                 "Otevřít jjako stránku"
+ :right-side-bar/pane-more                         "Více"
+ :left-side-bar/switch                             "Přepnout na:"
+ :left-side-bar/journals                           "Deníky"
+ :left-side-bar/nav-favorites                      "Oblíbené"
+ :left-side-bar/nav-recent-pages                   "Nedávné"
+ :page/something-went-wrong                        "Něco se pokazilo"
+ :page/logseq-is-having-a-problem                  "Logseq má problém. Chcete-li se pokusit uvést jej zpět do funkčního stavu, vyzkoušejte prosím následující bezpečné kroky v uvedeném pořadí:"
+ :page/step                                        "Krok {1}"
+ :page/try                                         "Zkusit"
+ :page/slide-view                                  "Zobrazit jjako snímky"
+ :page/slide-view-tip-go-fullscreen                (fn [] [[:span.opacity-70 "Tip: stiskněte "] [:code "f"] [:span.opacity-70 " pro celou obrazovku"]])
+ :page/delete-confirmation                         "Jste si jisti, že chcete tuto stránku a její soubor odstranit?"
+ :page/open-in-finder                              "Otevřít v adresáři"
+ :page/open-with-default-app                       "Otevřít pomocí výchozí aplikace"
+ :page/make-public                                 "Označit stránku jjako veřejnou"
+ :page/version-history                             "Zobrazit historii stránky"
+ :page/open-backup-directory                       "Otevřít adresář se zálohami stránek"
+ :page/make-private                                "Označit stránku jjako soukromou"
+ :page/delete                                      "Smazat stránku"
+ :page/add-to-favorites                            "Přidat do oblíbených"
+ :page/unfavorite                                  "Odebrat z oblíbených"
+ :block/name                                       "Název stránky"
+ :page/copy-page-url                               "Zkopírovat adresu URL stránky"
+ :page/illegal-page-name                           "Neplatný název stránky!"
+ :page/page-already-exists                         "Stránka “{1}” už existuje!"
+ :page/whiteboard-to-journal-error                 "Stránky tabule nelze přejmenovat na názvy deníků!"
+ :file/name                                        "Název souboru"
+ :file/last-modified-at                            "Naposledy upraveno v"
+ :file/no-data                                     "Žádná data"
+ :file/format-not-supported                        "Formát .{1} není podporován."
+ :file/validate-existing-file-error                "Stránka již existuje u jiného souboru: {1}, aktuální soubor: {2}. Ponechte si prosím pouze jeden z nich a graf znovu zaindexujte."
+ :page/created-at                                  "Vytvořeno v"
+ :page/updated-at                                  "Aktualizováno v"
+ :page/backlinks                                   "Zpětné odkazy"
+ :linked-references/filter-search                  "Vyhledávání v odkazovaných stránkách"
+ :editor/block-search                              "Vyhledání bloku"
+ :text/image                                       "Obrázek"
+ :asset/show-in-folder                             "Zobrazit obrázek ve složce"
+ :asset/open-in-browser                            "Otevřít obrázek v prohlížeči"
+ :asset/delete                                     "Odstranit obrázek"
+ :asset/copy                                       "Kopírovat obrázek"
+ :asset/maximize                                   "Maximalizovat obrázek"
+ :asset/confirm-delete                             "Jste si jisti, že chcete odstranit tento {1}?"
+ :asset/physical-delete                            "Odstraňte i tento soubor (všimněte si, že jej nelze obnovit)."
+ :color/gray                                       "Šedá"
+ :color/red                                        "Červená"
+ :color/yellow                                     "Žlutá"
+ :color/green                                      "Zelená"
+ :color/blue                                       "Modrá"
+ :color/purple                                     "Fialová"
+ :color/pink                                       "Růžová"
+ :editor/copy                                      "Kopírovat"
+ :editor/cut                                       "Vyjmout"
+ :editor/expand-block-children                     "Rozbalit vše"
+ :editor/collapse-block-children                   "Sbalit vše"
+ :editor/delete-selection                          "Odstranit vybrané bloky"
+ :editor/cycle-todo                                "Přepnout stav TODO"
+ :dev/show-page-data                               "(Dev) Zobrazit data stránky"
+ :dev/show-block-data                              "(Dev) Zobrazit data stránky bloku"
+ :dev/show-block-ast                               "(Dev) Zobrazit AST blok"
+ :dev/show-page-ast                                "(Dev) Zobrazit AST stránky"
+ :content/copy-export-as                           "Kopírovat / Exportovat jako..."
+ :content/copy-block-url                           "Kopírovat adresu bloku"
+ :content/copy-block-ref                           "Kopírovat odkaz bloku"
+ :content/copy-block-emebed                        "Kopírovat blok jako vložený"
+ :content/copy-ref                                 "Kopírovat odkaz"
+ :content/delete-ref                               "Odstranit odkaz"
+ :content/replace-with-text                        "Nahradit textem"
+ :content/replace-with-embed                       "Nahradit vložením"
+ :content/open-in-sidebar                          "Otevřít v postranním panelu"
+ :content/click-to-edit                            "Kliknutím upravit"
+ :context-menu/make-a-flashcard                    "Vytvořit kartičku"
+ :context-menu/toggle-number-list                  "Přepnout seznam čísel"
+ :context-menu/preview-flashcard                   "Náhled kartičky"
+ :context-menu/make-a-template                     "Vytvořit šablonu"
+ :context-menu/input-template-name                 "Jak se jmenuje šablona?"
+ :context-menu/template-include-parent-block       "Včetně nadřazeného bloku v šabloně?"
+ :context-menu/template-exists-warning             "Šablóna už existuje!"
+ :settings-page/git-tip                            "Pokud máte povolenou synchronizaci Logseq, můžete si historii úprav stránky zobrazit přímo. Tato část je určena pouze pro technicky zdatné uživatele."
+ :settings-page/git-desc-1                         "Chcete-li zobrazit historii úprav stránky, klikněte na tři vodorovné tečky v pravém horním rohu a vyberte možnost „Zobrazit historii stránky“."
+ :settings-page/git-desc-2                         "Pro profesionální uživatele podporuje Logseq také použití "
+ :settings-page/git-desc-3                         " pro správu verzí. Používání systému Git je na vlastní nebezpečí, protože obecné záležitosti systému Git nejsou týmem Logseq podporovány."
+ :settings-page/git-switcher-label                 "Povolit automatický zápis do Git"
+ :settings-page/git-commit-delay                   "Automatický zápis do Git po sekundách"
+ :settings-page/edit-config-edn                    "upravit config.edn"
+ :settings-page/edit-global-config-edn             "upravit globální config.edn"
+ :settings-page/edit-custom-css                    "upravit custom.css"
+ :settings-page/edit-export-css                    "upravit export.css"
+ :settings-page/custom-configuration               "Vlastní konfigurace"
+ :settings-page/custom-global-configuration        "Vlastní globální konfigurace"
+ :settings-page/theme-light                        "světlý"
+ :settings-page/theme-dark                         "tmavý"
+ :settings-page/theme-system                       "systémový"
+ :settings-page/custom-theme                       "Vlastní motiv"
+ :settings-page/export-theme                       "Exportovat motiv"
+ :settings-page/show-brackets                      "Zobrazit závorky"
+ :settings-page/spell-checker                      "Kontrola pravopisu"
+ :settings-page/auto-updater                       "Automatická aktualizace"
+ :settings-page/disable-sentry                     "Odesílání dat o používání a diagnostiky do systému Logseq"
+ :settings-page/disable-sentry-desc                "Společnost Logseq nikdy nebude shromažďovat vaši místní databázi grafů ani prodávat vaše data."
+ :settings-page/preferred-outdenting               "Logické odsazení"
+ :settings-page/preferred-outdenting-tip           "Levá strana ukazuje odsazení s předvoleným nastavením, a pravá ukazuje odsazení s logickým odsadením "
+ :settings-page/preferred-outdenting-tip-more      "→ Další informace"
+ :settings-page/show-full-blocks                   "Zobrazení všech řádků odkazu na blok"
+ :settings-page/auto-expand-block-refs             "Automatické rozbalení odkazů na bloky při zvětšení"
+ :settings-page/auto-expand-block-refs-tip         "Tato volba určuje, zda se mají odkazy na bloky automaticky rozbalovat při zvětšení."
+ :settings-page/custom-date-format                 "Preferovaný formát data"
+ :settings-page/custom-date-format-warning         "Vyžadováno opětovné indexování! Stávající odkazy na deníky by byly poškozeny!"
+ :settings-page/custom-date-format-notification    "Aby se tato změna projevila, je nutné graf znovu přeindexovat."
+ :settings-page/preferred-pasting-file-hint        "Je-li tato možnost povolena, vložení obrázku z internetu jej stáhne a vloží. Je-li zakázáno, vloží se odkaz na obrázek."
+ :settings-page/preferred-file-format              "Preferovaný formát souboru"
+ :settings-page/preferred-workflow                 "Preferovaný pracovní postup"
+ :settings-page/preferred-pasting-file             "Uprednostnit vložení souboru"
+ :settings-page/enable-shortcut-tooltip            "Povolit nápovědy ke klávesovým zkratkám"
+ :settings-page/enable-timetracking                "Sledování času"
+ :settings-page/enable-tooltip                     "Povolit okno s nápovědou"
+ :settings-page/enable-journals                    "Denníky"
+ :settings-page/enable-all-pages-public            "Všechny stránky jsou při publikování veřejné"
+ :settings-page/home-default-page                  "Nastavení výchozí domovské stránky"
+ :settings-page/clear-cache                        "Vymazat mezipaměť"
+ :settings-page/clear                              "Vymazat"
+ :settings-page/clear-cache-warning                "Vymazáním mezipaměti dojde k odstranění otevřených grafů. Ztratíte neuložené změny."
+ :settings-page/developer-mode                     "Vývojářský režim"
+ :settings-page/developer-mode-desc                "Vývojářský režim pomáhá přispěvatelům a vývojářům rozšíření efektivněji testovat jejich integrace se službou Logseq."
+ :settings-page/current-version                    "Aktuální verze"
+ :settings-page/tab-general                        "Obecné"
+ :settings-page/tab-keymap                         "Klávesové zkratky"
+ :settings-page/tab-version-control                "Verzování"
+ :settings-page/tab-account                        "Účet"
+ :settings-page/tab-advanced                       "Pokročilé"
+ :settings-page/tab-assets                         "Zdroje"
+ :settings-page/tab-features                       "Funkce"
+ :settings-page/plugin-system                      "Zásuvné moduly"
+ :settings-page/enable-flashcards                  "Kartičky"
+ :settings-page/network-proxy                      "Síťový proxy server"
+ :settings-page/alpha-features                     "Alfa funkce"
+ :settings-page/beta-features                      "Beta funkce"
+ :settings-page/login-prompt                       "Chcete-li získat přístup k novým funkcím dříve než ostatní, musíte být sponzorem nebo podporovatelem Open Collective společnosti Logseq, následně sa musíte přihlásit."
+ :settings-page/sync                               "Synchronizovat"
+ :settings-page/sync-desc-1                        "Klikněte"
+ :settings-page/sync-desc-2                        "sem"
+ :settings-page/sync-desc-3                        "pokyny k nastavení a použití synchronizace."
+ :settings-page/sync-diff-merge                    "Povolit inteligentní slučování při synchronizaci"
+ :settings-page/sync-diff-merge-desc               "Automatické sloučení místních aktualizací se vzdálenými soubory, pokud dojde ke konfliktu, namísto přepisování vzdálených souborů."
+ :settings-page/sync-diff-merge-warn               "Schopnost inteligentního slučování je v nové verzi aplikace Logseq aktivována na klientovi až po první úspěšné synchronizaci se vzdáleným serverem na grafu. Povolením této funkce na všech zařízeních dosáhnete nejlepšího zážitku."
+ :settings-page/enable-whiteboards                 "Tabule"
+ :settings-page/native-titlebar                    "Nativní záhlaví okna"
+ :settings-page/native-titlebar-desc               "Aktivuje nativní záhlaví okna na systémech Windows a Linux."
+ :settings-page/check-for-updates                  "Kontrola aktualizací"
+ :settings-page/checking                           "Kontrola ..."
+ :settings-page/revision                           "Revize: "
+ :settings-page/changelog                          "Co je nového?"
+ :settings-page/app-updated                        "Vaše aplikace je aktuální 🎉"
+ :settings-page/update-available                   "Nalezena nová verze "
+ :settings-page/update-error-1                     "⚠️ Ups, něco se pokazilo!"
+ :settings-page/update-error-2                     " Podívejte se prosím na "
+ :settings-permission/start-granting               "Povolit"
+
+ :settings-page/auto-chmod                         "Automatická změna oprávnění k souborům"
+ :settings-page/auto-chmod-desc                    "Zakázat, abyste umožnili úpravy více uživatelům s oprávněními udělenými na základě členství ve skupině."
+ :yes                                              "Ano"
+
+ :submit                                           "Odeslat"
+ :cancel                                           "Zrušit"
+ :close                                            "Zavřít"
+ :delete                                           "Odstranit"
+ :save                                             "Uložit"
+ :type                                             "Typ"
+ :re-index                                         "Přeindexovat"
+ :re-index-detail                                  "Znovu sestavit graf"
+ :re-index-multiple-windows-warning                "Pred přeindexováním grafu musíte zavřít ostatní okna."
+ :re-index-discard-unsaved-changes-warning         "Přeindexovaní zahodí aktuální graf a poté znovu zpracuje všechny soubory tak, jak jsou aktuálně uloženy na disku. Ztratíte neuložené změny a může to chvíli trvat. Pokračovat?"
+ :sync-from-local-files                            "Obnovit"
+ :sync-from-local-files-detail                     "Importovat změny z místních souborů"
+ :sync-from-local-changes-detected                 "Funkce Obnovit detekuje a zpracovává soubory změněné na vašem disku, které se liší od aktuálního obsahu stránky Logseq. Pokračovat?"
+
+ :whiteboard/align-left                            "Zarovnat vlevo"
+ :whiteboard/align-center-horizontally             "Zarovnat vodorovně na střed"
+ :whiteboard/align-right                           "Zarovnat vpravo"
+ :whiteboard/distribute-horizontally               "Rozložit vodorovně"
+ :whiteboard/align-top                             "Zarovnat nahoru"
+ :whiteboard/align-center-vertically               "Zarovnat na střed vertikálně"
+ :whiteboard/align-bottom                          "Zarovnat dolů"
+ :whiteboard/distribute-vertically                 "Rozdělit vertikálně"
+ :whiteboard/pack-into-rectangle                   "Sbalit do obdélníku"
+ :whiteboard/zoom-to-fit                           "Přiblížit na míru"
+ :whiteboard/ungroup                               "Zrušit seskupení"
+ :whiteboard/group                                 "Seskupit"
+ :whiteboard/cut                                   "Vyjmout"
+ :whiteboard/copy                                  "Kopírovat"
+ :whiteboard/paste                                 "Vložit"
+ :whiteboard/paste-as-link                         "Vložit jako odkaz"
+ :whiteboard/export                                "Exportovat"
+ :whiteboard/select-all                            "Vybrat vše"
+ :whiteboard/deselect-all                          "Zrušit výběr všech"
+ :whiteboard/lock                                  "Zamknout"
+ :whiteboard/unlock                                "Odemknout"
+ :whiteboard/delete                                "Odstranit"
+ :whiteboard/flip-horizontally                     "Převrátit vodorovně"
+ :whiteboard/flip-vertically                       "Převrátit svisle"
+ :whiteboard/move-to-front                         "Přesunout dopředu"
+ :whiteboard/move-to-back                          "Přesunout dozadu"
+ :whiteboard/dev-print-shape-props                 "(Dev) Zobrazit vlastnosti tvaru"
+ :whiteboard/auto-resize                           "Automatická změna velikosti"
+ :whiteboard/expand                                "Rozbalit"
+ :whiteboard/collapse                              "Sbalit"
+ :whiteboard/website-url                           "Webová adresa"
+ :whiteboard/reload                                "Znovu načíst"
+ :whiteboard/open-website-url                      "Otevřít webovou adresu"
+ :whiteboard/youtube-url                           "YouTube adresa"
+ :whiteboard/open-youtube-url                      "Otevřít YouTube adresu"
+ :whiteboard/twitter-url                           "Twitter adresa"
+ :whiteboard/open-twitter-url                      "Otevřít Twitter adresu"
+ :whiteboard/fill                                  "Vyplnit"
+ :whiteboard/stroke-type                           "Typ čáry"
+ :whiteboard/arrow-head                            "Hlava šipky"
+ :whiteboard/bold                                  "Tučně"
+ :whiteboard/italic                                "Kurzíva"
+ :whiteboard/undo                                  "Zpátky"
+ :whiteboard/redo                                  "Znova"
+ :whiteboard/zoom-in                               "Přiblížit"
+ :whiteboard/zoom-out                              "Oddálit"
+ :whiteboard/select                                "Vybrat"
+ :whiteboard/pan                                   "Posouvat"
+ :whiteboard/add-block-or-page                     "Přidat blok nebo stránku"
+ :whiteboard/draw                                  "Kreslit"
+ :whiteboard/highlight                             "Zvýraznit"
+ :whiteboard/eraser                                "Guma"
+ :whiteboard/connector                             "Konektor"
+ :whiteboard/color                                 "Barva"
+ :whiteboard/select-custom-color                   "Výběr vlastní barvy"
+ :whiteboard/opacity                               "Průhlednost"
+ :whiteboard/extra-small                           "Extra malé"
+ :whiteboard/small                                 "Malé"
+ :whiteboard/medium                                "Střední"
+ :whiteboard/large                                 "Velké"
+ :whiteboard/extra-large                           "Extra velké"
+ :whiteboard/huge                                  "Obrovské"
+ :whiteboard/scale-level                           "Úroveň měřítka"
+ :whiteboard/rectangle                             "Obdélník"
+ :whiteboard/circle                                "Kruh"
+ :whiteboard/triangle                              "Trojúhelník"
+ :whiteboard/shape                                 "Tvar"
+ :whiteboard/open-page                             "Otevřít stránku"
+ :whiteboard/open-page-in-sidebar                  "Otevřít stránku v postranním panelu"
+ :whiteboard/remove-link                           "Odstranit odkaz"
+ :whiteboard/link                                  "Odkaz"
+ :whiteboard/references                            "Odkazy"
+ :whiteboard/link-to-any-page-or-block             "Odkaz na libovolnou stránku nebo blok"
+ :whiteboard/start-typing-to-search                "Začněte psát pro vyhledávání ..."
+ :whiteboard/new-block-no-colon                    "Nový blok"
+ :whiteboard/new-block                             "Nový blok:"
+ :whiteboard/new-page                              "Nová stránka:"
+ :whiteboard/new-whiteboard                        "Nová tabule"
+ :whiteboard/search-only-blocks                    "Vyhledávat pouze bloky"
+ :whiteboard/search-only-pages                     "Vyhledávat pouze stránky"
+ :whiteboard/shape-quick-links                     "Rýchlé odkazy na tvary"
+ :whiteboard/edit-pdf                              "upravit PDF"
+ :whiteboard/dashboard-card-new-whiteboard         "Nová tabule"
+ :whiteboard/dashboard-card-created                "Vytvořené "
+ :whiteboard/dashboard-card-edited                 "Upravené "
+ :whiteboard/toggle-grid                           "Přepnout mřížku"
+ :whiteboard/snap-to-grid                          "Zarovnat do mřížky"
+ :whiteboard/toggle-pen-mode                       "Přepnout na režim pera"
+ :flashcards/modal-welcome-title                   "Čas vytvořit kartu!"
+ :flashcards/modal-welcome-desc-1                  "Můžete přidat \"#card\" k jakémukoli bloku, abyste jej změnili na kartu, nebo spustit \"/cloze\", abyste přidali nějaké cloze."
+ :flashcards/modal-welcome-desc-2                  "Můžete "
+ :flashcards/modal-welcome-desc-3                  "kliknout na tento odkaz"
+ :flashcards/modal-welcome-desc-4                  " a podívejte se do dokumentace."
+ :flashcards/modal-btn-show-answers                "Zobrazit odpovědi"
+ :flashcards/modal-btn-hide-answers                "Skrýt odpovědi"
+ :flashcards/modal-btn-show-clozes                 "Zobrazit clozes"
+ :flashcards/modal-btn-next-card                   "Další"
+ :flashcards/modal-btn-reset                       "Obnovit"
+ :flashcards/modal-btn-reset-tip                   "Obnovte tuto kartu, abyste si ji mohli okamžitě prohlédnout."
+ :flashcards/modal-btn-forgotten                   "Zapomenuté"
+ :flashcards/modal-btn-remembered                  "Zapamatováno"
+ :flashcards/modal-btn-recall                      "Chvíli trvalo, než jsem si vzpomněl"
+ :flashcards/modal-finished                        "Gratulujeme, prohlédli jste si všechny karty pro tento dotaz, uvidíme se příště! 💯"
+ :flashcards/modal-select-all                      "Všechny"
+ :flashcards/modal-select-switch                   "Přepnout na"
+ :flashcards/modal-current-total                   "Aktuálně/Celkem"
+ :flashcards/modal-overdue-total                   "Překročeno/Celkem"
+ :flashcards/modal-toggle-preview-mode             "Přepnout do režimu náhledu"
+ :flashcards/modal-toggle-random-mode              "Přepnout do náhodného režimu"
+
+ :home                                             "Domů"
+ :new-page                                         "Nová stránka:"
+ :whiteboards                                      "Tabule"
+ :new-graph                                        "Přidat nový graf"
+ :graph                                            "Graf"
+ :graph/all-graphs                                 "Všechny grafy"
+ :graph/local-graphs                               "Místní grafy:"
+ :graph/remote-graphs                              "Vzdálené grafy:"
+ :export                                           "Exportovat"
+ :export-graph                                     "Exportovat graf"
+ :export-page                                      "Exportovat stránku"
+ :export-markdown                                  "Export jako standardní Markdown (bez vlastností bloků)"
+ :export-opml                                      "Exportovat jako OPML"
+ :export-public-pages                              "Exportovat veřejné stránky"
+ :export-json                                      "Exportovat jako JSON"
+ :export-roam-json                                 "Exportovat jako Roam JSON"
+ :export-edn                                       "Exportovat jako EDN"
+ :export-transparent-background                    "Průhledné pozadí"
+ :export-copy-to-clipboard                         "Kopírovat do schránky"
+ :export-copied-to-clipboard                       "Zkopírováno do schránky!"
+ :export-save-to-file                              "Uložit do souboru"
+ :all-graphs                                       "Všechny grafy"
+ :all-pages                                        "Všechny stránky"
+ :all-whiteboards                                  "Všechny tabule"
+ :all-files                                        "Všechny soubory"
+ :remove-orphaned-pages                            "Odstranit stránky bez rodičovské stránky?"
+ :all-journals                                     "Všechny denníky"
+ :settings                                         "Nastavení"
+ :settings-of-plugins                              "Zásuvné moduly"
+ :plugins                                          "Zásuvné moduly"
+ :themes                                           "Motivy"
+ :relaunch-confirm-to-work                         "Aby aplikace fungovala správně, je potřeba ji restartovat. Chcete ji restartovat teď?"
+ :import                                           "Importovat"
+ :importing                                        "Importuje se"
+ :help-shortcut-title                              "Kliknutím zobrazíte zkratky a další tipy"
+ :loading                                          "Načítání ..."
+ :parsing-files                                    "Parsování souborů"
+ :loading-files                                    "Načítání souborů"
+ :login                                            "Přihlášení"
+ :logout                                           "Odhlášení"
+ :download                                         "Stáhnout"
+ :language                                         "Jazyk"
+ :remove-background                                "Odstranit pozadí"
+ :remove-heading                                   "Odstranit nadpis"
+ :heading                                          "Nadpis {1}"
+ :auto-heading                                     "Automatický nadpis"
+ :open-a-directory                                 "Otevřít místní adresár"
+ :toggle-theme                                     "Přepnout motiv"
+
+ :help/shortcut-page-title                         "Klávesové zkratky"
+
+ :plugin/installed                                 "Nainstalováno"
+ :plugin/installed-plugin                          "Nainstalovaný zásuvný modul: {1}"
+ :plugin/not-installed                             "Nenainstalováno"
+ :plugin/installing                                "Instaluje sa"
+ :plugin/install                                   "Instalovat"
+ :plugin/reload                                    "Znovu načíst"
+ :plugin/update                                    "Aktualizovat"
+ :plugin/update-plugin                             "Aktualizovat zásuvný modul: {1} - {2}"
+ :plugin/check-update                              "Zkontrolovat aktualizaci"
+ :plugin/check-all-updates                         "Zkontrolovat všechny aktualizace"
+ :plugin/found-updates                             "Nové aktualizace"
+ :plugin/found-n-updates                           "Nalezené {1} aktualizace"
+ :plugin/update-all-selected                       "Aktualizovat všechny vybrané"
+ :plugin/all-updated                               "Všechny aktualizovány!"
+ :plugin/updates-downloading                       "Stahování aktualizací"
+ :plugin/refresh-lists                             "Obnovit seznamy"
+ :plugin/enabled                                   "Povolené"
+ :plugin/disabled                                  "Zakázané"
+ :plugin/update-available                          "K dispozici je aktualizace"
+ :plugin/updating                                  "Aktualizuje se"
+ :plugin/uninstall                                 "Odinstalovat"
+ :plugin/marketplace                               "Obchod"
+ :plugin/downloads                                 "Počet stáhnutí"
+ :plugin/stars                                     "Počet hvězdiček"
+ :plugin/title                                     "Název ({1})"
+ :plugin/all                                       "Všechny"
+ :plugin/unpacked                                  "Rozbalené"
+ :plugin/delete-alert                              "Opravdu chcete odinstalovat zásuvný modul [{1}]?"
+ :plugin/open-settings                             "Otevřít nastavení"
+ :plugin/open-package                              "Otevřít balík"
+ :plugin/load-unpacked                             "Načíst rozbalený zásuvný modul"
+ :plugin/restart                                   "Restartujte aplikaci"
+ :plugin/unpacked-tips                             "Vyberte adresář zásuvného modulu"
+ :plugin/contribute                                "✨ Napište a odešlete nový zásuvný modul"
+ :plugin/up-to-date                                "Je aktuální {1}"
+ :plugin/custom-js-alert                           "Nalezen soubor custom.js, je povoleno jeho spuštění? (Pokud nerozumíte obsahu tohoto souboru, doporučujeme spuštění nepovolit, protože má určitá bezpečnostní rizika)."
+ :plugin/security-warning                          "Zásuvné moduly mohou přistupovat k vašemu grafu a místním souborům, vydávat síťové požadavky. Mohou také způsobit poškození nebo ztrátu dat. Pracujeme na správných pravidlech přístupu ke grafům. Mezitím se ujistěte, že pravidelně zálohujete své grafy, a instalujte zásuvné moduly pouze tehdy, když jste schopni číst zdrojový kód a rozumíte mu."
+ :plugin/search-plugin                             "Vyhledávání zásuvných modulů"
+ :plugin/open-preferences                          "Otevřít předvolby"
+ :plugin/open-logseq-dir                           "Otevřít Logseq adresář"
+ :plugin/remote-error                              "Vzdálená chyba: "
+ :plugin/checking-for-updates                      "Kontrola aktualizací zásuvných modulů ..."
+ :plugin/list-of-updates                           "Aktualizace zásuvných modulů: "
+ :plugin/auto-check-for-updates                    "Automatická kontrola aktualizací"
+ :plugin.install-from-file/menu-title              "Instalace ze souboru plugins.edn"
+ :plugin.install-from-file/title                   "Instalace zásuvných modulů ze souboru plugins.edn"
+ :plugin.install-from-file/notice                  "Následující zásuvné moduly nahradí vaše zásuvné moduly::"
+ :plugin.install-from-file/success                 "Všechny zásuvné moduly byly úspěšně nainstalované!"
+ :pdf/copy-ref                                     "Kopírovat odkaz"
+ :pdf/copy-text                                    "Kopírovat text"
+ :pdf/linked-ref                                   "Prepojené odkazy"
+ :pdf/toggle-dashed                                "Čárkovaný styl pro zvýraznění oblasti"
+ :pdf/hl-block-colored                             "Barevný štítek pro zvýraznění bloku"
+ :pdf/auto-open-context-menu                       "Automatické otevření kontextové nabídky pro výběry"
+ :pdf/doc-metadata                                 "Metadata dokumentu"
+
+ :updater/new-version-install                      "Byla stažena nová verze."
+ :updater/quit-and-install                         "Restartujte pro instalaci"
+
+ :tips/all-done                                    "Hotovo!"
+
+ :select/default-prompt                            "Vyberte jednu"
+ :select/default-select-multiple                   "Vyberte jednu nebo více možností"
+ :select.graph/prompt                              "Vyberte graf"
+ :select.graph/empty-placeholder-description       "Žádné odpovídající grafy. Chcete přidat další?"
+ :select.graph/add-graph                           "Ano, přidejte další graf"
+
+ :file-sync/other-user-graph                       "Aktuální místní graf je svázán se vzdáleným grafem jiného uživatele. Nelze tedy zahájit synchronizaci."
+ :file-sync/graph-deleted                          "Aktuální vzdálený graf byl odstraněn"
+ :file-sync/rsapi-cannot-upload-err                "Nelze spustit synchronizaci, zkontrolujte, zda je místní čas správný."
+ :file-sync/connectivity-testing-failed            "Testování síťového připojení se nezdařilo. Zkontrolujte prosím nastavení sítě. Otestujte adresy URL: "
+
+ :notification/clear-all                           "Odstranit vše"
+
+ :shortcut.category/basics                         "Základy"
+ :shortcut.category/formatting                     "Formátování"
+ :shortcut.category/navigating                     "Navigace"
+ :shortcut.category/block-editing                  "Obecné úpravy bloků"
+ :shortcut.category/block-command-editing          "Příkazy na úpravu bloků"
+ :shortcut.category/block-selection                "Výběr bloku (výběr ukončíte stisknutím klávesy Esc)"
+ :shortcut.category/toggle                         "Přepínače"
+ :shortcut.category/others                         "Ostatní"
+ :shortcut.category/plugins                        "Zásuvné moduly"
+ :shortcut.category/whiteboard                     "Tabule"
+
+ :keymap/all                                       "Všechny"
+ :keymap/disabled                                  "Zakázané"
+ :keymap/unset                                     "Nenastavené"
+ :keymap/custom                                    "Vlastní"
+ :keymap/search                                    "Hledat"
+ :keymap/total                                     "Celkový počet zkratek"
+ :keymap/keystroke-filter                          "Filtrovat klávesové zkratky"
+ :keymap/keystroke-record-desc                     "Stisknutím libovolné sekvence kláves můžete filtrovat zkratky"
+ :keymap/keystroke-record-setup-label              "Stisknutím libovolné sekvence kláves nastavíte zkratku"
+ :keymap/restore-to-default                        "Obnovení výchozího nastavení systému"
+ :keymap/customize-for-label                       "Přizpůsobení zkratek"
+ :keymap/conflicts-for-label                       "Konflikty kláves pro"
+
+ :window/minimize                                  "Minimalizovat"
+ :window/maximize                                  "Maximalizace"
+ :window/restore                                   "Obnovit"
+ :window/close                                     "Zavřít"
+ :window/exit-fullscreen                           "Ukončit režim celé obrazovky"
+
+ :header/toggle-left-sidebar                       "Zobrazit/Skrýt levý boční panel"
+ :header/search                                    "Hledat"
+ :header/more                                      "více"
+ :header/go-back                                   "Zpátky"
+ :header/go-forward                                "Vpřed"
+
+ :command.pdf/previous-page                        "Pdf: Předchozí stránka aktuálního pdf dokumentu"
+ :command.pdf/next-page                            "Pdf: Další stránka aktuálního pdf dokumentu"
+ :command.pdf/close                                "Pdf: Zavřít aktuální pdf dokument"
+ :command.pdf/find                                 "Pdf: Vyhledávání textu aktuálního pdf dokumentu"
+ :command.auto-complete/complete                   "Automatické doplňování: Vybrat vybranou položku"
+ :command.auto-complete/prev                       "Automatické doplňování: Výběr předchozí položky"
+ :command.auto-complete/next                       "Automatické doplňování: Vyberte další položku"
+ :command.auto-complete/shift-complete             "Automatické doplňování: Otevřít vybranou položku v postranním panelu"
+ :command.cards/toggle-answers                     "Karty: zobrazit/skrýt odpovědi"
+ :command.cards/next-card                          "Karty: další karta"
+ :command.cards/forgotten                          "Karty: zapomenuté"
+ :command.cards/remembered                         "Karty: zapamatované"
+ :command.cards/recall                             "Karty: chvíli trvalo, než jsem si vzpomněl"
+ :command.editor/escape-editing                    "Zrušit úpravu"
+ :command.editor/backspace                         "Odstranit dozadu"
+ :command.editor/delete                            "Odstranit dopředu"
+ :command.editor/new-block                         "Vytvořit nový blok"
+ :command.editor/new-line                          "Nový řádek v aktuálním bloku"
+ :command.editor/new-whiteboard                    "Nová tabule"
+ :command.editor/follow-link                       "Přejít na odkaz pod kurzorom"
+ :command.editor/open-link-in-sidebar              "Otevřít odkaz v postranním panelu"
+ :command.editor/bold                              "Tučný"
+ :command.editor/italics                           "Kurzíva"
+ :command.editor/highlight                         "Zvýraznit"
+ :command.editor/strike-through                    "Preškrtnout"
+ :command.editor/clear-block                       "Odstranit celý obsah bloku"
+ :command.editor/kill-line-before                  "Odstranit řádek před pozicí kurzoru"
+ :command.editor/copy-embed                        "Kopírovat vložený blok směřující k aktuálnímu bloku"
+ :command.editor/kill-line-after                   "Odstranit řádek za pozicí kurzoru"
+ :command.editor/beginning-of-block                "Přesunout kurzor na začátek bloku"
+ :command.editor/end-of-block                      "Přesunout kurzor na konec bloku"
+ :command.editor/forward-word                      "Posunout kurzor o slovo dopředu"
+ :command.editor/backward-word                     "Přesunout kurzor o slovo dozadu"
+ :command.editor/forward-kill-word                 "Vymazat slovo dopředu"
+ :command.editor/backward-kill-word                "Vymazat slovo dozadu"
+ :command.editor/replace-block-reference-at-point  "Nahraďte odkaz na blok jeho obsahem na daném místě"
+ :command.editor/paste-text-in-one-block-at-point  "Vložit text do jednoho bloku na daném místě"
+ :command.editor/insert-youtube-timestamp          "Vložit časové razítko z YouTube"
+ :command.editor/cycle-todo                        "Zmenit stav TODO aktuální položky"
+ :command.editor/up                                "Posunout kurzor nahoru/vybrat nahoru"
+ :command.editor/down                              "Posunout kurzor dole/vybrat dolů"
+ :command.editor/left                              "Posunout kurzor doleva/otevřít vybraný blok na začátku"
+ :command.editor/right                             "Posunout kurzor doprava/otevřít vybraný blok na konci"
+ :command.editor/select-up                         "Vybrat obsah nad"
+ :command.editor/select-down                       "Vybrat obsah pod"
+ :command.editor/move-block-up                     "Posunout blok nahoru"
+ :command.editor/move-block-down                   "Posunout blok dolů"
+ :command.editor/open-edit                         "Upravit vybraný blok"
+ :command.editor/select-block-up                   "Vybrat blok nad"
+ :command.editor/select-block-down                 "Vybrat blok nižšpodie"
+ :command.editor/delete-selection                  "Odstranit vybrané bloky"
+ :command.editor/expand-block-children             "Rozbalit"
+ :command.editor/collapse-block-children           "Sbalit"
+ :command.editor/indent                            "Odsadit blok"
+ :command.editor/outdent                           "Zrušit odsazení bloku"
+ :command.editor/copy                              "Kopírovat (kopíruje buď výběr, nebo odkaz na blok)"
+ :command.editor/copy-text                         "Kopírovat výběr jako text"
+ :command.editor/cut                               "Vyjmout"
+ :command.editor/undo                              "Zpátky"
+ :command.editor/redo                              "Znovu"
+ :command.editor/insert-link                       "HTML odkaz"
+ :command.editor/select-all-blocks                 "Vybrat všechny bloky"
+ :command.editor/select-parent                     "Vybrat rodičovský blok"
+ :command.editor/zoom-in                           "Přiblížit upravovaný blok / Jinak dopředu"
+ :command.editor/zoom-out                          "Oddálit upravaný blok / Jinak dozadu"
+ :command.editor/toggle-number-list                "Přepnout číslovaný seznam"
+ :command.whiteboard/select                        "Nástroj výběru"
+ :command.whiteboard/pan                           "Nástroj posunu"
+ :command.whiteboard/portal                        "Nástroj portálu"
+ :command.whiteboard/pencil                        "Nástroj tužky"
+ :command.whiteboard/highlighter                   "Nástroj zvýrazňovače"
+ :command.whiteboard/eraser                        "Nástroj gumy"
+ :command.whiteboard/connector                     "Nástroj konektora"
+ :command.whiteboard/text                          "Nástroj textu"
+ :command.whiteboard/rectangle                     "Nástroj obdélníku"
+ :command.whiteboard/ellipse                       "Nástroj elipsy"
+ :command.whiteboard/reset-zoom                    "Obnovit zvětšení"
+ :command.whiteboard/zoom-to-fit                   "Přiblížit na výkres"
+ :command.whiteboard/zoom-to-selection             "Přiblížit na výběr"
+ :command.whiteboard/zoom-out                      "Oddálit"
+ :command.whiteboard/zoom-in                       "Přiblížit"
+ :command.whiteboard/send-backward                 "Posunout dozadu"
+ :command.whiteboard/send-to-back                  "Posunout na pozadí"
+ :command.whiteboard/bring-forward                 "Posunout dopředu"
+ :command.whiteboard/bring-to-front                "Posunout na popředí"
+ :command.whiteboard/lock                          "Zamknout výběr"
+ :command.whiteboard/unlock                        "Odemknout výběr"
+ :command.whiteboard/group                         "Seskupit výběr"
+ :command.whiteboard/ungroup                       "Rozskupit výběr"
+ :command.whiteboard/toggle-grid                   "Přepnout mřížku plátna"
+ :command.ui/toggle-brackets                       "Přepnout zobrazení závorek"
+ :command.go/electron-find-in-page                 "Najít text na stránce"
+ :command.go/electron-jump-to-the-next             "Přeskočit na další shodu s vyhledáváním v panelu Hledat"
+ :command.go/electron-jump-to-the-previous         "Přeskočit na předchozí shodu vyhledávání v panelu Hledat"
+ :command.go/search                                "Hledat stránky a bloky"
+ :command.go/search-in-page                        "Hledat bloky na stránce"
+ :command.command-palette/toggle                   "Hledat příkazy"
+ :command.go/journals                              "Přejít na deníky"
+ :command.go/backward                              "Zpátky"
+ :command.go/forward                               "Vpřed"
+ :command.search/re-index                          "Přestavět vyhledávací index"
+ :command.sidebar/open-today-page                  "Otevřít dnešní stránku v pravém postranním panelu"
+ :command.sidebar/close-top                        "Zavřít horní položku v pravém postranním panelu"
+ :command.sidebar/clear                            "Vymazat vše v pravém postranním panelu"
+ :command.misc/copy                                "Kopírovat"
+ :command.graph/export-as-html                     "Exportovat veřejné stránky grafu jako HTML"
+ :command.graph/open                               "Vybrat graf na otevření"
+ :command.graph/remove                             "Odstranit graf"
+ :command.graph/add                                "Přidat graf"
+ :command.graph/re-index                           "Znovu zaindexovat aktuální graf"
+ :command.command/run                              "Spustiť příkaz GIT"
+ :command.go/home                                  "Přejít na domovskou stránku"
+ :command.go/all-graphs                            "Přejít na všechny grafy"
+ :command.go/whiteboards                           "Přejít na tabule"
+ :command.go/all-pages                             "Přejít na všechny stránky"
+ :command.go/graph-view                            "Přejít na zobrazení grafu"
+ :command.go/keyboard-shortcuts                    "Přejít na klávesové zkratky"
+ :command.go/tomorrow                              "Přejít na zítřejší denník"
+ :command.go/next-journal                          "Přejít na další denník"
+ :command.go/prev-journal                          "Přejít na předcházející denník"
+ :command.go/flashcards                            "Zobrazit/Skrýt kartičky"
+ :command.ui/toggle-document-mode                  "Zobrazit/Skrýt režim dokumentu"
+ :command.ui/toggle-settings                       "Přepnout nastavení"
+ :command.ui/toggle-right-sidebar                  "Zobrazit/Skrýt pravý postranní panel"
+ :command.ui/toggle-left-sidebar                   "Zobrazit/Skrýt levý postranní panel"
+ :command.ui/toggle-help                           "Zobrazit/Skrýt nápovědu"
+ :command.ui/toggle-theme                          "Přepínání mezi tmavým/světlým motivem"
+ :command.ui/toggle-contents                       "Zobrazit/Skrýt obsah na postranním panelu"
+ :command.command/toggle-favorite                  "Přidat/Odstranit z oblíbených"
+ :command.editor/open-file-in-default-app          "Otevřít soubor ve výchozí aplikaci"
+ :command.editor/open-file-in-directory            "Otevřít soubor v nadřazeném adresáři"
+ :command.editor/copy-current-file                 "Kopírovat aktuální soubor"
+ :command.editor/copy-page-url                     "Kopírovat URL adresu stránky"
+ :command.ui/toggle-wide-mode                      "Přepnout širokouhlý režim"
+ :command.ui/select-theme-color                    "Vybrat dostupné barvy motivu"
+ :command.ui/goto-plugins                          "Přejít na ovládací panel zásuvných modulů"
+ :command.ui/install-plugins-from-file             "Nainstalovat zásuvné moduly ze souboru plugins.edn“"
+ :command.editor/toggle-open-blocks                "Přepnout otevřené bloky (sbalit nebo rozbalit všechny bloky)"
+ :command.ui/clear-all-notifications               "Vymazat všechna oznámení"
+ :command.git/commit                               "Spustiť příkaz git commit so zprávou"
+ :command.dev/show-block-data                      "(Dev) Zobrazit údaje bloku"
+ :command.dev/show-block-ast                       "(Dev) Zobrazit AST bloku"
+ :command.dev/show-page-data                       "(Dev) Zobrazit údaje stránky"
+ :command.dev/show-page-ast                        "(Dev) Zobrazit AST stránky"
+ :command.window/close                             "Zavřít okno"
+ :host "Host"
+ :port "Port"
+:command.editor/toggle-block-children "Přepnout rozbalit/sbalit"
+:command.whiteboard/clone-down "Klonování dolů"
+:command.whiteboard/clone-left "Klonování doleva"
+:command.whiteboard/clone-right "Klonování doprava"
+:command.whiteboard/clone-up "Klonování nahoru"
+:help/awesome-logseq "Úžasný Logseq"
+:help/blog "Logseq blog"
+:help/markdown-syntax "Markdown syntaxe"
+:help/org-mode-syntax "Org mode syntaxe"
+:linked-references/filter-directions "Kliknutím na tlačítko zahrnete a kliknutím s klávesou Shift vyloučíte. Dalším kliknutím odstraníte."
+:linked-references/filter-excludes "Nezahrnuje: "
+:linked-references/filter-heading "Filtruje"
+:linked-references/filter-includes "Zahrnuje: "
+:linked-references/reference-count (fn [filtered-count total] (str (when filtered-count (str filtered-count " z ")) total (if (= total 1) " Propojený odkaz" " Propojené odkazy")))
+:settings-page/git-commit-on-close "Git commit při zavření okna"
+:settings-page/tab-editor "Editor"
+:unlinked-references/reference-count (fn [total] (str total (if (= total 1) " Nepropojený odkaz" " Nepropojené odkazy")))
+:whiteboard/text "Text"}

+ 25 - 2
src/resources/dicts/ja.edn

@@ -4,7 +4,7 @@
  :on-boarding/welcome-whiteboard-modal-skip "スキップ"
  :on-boarding/welcome-whiteboard-modal-start "ホワイトボードを使い始める"
  :on-boarding/tour-whiteboard-home "{1} ホワイトボードのホーム"
- :on-boarding/tour-whiteboard-home-description "すぐに見つけられるように、また簡単に新規作成したり削除したりできるように、ホワイトボードは専用のが用意されています。"
+ :on-boarding/tour-whiteboard-home-description "すぐに見つけられるように、また簡単に新規作成したり削除したりできるように、ホワイトボードは専用のボタンが用意されています。"
  :on-boarding/tour-whiteboard-new "{1} ホワイトボードを新規作成する"
  :on-boarding/tour-whiteboard-new-description "ホワイトボードの新規作成には、複数の方法があります。ダッシュボードのちょうどこの位置に配置されたボタンもその一つです。"
  :handbook/title "ヘルプ"
@@ -160,6 +160,24 @@
  :page/updated-at "更新日時 "
  :page/backlinks "バックリンク"
  :linked-references/filter-search "リンクされたページを検索"
+ :linked-references/filter-heading "絞り込む"
+ :linked-references/filter-directions "クリックすると含むもの、Shiftを押しながらクリックすると含まないものを検索します。もう一度クリックすると取り消します。"
+ :linked-references/filter-includes "含む: "
+ :linked-references/filter-excludes "含まない: "
+ :linked-references/reference-count (fn [filtered-count total]
+                                      ;; 1 Linked Reference
+                                      ;; 1 of 1 Linked Reference
+                                      ;; 2 of 5 Linked References
+                                      (str
+                                       total
+                                       (when filtered-count
+                                         (str "個中" filtered-count))
+                                       "個のリンクによる参照"))
+ :unlinked-references/reference-count (fn [total]
+                                        ;; 1 Unlinked Reference
+                                        ;; 5 Unlinked References
+                                        (str total
+                                             "個のリンクでない参照"))
  :editor/block-search "ブロックを検索"
  :text/image "画像"
  :asset/show-in-folder "画像のフォルダを開く"
@@ -207,6 +225,7 @@
  :settings-page/git-desc-1 "ページの編集履歴を見たい場合は、右上の端にある横向き三点ボタンをクリックし、「ページ履歴の確認」を選択してください。"
  :settings-page/git-desc-2 "玄人なユーザーには、バージョンコントロールとして"
  :settings-page/git-desc-3 " の利用も用意しています。Gitの利用はご自身の責任で行ってください。一般的なGitの問題について、Logseqチームはサポートしません。"
+ :settings-page/git-commit-on-close "ウィンドウを閉じたときにGitにコミットする"
  :settings-page/git-switcher-label "Gitの自動コミットを有効化"
  :settings-page/git-commit-delay "Gitの自動コミット間隔(秒)"
  :settings-page/edit-config-edn "config.ednを編集"
@@ -637,6 +656,7 @@
  :command.editor/delete-selection "選択したブロックを削除"
  :command.editor/expand-block-children "子ブロックを展開する"
  :command.editor/collapse-block-children "子ブロックを折り畳む"
+ :command.editor/toggle-block-children "展開する/折り畳む"
  :command.editor/indent "インデント"
  :command.editor/outdent "アウトデント"
  :command.editor/copy "コピー"
@@ -674,6 +694,10 @@
  :command.whiteboard/group "選択範囲のグループ化"
  :command.whiteboard/ungroup "選択範囲のグループ化解除"
  :command.whiteboard/toggle-grid "キャンバスの格子の表示を切り替える"
+ :command.whiteboard/clone-right "右に複製"
+ :command.whiteboard/clone-left "左に複製"
+ :command.whiteboard/clone-up "上に複製"
+ :command.whiteboard/clone-down "下に複製"
  :command.ui/toggle-brackets "ブラケットの表示/非表示"
  :command.go/electron-find-in-page "ページ内で文字列検索"
  :command.go/electron-jump-to-the-next "文字列検索で次を検索"
@@ -712,7 +736,6 @@
  :command.ui/toggle-help "ヘルプの表示/非表示"
  :command.ui/toggle-theme "テーマの切り替え"
  :command.ui/toggle-contents "目次の開閉"
-
  :command.command/toggle-favorite "お気に入りへ追加/削除"
  :command.editor/open-file-in-default-app "ファイルを既定のアプリで開く"
  :command.editor/open-file-in-directory "ファイルをディレクトリで開く"

+ 449 - 11
src/resources/dicts/pl.edn

@@ -1,6 +1,6 @@
 {:help/start "Rozpocznij swoją przygodę z Logseq"
  :help/about "O Logseq"
- :help/roadmap "Roadmapa"
+ :help/roadmap "Plany rozwoju"
  :help/bug "Zgłoś błąd"
  :help/feature "Zgłoś zapotrzebowanie na funkcjonalność"
  :help/changelog "Log zmian"
@@ -28,7 +28,7 @@
  :right-side-bar/switch-theme "Zmień motyw"
  :right-side-bar/contents "Treść"
  :right-side-bar/page-graph "Graf strony"
- :right-side-bar/block-ref "Ref bloku"
+ :right-side-bar/block-ref "Referencja bloku"
  :right-side-bar/graph-view "Widok grafu"
  :right-side-bar/all-pages "Wszystkie strony"
  :right-side-bar/flashcards "Fiszki"
@@ -36,11 +36,11 @@
  :left-side-bar/journals "Dzienniki"
  :left-side-bar/nav-favorites "Ulubione"
  :left-side-bar/nav-recent-pages "Ostatnio odwiedzane"
- :page/delete-confirmation "Czy jesteś pewien że chcesz usunąć tę stronę i jej plik?"
+ :page/delete-confirmation "Czy jesteś pewien że chcesz usunąć tę stronę i jej plik?"
  :page/open-in-finder "Otwórz w przeglądarce plików"
  :page/open-with-default-app "Otwórz w domyślnej aplikacji"
  :page/make-public "Oznacz jako publiczną"
- :page/version-history "Sprawdź historię zmian"
+ :page/version-history "Sprawdź historię zmian"
  :page/open-backup-directory "Otwórz katalog z kopiami bezpieczeństwa"
  :page/make-private "Oznacz jako prywatną"
  :page/delete "Usuń stronę"
@@ -54,7 +54,7 @@
  :file/format-not-supported "Format .{1} nie jest wspierany."
  :page/created-at "Utworzony"
  :page/updated-at "Zaktualizowany"
- :page/backlinks "Backlinki"
+ :page/backlinks "Linki zwrotne"
  :linked-references/filter-search "Szukaj w powiązanych stronach"
  :editor/block-search "Szukaj bloku"
  :text/image "Obrazek"
@@ -62,7 +62,7 @@
  :asset/physical-delete "Usuń ten plik z dysku (operacja nie może zostać cofnięta)"
  :editor/copy "Kopiuj"
  :editor/cut "Wytnij"
- :content/copy-block-ref "Kopiuj ref bloku"
+ :content/copy-block-ref "Kopiuj referencję bloku"
  :content/copy-block-emebed "Kopiuj blok jako embed"
  :content/open-in-sidebar "Otwórz w panelu bocznym"
  :content/click-to-edit "Kliknij, aby edytować"
@@ -82,7 +82,7 @@
  :settings-page/custom-date-format "Preferowany format daty"
  :settings-page/custom-date-format-warning "Wymagane ponowne indeksowanie! Istniejące odniesienia mogą zostać uszkodzone!"
  :settings-page/preferred-file-format "Preferowany format pliku"
- :settings-page/preferred-workflow "Preferowany flow TODOsów"
+ :settings-page/preferred-workflow "Preferowany tryb zadań"
  :settings-page/enable-shortcut-tooltip "Włącz podpowiedzi skrótu klawiszowego"
  :settings-page/enable-timetracking "Mierzenie czasu"
  :settings-page/enable-tooltip "Podpowiedzi"
@@ -182,9 +182,9 @@
  :plugin/contribute "✨ Napisz i prześlij nowy plugin"
  :plugin/custom-js-alert "Wykryto plik custom.js, czy chcesz go wykonać? Ze względów bezpieczeństwa, nie rekomendujemy wykonywania tego pliku, jeżeli nie znasz jego zawartości."
 
- :pdf/copy-ref "Kopiuj ref"
+ :pdf/copy-ref "Kopiuj referencję"
  :pdf/copy-text "Kopiuj tekst"
- :pdf/linked-ref "Połączone odwołania"
+ :pdf/linked-ref "Połączone referencje"
  :pdf/toggle-dashed "Styl kreskowany dla podświetlenia obszaru"
 
  :updater/new-version-install "Nowa wersja została ściągnięta."
@@ -292,7 +292,7 @@
  :command.ui/toggle-settings                      "Wyświetl / Ukryj ustawienia"
  :command.ui/toggle-right-sidebar                 "Pokaż / Ukryj prawy panel"
  :command.ui/toggle-left-sidebar                  "Pokaż / Ukryj lewy panel"
- :command.ui/toggle-help                          "Pogaż / Ukryj pomoc"
+ :command.ui/toggle-help                          "Pokaż / Ukryj pomoc"
  :command.ui/toggle-theme                         "Zmień motyw graficzny na ciemny/jasny"
  :command.editor/open-file-in-default-app         "Otwórz plik w domyślnej aplikacji"
  :command.editor/open-file-in-directory           "Otwórz plik w nadrzędnym katalogu"
@@ -300,4 +300,442 @@
  :command.editor/toggle-open-blocks               "Zwiń / Rozwiń otwarte bloki"
  :command.ui/toggle-wide-mode                     "Włącz / wyłącz tryb szeroki"
  :command.ui/goto-plugins                         "Przejdź do dashboardu pluginów"
- :command.git/commit                              "Wykonaj GIT COMMIT z wiadomością"}
+ :command.git/commit                              "Wykonaj GIT COMMIT z wiadomością"
+ :all-whiteboards                                 "Wszystkie tablice"
+ :auto-heading                                    "Automatyczne nagłówki"
+ :export-copied-to-clipboard                      "Skopiowano do schowka!"
+ :export-copy-to-clipboard                        "Skopiuj do schowka"
+ :export-save-to-file                             "Zapisz do pliku"
+ :export-transparent-background                   "Przezroczyste tło"
+ :heading                                         "Nagłówek {1}"
+ :home                                            "Home"
+ :host                                            "Host"
+ :importing                                       "Importowanie"
+ :loading                                         "Wczytywanie..."
+ :new-page                                        "Nowa strona:"
+ :remove-heading                                  "Usuń nagłówek"
+ :remove-orphaned-pages                           "Usunąć nie linkowane strony?"
+ :toggle-theme                                    "Przełącz motyw"
+ :untitled                                        "Bez tytułu"
+ :whiteboards                                     "Tablice"
+ :accessibility/skip-to-main-content              "Pomiń do głównej zawartości"
+ :asset/copy                                      "Kopiuj obrazek"
+ :asset/delete                                    "Usuń obrazek"
+ :asset/maximize                                  "Powiększ obrazek"
+ :asset/open-in-browser                           "Otwórz obrazek w przeglądarce"
+ :asset/show-in-folder                            "Pokaż obrazek w folderze"
+ :bug-report/clipboard-inspector-title            "Inspektor danych schowka"
+ :bug-report/inspector-page-btn-back              "Cofnij"
+ :bug-report/inspector-page-btn-copy              "Kopiuj rezultat"
+ :bug-report/inspector-page-btn-create-issue      "Zgłoś problem"
+ :bug-report/inspector-page-copy-notif            "Skopiowano do schowka!"
+ :bug-report/inspector-page-desc-1                "Wciśnij Ctrl+V / ⌘+V, żeby sprawdzić zawartość schowka"
+ :bug-report/inspector-page-desc-2                "lub kliknij tu, żeby wkleić jeśli używasz komórki..."
+ :bug-report/inspector-page-desc-clipboard        "Dane wczytane ze schowka."
+ :bug-report/inspector-page-desc-copy             "Wciśnij Kopiuj, jeśli zgadzasz się na udostępnianie."
+ :bug-report/inspector-page-desc-create-issue     "Teraz możesz zaraportować rezultat wklejony ze schowka. Wklej rezultat do sekcji 'Dodatkowy kontekst' i napisz skąd skopiowałeś oryginalną zawartość. Dzięki!"
+ :bug-report/inspector-page-placeholder           "Kliknij i przytrzymaj, żeby wkleić jeśli pracujesz na komórce"
+ :bug-report/inspector-page-tip                   "Coś poszło nie tak? Nie ma problemu, kliknij, żeby wrócić do poprzedniego kroku."
+ :bug-report/main-desc                            "Czy mógłbyś nam pomóc zgłaszając raport o błędzie? Naprawimy go tak szybko jak się da."
+ :bug-report/main-title                           "Raport błędu"
+ :bug-report/section-clipboard-btn-desc           "Inspekcja i kolekcja danych ze schowka"
+ :bug-report/section-clipboard-btn-title          "Pomocnik schowka"
+ :bug-report/section-clipboard-desc               "Możesz użyć tych przydatnych narzędzi, żeby przesłać nam dodatkowe informacje."
+ :bug-report/section-clipboard-title              "Czy błąd, który napotkałeś jest związany z tymi funkcjonalnościami?"
+ :bug-report/section-issues-btn-desc              "Pomóż nam ulepszyć Logseq!"
+ :bug-report/section-issues-btn-title             "Zgłoś raport o błędzie"
+ :bug-report/section-issues-desc                  "Jeśli brak narzędzi, żeby zebrać dodatkowe informacje, zgłoś błąd bezpośrednio."
+ :bug-report/section-issues-title                 "Lub..."
+ :color/blue                                      "Niebieski"
+ :color/gray                                      "Szary"
+ :color/green                                     "Zielony"
+ :color/pink                                      "Różowy"
+ :color/purple                                    "Fioletowy"
+ :color/red                                       "Czerwony"
+ :color/yellow                                    "Żółty"
+ :command.command-palette/toggle                  "Szukaj komend"
+ :command.dev/show-block-ast                      "(Dev) Pokaż AST bloku"
+ :command.dev/show-block-data                     "(Dev) Pokaż dane bloku"
+ :command.dev/show-page-ast                       "(Dev) Pokaż AST strony"
+ :command.dev/show-page-data                      "(Dev) Pokaż dane strony"
+ :command.editor/copy-page-url                    "Kopiuj URL strony"
+ :command.editor/insert-link                      "Link HTML"
+ :command.editor/new-whiteboard                   "Nowa tablica"
+ :command.editor/select-parent                    "Zaznacz blok nadrzędny"
+ :command.editor/toggle-block-children            "Rozwiń/zwiń"
+ :command.editor/toggle-number-list               "Przełącz listę numerowaną"
+ :command.editor/zoom-in                          "Wejdź do edytowanego bloku lub w przód"
+ :command.editor/zoom-out                         "Wyjdź z edytowanego bloku lub wstecz"
+ :command.go/all-graphs                           "Idź do Wszystkich grafów"
+ :command.go/electron-find-in-page                "Znajdź tekst na stronie"
+ :command.go/electron-jump-to-the-next            "Skocz do następnego rezultatu wyszukiwania"
+ :command.go/electron-jump-to-the-previous        "Skocz do poprzedniego rezultatu wyszukiwania"
+ :command.go/search-in-page                       "Szukaj bloków na stronie"
+ :command.go/whiteboards                          "Idź do tablic"
+ :command.graph/export-as-html                    "Eksportuj publiczne strony grafu jako HTML"
+ :command.misc/copy                               "Kopiuj"
+ :command.pdf/find                                "PDF: Szukaj tekstu w otwartym dokumencie PDF"
+ :command.sidebar/close-top                       "Zamyka górny element w prawym panelu"
+ :command.ui/clear-all-notifications              "Wyczyść wszystkie powiadomienia"
+ :command.ui/install-plugins-from-file            "Zainstaluj pluginy z plugins.edn"
+ :command.ui/select-theme-color                   "Wybierz dostępne kolory motywu"
+ :command.ui/toggle-contents                      "Przełącz Treść w panelu bocznym"
+ :command.whiteboard/bring-forward                "Przesuń wyżej"
+ :command.whiteboard/bring-to-front               "Przesuń na wierzch"
+ :command.whiteboard/clone-down                   "Sklonuj w dół"
+ :command.whiteboard/clone-left                   "Sklonuj w lewo"
+ :command.whiteboard/clone-right                  "Sklonuj w prawo"
+ :command.whiteboard/clone-up                     "Sklonuj w górę"
+ :command.whiteboard/connector                    "Łącznik"
+ :command.whiteboard/ellipse                      "Elipsa"
+ :command.whiteboard/eraser                       "Gumka"
+ :command.whiteboard/group                        "Grupuj"
+ :command.whiteboard/highlighter                  "Marker"
+ :command.whiteboard/lock                         "Zablokuj zaznaczenie"
+ :command.whiteboard/pan                          "Przesuwanie obszaru"
+ :command.whiteboard/pencil                       "Ołówek"
+ :command.whiteboard/portal                       "Portal"
+ :command.whiteboard/rectangle                    "Prostokąt"
+ :command.whiteboard/reset-zoom                   "Zresetuj przybliżenie"
+ :command.whiteboard/select                       "Narzędzie zaznaczania"
+ :command.whiteboard/send-backward                "Przesuń niżej"
+ :command.whiteboard/send-to-back                 "Przesuń na spód"
+ :command.whiteboard/text                         "Tekst"
+ :command.whiteboard/toggle-grid                  "Przełącz siatkę"
+ :command.whiteboard/ungroup                      "Rozłącz grupę"
+ :command.whiteboard/unlock                       "Odblokuj zaznaczenie"
+ :command.whiteboard/zoom-in                      "Przybliż"
+ :command.whiteboard/zoom-out                     "Oddal"
+ :command.whiteboard/zoom-to-fit                  "Przybliż pokazując wszystkie elementy"
+ :command.whiteboard/zoom-to-selection            "Przybliż do rozmiaru zaznaczenia"
+ :command.window/close                            "Zamknij okno"
+ :content/copy-block-url                          "Kopiuj URL bloku"
+ :content/copy-export-as                          "Kopiuj / Eksportuj jako.."
+ :content/copy-ref                                "Kopiuj referencję"
+ :content/delete-ref                              "Usuń referencję"
+ :content/replace-with-embed                      "Zastąp osadzeniem"
+ :content/replace-with-text                       "Zastąp tekstem"
+ :context-menu/input-template-name                "Podaj nazwę szablonu"
+ :context-menu/make-a-flashcard                   "Utwórz fiszkę"
+ :context-menu/make-a-template                    "Utwórz szablon"
+ :context-menu/preview-flashcard                  "Podgląd fiszki"
+ :context-menu/template-exists-warning            "Szablon już istnieje!"
+ :context-menu/template-include-parent-block      "Włączyć blok nadrzędny do szablonu?"
+ :context-menu/toggle-number-list                 "Przełącz listę numerowaną"
+ :dev/show-block-ast                              "(Dev) Pokaż AST bloku"
+ :dev/show-block-data                             "(Dev) Pokaż dane bloku"
+ :dev/show-page-ast                               "(Dev) Pokaż AST strony"
+ :dev/show-page-data                              "(Dev) Pokaż dane strony"
+ :editor/collapse-block-children                  "Zwiń wszystkie bloki"
+ :editor/cycle-todo                               "Rotuj stan TODO"
+ :editor/delete-selection                         "Usuń zaznaczone bloki"
+ :editor/expand-block-children                    "Rozwiń wszystkie bloki"
+ :file/validate-existing-file-error               "Strona już istnieje w innym pliku: {1}, obecny plik to: {2}. Zachowaj tylko jeden z nich i przebuduj ponownie swój graf."
+ :file-sync/connectivity-testing-failed           "Test połączenia sieciowego się nie powiódł. Sprawdź ustawienia sieci. Testowe URLe: "
+ :file-sync/rsapi-cannot-upload-err               "Nie można uruchomić synchronizacji, sprawdź czy czas jest ustawiony prawidłowo."
+ :flashcards/modal-btn-forgotten                  "Zapomniane"
+ :flashcards/modal-btn-hide-answers               "Schowaj odpowiedzi"
+ :flashcards/modal-btn-next-card                  "Następna fiszka"
+ :flashcards/modal-btn-recall                     "Trochę zabrało, żeby przypomnieć"
+ :flashcards/modal-btn-remembered                 "Zapamiętane"
+ :flashcards/modal-btn-reset                      "Zresetuj"
+ :flashcards/modal-btn-reset-tip                  "Zresetuj tą fiszkę, żeby od razu ją przejrzeć."
+ :flashcards/modal-btn-show-answers               "Pokaż odpowiedzi"
+ :flashcards/modal-btn-show-clozes                "Pokaż luki"
+ :flashcards/modal-current-total                  "Aktualne/Wszystkie"
+ :flashcards/modal-finished                       "Gratulacje, przejrzałeś wszystkie fiszki w tym pytaniu, do zobaczenia następnym razem! 💯"
+ :flashcards/modal-overdue-total                  "Zaległe/Wszystkie"
+ :flashcards/modal-select-all                     "Wszystkie"
+ :flashcards/modal-select-switch                  "Przełącz się na"
+ :flashcards/modal-toggle-preview-mode            "Włącz tryb podglądu"
+ :flashcards/modal-toggle-random-mode             "Włącz tryb losowy"
+ :flashcards/modal-welcome-desc-1                 "Możesz dodać \"#card\" do dowolnego bloku, żeby zmienić go na fiszkę lub wpisz \"/cloze\" żeby dodać luki do wypełnienia."
+ :flashcards/modal-welcome-desc-2                 "Możesz "
+ :flashcards/modal-welcome-desc-3                 "kliknij ten link"
+ :flashcards/modal-welcome-desc-4                 " żeby sprawdzić dokumentację."
+ :flashcards/modal-welcome-title                  "Czas na nową fiszkę!"
+ :graph/all-graphs                                "Wszystkie grafy"
+ :graph/local-graphs                              "Grafy lokalne:"
+ :graph/remote-graphs                             "Grafy zdalne:"
+ :handbook/close                                  "Zamknij"
+ :handbook/help-categories                        "Kategorie pomocy"
+ :handbook/home                                   "Strona domowa"
+ :handbook/popular-topics                         "Popularne tematy"
+ :handbook/search                                 "Szukaj"
+ :handbook/settings                               "Ustawienia"
+ :handbook/title                                  "Pomoc"
+ :handbook/topics                                 "Tematy"
+ :header/go-back                                  "Wstecz"
+ :header/go-forward                               "W przód"
+ :header/more                                     "Więcej"
+ :header/search                                   "Szukaj"
+ :header/toggle-left-sidebar                      "Przełącz lewy panel"
+ :help/awesome-logseq                             "Wspaniały Logseq"
+ :help/search                                     "Szukaj stron/bloki/komend"
+ :help/title-about                                "O Logseq"
+ :help/title-community                            "Wspólnota"
+ :help/title-development                          "Programowanie"
+ :help/title-terms                                "Warunki korzystania"
+ :help/title-usage                                "Zasady używania"
+ :keymap/all                                      "Wszystkie"
+ :keymap/conflicts-for-label                      "Konflikty skrótów klawiaturowych dla"
+ :keymap/custom                                   "Własny"
+ :keymap/customize-for-label                      "Dostosuj skrót klawiatury"
+ :keymap/disabled                                 "Wyłączone"
+ :keymap/keystroke-filter                         "Filtr klawiszy"
+ :keymap/keystroke-record-desc                    "Wciśnij dowolną sekwencję klawiszy, żeby ustawić filtr"
+ :keymap/keystroke-record-setup-label             "Wciśnij dowolną sekwencję klawiszy, żeby ustawić skrót klawiaturowy"
+ :keymap/restore-to-default                       "Powrót do ustawień domyślnych"
+ :keymap/search                                   "Szukaj"
+ :keymap/total                                    "Wszystkie skróty klawiaturowe"
+ :keymap/unset                                    "Przywróć ustawienie"
+ :left-side-bar/switch                            "Przełącz na:"
+ :linked-references/filter-directions             "Kliknij, żeby uwzględnić lub kliknij z shiftem, żeby wykluczyć. Kliknij ponownie, żeby usunąć."
+ :linked-references/filter-excludes               "Wyklucza: "
+ :linked-references/filter-heading                "Filtruj"
+ :linked-references/filter-includes               "Uwzględnia: "
+ :linked-references/reference-count                (fn [filtered-count total]
+                                                    ;; 1 Linked Reference
+                                                    ;; 1 of 1 Linked Reference
+                                                    ;; 2 of 5 Linked References
+                                                    (str
+                                                        (when filtered-count
+                                                            (str filtered-count " z "))
+                                                        total
+                                                        (if (= total 1) " Powiązana referencja" " Powiązane referencje")))
+ :notification/clear-all                          "Wyczyść wszystkie"
+ :on-boarding/command-palette-quick-tour          "Szybka prezentacja wprowadzająca"
+ :on-boarding/importing-desc                      "Logseq może z nimi pracować jeśli są w formacie JSON, EDN lub Markdown."
+ :on-boarding/importing-lsq-desc                  "Zaimportuj eksport EDN lub JSON swojego grafu Logseq"
+ :on-boarding/importing-main-desc                 "Możesz to zrobić później w aplikacji"
+ :on-boarding/importing-main-title                "Zaimportuj notatki"
+ :on-boarding/importing-opml-desc                 "Zaimportuj pliki OPML"
+ :on-boarding/importing-roam-desc                 "Zaimportuj eksport JSON ze swojego grafu Roam"
+ :on-boarding/importing-title                     "Masz notatki, które chcesz zaimportować?"
+ :on-boarding/main-desc                           "Najpierw wybierz folder, w którym Logseq będzie zapisywał twoje myśli, pomysły i notatki."
+ :on-boarding/main-title                          (fn [] ["Witamy w " [:strong "Logseq!"]])
+ :on-boarding/quick-tour-btn-back                 "Poprzedni"
+ :on-boarding/quick-tour-btn-finish               "Koniec"
+ :on-boarding/quick-tour-btn-next                 "Następny"
+ :on-boarding/quick-tour-btn-skip                 "Pomiń"
+ :on-boarding/quick-tour-favorites-desc-1         "Przypnij swoje ulubione strony używając `...` menu na dowolnej stronie."
+ :on-boarding/quick-tour-favorites-desc-2         "Dodaliśmy kilka przykładowych szablonów, żeby ci pomóc. Możesz je usunąć jak zaczniesz pisać swoje własne notatki."
+ :on-boarding/quick-tour-favorites-title          "⭐️ Ulubione"
+ :on-boarding/quick-tour-help-desc                "Możesz zawsze tu kliknąć, żeby poszukac pomocy lub innych informacji o Logseq."
+ :on-boarding/quick-tour-help-title               "❓ Pomoc"
+ :on-boarding/quick-tour-journal-page-desc-1      "To jest dzisiejsza strona dziennika. Możesz tu wrzucać swoje myśli, to czego się nauczyłeś, czy pomysły. Nie skupiaj się na organizowaniu treści. Po prosu pisz i"
+ :on-boarding/quick-tour-journal-page-desc-2      "[[linkuj]]"
+ :on-boarding/quick-tour-journal-page-desc-3      "swoje myśli"
+ :on-boarding/quick-tour-journal-page-title       "📆 Codzienna strona dziennika"
+ :on-boarding/quick-tour-left-sidebar-desc        "Otwórz lewy panel, żeby sprawdzić ważne elementy menu Logseq explore important menu items in Logseq."
+ :on-boarding/quick-tour-left-sidebar-title       "👀 Lewy panel"
+ :on-boarding/quick-tour-steps                    "KROK "
+ :on-boarding/tour-whiteboard-btn-back            "Wstecz"
+ :on-boarding/tour-whiteboard-btn-finish          "Koniec"
+ :on-boarding/tour-whiteboard-btn-next            "W przód"
+ :on-boarding/tour-whiteboard-home                "{1} Strona domowa dla twoich tablic"
+ :on-boarding/tour-whiteboard-home-description    "Tablice mają własną sekcję w aplikacji, gdzie możesz zobaczyć je wszystkie na raz, tworzyć nowe lub łatwo usuwać."
+ :on-boarding/tour-whiteboard-new                 "{1} Utwórz nową tablicę"
+ :on-boarding/tour-whiteboard-new-description     "Jest kilka sposobów na utworzenie nowej tablicy. Jeden z nich jest zawsze dostępny tu w dashboardzie"
+ :on-boarding/welcome-whiteboard-modal-description "Tablice są świetnym narzędziem do burzy mózgów i organizacji. Możesz umieścić dowolną myśl z bazy wiedzy lub nową jedną obok drugiej na dużym obszarze, żeby je połączyć, kojarzyć i zrozumieć w nowy sposób."
+ :on-boarding/welcome-whiteboard-modal-skip       "Pomiń"
+ :on-boarding/welcome-whiteboard-modal-start      "Zacznij używać tablicy"
+ :on-boarding/welcome-whiteboard-modal-title      "Nowy obszar dla twoich myśli."
+ :page/illegal-page-name                          "Nieprawidłowa nazwa strony!"
+ :page/logseq-is-having-a-problem                 "Logseq ma problem. Żeby przywrócić go do działającego stanu wykonaj po kolei następujące bezpieczne kroki:"
+ :page/page-already-exists                        "Strona “{1}” już istnieje!"
+ :page/slide-view                                 "Pokaż jako slajdy"
+ :page/slide-view-tip-go-fullscreen               (fn [] [[:span.opacity-70 "Wskazówka: wciśnij "] [:code "f"] [:span.opacity-70 " żeby otworzyć na pełnym ekranie"]])
+ :page/something-went-wrong                       "Coś poszło nie tak"
+ :page/step                                       "Step (1)"
+ :page/try                                        "Spróbuj"
+ :page/whiteboard-to-journal-error                "Tablice nie mogą być nazwane tak jak tytuły dziennika!"
+ :pdf/auto-open-context-menu                      "Automatycznie otwórz menu kontekstowe dla zaznaczeń"
+ :pdf/doc-metadata                                "Meta dane dokumentu"
+ :pdf/hl-block-colored                            "Kolorowa etykieta dla podkreślonego bloku"
+ :plugin/all-updated                              "Wszystkie aktualne!"
+ :plugin/auto-check-for-updates                   "Automatycznie sprawdzaj aktualizacje"
+ :plugin/checking-for-updates                     "Sprawdzam aktualizacje pluginów..."
+ :plugin/found-n-updates                          "Znaleziono {1} aktualizacji"
+ :plugin/found-updates                            "Nowe aktualizacje"
+ :plugin/installed-plugin                         "Zainstalowano plugin: {1}"
+ :plugin/list-of-updates                          "Aktualizacje pluginów: "
+ :plugin/marketplace                              "Marketplace"
+ :plugin/open-logseq-dir                          "Otwórz"
+ :plugin/open-preferences                         "Otwórz preferencje"
+ :plugin/remote-error                             "Zdalny błąd: "
+ :plugin/search-plugin                            "Szukaj pluginów"
+ :plugin/security-warning                         "Pluginy mają dostęp do twojego grafu, plików lokalnych i mogą nawiązywać połączenia sieciowe.
+       Mogą też powodować uszkodzenia danych lub ich utratę. Pracujemy nad odpowiednimi regułami dostepu do twoich grafów.
+       W międzyczasie upewnij się, że wykonujesz regularne kopie zapasowe swoich grafów i instalujesz pluginy tylko kiedy możesz sprawdzić i
+       zrozumieć ich kod źródłowy."
+ :plugin/title                                    "Tytuł ({1})"
+ :plugin/up-to-date                               "Wszystko jest aktualne {1}"
+ :plugin/update-all-selected                      "Aktualizuj zaznaczone"
+ :plugin/update-plugin                            "Aktualizuj"
+ :plugin/updates-downloading                      "Ściąganie aktualizacji"
+ :plugin.install-from-file/menu-title             "Zainstaluj z pliku plugins.edn"
+ :plugin.install-from-file/notice                 "Następujące pluginy zastąpią twoje pluginy:"
+ :plugin.install-from-file/success                "Wszystkie pluginy zainstalowane!"
+ :plugin.install-from-file/title                  "Zainstaluj pluginy z pliku plugins.edn"
+ :query/config-property-settings                  "Ustawienie właściwości dla tego zapytania"
+ :right-side-bar/pane-close                       "Zamknij"
+ :right-side-bar/pane-close-all                   "Zamknij wszystkie"
+ :right-side-bar/pane-close-others                "Zamknij inne"
+ :right-side-bar/pane-collapse                    "Zwiń"
+ :right-side-bar/pane-collapse-all                "Zwiń wszystkie"
+ :right-side-bar/pane-collapse-others             "Zwiń inne"
+ :right-side-bar/pane-expand                      "Rozwiń"
+ :right-side-bar/pane-expand-all                  "Rozwiń wszystkie"
+ :right-side-bar/pane-more                        "Więcej"
+ :right-side-bar/pane-open-as-page                "Otwórz jako stronę"
+ :right-side-bar/separator                        "Separator"
+ :right-side-bar/toggle-right-sidebar             "Przełącz prawy panel"
+ :right-side-bar/whiteboards                      "Tablice"
+ :search-item/no-result                           "Brak pasujących rezultatów"
+ :search-item/page                                "Strona"
+ :search-item/whiteboard                          "Tablica"
+ :select/default-select-multiple                  "Zaznacz jeden lub wiele"
+ :settings-page/alpha-features                    "Funkcjonalności alfa"
+ :settings-page/app-updated                       "Twoja aplikacja jest aktualna 🎉"
+ :settings-page/auto-chmod                        "Automatycznie zmień uprawnienia do pliku"
+ :settings-page/auto-chmod-desc                   "Wyłącz, żeby zezwolić na edytowanie różnym użytkownikom z uprawnieniami grupy do pliku."
+ :settings-page/auto-expand-block-refs            "Rozwiń referencje bloku automatycznie przy przybliżeniu"
+ :settings-page/auto-expand-block-refs-tip        "Ta opcja kontroluje czy automatycznie rozwinąć referencję bloku przy przybliżaniu."
+ :settings-page/beta-features                     "Funkcjonalności beta"
+ :settings-page/check-for-updates                 "Sprawdź aktualizacje"
+ :settings-page/checking                          "Sprawdzanie..."
+ :settings-page/custom-date-format-notification   "Musisz przebudować ponownie graf, żeby zmiany zostały zastosowane"
+ :settings-page/custom-global-configuration       "Własna konfiguracja globalna"
+ :settings-page/edit-global-config-edn            "Edytuj globalny config.edn"
+ :settings-page/enable-whiteboards                "Tablice"
+ :settings-page/git-commit-on-close               "Wykonaj git commit po zamknięciu"
+ :settings-page/git-desc-1                        "Kliknij trzy kropki w prawym górnym rogu i wybierz \"Podgląd historii strony\", żeby zobaczyć historię edycji stron."
+ :settings-page/git-desc-2                        "Dla profesjonalistów, Logseq wspiera także używanie "
+ :settings-page/git-desc-3                        " do kontroli wersji. Używasz Git na własne ryzyko, ponieważ problemy z Git nie są wspierane przez zespół Logseq."
+ :settings-page/git-tip                           "Jeśli synchronizacja w Logseq jest włączona, możesz zobaczyć historię edycji stron bezpośrednio. Ta sekcja jest dla znających się na technologii."
+ :settings-page/login-prompt                      "Żeby zyskać dostęp do nowych funkcjonalności przed innymi, musisz mieś status Open Collective Sponsor lub Logseq Backer i musisz się zalogować."
+ :settings-page/native-titlebar                   "Natywny pasek tytułu"
+ :settings-page/native-titlebar-desc              "Włącza natywny pasek tytułu w Windowsie lub Linuksie"
+ :settings-page/preferred-outdenting-tip          "Lewa strona pokazuje zmniejszenie wcięcia z domyślnymi ustawieniami, a prawa z włączonym logicznym zmniejszeniem wcięć "
+ :settings-page/preferred-outdenting-tip-more     "→ Dowiedz się więcej"
+ :settings-page/preferred-pasting-file            "Preferuj wklejanie pliku"
+ :settings-page/preferred-pasting-file-hint       "Kiedy włączone, wklejanie obrazka z Internetu ściągnie i wstawi obrazek. Kiedy wyłączone, wklei link do obrazka."
+ :settings-page/revision                          "Rewizja: "
+ :settings-page/show-full-blocks                  "Pokaż wszystkie linie referencji bloku"
+ :settings-page/sync                              "Synchronizacja"
+ :settings-page/sync-desc-1                       "Kliknij"
+ :settings-page/sync-desc-2                       "tutaj"
+ :settings-page/sync-desc-3                       "żeby zobaczyć instrukcje jak skonfigurować i używać synchronizacji."
+ :settings-page/sync-diff-merge                   "Włącz inteligentne scalanie podczas synchronizacji"
+ :settings-page/sync-diff-merge-desc              "W przypadku wystąpienia konfliktu automatycznie łącz aktualizacje lokalne z plikami zdalnymi, zamiast nadpisywać pliki zdalne"
+ :settings-page/sync-diff-merge-warn              "Możliwość inteligentnego łączenia jest aktywowana na kliencie dopiero po pierwszej udanej synchronizacji grafu ze zdalnym serwerem w nowej wersji Logseq. Włącz tę opcję na wszystkich urządzeniach, aby uzyskać najlepszą jakość."
+ :settings-page/tab-account                       "Konto"
+ :settings-page/tab-assets                        "Zasoby"
+ :settings-page/tab-features                      "Właściwości"
+ :settings-page/update-available                  "Znaleziono nowe wydanie"
+ :settings-page/update-error-1                    "⚠️ Oops, coś poszło nie tak!"
+ :settings-page/update-error-2                    "Sprawdź proszę "
+ :settings-permission/start-granting              "Nadanie"
+ :shortcut.category/plugins                       "Pluginy"
+ :shortcut.category/whiteboard                    "Tablice"
+ :tips/all-done                                   "Wszystko zrobione!"
+ :unlinked-references/reference-count             (fn [total]
+                                        ;; 1 Unlinked Reference
+                                        ;; 5 Unlinked References
+                                        (str total
+                                         (if (= total 1) " Niepowiązana referencja" " Niepowiązane referencje")))
+ :whiteboard/add-block-or-page                    "Dodaj blok lb stronę"
+ :whiteboard/align-bottom                         "Wyrównaj do dołu"
+ :whiteboard/align-center-horizontally            "Wycentruj w poziomie"
+ :whiteboard/align-center-vertically              "Wycentruj w pionie"
+ :whiteboard/align-left                           "Wyrównaj do lewej"
+ :whiteboard/align-right                          "Wyrównaj do prawej"
+ :whiteboard/align-top                            "Wyrównaj do góry"
+ :whiteboard/arrow-head                           "Strzałka"
+ :whiteboard/auto-resize                          "Zmień rozmiar automatycznie"
+ :whiteboard/bold                                 "Pogrubienie"
+ :whiteboard/circle                               "Okrąg"
+ :whiteboard/collapse                             "Zwiń"
+ :whiteboard/color                                "Kolor"
+ :whiteboard/connector                            "Łącznik"
+ :whiteboard/copy                                 "Kopiuj"
+ :whiteboard/cut                                  "Wytnij"
+ :whiteboard/dashboard-card-created               "Utworzono "
+ :whiteboard/dashboard-card-edited                "Edytowano "
+ :whiteboard/dashboard-card-new-whiteboard        "Nowa tablica"
+ :whiteboard/delete                               "Usuń"
+ :whiteboard/deselect-all                         "Odznacz wszystko"
+ :whiteboard/dev-print-shape-props                "(Dev) Drukuj właściwości strony"
+ :whiteboard/distribute-horizontally              "Rozmieść w poziomie"
+ :whiteboard/distribute-vertically                "Rozmieść w pionie"
+ :whiteboard/draw                                 "Rysuj"
+ :whiteboard/edit-pdf                             "Edytuj PDF"
+ :whiteboard/eraser                               "Gumka"
+ :whiteboard/expand                               "Rozwiń"
+ :whiteboard/export                               "Eksportuj"
+ :whiteboard/extra-large                          "Super duże"
+ :whiteboard/extra-small                          "Super małe"
+ :whiteboard/fill                                 "Wypełnij"
+ :whiteboard/flip-horizontally                    "Odwróć w poziomie"
+ :whiteboard/flip-vertically                      "Odwróć w pionie"
+ :whiteboard/group                                "Grupuj"
+ :whiteboard/highlight                            "Wyróżnij"                            
+ :whiteboard/huge                                 "Ogromne"
+ :whiteboard/italic                               "Kursywa"
+ :whiteboard/large                                "Duże"
+ :whiteboard/link                                 "Link"
+ :whiteboard/link-to-any-page-or-block            "Link do dowolnej strony lub bloku"
+ :whiteboard/lock                                 "Zablokuj"
+ :whiteboard/medium                               "Średnie"
+ :whiteboard/move-to-back                         "Przesuń na spód"
+ :whiteboard/move-to-front                        "Przesuń na wierzch"
+ :whiteboard/new-block                            "Nowy blok"
+ :whiteboard/new-block-no-colon                   "Nowy blok:"
+ :whiteboard/new-page                             "Nowa strona"
+ :whiteboard/new-whiteboard                       "Nowa tablica"
+ :whiteboard/opacity                              "Przezroczystość"
+ :whiteboard/open-page                            "Otwórz stronę"
+ :whiteboard/open-page-in-sidebar                 "Otwórz stronę w panelu"
+ :whiteboard/open-twitter-url                     "Otwórz URL X (Twitter)"
+ :whiteboard/open-website-url                     "Otwórz URL strony"
+ :whiteboard/open-youtube-url                     "Otwórz URL Youtube"
+ :whiteboard/pack-into-rectangle                  "Umieść w prostokącie"
+ :whiteboard/pan                                  "Przesuwanie obrazu"
+ :whiteboard/paste                                "Wklej"
+ :whiteboard/paste-as-link                        "Wklej jako link"
+ :whiteboard/rectangle                            "Prostokąt"
+ :whiteboard/redo                                 "Ponów"
+ :whiteboard/references                           "Referencje"
+ :whiteboard/reload                               "Załaduj ponownie"
+ :whiteboard/remove-link                          "Usuń link"
+ :whiteboard/scale-level                          "Poziom skali"
+ :whiteboard/search-only-blocks                   "Szukaj tylko bloki"
+ :whiteboard/search-only-pages                    "Szukaj tylko strony"
+ :whiteboard/select                               "Zaznacz"
+ :whiteboard/select-all                           "Zaznacz wszystko"
+ :whiteboard/select-custom-color                  "Wybierz własny kolor"
+ :whiteboard/shape                                "Kształt"
+ :whiteboard/shape-quick-links                    "Szybkie linki do kształtów"
+ :whiteboard/small                                "Małe"
+ :whiteboard/snap-to-grid                         "Wyrównaj do siatki"
+ :whiteboard/start-typing-to-search               "Zacznij pisać, żeby wyszukać..."
+ :whiteboard/stroke-type                          "Rodzaj linii"
+ :whiteboard/text                                 "Tekst"
+ :whiteboard/toggle-grid                          "Przełącz siatkę"
+ :whiteboard/toggle-pen-mode                      "Przełącz tryb pióra"
+ :whiteboard/triangle                             "Trójkąt"
+ :whiteboard/twitter-url                          "URL X (Twitter)"
+ :whiteboard/undo                                 "Cofnij"
+ :whiteboard/ungroup                              "Rozłącz grupę"
+ :whiteboard/unlock                               "Odblokuj"
+ :whiteboard/website-url                          "URL strony"
+ :whiteboard/youtube-url                          "URL Youtube"
+ :whiteboard/zoom-in                              "Przybliż"
+ :whiteboard/zoom-out                             "Oddal"
+ :whiteboard/zoom-to-fit                          "Przybliż do wszystkich elementów"
+ :window/close                                    "Zamknij"
+ :window/exit-fullscreen                          "Wyjdź z trybu pełnego ekranu"
+ :window/maximize                                 "Maksymalizuj"
+ :window/minimize                                 "Minimalizuj"
+ :window/restore                                  "Przywróć"}

+ 1 - 1
src/resources/dicts/tr.edn

@@ -253,7 +253,7 @@
  :settings-page/custom-date-format "Tercih edilen tarih biçimi"
  :settings-page/custom-date-format-warning "Yeniden dizin oluşturma gerekli! Mevcut günlük referansları bozulabilir!"
  :settings-page/custom-date-format-notification "Bu değişikliğin etkili olması için grafınızın yeniden dizin olusturması gerekir"
- :settings-page/preferred-pasting-file-hint "Etkinleştirildiğinde, internetten bir resim yapıştırmak görüntüyü indirir ve ekler. Devre dışı bırakıldığında, bağlantıyı görüntüye yapıştırır."
+ :settings-page/preferred-pasting-file-hint "Etkinleştirildiğinde, internetten bir resim yapıştırmak görüntüyü indirir ve ekler. Devre dışı bırakıldığında sadece resim bağlantısı yapıştırılır."
  :settings-page/preferred-file-format "Tercih edilen dosya biçimi"
  :settings-page/preferred-workflow "Tercih edilen iş akışı"
  :settings-page/preferred-pasting-file "Dosya yapıştırmayı tercih et"

+ 14 - 0
src/resources/tutorials/dummy-notes-ca.md

@@ -0,0 +1,14 @@
+---
+title: Com prendre notes fictícies?
+---
+
+- Hola, ¡jo sóc un bloc!
+:PROPERTIES:
+:id: 5f713e91-8a3c-4b04-a33a-c39482428e2d
+:END:
+    - ¡jo sóc un bloc fill!
+    - ¡jo sóc un altre bloc fill!
+- ¡Escolta!, ¡jo sóc un altre bloc!
+:PROPERTIES:
+:id: 5f713ea8-8cba-403d-ac00-9964b1ec7190
+:END:

+ 14 - 0
src/resources/tutorials/dummy-notes-cs.md

@@ -0,0 +1,14 @@
+---
+title: Jak vytvořit poznámky?
+---
+
+- Ahoj, já jsem blok!
+:PROPERTIES:
+:id: 5f713e91-8a3c-4b04-a33a-c39482428e2d
+:END:
+    - Já jsem podřazený blok!
+    - Já jsem další podřazený blok!
+- Hej, Já jsem další blok!
+:PROPERTIES:
+:id: 5f713ea8-8cba-403d-ac00-9964b1ec7190
+:END:

+ 26 - 0
src/resources/tutorials/tutorial-ca.md

@@ -0,0 +1,26 @@
+## Hola, ¡Benvingut a Logseq!
+- Logseq es una plataforma _privacitat-primer_, [open-source](https://github.com/logseq/logseq) per la gestió del _coneixement_ i col·laboració.
+- Aquest es un tutorial de 3 minuts sobre com utilitzar Logseq. ¡Comencem!
+- Aquests són alguns consells que poden ser útils.
+#+BEGIN_TIP
+Pressionar clic per editar un bloc.
+Pressionar `Enter` per crear un nou bloc.
+Pressionar `Shift+Enter` per crear una nova línia.
+Escriure `/` per mostrar totes les ordres.
+#+END_TIP
+- 1. Creem una pàgina anomenada [[Com prendre notes fictícies?]]. Pot pressionar clic en l'enllaç per anar a la pàgina, o pot pressionar `Shift+Clic` per obrir-la al panell lateral dret. Ara hauria de veure tant _Referències Enllaçades_ com  _Referències sense enllaçar_.
+- 2. Referenciem alguns blocs de [[Com prendre notes fictícies?]], pot pressionar `Shift+Click` sobre qualsevol referència de un bloc per obrir-la al panell lateral dret. Provi de fer alguns canvis al panell lateral dret, ¡Els blocs referenciats també canvien!
+    - ((5f713e91-8a3c-4b04-a33a-c39482428e2d)) : Aquesta es una referència de bloc.
+    - ((5f713ea8-8cba-403d-ac00-9964b1ec7190)) : Aquesta es una altra referència de bloc.
+- 3. Logseq suporta etiquetes?
+    - I tant, aquesta es una etiqueta #ficticia.
+- 4. Logseq suporta tasques com pendents/en execució/finalitzada i prioritats?
+    - Si, escrigui `/` i seleccioni la seva paraula clau preferida (TODO, DOING, DONE, WAITING, CANCELED, NOW, LATER) o prioritat (A/B/C).
+    - NOW [#A] Un tutorial fictici sobre "Com prendre notes fictícies?"
+    - LATER [#A] Revisar aquest sorprenent vídeo de [:a {:href "https://twitter.com/shuomi3" :target "_blank"} "@shuomi3"] sobre com utilitzar Logseq per prendre notes i organitzar la seva vida!
+    {{youtube https://www.youtube.com/watch?v=BhHfF0P9A80&ab_channel=ShuOmi}}
+
+    - DONE Crear una pàgina
+    - CANCELED [#C] Escriure una pàgina amb mes de 1000 blocs
+- ¡Això es tot! ¡Ara ja pot crear mes punts o obrir un directori local per importar algunes notes!
+- Pot descarregar la nostra aplicació d'escriptori des de https://github.com/logseq/logseq/releases

+ 27 - 0
src/resources/tutorials/tutorial-cs.md

@@ -0,0 +1,27 @@
+## Dobrý den, vítejte do Logseq!
+- Logseq je _privacy-first_, [open-source](https://github.com/logseq/logseq) platforma pro správu _znalostí_ a spolupráci.
+- Toto je tříminutový návod, jak používat Logseq. Pusťme se do toho!
+- Zde je několik tipů, které se mohou hodit.
+#+BEGIN_TIP
+Kliknutím upravíte libovolný blok.
+Zadáním `Enter` vytvoříte nový blok.
+Zadáním `Shift+Enter` vytvoříte nový řádek.
+Zadáním `/` zobrazíte všechny příkazy.
+#+END_TIP
+- 1. Vytvoříme stránku s názvem [[Jak si dělat cvičné poznámky?]]. Klepnutím na ni přejdete na tuto stránku, nebo ji můžete `Shift+Kliknutím` otevřít v pravém postranním panelu! Nyní byste měli vidět jak _Propojené odkazy_, tak _Nepropojené odkazy_.
+- 2. Odkažme se na některé bloky na stránce [[Jak si dělat cvičné poznámky?]], můžete `Shift+kliknout` na libovolný odkaz na blok, aby se otevřel v pravém postranním panelu. Zkuste si vytvořit
+nějaké změny na pravém postranním panelu, změní se i tyto odkazované bloky!
+    - ((5f713e91-8a3c-4b04-a33a-c39482428e2d)) : Toto je odkaz na blok.
+    - ((5f713ea8-8cba-403d-ac00-9964b1ec7190)) : Toto je další odkaz na blok.
+- 3. Podporujete tagy?
+     - Samozřejmě, toto je #cvičný tag.
+- 4. Podporujete úkoly jako todo/doing/done a priority?
+    - Ano, napište `/` a vyberte své oblíbené klíčové slovo todo nebo prioritu (A/B/C).
+    - NOW [#A] Cvičný návod na téma „Jak si dělat cvičné poznámky?“.
+    - LATER [#A] Podívejte se na toto úžasné video od [:a {:href "https://twitter.com/shuomi3" :target "_blank"} "@shuomi3"] o tom, jak používat Logseq k psaní poznámek a organizaci života!
+    {{youtube https://www.youtube.com/watch?v=BhHfF0P9A80&ab_channel=ShuOmi}}
+
+    - DONE Vytvořit stránku
+    - CANCELED [#C] Vytvořit stránku s více než 1000 bloky
+- To je vše! Nyní můžete vytvořit další odrážky nebo otevřít místní adresář a importovat nějaké poznámky!
+- Můžete si také stáhnout naši aplikaci pro stolní počítače na adrese https://github.com/logseq/logseq/releases.