| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611 |
- (ns frontend.extensions.pdf.toolbar
- (:require [cljs-bean.core :as bean]
- [clojure.string :as string]
- [frontend.components.svg :as svg]
- [frontend.context.i18n :refer [t]]
- [frontend.extensions.pdf.assets :as pdf-assets]
- [frontend.extensions.pdf.utils :as pdf-utils]
- [frontend.extensions.pdf.windows :refer [resolve-own-container] :as pdf-windows]
- [frontend.handler.assets :as assets-handler]
- [frontend.handler.notification :as notification]
- [frontend.rum :refer [use-atom]]
- [frontend.state :as state]
- [frontend.storage :as storage]
- [frontend.ui :as ui]
- [frontend.util :as util]
- [logseq.shui.ui :as shui]
- [promesa.core :as p]
- [rum.core :as rum]))
- (declare make-docinfo-in-modal)
- (def *area-dashed? (atom ((fnil identity false) (storage/get (str "ls-pdf-area-is-dashed")))))
- (def *area-mode? (atom false))
- (def *highlight-mode? (atom false))
- #_:clj-kondo/ignore
- (rum/defcontext *highlights-ctx*)
- (rum/defc pdf-settings
- [^js viewer theme {:keys [hide-settings! select-theme! t]}]
- (let [*el-popup (rum/use-ref nil)
- [area-dashed? set-area-dashed?] (use-atom *area-dashed?)
- [hl-block-colored? set-hl-block-colored?] (rum/use-state (state/sub :pdf/block-highlight-colored?))
- [auto-open-ctx-menu? set-auto-open-ctx-menu!] (rum/use-state (state/sub :pdf/auto-open-ctx-menu?))]
- (rum/use-effect!
- (fn []
- (let [el-popup (rum/deref *el-popup)
- cb (fn [^js e]
- (and (= (.-which e) 27) (hide-settings!)))]
- (js/setTimeout #(.focus el-popup))
- (.addEventListener el-popup "keyup" cb)
- #(.removeEventListener el-popup "keyup" cb)))
- [])
- (rum/use-effect!
- (fn []
- (storage/set "ls-pdf-area-is-dashed" (boolean area-dashed?)))
- [area-dashed?])
- (rum/use-effect!
- (fn []
- (let [b (boolean hl-block-colored?)]
- (state/set-state! :pdf/block-highlight-colored? b)
- (storage/set "ls-pdf-hl-block-is-colored" b)))
- [hl-block-colored?])
- (rum/use-effect!
- (fn []
- (let [b (boolean auto-open-ctx-menu?)]
- (state/set-state! :pdf/auto-open-ctx-menu? b)
- (storage/set "ls-pdf-auto-open-ctx-menu" b)))
- [auto-open-ctx-menu?])
- (rum/use-effect!
- (fn []
- (let [cb #(let [^js target (.-target %)]
- (when (and (not (some-> (rum/deref *el-popup) (.contains target)))
- (nil? (.closest target ".ui__modal")))
- (hide-settings!)))
- doc (resolve-own-container viewer)]
- (js/setTimeout
- #(.addEventListener doc "click" cb))
- #(.removeEventListener doc "click" cb)))
- [])
- [:div.extensions__pdf-settings.hls-popup-overlay.visible
- [:div.extensions__pdf-settings-inner.hls-popup-box
- {:ref *el-popup
- :tab-index -1}
- [:div.extensions__pdf-settings-item.theme-picker
- (map (fn [it]
- [:button.flex.items-center.justify-center
- {:key it :class it :on-click #(select-theme! it)}
- (when (= theme it) (svg/check))])
- ["light", "warm", "dark"])]
- [:div.extensions__pdf-settings-item.toggle-input
- [:label (t :pdf/toggle-dashed)]
- (ui/toggle area-dashed? #(set-area-dashed? (not area-dashed?)) true)]
- [:div.extensions__pdf-settings-item.toggle-input.is-between
- [:label (t :pdf/hl-block-colored)]
- (ui/toggle hl-block-colored? #(set-hl-block-colored? (not hl-block-colored?)) true)]
- [:div.extensions__pdf-settings-item.toggle-input.is-between
- [:label (t :pdf/auto-open-context-menu)]
- (ui/toggle auto-open-ctx-menu? #(set-auto-open-ctx-menu! (not auto-open-ctx-menu?)) true)]
- [:div.extensions__pdf-settings-item.toggle-input
- [:a.is-info.w-full.text-gray-500
- {:title (t :pdf/doc-metadata)
- :on-click (fn []
- (p/let [ret (pdf-utils/get-meta-data$ viewer)]
- (hide-settings!)
- (shui/dialog-open! (make-docinfo-in-modal ret))))}
- [:span.flex.items-center.justify-between.w-full
- (t :pdf/doc-metadata)
- (svg/icon-info)]]]]]))
- (rum/defc docinfo-display
- [info close-fn!]
- [:div#pdf-docinfo.extensions__pdf-doc-info
- [:div.inner-text
- (for [[k v] info
- :let [k (str (string/replace-first (pr-str k) #"^\:" "") "::")]]
- [:p {:key k} [:strong k] " " [:i (pr-str v)]])]
- [:div.flex.items-center.justify-center.pt-2.pb--2
- (ui/button "Copy all"
- :on-click
- (fn []
- (let [text (.-innerText (js/document.querySelector "#pdf-docinfo > .inner-text"))
- text (string/replace text #"[\n\t]+" "\n")]
- (util/copy-to-clipboard! text)
- (notification/show! "Copied!" :success)
- (close-fn!))))]])
- (defn make-docinfo-in-modal
- [info]
- (fn [close-fn!]
- (docinfo-display info close-fn!)))
- (defonce find-status
- {0 ::found
- 1 ::not-found
- 2 ::wrapped
- 3 ::pending})
- (rum/defc ^:large-vars/data-var pdf-finder
- [^js viewer {:keys [hide-finder!]}]
- (let [*el-finder (rum/use-ref nil)
- *el-input (rum/use-ref nil)
- ^js bus (.-eventBus viewer)
- [case-sensitive?, set-case-sensitive?] (rum/use-state nil)
- [input, set-input!] (rum/use-state "")
- [matches, set-matches!] (rum/use-state {:current 0 :total 0})
- [find-state, set-find-state!] (rum/use-state {:status nil :current 0 :total 0 :query ""})
- [entered-active0?, set-entered-active0?] (rum/use-state false)
- [entered-active?, set-entered-active?] (rum/use-state false)
- reset-finder! (fn []
- (.dispatch bus "findbarclose" nil)
- (set-matches! nil)
- (set-find-state! nil)
- (set-entered-active? false)
- (set-entered-active0? false))
- close-finder! (fn []
- (reset-finder!)
- (hide-finder!))
- do-find! (fn [{:keys [type prev?] :as opts}]
- (when-let [type (if (keyword? opts) opts type)]
- (.dispatch bus "find"
- #js {:source nil
- :type (name type)
- :query input
- :phraseSearch true
- :caseSensitive case-sensitive?
- :highlightAll true
- :findPrevious prev?
- :matchDiacritics false})))]
- (rum/use-effect!
- (fn []
- (when-let [^js doc (resolve-own-container viewer)]
- (let [handler (fn [^js e]
- (when-let [^js target (and (string/blank? (.-value (rum/deref *el-input)))
- (.-target e))]
- (when (and (not= "Search" (.-title target))
- (not (some-> (rum/deref *el-finder) (.contains target))))
- (close-finder!))))]
- (.addEventListener doc "click" handler)
- #(.removeEventListener doc "click" handler))))
- [viewer])
- (rum/use-effect!
- (fn []
- (when-let [^js bus (.-eventBus viewer)]
- (.on bus "updatefindmatchescount" (fn [^js e]
- (let [matches (bean/->clj (.-matchesCount e))]
- (set-matches! matches)
- (set-find-state! (fn [s] (merge s matches))))))
- (.on bus "updatefindcontrolstate" (fn [^js e]
- (set-find-state!
- (merge
- {:status (get find-status (.-state e))
- :query (.-rawQuery e)}
- (bean/->clj (.-matchesCount e))))))))
- [viewer])
- (rum/use-effect!
- (fn []
- (when-not (nil? case-sensitive?)
- (do-find! :casesensitivitychange)))
- [case-sensitive?])
- [:div.extensions__pdf-finder-wrap.hls-popup-overlay.visible
- {:on-click #()}
- [:div.extensions__pdf-finder.hls-popup-box
- {:ref *el-finder
- :tab-index -1}
- [:div.input-inner.flex.items-center
- [:div.input-wrap.relative
- [:input
- {:placeholder "search"
- :type "text"
- :ref *el-input
- :auto-focus true
- :value input
- :on-change (fn [^js e]
- (let [val (.-value (.-target e))]
- (set-input! val)
- (set-entered-active0? (not (string/blank? (util/trim-safe val))))
- (set-entered-active? false)))
- :on-key-up (fn [^js e]
- (case (.-which e)
- 13 ;; enter
- (let [shift? (.-shiftKey e)]
- (do-find! {:type :again :prev? shift?})
- (set-entered-active? true))
- 27 ;; esc
- (if (string/blank? input)
- (close-finder!)
- (do
- (reset-finder!)
- (set-input! "")))
- :dune))}]
- (when entered-active0?
- (ui/button {:icon "arrow-back"
- :intent "link"
- :title "Enter to search"
- :class "icon-enter"
- :small? true}))]
- (ui/button {:icon "letter-case"
- :intent "link"
- :class (string/join " " (util/classnames [{:active case-sensitive?}]))
- :small? true :on-click #(set-case-sensitive? (not case-sensitive?))})
- (ui/button {:icon "chevron-up"
- :intent "link"
- :small? true :on-click #(do (do-find! {:type :again :prev? true}) (util/stop %))})
- (ui/button
- {:icon "chevron-down"
- :intent "link"
- :small? true :on-click #(do (do-find! {:type :again}) (util/stop %))})
- (ui/button
- {:icon "x"
- :intent "link"
- :small? true :on-click close-finder!})]
- [:div.result-inner
- (when-let [status (and entered-active?
- (not (string/blank? input))
- (:status find-state))]
- (if-not (= ::not-found status)
- [:div.flex.px-3.py-3.text-xs.opacity-90
- (apply max (map :current [find-state matches])) " of "
- (:total find-state)
- (str " matches (\"" (:query find-state) "\")")]
- [:div.px-3.py-3.text-xs.opacity-80.text-red-600 "Not found."]))]]]))
- (rum/defc pdf-outline-item
- [^js viewer
- {:keys [title items parent dest expanded]}
- {:keys [upt-outline-node!] :as ops}]
- (let [has-child? (seq items)
- expanded? (boolean expanded)]
- [:div.extensions__pdf-outline-item
- {:class (util/classnames [{:has-children has-child? :is-expand expanded?}])}
- [:div.inner
- [:a
- {:data-dest (js/JSON.stringify (bean/->js dest))
- :on-click (fn [^js/MouseEvent e]
- (let [target (.-target e)]
- (if (.closest target "i")
- (let [path (map #(if (re-find #"\d+" %) (int %) (keyword %))
- (string/split parent #"\-"))]
- (.preventDefault e)
- (upt-outline-node! path {:expanded (not expanded?)}))
- (when-let [^js dest (and dest (bean/->js dest))]
- (.goToDestination (.-linkService viewer) dest)))))}
- [:i.arrow svg/arrow-right-v2]
- [:span title]]]
- ;; children
- (when (and has-child? expanded?)
- [:div.children
- (map-indexed
- (fn [idx itm]
- (let [parent (str parent "-items-" idx)]
- (rum/with-key
- (pdf-outline-item
- viewer
- (merge itm {:parent parent})
- ops) parent))) items)])]))
- (rum/defc pdf-outline
- [^js viewer _visible? set-visible!]
- (when-let [^js pdf-doc (and viewer (.-pdfDocument viewer))]
- (let [*el-outline (rum/use-ref nil)
- [outline-data, set-outline-data!] (rum/use-state [])
- upt-outline-node! (rum/use-callback
- (fn [path attrs]
- (set-outline-data! (update-in outline-data path merge attrs)))
- [outline-data])]
- (rum/use-effect!
- (fn []
- (p/catch
- (p/let [^js data (.getOutline pdf-doc)]
- #_:clj-kondo/ignore
- (when-let [data (and data (.map data (fn [^js it]
- (set! (.-href it) (.. viewer -linkService (getDestinationHash (.-dest it))))
- (set! (.-expanded it) false)
- it)))])
- (set-outline-data! (bean/->clj data)))
- (fn [e]
- (js/console.error "[Load outline Error]" e))))
- [pdf-doc])
- (rum/use-effect!
- (fn []
- (let [el-outline (rum/deref *el-outline)
- cb (fn [^js e]
- (and (= (.-which e) 27) (set-visible! false)))]
- (js/setTimeout #(.focus el-outline))
- (.addEventListener el-outline "keyup" cb)
- #(.removeEventListener el-outline "keyup" cb)))
- [])
- [:div.extensions__pdf-outline-list-content
- {:ref *el-outline
- :tab-index -1}
- (if (seq outline-data)
- [:section
- (map-indexed (fn [idx itm]
- (rum/with-key
- (pdf-outline-item
- viewer
- (merge itm {:parent idx})
- {:upt-outline-node! upt-outline-node!})
- idx))
- outline-data)]
- [:section.is-empty "No outlines"])])))
- (rum/defc pdf-highlights-list
- [^js viewer]
- (let [[active, set-active!] (rum/use-state false)]
- (rum/with-context
- [hls-state *highlights-ctx*]
- (let [hls (sort-by :page (or (seq (:initial-hls hls-state))
- (:latest-hls hls-state)))]
- (for [{:keys [id content properties page] :as hl} hls
- :let [goto-ref! #(pdf-assets/goto-block-ref! hl)]]
- [:div.extensions__pdf-highlights-list-item
- {:key id
- :class (when (= active id) "active")
- :on-click (fn []
- (pdf-utils/scroll-to-highlight viewer hl)
- (set-active! id))
- :on-double-click goto-ref!}
- [:h6.flex
- [:span.flex.items-center
- [:small {:data-color (:color properties)}]
- [:strong "Page " page]]
- [:button
- {:title (t :pdf/linked-ref)
- :on-click goto-ref!}
- (ui/icon "external-link")]]
- (if-let [img-stamp (:image content)]
- (let [fpath (pdf-assets/resolve-area-image-file
- img-stamp (state/get-current-pdf) hl)
- fpath (assets-handler/<make-asset-url fpath)]
- [:p.area-wrap
- [:img {:src fpath}]])
- [:p.text-wrap (:text content)])])))))
- (rum/defc pdf-outline-&-highlights
- [^js viewer visible? set-visible!]
- (let [*el-container (rum/use-ref nil)
- [active-tab, set-active-tab!] (rum/use-state "contents")
- set-outline-visible! #(set-active-tab! "contents")
- contents? (= active-tab "contents")]
- (rum/use-effect!
- (fn []
- (when-let [^js doc (resolve-own-container viewer)]
- (let [cb (fn [^js e]
- (when-let [^js target (.-target e)]
- (when (and
- (not= "Outline" (.-title target))
- (not (some-> (rum/deref *el-container) (.contains target))))
- (set-visible! false)
- (set-outline-visible!))))]
- (.addEventListener doc "click" cb)
- #(.removeEventListener doc "click" cb))))
- [viewer])
- [:div.extensions__pdf-outline-wrap.hls-popup-overlay
- {:class (util/classnames [{:visible visible?}])}
- [:div.extensions__pdf-outline.hls-popup-box
- {:ref *el-container
- :tab-index -1}
- [:div.extensions__pdf-outline-tabs
- [:div.inner
- [:button {:class (when contents? "active")
- :on-click #(set-active-tab! "contents")} "Contents"]
- [:button {:class (when-not contents? "active")
- :on-click #(set-active-tab! "highlights")} "Highlights"]]]
- [:div.extensions__pdf-outline-panels
- (if contents?
- (pdf-outline viewer contents? set-outline-visible!)
- (pdf-highlights-list viewer))]]]))
- (rum/defc ^:large-vars/cleanup-todo pdf-toolbar
- [^js viewer {:keys [on-external-window!]}]
- (let [[area-mode?, set-area-mode!] (use-atom *area-mode?)
- [outline-visible?, set-outline-visible!] (rum/use-state false)
- [finder-visible?, set-finder-visible!] (rum/use-state false)
- [highlight-mode?, set-highlight-mode!] (use-atom *highlight-mode?)
- [settings-visible?, set-settings-visible!] (rum/use-state false)
- *page-ref (rum/use-ref nil)
- [current-page-num, set-current-page-num!] (rum/use-state 1)
- [total-page-num, set-total-page-num!] (rum/use-state 1)
- [viewer-theme, set-viewer-theme!] (rum/use-state (or (storage/get "ls-pdf-viewer-theme") "light"))
- group-id (.-$groupIdentity viewer)
- in-system-window? (.-$inSystemWindow viewer)
- doc (pdf-windows/resolve-own-document viewer)]
- ;; themes hooks
- (rum/use-effect!
- (fn []
- (when-let [^js el (some-> doc (.getElementById (str "pdf-layout-container_" group-id)))]
- (set! (. (. el -dataset) -theme) viewer-theme)
- (storage/set "ls-pdf-viewer-theme" viewer-theme)
- #(js-delete (. el -dataset) "theme")))
- [viewer-theme])
- ;; export page state
- (rum/use-effect!
- (fn []
- (when viewer
- (.dispatch (.-eventBus viewer) (name :ls-update-extra-state)
- #js {:page current-page-num})))
- [viewer current-page-num])
- ;; pager hooks
- (rum/use-effect!
- (fn []
- (when-let [total (and viewer (.-numPages (.-pdfDocument viewer)))]
- (let [^js bus (.-eventBus viewer)
- page-fn (fn [^js evt]
- (let [num (.-pageNumber evt)]
- (set-current-page-num! num)))]
- (set-total-page-num! total)
- (set-current-page-num! (.-currentPageNumber viewer))
- (.on bus "pagechanging" page-fn)
- #(.off bus "pagechanging" page-fn))))
- [viewer])
- (rum/use-effect!
- (fn []
- (let [^js input (rum/deref *page-ref)]
- (set! (. input -value) current-page-num)))
- [current-page-num])
- [:div.extensions__pdf-header
- [:div.extensions__pdf-toolbar
- [:div.inner
- [:div.r.flex.buttons
- ;; appearance
- [:a.button
- {:title "More settings"
- :on-click #(set-settings-visible! (not settings-visible?))}
- (svg/adjustments 18)]
- ;; selection
- [:a.button
- {:title (str "Area highlight (" (if util/mac? "⌘" "Shift") ")")
- :class (when area-mode? "is-active")
- :on-click #(set-area-mode! (not area-mode?))}
- (svg/icon-area 18)]
- [:a.button
- {:title "Highlight mode"
- :class (when highlight-mode? "is-active")
- :on-click #(set-highlight-mode! (not highlight-mode?))}
- (svg/highlighter 16)]
- ;; zoom
- [:a.button
- {:title "Zoom out"
- :on-click (partial pdf-utils/zoom-out-viewer viewer)}
- (svg/zoom-out 18)]
- [:a.button
- {:title "Zoom in"
- :on-click (partial pdf-utils/zoom-in-viewer viewer)}
- (svg/zoom-in 18)]
- [:a.button
- {:title "Outline"
- :on-click #(set-outline-visible! (not outline-visible?))}
- (svg/view-list 16)]
- ;; search
- [:a.button
- {:title "Search"
- :on-click #(set-finder-visible! (not finder-visible?))}
- (svg/search2 19)]
- ;; annotations
- [:a.button
- {:title "Annotations page"
- :on-click #(pdf-assets/goto-annotations-page! (:pdf/current @state/state))}
- (svg/annotations 16)]
- ;; system window
- [:a.button
- {:title (if in-system-window?
- "Open in app window"
- "Open in external window")
- :on-click #(if in-system-window?
- (pdf-windows/exit-pdf-in-system-window! true)
- (on-external-window!))}
- (ui/icon (if in-system-window?
- "window-minimize"
- "window-maximize"))]
- ;; pager
- [:div.pager.flex.items-center.ml-1
- [:span.nu.flex.items-center.opacity-70
- [:input {:ref *page-ref
- :type "number"
- :min 1
- :max total-page-num
- :class (util/classnames [{:is-long (> (util/safe-parse-int current-page-num) 999)}])
- :default-value current-page-num
- :on-mouse-enter #(.select ^js (.-target %))
- :on-key-up (fn [^js e]
- (let [^js input (.-target e)
- value (util/safe-parse-int (.-value input))]
- (set-current-page-num! value)
- (when (and (= (.-keyCode e) 13) value (> value 0))
- (->> (if (> value total-page-num) total-page-num value)
- (set! (. viewer -currentPageNumber))))))}]
- [:small "/ " total-page-num]]
- [:span.ct.flex.items-center
- [:a.button {:on-click #(. viewer previousPage)} (svg/up-narrow)]
- [:a.button {:on-click #(. viewer nextPage)} (svg/down-narrow)]]]
- [:a.button
- {:on-click #(if in-system-window?
- (pdf-windows/exit-pdf-in-system-window! false)
- (state/set-current-pdf! nil))}
- (t :close)]]]
- ;; contents outline
- (pdf-outline-&-highlights viewer outline-visible? set-outline-visible!)
- ;; finder
- (when finder-visible?
- (pdf-finder viewer {:hide-finder! #(set-finder-visible! false)}))
- ;; settings
- (when settings-visible?
- (pdf-settings
- viewer
- viewer-theme
- {:t t
- :hide-settings! #(set-settings-visible! false)
- :select-theme! #(set-viewer-theme! %)}))]]))
|