Feat: Export to image (#9037)

* feat: export to image

* chore: export selection on whiteboards

* fix: whiteboards zoom on export

* fix: loading position

* chore: support video thumb

* core: add export to  whiteboards context menu

* fix: context menu entry

* fix; copy image to clipboard

* fix: copy / export label

* fix: hide ui elements

* fix: remove random character

* fix: graph export

* chore: remove console log and jpg format

* style: run prettier

* fix: disable on multiple selected blocks

* fix: multiple blocks

* enhance: restrict bounds of selected shapes

* chore: export selection on whiteboards

* fix: whiteboards zoom on export

* chore: support video thumb

* core: add export to  whiteboards context menu

* fix: context menu entry

* fix; copy image to clipboard

* fix: copy / export label

* fix: hide ui elements

* fix: remove random character

* fix: graph export

* chore: remove console log and jpg format

* style: run prettier

* fix: disable on multiple selected blocks

* fix: multiple blocks

* enhance: restrict bounds of selected shapes

* Fix any html2canvas related functionality failing in publishing

* fix: portal header gradient on export

* chore: add comment about html2canvas-ignore attr

* fix: use export padding constant

* fix: export collapsed portals with size >  medium

* fix: reset export type

* enhance: export filename

---------

Co-authored-by: Gabriel Horner <gabriel@logseq.com>
Co-authored-by: Tienson Qin <tiensonqin@gmail.com>
This commit is contained in:
Konstantinos
2023-04-12 12:39:22 +03:00
committed by GitHub
parent 339fb7ceb2
commit 95149e13f6
34 changed files with 343 additions and 141 deletions

View File

@@ -1,9 +1,13 @@
(ns frontend.components.export
(:require [frontend.context.i18n :refer [t]]
(:require [cljs-time.core :as t]
["/frontend/utils" :as utils]
[frontend.context.i18n :refer [t]]
[frontend.db :as db]
[frontend.handler.export.text :as export-text]
[frontend.handler.export.html :as export-html]
[frontend.handler.export.opml :as export-opml]
[frontend.handler.export :as export]
[frontend.image :as image]
[frontend.mobile.util :as mobile-util]
[frontend.state :as state]
[frontend.ui :as ui]
@@ -74,6 +78,46 @@
current-repo block-uuids-or-page-name {:remove-options text-remove-options :other-options text-other-options})
"")))
(defn- get-zoom-level
[page-uuid]
(let [uuid (:block/uuid (db/get-page page-uuid))
whiteboard-camera (->> (str "logseq.tldraw.camera:" uuid)
(.getItem js/sessionStorage)
(js/JSON.parse)
(js->clj))]
(or (get whiteboard-camera "zoom") 1)))
(defn- get-image-blob
[block-uuids-or-page-name {:keys [transparent-bg? x y width height zoom]} callback]
(let [html js/document.body.parentNode
style (js/window.getComputedStyle html)
background (when-not transparent-bg? (.getPropertyValue style "--ls-primary-background-color"))
page? (string? block-uuids-or-page-name)
selector (if page?
"#main-content-container"
(str "[blockid='" (str (first block-uuids-or-page-name)) "']"))
container (js/document.querySelector selector)
scale (if page? (/ 1 (or zoom (get-zoom-level block-uuids-or-page-name))) 1)
options #js {:allowTaint true
:useCORS true
:backgroundColor (or background "transparent")
:x (or (/ x scale) 0)
:y (or (/ y scale) 0)
:width (when width (/ width scale))
:height (when height (/ height scale))
:scrollX 0
:scrollY 0
:scale scale
:windowHeight (when (string? block-uuids-or-page-name)
(.-scrollHeight container))}]
(-> (js/html2canvas container options)
(.then (fn [canvas] (.toBlob canvas (fn [blob]
(when blob
(let [img (js/document.getElementById "export-preview")
img-url (image/create-object-url blob)]
(set! (.-src img) img-url)
(callback blob)))) "image/png"))))))
(rum/defcs ^:large-vars/cleanup-todo
export-blocks < rum/static
(rum/local false ::copied?)
@@ -82,13 +126,18 @@
(rum/local nil ::text-other-options)
(rum/local nil ::content)
{:will-mount (fn [state]
(let [content (export-helper (last (:rum/args state)))]
(reset! (::content state) content)
(reset! (::text-remove-options state) (set (state/get-export-block-text-remove-options)))
(reset! (::text-indent-style state) (state/get-export-block-text-indent-style))
(reset! (::text-other-options state) (state/get-export-block-text-other-options))
state))}
[state root-block-uuids-or-page-name]
(reset! *export-block-type (if (:whiteboard? (last (:rum/args state))) :png :text))
(if (= @*export-block-type :png)
(do (reset! (::content state) nil)
(get-image-blob (first (:rum/args state))
(merge (second (:rum/args state)) {:transparent-bg? false})
(fn [blob] (reset! (::content state) blob))))
(reset! (::content state) (export-helper (first (:rum/args state)))))
(reset! (::text-remove-options state) (set (state/get-export-block-text-remove-options)))
(reset! (::text-indent-style state) (state/get-export-block-text-indent-style))
(reset! (::text-other-options state) (state/get-export-block-text-other-options))
state)}
[state root-block-uuids-or-page-name {:keys [whiteboard?] :as options}]
(let [tp @*export-block-type
*text-other-options (::text-other-options state)
*text-remove-options (::text-remove-options state)
@@ -96,117 +145,147 @@
*copied? (::copied? state)
*content (::content state)]
[:div.export.resize
[:div.flex
{:class "mb-2"}
(ui/button "Text"
:class "mr-4 w-20"
:on-click #(do (reset! *export-block-type :text)
(reset! *content (export-helper root-block-uuids-or-page-name))))
(ui/button "OPML"
:class "mr-4 w-20"
:on-click #(do (reset! *export-block-type :opml)
(reset! *content (export-helper root-block-uuids-or-page-name))))
(ui/button "HTML"
:class "w-20"
:on-click #(do (reset! *export-block-type :html)
(reset! *content (export-helper root-block-uuids-or-page-name))))]
[:textarea.overflow-y-auto.h-96 {:value @*content :read-only true}]
(let [options (->> text-indent-style-options
(mapv (fn [opt]
(if (= @*text-indent-style (:label opt))
(assoc opt :selected true)
opt))))]
[:div [:div.flex.items-center
[:label.mr-4
{:style {:visibility (if (= :text tp) "visible" "hidden")}}
"Indentation style:"]
[:select.block.my-2.text-lg.rounded.border.py-0.px-1
{:style {:visibility (if (= :text tp) "visible" "hidden")}
:on-change (fn [e]
(let [value (util/evalue e)]
(state/set-export-block-text-indent-style! value)
(reset! *text-indent-style value)
(reset! *content (export-helper root-block-uuids-or-page-name))))}
(for [{:keys [label value selected]} options]
[:option (cond->
{:key label
:value (or value label)}
selected
(assoc :selected selected))
label])]]
[:div.flex.items-center
(ui/checkbox {:class "mr-2"
:style {:visibility (if (#{:text :html :opml} tp) "visible" "hidden")}
:checked (contains? @*text-remove-options :page-ref)
:on-change (fn [e]
(state/update-export-block-text-remove-options! e :page-ref)
(reset! *text-remove-options (state/get-export-block-text-remove-options))
(reset! *content (export-helper root-block-uuids-or-page-name)))})
[:div {:style {:visibility (if (#{:text :html :opml} tp) "visible" "hidden")}}
"[[text]] -> text"]
(when-not whiteboard?
[:div.flex
{:class "mb-2"}
(ui/button "Text"
:class "mr-4 w-20"
:on-click #(do (reset! *export-block-type :text)
(reset! *content (export-helper root-block-uuids-or-page-name))))
(ui/button "OPML"
:class "mr-4 w-20"
:on-click #(do (reset! *export-block-type :opml)
(reset! *content (export-helper root-block-uuids-or-page-name))))
(ui/button "HTML"
:class "mr-4 w-20"
:on-click #(do (reset! *export-block-type :html)
(reset! *content (export-helper root-block-uuids-or-page-name))))
(when-not (seq? root-block-uuids-or-page-name)
(ui/button "PNG"
:class "w-20"
:on-click #(do (reset! *export-block-type :png)
(reset! *content nil)
(get-image-blob root-block-uuids-or-page-name (merge options {:transparent-bg? false}) (fn [blob] (reset! *content blob))))))])
(ui/checkbox {:class "mr-2 ml-4"
:style {:visibility (if (#{:text :html :opml} tp) "visible" "hidden")}
:checked (contains? @*text-remove-options :emphasis)
:on-change (fn [e]
(state/update-export-block-text-remove-options! e :emphasis)
(reset! *text-remove-options (state/get-export-block-text-remove-options))
(reset! *content (export-helper root-block-uuids-or-page-name)))})
(if (= :png tp)
[:div.flex.items-center.justify-center.relative
(when (not @*content) [:div.absolute (ui/loading "")])
[:img {:alt "export preview" :id "export-preview" :class "my-4" :style {:visibility (when (not @*content) "hidden")}}]]
[:div {:style {:visibility (if (#{:text :html :opml} tp) "visible" "hidden")}}
"remove emphasis"]
[:textarea.overflow-y-auto.h-96 {:value @*content :read-only true}])
(ui/checkbox {:class "mr-2 ml-4"
:style {:visibility (if (#{:text :html :opml} tp) "visible" "hidden")}
:checked (contains? @*text-remove-options :tag)
:on-change (fn [e]
(state/update-export-block-text-remove-options! e :tag)
(reset! *text-remove-options (state/get-export-block-text-remove-options))
(reset! *content (export-helper root-block-uuids-or-page-name)))})
(if (= :png tp)
[:div.flex.items-center
[:div "Transparent background"]
(ui/checkbox {:class "mr-2 ml-4"
:on-change (fn [e]
(reset! *content nil)
(get-image-blob root-block-uuids-or-page-name (merge options {:transparent-bg? e.currentTarget.checked}) (fn [blob] (reset! *content blob))))})]
(let [options (->> text-indent-style-options
(mapv (fn [opt]
(if (= @*text-indent-style (:label opt))
(assoc opt :selected true)
opt))))]
[:div [:div.flex.items-center
[:label.mr-4
{:style {:visibility (if (= :text tp) "visible" "hidden")}}
"Indentation style:"]
[:select.block.my-2.text-lg.rounded.border.py-0.px-1
{:style {:visibility (if (= :text tp) "visible" "hidden")}
:on-change (fn [e]
(let [value (util/evalue e)]
(state/set-export-block-text-indent-style! value)
(reset! *text-indent-style value)
(reset! *content (export-helper root-block-uuids-or-page-name))))}
(for [{:keys [label value selected]} options]
[:option (cond->
{:key label
:value (or value label)}
selected
(assoc :selected selected))
label])]]
[:div.flex.items-center
(ui/checkbox {:class "mr-2"
:style {:visibility (if (#{:text :html :opml} tp) "visible" "hidden")}
:checked (contains? @*text-remove-options :page-ref)
:on-change (fn [e]
(state/update-export-block-text-remove-options! e :page-ref)
(reset! *text-remove-options (state/get-export-block-text-remove-options))
(reset! *content (export-helper root-block-uuids-or-page-name)))})
[:div {:style {:visibility (if (#{:text :html :opml} tp) "visible" "hidden")}}
"[[text]] -> text"]
[:div {:style {:visibility (if (#{:text :html :opml} tp) "visible" "hidden")}}
"remove #tags"]]
(ui/checkbox {:class "mr-2 ml-4"
:style {:visibility (if (#{:text :html :opml} tp) "visible" "hidden")}
:checked (contains? @*text-remove-options :emphasis)
:on-change (fn [e]
(state/update-export-block-text-remove-options! e :emphasis)
(reset! *text-remove-options (state/get-export-block-text-remove-options))
(reset! *content (export-helper root-block-uuids-or-page-name)))})
[:div.flex.items-center
(ui/checkbox {:class "mr-2"
:style {:visibility (if (#{:text} tp) "visible" "hidden")}
:checked (boolean (:newline-after-block @*text-other-options))
:on-change (fn [e]
(state/update-export-block-text-other-options!
:newline-after-block (boolean (util/echecked? e)))
(reset! *text-other-options (state/get-export-block-text-other-options))
(reset! *content (export-helper root-block-uuids-or-page-name)))})
[:div {:style {:visibility (if (#{:text} tp) "visible" "hidden")}}
"newline after block"]
[:div {:style {:visibility (if (#{:text :html :opml} tp) "visible" "hidden")}}
"remove emphasis"]
(ui/checkbox {:class "mr-2 ml-4"
:style {:visibility (if (#{:text} tp) "visible" "hidden")}
:checked (contains? @*text-remove-options :property)
:on-change (fn [e]
(state/update-export-block-text-remove-options! e :property)
(reset! *text-remove-options (state/get-export-block-text-remove-options))
(reset! *content (export-helper root-block-uuids-or-page-name)))})
[:div {:style {:visibility (if (#{:text} tp) "visible" "hidden")}}
"remove properties"]]
(ui/checkbox {:class "mr-2 ml-4"
:style {:visibility (if (#{:text :html :opml} tp) "visible" "hidden")}
:checked (contains? @*text-remove-options :tag)
:on-change (fn [e]
(state/update-export-block-text-remove-options! e :tag)
(reset! *text-remove-options (state/get-export-block-text-remove-options))
(reset! *content (export-helper root-block-uuids-or-page-name)))})
[:div.flex.items-center
[:label.mr-2 {:style {:visibility (if (#{:text :html :opml} tp) "visible" "hidden")}}
"level <="]
[:select.block.my-2.text-lg.rounded.border.px-2.py-0
{:style {:visibility (if (#{:text :html :opml} tp) "visible" "hidden")}
:value (or (:keep-only-level<=N @*text-other-options) :all)
:on-change (fn [e]
(let [value (util/evalue e)
level (if (= "all" value) :all (util/safe-parse-int value))]
(state/update-export-block-text-other-options! :keep-only-level<=N level)
(reset! *text-other-options (state/get-export-block-text-other-options))
(reset! *content (export-helper root-block-uuids-or-page-name))))}
(for [n (cons "all" (range 1 10))]
[:option {:key n :value n} n])]]])
[:div {:style {:visibility (if (#{:text :html :opml} tp) "visible" "hidden")}}
"remove #tags"]]
[:div.mt-4
(ui/button (if @*copied? "Copied to clipboard!" "Copy to clipboard")
:on-click (fn []
(util/copy-to-clipboard! @*content
:html (when (= tp :html) @*content))
(reset! *copied? true)))]]))
[:div.flex.items-center
(ui/checkbox {:class "mr-2"
:style {:visibility (if (#{:text} tp) "visible" "hidden")}
:checked (boolean (:newline-after-block @*text-other-options))
:on-change (fn [e]
(state/update-export-block-text-other-options!
:newline-after-block (boolean (util/echecked? e)))
(reset! *text-other-options (state/get-export-block-text-other-options))
(reset! *content (export-helper root-block-uuids-or-page-name)))})
[:div {:style {:visibility (if (#{:text} tp) "visible" "hidden")}}
"newline after block"]
(ui/checkbox {:class "mr-2 ml-4"
:style {:visibility (if (#{:text} tp) "visible" "hidden")}
:checked (contains? @*text-remove-options :property)
:on-change (fn [e]
(state/update-export-block-text-remove-options! e :property)
(reset! *text-remove-options (state/get-export-block-text-remove-options))
(reset! *content (export-helper root-block-uuids-or-page-name)))})
[:div {:style {:visibility (if (#{:text} tp) "visible" "hidden")}}
"remove properties"]]
[:div.flex.items-center
[:label.mr-2 {:style {:visibility (if (#{:text :html :opml} tp) "visible" "hidden")}}
"level <="]
[:select.block.my-2.text-lg.rounded.border.px-2.py-0
{:style {:visibility (if (#{:text :html :opml} tp) "visible" "hidden")}
:value (or (:keep-only-level<=N @*text-other-options) :all)
:on-change (fn [e]
(let [value (util/evalue e)
level (if (= "all" value) :all (util/safe-parse-int value))]
(state/update-export-block-text-other-options! :keep-only-level<=N level)
(reset! *text-other-options (state/get-export-block-text-other-options))
(reset! *content (export-helper root-block-uuids-or-page-name))))}
(for [n (cons "all" (range 1 10))]
[:option {:key n :value n} n])]]]))
(when @*content
[:div.mt-4
(ui/button (if @*copied? "Copied to clipboard!" "Copy to clipboard")
:class "mr-4"
:on-click (fn []
(if (= tp :png)
(js/navigator.clipboard.write [(js/ClipboardItem. #js {"image/png" @*content})])
(util/copy-to-clipboard! @*content :html (when (= tp :html) @*content)))
(reset! *copied? true)))
(ui/button "Save to file"
:on-click #(let [file-name (if (string? root-block-uuids-or-page-name)
(-> (db/get-page root-block-uuids-or-page-name)
(util/get-page-original-name))
(t/now))]
(utils/saveToFile (js/Blob. [@*content]) (str "logseq_" file-name) (if (= tp :text) "txt" (name tp)))))])]))