enhance(ux): add progress bar when exporting zip

This commit is contained in:
Tienson Qin
2026-02-01 16:16:29 +08:00
parent 199ecb6bbd
commit 267587a551
8 changed files with 153 additions and 44 deletions

View File

@@ -8,14 +8,16 @@
#?(:cljs
(defn make-export-zip
"Makes a zipfile for an exported version of graph. Removes files with blank content"
[zip-filename file-name-content]
[zip-filename file-name-content & {:as file-opts}]
(let [zip (JSZip.)
folder (.folder zip zip-filename)]
folder (.folder zip zip-filename)
file-opts (when (some? file-opts) (clj->js file-opts))]
(doseq [[file-name content] file-name-content]
(when-not (string/blank? content)
(.file folder (-> file-name
(string/replace #"^/+" ""))
content)))
content
file-opts)))
zip)))
;; Macros are defined at top-level for frontend and nbb

View File

@@ -429,11 +429,12 @@
(rum/defc indicator-progress < rum/reactive
[]
(let [{:keys [total current-idx current-page]} (state/sub :graph/importing-state)
(let [{:keys [total current-idx current-page label]} (state/sub :graph/importing-state)
label (or label (t :importing))
left-label (if (and current-idx total (= current-idx total))
[:div.flex.flex-row.font-bold "Loading ..."]
[:div.flex.flex-row.font-bold
(t :importing)
label
[:div.hidden.md:flex.flex-row
[:span.mr-1 ": "]
[:div.text-ellipsis-wrapper {:style {:max-width 300}}

View File

@@ -0,0 +1,22 @@
(ns frontend.components.progress
(:require [frontend.state :as state]
[rum.core :as rum]))
(defn- progress-bar
[width]
[:div.w-full.rounded-full.h-2\.5.animate-pulse.bg-gray-06-alpha
[:div.bg-gray-09-alpha.h-2\.5.rounded-full {:style {:width (str width "%")}
:transition "width 1s"}]])
(rum/defc progress-indicator < rum/reactive
[]
(let [{:keys [total current-idx current-page label]} (state/sub :graph/importing-state)
label (or label "Processing")
width (js/Math.round (* (.toFixed (/ (or current-idx 0) (max 1 total)) 2) 100))]
[:div.p-5
[:div.flex.justify-between.mb-1
[:span.text-base label]
[:span.text-sm.font-medium (when (and total current-idx)
(str current-idx "/" total))]]
[:div.text-xs.opacity-70.mb-2 current-page]
(progress-bar width)]))

View File

@@ -9,7 +9,17 @@
(aset args "lastModified" last-modified)
(js/File. blob-content file-name args)))
(defn make-zip [zip-filename file-name-content _repo]
(let [zip (cli-common-util/make-export-zip zip-filename file-name-content)]
(p/let [zip-blob (.generateAsync zip #js {:type "blob"})]
(defn make-zip
[zip-filename file-name-content _repo & {:keys [progress-fn compression]}]
(let [compression (or compression "STORE")
zip (cli-common-util/make-export-zip zip-filename
file-name-content
{:compression compression})
opts #js {:type "blob"
:streamFiles true
:compression compression}]
(p/let [zip-blob (.generateAsync zip opts
(when progress-fn
(fn [metadata]
(progress-fn (.-percent metadata)))))]
(make-file zip-blob (str zip-filename ".zip") {:type "application/zip"}))))

View File

@@ -19,6 +19,7 @@
[frontend.handler.db-based.vector-search-flows :as vector-search-flows]
[frontend.handler.e2ee]
[frontend.handler.events :as events]
[frontend.handler.events.export]
[frontend.handler.events.rtc]
[frontend.handler.events.ui]
[frontend.handler.global-config :as global-config-handler]

View File

@@ -133,32 +133,34 @@
:current-page "Assets"}))]
(if-not entry
(notification/show! "Zip missing db.sqlite. Please check the archive structure." :error)
(p/let [sqlite-buffer (.async ^js (:entry entry) "arraybuffer")]
(import-from-sqlite-db!
sqlite-buffer
bare-graph-name
(fn []
(p/let [repo (state/get-current-repo)
{:keys [copied failed]}
(<copy-zip-assets!
repo
entries
{:total asset-total
:progress-fn (fn [current total]
(when (pos? total)
(state/set-state! :graph/importing-state {:total total
:current-idx current
:current-page "Assets"})))})]
(when (pos? copied)
(notification/show! (str "Imported " copied " assets.") :success))
(when (seq failed)
(notification/show!
(str "Skipped " (count failed) " assets. See console for details.")
:warning false)
(js/console.warn "Zip import skipped assets:" (clj->js failed)))
(state/set-state! :graph/importing nil)
(state/set-state! :graph/importing-state nil)
(finished-ok-handler)))))))
(do
(shui/dialog-close!)
(p/let [sqlite-buffer (.async ^js (:entry entry) "arraybuffer")]
(import-from-sqlite-db!
sqlite-buffer
bare-graph-name
(fn []
(p/let [repo (state/get-current-repo)
{:keys [copied failed]}
(<copy-zip-assets!
repo
entries
{:total asset-total
:progress-fn (fn [current total]
(when (pos? total)
(state/set-state! :graph/importing-state {:total total
:current-idx current
:current-page "Assets"})))})]
(when (pos? copied)
(notification/show! (str "Imported " copied " assets.") :success))
(when (seq failed)
(notification/show!
(str "Skipped " (count failed) " assets. See console for details.")
:warning false)
(js/console.warn "Zip import skipped assets:" (clj->js failed)))
(state/set-state! :graph/importing nil)
(state/set-state! :graph/importing-state nil)
(finished-ok-handler))))))))
(p/catch
(fn [e]
(js/console.error e)

View File

@@ -0,0 +1,45 @@
(ns frontend.handler.events.export
"Export events"
(:require [frontend.handler.events :as events]
[frontend.state :as state]
[frontend.ui :as ui]
[logseq.shui.dialog.core :as shui-dialog]
[logseq.shui.ui :as shui]
[rum.core :as rum]))
(rum/defc indicator-progress < rum/reactive
[]
(let [{:keys [total current-idx current-page label]} (state/sub :graph/exporting-state)
label (or label "Exporting")
left-label (if (and current-idx total (= current-idx total))
[:div.flex.flex-row.font-bold "Loading ..."]
[:div.flex.flex-row.font-bold
label
[:div.hidden.md:flex.flex-row
[:span.mr-1 ": "]
[:div.text-ellipsis-wrapper {:style {:max-width 300}}
current-page]]])
width (js/Math.round (* (.toFixed (/ current-idx total) 2) 100))
process (when (and total current-idx)
(str current-idx "/" total))]
[:div.p-5
(ui/progress-bar-with-label width left-label process)]))
(defmethod events/handle :dialog/export-zip [[_ label]]
(shui/dialog-close!)
(state/set-state! :graph/exporting :export-zip)
(state/set-state! :graph/exporting-state {:total 100
:current-idx 0
:current-page label
:label "Exporting"})
(when-not (shui-dialog/get-modal :export-indicator)
(shui/dialog-open! indicator-progress
{:id :export-indicator
:content-props
{:onPointerDownOutside #(.preventDefault %)
:onOpenAutoFocus #(.preventDefault %)}})))
(defmethod events/handle :dialog/close-export-zip [_]
(state/set-state! :graph/exporting nil)
(state/set-state! :graph/exporting-state nil)
(shui/dialog-close! :export-indicator))

View File

@@ -44,16 +44,42 @@
(defn db-based-export-repo-as-zip!
[repo]
(p/let [db-data (persist-db/<export-db repo {:return-data? true})
filename "db.sqlite"
repo-name (common-sqlite/sanitize-db-name repo)
assets (assets-handler/<get-all-assets)
files (cons [filename db-data] assets)
zipfile (zip/make-zip repo-name files repo)]
(when-let [anchor (gdom/getElement "download-as-zip")]
(.setAttribute anchor "href" (js/window.URL.createObjectURL zipfile))
(.setAttribute anchor "download" (.-name zipfile))
(.click anchor))))
(state/pub-event! [:dialog/export-zip "Preparing zip"])
(-> (p/let [db-data (persist-db/<export-db repo {:return-data? true})
filename "db.sqlite"
repo-name (common-sqlite/sanitize-db-name repo)
_ (state/set-state! :graph/exporting-state {:total 100
:current-idx 20
:current-page "Collecting assets"
:label "Exporting"})
assets (assets-handler/<get-all-assets)
files (cons [filename db-data] assets)
_ (state/set-state! :graph/exporting-state {:total 100
:current-idx 40
:current-page "Creating zip"
:label "Exporting"})
zipfile (zip/make-zip repo-name files repo
{:compression "STORE"
:progress-fn (fn [percent]
(let [scaled (+ 40 (* 0.6 percent))]
(state/set-state! :graph/exporting-state
{:total 100
:current-idx (js/Math.round scaled)
:current-page "Creating zip"
:label "Exporting"})))})]
(state/set-state! :graph/exporting-state {:total 100
:current-idx 100
:current-page "Finalizing"
:label "Exporting"})
(when-let [anchor (gdom/getElement "download-as-zip")]
(.setAttribute anchor "href" (js/window.URL.createObjectURL zipfile))
(.setAttribute anchor "download" (.-name zipfile))
(.click anchor)))
(p/catch (fn [error]
(js/console.error error)
(notification/show! "Export zip failed." :error)))
(p/finally (fn []
(state/pub-event! [:dialog/close-export-zip])))))
(defn export-repo-as-zip!
[repo]