export.cljs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. (ns frontend.components.export
  2. (:require [cljs-time.core :as t]
  3. ["/frontend/utils" :as utils]
  4. [frontend.context.i18n :refer [t]]
  5. [frontend.db :as db]
  6. [frontend.handler.export.text :as export-text]
  7. [frontend.handler.export.html :as export-html]
  8. [frontend.handler.export.opml :as export-opml]
  9. [frontend.handler.export :as export]
  10. [frontend.image :as image]
  11. [frontend.mobile.util :as mobile-util]
  12. [frontend.state :as state]
  13. [frontend.ui :as ui]
  14. [frontend.util :as util]
  15. [rum.core :as rum]))
  16. (rum/defc export
  17. []
  18. (when-let [current-repo (state/get-current-repo)]
  19. [:div.export
  20. [:h1.title (t :export)]
  21. [:ul.mr-1
  22. [:li.mb-4
  23. [:a.font-medium {:on-click #(export/export-repo-as-edn-v2! current-repo)}
  24. (t :export-edn)]]
  25. [:li.mb-4
  26. [:a.font-medium {:on-click #(export/export-repo-as-json-v2! current-repo)}
  27. (t :export-json)]]
  28. (when (util/electron?)
  29. [:li.mb-4
  30. [:a.font-medium {:on-click #(export/download-repo-as-html! current-repo)}
  31. (t :export-public-pages)]])
  32. (when-not (mobile-util/native-platform?)
  33. [:li.mb-4
  34. [:a.font-medium {:on-click #(export-text/export-repo-as-markdown! current-repo)}
  35. (t :export-markdown)]])
  36. (when-not (mobile-util/native-platform?)
  37. [:li.mb-4
  38. [:a.font-medium {:on-click #(export-opml/export-repo-as-opml! current-repo)}
  39. (t :export-opml)]])
  40. (when-not (mobile-util/native-platform?)
  41. [:li.mb-4
  42. [:a.font-medium {:on-click #(export/export-repo-as-roam-json! current-repo)}
  43. (t :export-roam-json)]])]
  44. [:a#download-as-edn-v2.hidden]
  45. [:a#download-as-json-v2.hidden]
  46. [:a#download-as-roam-json.hidden]
  47. [:a#download-as-html.hidden]
  48. [:a#download-as-zip.hidden]
  49. [:a#export-as-markdown.hidden]
  50. [:a#export-as-opml.hidden]
  51. [:a#convert-markdown-to-unordered-list-or-heading.hidden]]))
  52. (def *export-block-type (atom :text))
  53. (def text-indent-style-options [{:label "dashes"
  54. :selected false}
  55. {:label "spaces"
  56. :selected false}
  57. {:label "no-indent"
  58. :selected false}])
  59. (defn- export-helper
  60. [block-uuids-or-page-name]
  61. (let [current-repo (state/get-current-repo)
  62. text-indent-style (state/get-export-block-text-indent-style)
  63. text-remove-options (set (state/get-export-block-text-remove-options))
  64. text-other-options (state/get-export-block-text-other-options)
  65. tp @*export-block-type]
  66. (case tp
  67. :text (export-text/export-blocks-as-markdown
  68. current-repo block-uuids-or-page-name
  69. {:indent-style text-indent-style :remove-options text-remove-options :other-options text-other-options})
  70. :opml (export-opml/export-blocks-as-opml
  71. current-repo block-uuids-or-page-name {:remove-options text-remove-options :other-options text-other-options})
  72. :html (export-html/export-blocks-as-html
  73. current-repo block-uuids-or-page-name {:remove-options text-remove-options :other-options text-other-options})
  74. "")))
  75. (defn- get-zoom-level
  76. [page-uuid]
  77. (let [uuid (:block/uuid (db/get-page page-uuid))
  78. whiteboard-camera (->> (str "logseq.tldraw.camera:" uuid)
  79. (.getItem js/sessionStorage)
  80. (js/JSON.parse)
  81. (js->clj))]
  82. (or (get whiteboard-camera "zoom") 1)))
  83. (defn- get-image-blob
  84. [block-uuids-or-page-name {:keys [transparent-bg? x y width height zoom]} callback]
  85. (let [html js/document.body.parentNode
  86. style (js/window.getComputedStyle html)
  87. background (when-not transparent-bg? (.getPropertyValue style "--ls-primary-background-color"))
  88. page? (string? block-uuids-or-page-name)
  89. selector (if page?
  90. "#main-content-container"
  91. (str "[blockid='" (str (first block-uuids-or-page-name)) "']"))
  92. container (js/document.querySelector selector)
  93. scale (if page? (/ 1 (or zoom (get-zoom-level block-uuids-or-page-name))) 1)
  94. options #js {:allowTaint true
  95. :useCORS true
  96. :backgroundColor (or background "transparent")
  97. :x (or (/ x scale) 0)
  98. :y (or (/ y scale) 0)
  99. :width (when width (/ width scale))
  100. :height (when height (/ height scale))
  101. :scrollX 0
  102. :scrollY 0
  103. :scale scale
  104. :windowHeight (when (string? block-uuids-or-page-name)
  105. (.-scrollHeight container))}]
  106. (-> (js/html2canvas container options)
  107. (.then (fn [canvas] (.toBlob canvas (fn [blob]
  108. (when blob
  109. (let [img (js/document.getElementById "export-preview")
  110. img-url (image/create-object-url blob)]
  111. (set! (.-src img) img-url)
  112. (callback blob)))) "image/png"))))))
  113. (rum/defcs ^:large-vars/cleanup-todo
  114. export-blocks < rum/static
  115. (rum/local false ::copied?)
  116. (rum/local nil ::text-remove-options)
  117. (rum/local nil ::text-indent-style)
  118. (rum/local nil ::text-other-options)
  119. (rum/local nil ::content)
  120. {:will-mount (fn [state]
  121. (reset! *export-block-type (if (:whiteboard? (last (:rum/args state))) :png :text))
  122. (if (= @*export-block-type :png)
  123. (do (reset! (::content state) nil)
  124. (get-image-blob (first (:rum/args state))
  125. (merge (second (:rum/args state)) {:transparent-bg? false})
  126. (fn [blob] (reset! (::content state) blob))))
  127. (reset! (::content state) (export-helper (first (:rum/args state)))))
  128. (reset! (::text-remove-options state) (set (state/get-export-block-text-remove-options)))
  129. (reset! (::text-indent-style state) (state/get-export-block-text-indent-style))
  130. (reset! (::text-other-options state) (state/get-export-block-text-other-options))
  131. state)}
  132. [state root-block-uuids-or-page-name {:keys [whiteboard?] :as options}]
  133. (let [tp @*export-block-type
  134. *text-other-options (::text-other-options state)
  135. *text-remove-options (::text-remove-options state)
  136. *text-indent-style (::text-indent-style state)
  137. *copied? (::copied? state)
  138. *content (::content state)]
  139. [:div.export.resize
  140. (when-not whiteboard?
  141. [:div.flex
  142. {:class "mb-2"}
  143. (ui/button "Text"
  144. :class "mr-4 w-20"
  145. :on-click #(do (reset! *export-block-type :text)
  146. (reset! *content (export-helper root-block-uuids-or-page-name))))
  147. (ui/button "OPML"
  148. :class "mr-4 w-20"
  149. :on-click #(do (reset! *export-block-type :opml)
  150. (reset! *content (export-helper root-block-uuids-or-page-name))))
  151. (ui/button "HTML"
  152. :class "mr-4 w-20"
  153. :on-click #(do (reset! *export-block-type :html)
  154. (reset! *content (export-helper root-block-uuids-or-page-name))))
  155. (when-not (seq? root-block-uuids-or-page-name)
  156. (ui/button "PNG"
  157. :class "w-20"
  158. :on-click #(do (reset! *export-block-type :png)
  159. (reset! *content nil)
  160. (get-image-blob root-block-uuids-or-page-name (merge options {:transparent-bg? false}) (fn [blob] (reset! *content blob))))))])
  161. (if (= :png tp)
  162. [:div.flex.items-center.justify-center.relative
  163. (when (not @*content) [:div.absolute (ui/loading "")])
  164. [:img {:alt "export preview" :id "export-preview" :class "my-4" :style {:visibility (when (not @*content) "hidden")}}]]
  165. [:textarea.overflow-y-auto.h-96 {:value @*content :read-only true}])
  166. (if (= :png tp)
  167. [:div.flex.items-center
  168. [:div (t :export-transparent-background)]
  169. (ui/checkbox {:class "mr-2 ml-4"
  170. :on-change (fn [e]
  171. (reset! *content nil)
  172. (get-image-blob root-block-uuids-or-page-name (merge options {:transparent-bg? e.currentTarget.checked}) (fn [blob] (reset! *content blob))))})]
  173. (let [options (->> text-indent-style-options
  174. (mapv (fn [opt]
  175. (if (= @*text-indent-style (:label opt))
  176. (assoc opt :selected true)
  177. opt))))]
  178. [:div [:div.flex.items-center
  179. [:label.mr-4
  180. {:style {:visibility (if (= :text tp) "visible" "hidden")}}
  181. "Indentation style:"]
  182. [:select.block.my-2.text-lg.rounded.border.py-0.px-1
  183. {:style {:visibility (if (= :text tp) "visible" "hidden")}
  184. :on-change (fn [e]
  185. (let [value (util/evalue e)]
  186. (state/set-export-block-text-indent-style! value)
  187. (reset! *text-indent-style value)
  188. (reset! *content (export-helper root-block-uuids-or-page-name))))}
  189. (for [{:keys [label value selected]} options]
  190. [:option (cond->
  191. {:key label
  192. :value (or value label)}
  193. selected
  194. (assoc :selected selected))
  195. label])]]
  196. [:div.flex.items-center
  197. (ui/checkbox {:class "mr-2"
  198. :style {:visibility (if (#{:text :html :opml} tp) "visible" "hidden")}
  199. :checked (contains? @*text-remove-options :page-ref)
  200. :on-change (fn [e]
  201. (state/update-export-block-text-remove-options! e :page-ref)
  202. (reset! *text-remove-options (state/get-export-block-text-remove-options))
  203. (reset! *content (export-helper root-block-uuids-or-page-name)))})
  204. [:div {:style {:visibility (if (#{:text :html :opml} tp) "visible" "hidden")}}
  205. "[[text]] -> text"]
  206. (ui/checkbox {:class "mr-2 ml-4"
  207. :style {:visibility (if (#{:text :html :opml} tp) "visible" "hidden")}
  208. :checked (contains? @*text-remove-options :emphasis)
  209. :on-change (fn [e]
  210. (state/update-export-block-text-remove-options! e :emphasis)
  211. (reset! *text-remove-options (state/get-export-block-text-remove-options))
  212. (reset! *content (export-helper root-block-uuids-or-page-name)))})
  213. [:div {:style {:visibility (if (#{:text :html :opml} tp) "visible" "hidden")}}
  214. "remove emphasis"]
  215. (ui/checkbox {:class "mr-2 ml-4"
  216. :style {:visibility (if (#{:text :html :opml} tp) "visible" "hidden")}
  217. :checked (contains? @*text-remove-options :tag)
  218. :on-change (fn [e]
  219. (state/update-export-block-text-remove-options! e :tag)
  220. (reset! *text-remove-options (state/get-export-block-text-remove-options))
  221. (reset! *content (export-helper root-block-uuids-or-page-name)))})
  222. [:div {:style {:visibility (if (#{:text :html :opml} tp) "visible" "hidden")}}
  223. "remove #tags"]]
  224. [:div.flex.items-center
  225. (ui/checkbox {:class "mr-2"
  226. :style {:visibility (if (#{:text} tp) "visible" "hidden")}
  227. :checked (boolean (:newline-after-block @*text-other-options))
  228. :on-change (fn [e]
  229. (state/update-export-block-text-other-options!
  230. :newline-after-block (boolean (util/echecked? e)))
  231. (reset! *text-other-options (state/get-export-block-text-other-options))
  232. (reset! *content (export-helper root-block-uuids-or-page-name)))})
  233. [:div {:style {:visibility (if (#{:text} tp) "visible" "hidden")}}
  234. "newline after block"]
  235. (ui/checkbox {:class "mr-2 ml-4"
  236. :style {:visibility (if (#{:text} tp) "visible" "hidden")}
  237. :checked (contains? @*text-remove-options :property)
  238. :on-change (fn [e]
  239. (state/update-export-block-text-remove-options! e :property)
  240. (reset! *text-remove-options (state/get-export-block-text-remove-options))
  241. (reset! *content (export-helper root-block-uuids-or-page-name)))})
  242. [:div {:style {:visibility (if (#{:text} tp) "visible" "hidden")}}
  243. "remove properties"]]
  244. [:div.flex.items-center
  245. [:label.mr-2 {:style {:visibility (if (#{:text :html :opml} tp) "visible" "hidden")}}
  246. "level <="]
  247. [:select.block.my-2.text-lg.rounded.border.px-2.py-0
  248. {:style {:visibility (if (#{:text :html :opml} tp) "visible" "hidden")}
  249. :value (or (:keep-only-level<=N @*text-other-options) :all)
  250. :on-change (fn [e]
  251. (let [value (util/evalue e)
  252. level (if (= "all" value) :all (util/safe-parse-int value))]
  253. (state/update-export-block-text-other-options! :keep-only-level<=N level)
  254. (reset! *text-other-options (state/get-export-block-text-other-options))
  255. (reset! *content (export-helper root-block-uuids-or-page-name))))}
  256. (for [n (cons "all" (range 1 10))]
  257. [:option {:key n :value n} n])]]]))
  258. (when @*content
  259. [:div.mt-4.flex.flex-row.gap-2
  260. (ui/button (if @*copied? (t :export-copied-to-clipboard) (t :export-copy-to-clipboard))
  261. :class "mr-4"
  262. :on-click (fn []
  263. (if (= tp :png)
  264. (js/navigator.clipboard.write [(js/ClipboardItem. #js {"image/png" @*content})])
  265. (util/copy-to-clipboard! @*content :html (when (= tp :html) @*content)))
  266. (reset! *copied? true)))
  267. (ui/button (t :export-save-to-file)
  268. :on-click #(let [file-name (if (string? root-block-uuids-or-page-name)
  269. (-> (db/get-page root-block-uuids-or-page-name)
  270. (util/get-page-original-name))
  271. (t/now))]
  272. (utils/saveToFile (js/Blob. [@*content]) (str "logseq_" file-name) (if (= tp :text) "txt" (name tp)))))])]))