0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-01-08 16:00:19 -05:00

♻️ Refactor thumbnail rendering on workspace

This commit is contained in:
Aitor 2023-05-12 13:38:29 +02:00 committed by Alonso Torres
parent 1d69da1ca5
commit 48834f96d3
29 changed files with 644 additions and 616 deletions

View file

@ -48,8 +48,9 @@
(let [sql (str/concat
"select object_id, data, media_id "
" from file_object_thumbnail"
" where file_id=?")]
(->> (db/exec! conn [sql file-id])
" where file_id=?")
res (db/exec! conn [sql file-id])]
(->> res
(d/index-by :object-id (fn [row]
(or (some-> row :media-id get-public-uri)
(:data row))))
@ -57,14 +58,16 @@
([conn file-id object-ids]
(let [sql (str/concat
"select object_id, data "
"select object_id, data, media_id "
" from file_object_thumbnail"
" where file_id=? and object_id = ANY(?)")
ids (db/create-array conn "text" (seq object-ids))]
(->> (db/exec! conn [sql file-id ids])
(d/index-by :object-id (fn [row]
(or (some-> row :media-id get-public-uri)
(:data row))))))))
ids (db/create-array conn "text" (seq object-ids))
res (db/exec! conn [sql file-id ids])]
(d/index-by :object-id
(fn [row]
(or (some-> row :media-id get-public-uri)
(:data row)))
res))))
(sv/defmethod ::get-file-object-thumbnails
"Retrieve a file object thumbnails."
@ -248,125 +251,200 @@
;; --- MUTATION COMMAND: upsert-file-object-thumbnail
(def sql:upsert-object-thumbnail-1
(def sql:upsert-object-thumbnail
"insert into file_object_thumbnail(file_id, object_id, data)
values (?, ?, ?)
on conflict(file_id, object_id) do
update set data = ?;")
(def sql:upsert-object-thumbnail-2
"insert into file_object_thumbnail(file_id, object_id, media_id)
values (?, ?, ?)
on conflict(file_id, object_id) do
update set media_id = ?;")
(defn upsert-file-object-thumbnail!
[{:keys [::db/conn ::sto/storage]} {:keys [file-id object-id] :as params}]
[conn {:keys [file-id object-id data]}]
(if data
(db/exec-one! conn [sql:upsert-object-thumbnail file-id object-id data data])
(db/delete! conn :file-object-thumbnail {:file-id file-id :object-id object-id})))
;; NOTE: params can come with data set but with `nil` value, so we
;; need first check the existence of the key and then the value.
(cond
(contains? params :data)
(if-let [data (:data params)]
(db/exec-one! conn [sql:upsert-object-thumbnail-1 file-id object-id data data])
(db/delete! conn :file-object-thumbnail {:file-id file-id :object-id object-id}))
(contains? params :media)
(if-let [{:keys [path mtype] :as media} (:media params)]
(let [_ (media/validate-media-type! media)
_ (media/validate-media-size! media)
hash (sto/calculate-hash path)
data (-> (sto/content path)
(sto/wrap-with-hash hash))
media (sto/put-object! storage
{::sto/content data
::sto/deduplicate? false
:content-type mtype
:bucket "file-object-thumbnail"})]
(db/exec-one! conn [sql:upsert-object-thumbnail-2 file-id object-id (:id media) (:id media)]))
(db/delete! conn :file-object-thumbnail {:file-id file-id :object-id object-id}))))
;; FIXME: change it on validation refactor
(s/def ::data (s/nilable ::us/string))
(s/def ::media (s/nilable ::media/upload))
(s/def ::object-id ::us/string)
(s/def ::upsert-file-object-thumbnail
(s/keys :req [::rpc/profile-id]
:req-un [::file-id ::object-id]
:opt-un [::data ::media]))
:opt-un [::data]))
(sv/defmethod ::upsert-file-object-thumbnail
{::doc/added "1.17"
::doc/deprecated "1.19"
::audit/skip true}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id file-id)
(assert (or (contains? params :data)
(contains? params :media)))
(when-not (db/read-only? conn)
(upsert-file-object-thumbnail! conn params)
nil)))
;; --- MUTATION COMMAND: create-file-object-thumbnail
(def ^:private sql:create-object-thumbnail
"insert into file_object_thumbnail(file_id, object_id, media_id)
values (?, ?, ?)
on conflict(file_id, object_id) do
update set media_id = ?;")
(defn- create-file-object-thumbnail!
[{:keys [::db/conn ::sto/storage]} file-id object-id media]
(let [path (:path media)
mtype (:mtype media)
hash (sto/calculate-hash path)
data (-> (sto/content path)
(sto/wrap-with-hash hash))
media (sto/put-object! storage
{::sto/content data
::sto/deduplicate? false
:content-type mtype
:bucket "file-object-thumbnail"})]
(db/exec-one! conn [sql:create-object-thumbnail file-id object-id
(:id media) (:id media)])))
(s/def ::media (s/nilable ::media/upload))
(s/def ::create-file-object-thumbnail
(s/keys :req [::rpc/profile-id]
:req-un [::file-id ::object-id ::media]))
(sv/defmethod ::create-file-object-thumbnail
{:doc/added "1.19"
::audit/skip true}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id object-id media]}]
(db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id file-id)
(media/validate-media-type! media)
(media/validate-media-size! media)
(when-not (db/read-only? conn)
(-> cfg
(update ::sto/storage media/configure-assets-storage)
(assoc ::db/conn conn)
(create-file-object-thumbnail! file-id object-id media))
nil)))
;; --- MUTATION COMMAND: delete-file-object-thumbnail
(defn- delete-file-object-thumbnail!
[{:keys [::db/conn ::sto/storage]} file-id object-id]
(when-let [{:keys [media-id]} (db/get* conn :file-object-thumbnail
{:file-id file-id
:object-id object-id}
{::db/for-update? true})]
(when media-id
(sto/del-object! storage media-id))
(db/delete! conn :file-object-thumbnail
{:file-id file-id
:object-id object-id})
nil))
(s/def ::delete-file-object-thumbnail
(s/keys :req [::rpc/profile-id]
:req-un [::file-id ::object-id]))
(sv/defmethod ::delete-file-object-thumbnail
{:doc/added "1.19"
::audit/skip true}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id object-id]}]
(db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id file-id)
(when-not (db/read-only? conn)
(let [cfg (-> cfg
(update ::sto/storage media/configure-assets-storage)
(assoc ::db/conn conn))]
(upsert-file-object-thumbnail! cfg params)
nil))))
(-> cfg
(update ::sto/storage media/configure-assets-storage)
(assoc ::db/conn conn)
(delete-file-object-thumbnail! file-id object-id))
nil)))
;; --- MUTATION COMMAND: upsert-file-thumbnail
(def ^:private sql:upsert-file-thumbnail
"insert into file_thumbnail (file_id, revn, data, media_id, props)
values (?, ?, ?, ?, ?::jsonb)
"insert into file_thumbnail (file_id, revn, data, props)
values (?, ?, ?, ?::jsonb)
on conflict(file_id, revn) do
update set data=?, media_id=?, props=?, updated_at=now();")
update set data = ?, props=?, updated_at=now();")
(defn- upsert-file-thumbnail!
[{:keys [::db/conn ::sto/storage]} {:keys [file-id revn props] :as params}]
[conn {:keys [file-id revn data props]}]
(let [props (db/tjson (or props {}))]
(cond
(contains? params :data)
(when-let [data (:data params)]
(db/exec-one! conn [sql:upsert-file-thumbnail
file-id revn data nil props data nil props]))
(db/exec-one! conn [sql:upsert-file-thumbnail
file-id revn data props data props])))
(contains? params :media)
(when-let [{:keys [path mtype] :as media} (:media params)]
(let [_ (media/validate-media-type! media)
_ (media/validate-media-size! media)
hash (sto/calculate-hash path)
data (-> (sto/content path)
(sto/wrap-with-hash hash))
media (sto/put-object! storage
{::sto/content data
::sto/deduplicate? false
:content-type mtype
:bucket "file-thumbnail"})]
(db/exec-one! conn [sql:upsert-file-thumbnail
file-id revn nil (:id media) props nil (:id media) props]))))))
(s/def ::revn ::us/integer)
(s/def ::props map?)
(s/def ::media ::media/upload)
(s/def ::upsert-file-thumbnail
(s/keys :req [::rpc/profile-id]
:req-un [::file-id ::revn ::props]
:opt-un [::data ::media]))
:req-un [::file-id ::revn ::props ::data]))
(sv/defmethod ::upsert-file-thumbnail
"Creates or updates the file thumbnail. Mainly used for paint the
grid thumbnails."
{::doc/added "1.17"
::doc/deprecated "1.19"
::audit/skip true}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id file-id)
(when-not (db/read-only? conn)
(let [cfg (-> cfg
(update ::sto/storage media/configure-assets-storage)
(assoc ::db/conn conn))]
(upsert-file-thumbnail! cfg params))
(upsert-file-thumbnail! conn params)
nil)))
;; --- MUTATION COMMAND: create-file-thumbnail
(def ^:private sql:create-file-thumbnail
"insert into file_thumbnail (file_id, revn, media_id, props)
values (?, ?, ?, ?::jsonb)
on conflict(file_id, revn) do
update set media_id=?, props=?, updated_at=now();")
(defn- create-file-thumbnail!
[{:keys [::db/conn ::sto/storage]} {:keys [file-id revn props media] :as params}]
(media/validate-media-type! media)
(media/validate-media-size! media)
(let [props (db/tjson (or props {}))
path (:path media)
mtype (:mtype media)
hash (sto/calculate-hash path)
data (-> (sto/content path)
(sto/wrap-with-hash hash))
media (sto/put-object! storage
{::sto/content data
::sto/deduplicate? false
:content-type mtype
:bucket "file-thumbnail"})]
(db/exec-one! conn [sql:create-file-thumbnail file-id revn
(:id media) props
(:id media) props])))
(s/def ::media ::media/upload)
(s/def ::create-file-thumbnail
(s/keys :req [::rpc/profile-id]
:req-un [::file-id ::revn ::props ::media]))
(sv/defmethod ::create-file-thumbnail
"Creates or updates the file thumbnail. Mainly used for paint the
grid thumbnails."
{::doc/added "1.19"
::audit/skip true}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id file-id)
(when-not (db/read-only? conn)
(-> cfg
(update ::sto/storage media/configure-assets-storage)
(assoc ::db/conn conn)
(create-file-thumbnail! params))
nil)))

View file

@ -52,7 +52,7 @@
:parent-id uuid/zero
:type :frame}}])
data1 {::th/type :upsert-file-object-thumbnail
data1 {::th/type :create-file-object-thumbnail
::rpc/profile-id (:id profile)
:file-id (:id file)
:object-id "test-key-1"
@ -61,7 +61,7 @@
:path (th/tempfile "backend_tests/test_files/sample.jpg")
:mtype "image/jpeg"}}
data2 {::th/type :upsert-file-object-thumbnail
data2 {::th/type :create-file-object-thumbnail
::rpc/profile-id (:id profile)
:file-id (:id file)
:object-id (str page-id shid)
@ -156,7 +156,7 @@
:revn 1
:data "data:base64,1234123124"}
data2 {::th/type :upsert-file-thumbnail
data2 {::th/type :create-file-thumbnail
::rpc/profile-id (:id profile)
:file-id (:id file)
:props {}
@ -166,7 +166,7 @@
:path (th/tempfile "backend_tests/test_files/sample2.jpg")
:mtype "image/jpeg"}}
data3 {::th/type :upsert-file-thumbnail
data3 {::th/type :create-file-thumbnail
::rpc/profile-id (:id profile)
:file-id (:id file)
:props {}
@ -177,11 +177,11 @@
:mtype "image/jpeg"}}]
(let [out (th/command! data1)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (nil? (:result out))))
(let [out (th/command! data2)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (nil? (:result out))))
@ -251,10 +251,10 @@
(let [row (th/db-get :storage-object {:id (:media-id row1)} {::db/remove-deleted? false})]
(t/is (nil? row)))
(t/is (some? (sto/get-object storage (:media-id row3))))
(t/is (some? (sto/get-object storage (:media-id row3)))))
)))
))
(t/deftest get-file-object-thumbnail
(let [storage (::sto/storage th/*system*)
@ -269,7 +269,7 @@
:object-id "test-key-1"
:data "data:base64,1234123124"}
data2 {::th/type :upsert-file-object-thumbnail
data2 {::th/type :create-file-object-thumbnail
::rpc/profile-id (:id profile)
:file-id (:id file)
:object-id "test-key-2"

View file

@ -137,7 +137,7 @@
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)]
(->> (rp/command! :get-webhooks {:team-id team-id})
(->> (rp/cmd! :get-webhooks {:team-id team-id})
(rx/map team-webhooks-fetched))))))
;; --- EVENT: fetch-projects
@ -302,7 +302,7 @@
(ptk/reify ::fetch-builtin-templates
ptk/WatchEvent
(watch [_ _ _]
(->> (rp/command :retrieve-list-of-builtin-templates)
(->> (rp/cmd! :retrieve-list-of-builtin-templates)
(rx/map builtin-templates-fetched)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -555,7 +555,7 @@
{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)]
(->> (rp/command! :delete-webhook params)
(->> (rp/cmd! :delete-webhook params)
(rx/tap on-success)
(rx/catch on-error))))))
@ -578,7 +578,7 @@
{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)]
(->> (rp/command! :update-webhook params)
(->> (rp/cmd! :update-webhook params)
(rx/tap on-success)
(rx/catch on-error))))))
@ -598,7 +598,7 @@
{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)]
(->> (rp/command! :create-webhook params)
(->> (rp/cmd! :create-webhook params)
(rx/tap on-success)
(rx/catch on-error))))))

View file

@ -101,7 +101,7 @@
{:enabled true
:page-id page-id
:file-id file-id
:object-id (:id frame)
:object-id (:id frame)
:shape frame
:name (:name frame)})]
@ -145,7 +145,7 @@
ptk/WatchEvent
(watch [_ _ _]
(when (= status "ended")
(->> (rp/command! :export {:cmd :get-resource :blob? true :id resource-id})
(->> (rp/cmd! :export {:cmd :get-resource :blob? true :id resource-id})
(rx/delay 500)
(rx/map #(dom/trigger-download filename %)))))))
@ -165,9 +165,9 @@
:wait true}]
(rx/concat
(rx/of ::dwp/force-persist)
(->> (rp/command! :export params)
(->> (rp/cmd! :export params)
(rx/mapcat (fn [{:keys [id filename]}]
(->> (rp/command! :export {:cmd :get-resource :blob? true :id id})
(->> (rp/cmd! :export {:cmd :get-resource :blob? true :id id})
(rx/map (fn [data]
(dom/trigger-download filename data)
(clear-export-state uuid/zero))))))
@ -213,7 +213,7 @@
;; Launch the exportation process and stores the resource id
;; locally.
(->> (rp/command! :export params)
(->> (rp/cmd! :export params)
(rx/map (fn [{:keys [id] :as resource}]
(vreset! resource-id id)
(initialize-export-status exports cmd resource))))

View file

@ -535,7 +535,7 @@
(ptk/reify ::fetch-access-tokens
ptk/WatchEvent
(watch [_ _ _]
(->> (rp/command! :get-access-tokens)
(->> (rp/cmd! :get-access-tokens)
(rx/map access-tokens-fetched)))))
;; --- EVENT: create-access-token
@ -555,7 +555,7 @@
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)]
(->> (rp/command! :create-access-token params)
(->> (rp/cmd! :create-access-token params)
(rx/map access-token-created)
(rx/tap on-success)
(rx/catch on-error))))))
@ -571,6 +571,6 @@
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)]
(->> (rp/command! :delete-access-token params)
(->> (rp/cmd! :delete-access-token params)
(rx/tap on-success)
(rx/catch on-error))))))

View file

@ -267,6 +267,29 @@
(when needs-update?
(rx/of (dwl/notify-sync-file file-id)))))))
(defn- fetch-thumbnail-blob-uri
[uri]
(->> (http/send! {:uri uri
:response-type :blob
:method :get})
(rx/map :body)
(rx/map (fn [blob] (.createObjectURL js/URL blob)))))
(defn- fetch-thumbnail-blobs
[file-id]
(->> (rp/cmd! :get-file-object-thumbnails {:file-id file-id})
(rx/mapcat (fn [thumbnails]
(->> (rx/from thumbnails)
(rx/mapcat (fn [[k v]]
;; we only need to fetch the thumbnail if
;; it is a data:uri, otherwise we can just
;; use the value as is.
(if (.startsWith v "data:")
(->> (fetch-thumbnail-blob-uri v)
(rx/map (fn [uri] [k uri])))
(rx/of [k v])))))))
(rx/reduce conj {})))
(defn- fetch-bundle
[project-id file-id]
(ptk/reify ::fetch-bundle
@ -285,9 +308,8 @@
;; WTF is this?
share-id (-> state :viewer-local :share-id)
stoper (rx/filter (ptk/type? ::fetch-bundle) stream)]
(->> (rx/zip (rp/cmd! :get-file {:id file-id :features features})
(rp/cmd! :get-file-object-thumbnails {:file-id file-id})
(fetch-thumbnail-blobs file-id)
(rp/cmd! :get-project {:id project-id})
(rp/cmd! :get-team-users {:file-id file-id})
(rp/cmd! :get-profiles-for-file-comments {:file-id file-id :share-id share-id}))

View file

@ -94,7 +94,7 @@
(->> (rx/from uris)
(rx/filter (comp not svg-url?))
(rx/map prepare)
(rx/mapcat #(rp/command! :create-file-media-object-from-url %))
(rx/mapcat #(rp/cmd! :create-file-media-object-from-url %))
(rx/do on-image))
(->> (rx/from uris)

View file

@ -478,10 +478,10 @@
:content (data-uri->blob uri)}
{:name (extract-name uri)}))))
(rx/mapcat (fn [uri-data]
(->> (rp/command! (if (contains? uri-data :content)
:upload-file-media-object
:create-file-media-object-from-url)
uri-data)
(->> (rp/cmd! (if (contains? uri-data :content)
:upload-file-media-object
:create-file-media-object-from-url)
uri-data)
;; When the image uploaded fail we skip the shape
;; returning `nil` will afterward not create the shape.
(rx/catch #(rx/of nil))

View file

@ -14,8 +14,10 @@
[app.main.refs :as refs]
[app.main.repo :as rp]
[app.main.store :as st]
[app.main.worker :as uw]
[app.util.dom :as dom]
[app.util.timers :as ts]
[app.util.http :as http]
[app.util.timers :as tm]
[app.util.webapi :as wapi]
[beicon.core :as rx]
[potok.core :as ptk]))
@ -29,28 +31,22 @@
(rx/filter #(= % id))
(rx/take 1)))
(defn thumbnail-canvas-blob-stream
(defn get-thumbnail
[object-id]
;; Look for the thumbnail canvas to send the data to the backend
(let [node (dom/query (dm/fmt "canvas.thumbnail-canvas[data-object-id='%'][data-ready='true']" object-id))
(let [node (dom/query (dm/fmt "image.thumbnail-canvas[data-object-id='%'][data-ready='true']" object-id))
stopper (->> st/stream
(rx/filter (ptk/type? :app.main.data.workspace/finalize-page))
(rx/take 1))]
;; renders #svg image
(if (some? node)
;; Success: we generate the blob (async call)
(rx/create
(fn [subs]
(ts/raf
(fn []
(.toBlob node (fn [blob]
(rx/push! subs blob)
#_(rx/end! subs))
"image/png")))
(constantly nil)))
(->> (rx/from (js/createImageBitmap node))
(rx/switch-map #(uw/ask! {:cmd :thumbnails/render-offscreen-canvas} %))
(rx/map :result))
;; Not found, we retry after delay
(->> (rx/timer 250)
(rx/flat-map #(thumbnail-canvas-blob-stream object-id))
(rx/merge-map (partial get-thumbnail object-id))
(rx/take-until stopper)))))
(defn clear-thumbnail
@ -59,8 +55,33 @@
ptk/UpdateEvent
(update [_ state]
(let [object-id (dm/str page-id frame-id)]
(when-let [uri (dm/get-in state [:workspace-thumbnails object-id])]
(tm/schedule-on-idle (partial wapi/revoke-uri uri)))
(update state :workspace-thumbnails dissoc object-id)))))
(defn set-workspace-thumbnail
[object-id uri]
(let [prev-uri* (volatile! nil)]
(ptk/reify ::set-workspace-thumbnail
ptk/UpdateEvent
(update [_ state]
(let [prev-uri (dm/get-in state [:workspace-thumbnails object-id])]
(some->> prev-uri (vreset! prev-uri*))
(update state :workspace-thumbnails assoc object-id uri)))
ptk/EffectEvent
(effect [_ _ _]
(tm/schedule-on-idle #(some-> ^boolean @prev-uri* wapi/revoke-uri))))))
(defn duplicate-thumbnail
[old-id new-id]
(ptk/reify ::duplicate-thumbnail
ptk/UpdateEvent
(update [_ state]
(let [page-id (:current-page-id state)
thumbnail (dm/get-in state [:workspace-thumbnails (dm/str page-id old-id)])]
(update state :workspace-thumbnails assoc (dm/str page-id new-id) thumbnail)))))
(defn update-thumbnail
"Updates the thumbnail information for the given frame `id`"
([page-id frame-id]
@ -70,39 +91,33 @@
(ptk/reify ::update-thumbnail
ptk/WatchEvent
(watch [_ state _]
(let [object-id (dm/str page-id frame-id)
file-id (or file-id (:current-file-id state))
blob-result (thumbnail-canvas-blob-stream object-id)
params {:file-id file-id :object-id object-id :data nil}]
(let [object-id (dm/str page-id frame-id)
file-id (or file-id (:current-file-id state))]
(rx/concat
;; Delete the thumbnail first so if we interrupt we can regenerate after
(->> (rp/cmd! :upsert-file-object-thumbnail params)
(rx/catch #(rx/empty)))
;; Remove the thumbnail temporary. If the user changes pages the thumbnail is regenerated
(rx/of #(update % :workspace-thumbnails assoc object-id nil))
(->> (rp/cmd! :delete-file-object-thumbnail {:file-id file-id :object-id object-id})
(rx/catch rx/empty))
;; Send the update to the back-end
(->> blob-result
(->> (get-thumbnail object-id)
(rx/filter (fn [data] (and (some? data) (some? file-id))))
(rx/merge-map
(fn [blob]
(if (some? blob)
(wapi/read-file-as-data-url blob)
(rx/of nil))))
(fn [uri]
(rx/merge
(rx/of (set-workspace-thumbnail object-id uri))
(rx/merge-map
(fn [data]
(if (and (some? data) (some? file-id))
(let [params (assoc params :data data)]
(rx/merge
;; Update the local copy of the thumbnails so we don't need to request it again
(rx/of #(update % :workspace-thumbnails assoc object-id data))
(->> (rp/cmd! :upsert-file-object-thumbnail params)
(rx/catch #(rx/empty))
(rx/ignore))))
(->> (http/send! {:uri uri :response-type :blob :method :get})
(rx/map :body)
(rx/mapcat (fn [blob]
;; Send the data to backend
(let [params {:file-id file-id
:object-id object-id
:media blob}]
(rp/cmd! :create-file-object-thumbnail params))))
(rx/catch rx/empty)
(rx/ignore)))))
(rx/empty))))
(rx/catch #(do (.error js/console %)
(rx/empty))))))))))
@ -137,6 +152,7 @@
(and new-frame-id (not= uuid/zero new-frame-id))
(conj [page-id new-frame-id]))))]
(into #{}
(comp (mapcat extract-ids)
(mapcat get-frame-id))
@ -154,7 +170,7 @@
(rx/filter #(or (= :app.main.data.workspace/finalize-page (ptk/type %))
(= ::watch-state-changes (ptk/type %)))))
workspace-data-str
workspace-data-s
(->> (rx/concat
(rx/of nil)
(rx/from-atom refs/workspace-data {:emit-current-value? true}))
@ -162,33 +178,24 @@
;; deleted objects
(rx/buffer 2 1))
change-str
change-s
(->> stream
(rx/filter #(or (dch/commit-changes? %)
(= (ptk/type %) :app.main.data.workspace.notifications/handle-file-change)))
(rx/observe-on :async))
frame-changes-str
(->> change-str
(rx/with-latest-from workspace-data-str)
frame-changes-s
(->> change-s
(rx/with-latest-from workspace-data-s)
(rx/flat-map extract-frame-changes)
(rx/share))]
(->> (rx/merge
(->> frame-changes-str
(->> frame-changes-s
(rx/filter (fn [[page-id _]] (not= page-id (:current-page-id @st/state))))
(rx/map (fn [[page-id frame-id]] (clear-thumbnail page-id frame-id))))
(->> frame-changes-str
(->> frame-changes-s
(rx/filter (fn [[page-id _]] (= page-id (:current-page-id @st/state))))
(rx/map (fn [[_ frame-id]] (ptk/data-event ::force-render frame-id)))))
(rx/take-until stopper))))))
(defn duplicate-thumbnail
[old-id new-id]
(ptk/reify ::duplicate-thumbnail
ptk/UpdateEvent
(update [_ state]
(let [page-id (:current-page-id state)
thumbnail (dm/get-in state [:workspace-thumbnails (dm/str page-id old-id)])]
(update state :workspace-thumbnails assoc (dm/str page-id new-id) thumbnail)))))

View file

@ -10,29 +10,8 @@
[app.common.uri :as u]
[app.config :as cf]
[app.util.http :as http]
[beicon.core :as rx]))
(derive :get-all-projects ::query)
(derive :get-comment-threads ::query)
(derive :get-file ::query)
(derive :get-file-fragment ::query)
(derive :get-file-libraries ::query)
(derive :get-file-object-thumbnails ::query)
(derive :get-font-variants ::query)
(derive :get-profile ::query)
(derive :get-project ::query)
(derive :get-projects ::query)
(derive :get-team-invitations ::query)
(derive :get-team-members ::query)
(derive :get-team-shared-files ::query)
(derive :get-team-stats ::query)
(derive :get-team-users ::query)
(derive :get-teams ::query)
(derive :get-view-only-bundle ::query)
(derive :search-files ::query)
(derive :retrieve-list-of-builtin-templates ::query)
(derive :get-unread-comment-threads ::query)
(derive :get-team-recent-files ::query)
[beicon.core :as rx]
[cuerdas.core :as str]))
(defn handle-response
[{:keys [status body] :as response}]
@ -65,120 +44,66 @@
:status status
:data body})))
(defn- send-query!
"A simple helper for send and receive transit data on the penpot
query api."
([id params]
(send-query! id params nil))
([id params {:keys [raw-transit?]}]
(let [decode-transit (if raw-transit?
http/conditional-error-decode-transit
http/conditional-decode-transit)]
(->> (http/send! {:method :get
:uri (u/join @cf/public-uri "api/rpc/query/" (name id))
:headers {"accept" "application/transit+json"}
:credentials "include"
:query params})
(rx/map decode-transit)
(rx/mapcat handle-response)))))
(def default-options
{:update-file {:query-params [:id]}
:get-raw-file {:rename-to :get-file :raw-transit? true}
:upsert-file-object-thumbnail {:query-params [:file-id :object-id]}
:create-file-object-thumbnail {:query-params [:file-id :object-id]
:form-data? true}
:export-binfile {:response-type :blob}
:import-binfile {:form-data? true}
:retrieve-list-of-builtin-templates {:query-params :all}
})
(defn- send-mutation!
(defn- send!
"A simple helper for a common case of sending and receiving transit
data to the penpot mutation api."
[id params]
(->> (http/send! {:method :post
:uri (u/join @cf/public-uri "api/rpc/mutation/" (name id))
:headers {"accept" "application/transit+json"}
:credentials "include"
:body (http/transit-data params)})
(rx/map http/conditional-decode-transit)
(rx/mapcat handle-response)))
[id params options]
(let [{:keys [response-type
form-data?
raw-transit?
query-params
rename-to]}
(-> (get default-options id)
(merge options))
(defn- send-command!
"A simple helper for a common case of sending and receiving transit
data to the penpot mutation api."
[id params {:keys [response-type form-data? raw-transit? forward-query-params]}]
(let [decode-fn (if raw-transit?
decode-fn (if raw-transit?
http/conditional-error-decode-transit
http/conditional-decode-transit)
method (if (isa? id ::query) :get :post)]
(->> (http/send! {:method method
:uri (u/join @cf/public-uri "api/rpc/command/" (name id))
:credentials "include"
:headers {"accept" "application/transit+json"}
:body (when (= method :post)
(if form-data?
(http/form-data params)
(http/transit-data params)))
:query (if (= method :get)
params
(if forward-query-params
(select-keys params forward-query-params)
nil))
:response-type (or response-type :text)})
id (or rename-to id)
nid (name id)
method (cond
(= query-params :all) :get
(str/starts-with? nid "get-") :get
:else :post)
request {:method method
:uri (u/join @cf/public-uri "api/rpc/command/" (name id))
:credentials "include"
:headers {"accept" "application/transit+json"}
:body (when (= method :post)
(if form-data?
(http/form-data params)
(http/transit-data params)))
:query (if (= method :get)
params
(if query-params
(select-keys params query-params)
nil))
:response-type (or response-type :text)}]
(->> (http/send! request)
(rx/map decode-fn)
(rx/mapcat handle-response))))
(defn- dispatch [& args] (first args))
(defmulti cmd! (fn [id _] id))
(defmulti query dispatch)
(defmulti mutation dispatch)
(defmulti command dispatch)
(defmethod query :default
(defmethod cmd! :default
[id params]
(send-query! id params))
(send! id params nil))
(defmethod command :get-raw-file
[_id params]
(send-command! :get-file params {:raw-transit? true}))
(defmethod mutation :default
[id params]
(send-mutation! id params))
(defmethod command :default
[id params]
(send-command! id params nil))
(defmethod command :update-file
[id params]
(send-command! id params {:forward-query-params [:id]}))
(defmethod command :upsert-file-object-thumbnail
[id params]
(send-command! id params {:forward-query-params [:file-id :object-id]}))
(defmethod command :get-file-object-thumbnails
[id params]
(send-command! id params {:forward-query-params [:file-id]}))
(defmethod command :export-binfile
[id params]
(send-command! id params {:response-type :blob}))
(defmethod command :import-binfile
[id params]
(send-command! id params {:form-data? true}))
(defn query!
([id] (query id {}))
([id params] (query id params)))
(defn mutation!
([id] (mutation id {}))
([id params] (mutation id params)))
(defn command!
([id] (command id {}))
([id params] (command id params)))
(defn cmd!
([id] (command id {}))
([id params] (command id params)))
(defmethod command :login-with-oidc
(defmethod cmd! :login-with-oidc
[_ {:keys [provider] :as params}]
(let [uri (u/join @cf/public-uri "api/auth/oauth/" (d/name provider))
params (dissoc params :provider)]
@ -199,7 +124,7 @@
(rx/map http/conditional-decode-transit)
(rx/mapcat handle-response)))
(defmethod command :export
(defmethod cmd! :export
[_ params]
(let [default {:wait false :blob? false}]
(send-export (merge default params))))
@ -208,16 +133,7 @@
(derive :update-profile-photo ::multipart-upload)
(derive :update-team-photo ::multipart-upload)
(defmethod mutation ::multipart-upload
[id params]
(->> (http/send! {:method :post
:uri (u/join @cf/public-uri "api/rpc/mutation/" (name id))
:credentials "include"
:body (http/form-data params)})
(rx/map http/conditional-decode-transit)
(rx/mapcat handle-response)))
(defmethod command ::multipart-upload
(defmethod cmd! ::multipart-upload
[id params]
(->> (http/send! {:method :post
:uri (u/join @cf/public-uri "api/rpc/command/" (name id))

View file

@ -37,7 +37,7 @@
(defn- login-with-oidc
[event provider params]
(dom/prevent-default event)
(->> (rp/command! :login-with-oidc (assoc params :provider provider))
(->> (rp/cmd! :login-with-oidc (assoc params :provider provider))
(rx/subs (fn [{:keys [redirect-uri] :as rsp}]
(if redirect-uri
(.replace js/location redirect-uri)
@ -57,7 +57,7 @@
(dom/prevent-default event)
(dom/stop-propagation event)
(let [{:keys [on-error]} (meta params)]
(->> (rp/command! :login-with-ldap params)
(->> (rp/cmd! :login-with-ldap params)
(rx/subs (fn [profile]
(if-let [token (:invitation-token profile)]
(st/emit! (rt/nav :auth-verify-token {} {:token token}))

View file

@ -101,7 +101,7 @@
(fn [form _event]
(reset! submitted? true)
(let [cdata (:clean-data @form)]
(->> (rp/command! :prepare-register-profile cdata)
(->> (rp/cmd! :prepare-register-profile cdata)
(rx/map #(merge % params))
(rx/finalize #(reset! submitted? false))
(rx/subs
@ -232,7 +232,7 @@
(fn [form _event]
(reset! submitted? true)
(let [params (:clean-data @form)]
(->> (rp/command! :register-profile params)
(->> (rp/cmd! :register-profile params)
(rx/finalize #(reset! submitted? false))
(rx/subs on-success
(partial handle-register-error form))))))]

View file

@ -65,7 +65,7 @@
(mf/with-effect []
(dom/set-html-title (tr "title.default"))
(->> (rp/command! :verify-token {:token token})
(->> (rp/cmd! :verify-token {:token token})
(rx/subs
(fn [tdata]
(handle-token tdata))

View file

@ -177,7 +177,7 @@
(->> (rx/from files)
(rx/flat-map
(fn [file]
(->> (rp/command :has-file-libraries {:file-id (:id file)})
(->> (rp/cmd! :has-file-libraries {:file-id (:id file)})
(rx/map #(assoc file :has-libraries? %)))))
(rx/reduce conj [])
(rx/subs

View file

@ -46,7 +46,8 @@
(let [features (cond-> ffeat/enabled
(features/active-feature? :components-v2)
(conj "components/v2"))]
(wrk/ask! {:cmd :thumbnails/generate
(wrk/ask! {:cmd :thumbnails/generate-for-file
:revn (:revn file)
:file-id (:id file)
:file-name (:name file)

View file

@ -56,7 +56,7 @@
(when *assert*
["/debug/icons-preview" :debug-icons-preview])
["/debug/components-preview" :debug-components-preview]
;; Used for export
@ -98,7 +98,7 @@
;; We just recheck with an additional profile request; this avoids
;; some race conditions that causes unexpected redirects on
;; invitations workflows (and probably other cases).
(->> (rp/command! :get-profile)
(->> (rp/cmd! :get-profile)
(rx/subs (fn [{:keys [id] :as profile}]
(if (= id uuid/zero)
(st/emit! (rt/nav :auth-login))

View file

@ -55,7 +55,7 @@
(fn [form _]
(reset! loading true)
(let [data (:clean-data @form)]
(->> (rp/command! :send-user-feedback data)
(->> (rp/cmd! :send-user-feedback data)
(rx/subs on-succes on-error)))))]
[:& fm/form {:class "feedback-form"

View file

@ -88,36 +88,41 @@
(mf/defc frame-thumbnail-image
{::mf/wrap-props false}
[props]
(let [shape (unchecked-get props "shape")
bounds (or (unchecked-get props "bounds")
(gsh/points->selrect (:points shape)))
(let [shape (obj/get props "shape")
bounds (or (obj/get props "bounds") (gsh/points->selrect (:points shape)))]
shape-id (:id shape)
thumb (:thumbnail shape)
(when (:thumbnail shape)
[:*
[:image.frame-thumbnail
{:id (dm/str "thumbnail-" (:id shape))
:href (:thumbnail shape)
:x (:x bounds)
:y (:y bounds)
:width (:width bounds)
:height (:height bounds)
;; DEBUG
:style {:filter (when (and (not (cf/check-browser? :safari))(debug? :thumbnails)) "sepia(1)")}}]
debug? (debug? :thumbnails)
safari? (cf/check-browser? :safari)]
;; Safari don't support filters so instead we add a rectangle around the thumbnail
(when (and (cf/check-browser? :safari) (debug? :thumbnails))
[:rect {:x (+ (:x bounds) 4)
:y (+ (:y bounds) 4)
:width (- (:width bounds) 8)
:height (- (:height bounds) 8)
:stroke "red"
:stroke-width 2}])])))
[:*
[:image.frame-thumbnail
{:id (dm/str "thumbnail-" shape-id)
:href thumb
:decoding "async"
:x (:x bounds)
:y (:y bounds)
:width (:width bounds)
:height (:height bounds)
:style {:filter (when (and (not ^boolean safari?) ^boolean debug?) "sepia(1)")}}]
;; Safari don't support filters so instead we add a rectangle around the thumbnail
(when (and ^boolean safari? ^boolean debug?)
[:rect {:x (+ (:x bounds) 4)
:y (+ (:y bounds) 4)
:width (- (:width bounds) 8)
:height (- (:height bounds) 8)
:stroke "red"
:stroke-width 2}])]))
(mf/defc frame-thumbnail
{::mf/wrap-props false}
[props]
(let [shape (obj/get props "shape")]
(when (:thumbnail shape)
(let [shape (unchecked-get props "shape")]
(when ^boolean (:thumbnail shape)
[:> frame-container props
[:> frame-thumbnail-image props]])))

View file

@ -179,7 +179,7 @@
(->> (rx/of file)
(rx/flat-map
(fn [file]
(->> (rp/command :has-file-libraries {:file-id (:id file)})
(->> (rp/cmd! :has-file-libraries {:file-id (:id file)})
(rx/map #(assoc file :has-libraries? %)))))
(rx/reduce conj [])
(rx/subs

View file

@ -11,7 +11,6 @@
[app.common.pages.helpers :as cph]
[app.main.data.workspace.state-helpers :as wsh]
[app.main.data.workspace.thumbnails :as dwt]
[app.main.fonts :as fonts]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.context :as ctx]
@ -79,72 +78,70 @@
::mf/wrap-props false}
[props]
(let [shape (unchecked-get props "shape")
frame-id (:id shape)
objects (wsh/lookup-page-objects @st/state)
node-ref (mf/use-var nil)
modifiers-ref (mf/use-memo (mf/deps frame-id) #(refs/workspace-modifiers-by-frame-id frame-id))
modifiers (mf/deref modifiers-ref)]
(let [shape (unchecked-get props "shape")
thumbnail? (unchecked-get props "thumbnail?")
(fdm/use-dynamic-modifiers objects @node-ref modifiers)
page-id (mf/use-ctx ctx/current-page-id)
frame-id (:id shape)
(let [thumbnail? (unchecked-get props "thumbnail?")
fonts (mf/use-memo (mf/deps shape objects) #(ff/shape->fonts shape objects))
fonts (-> fonts (hooks/use-equal-memo))
objects (wsh/lookup-page-objects @st/state)
force-render (mf/use-state false)
node* (mf/use-var nil)
force-render* (mf/use-state false)
force-render? (deref force-render*)
;; Thumbnail data
page-id (mf/use-ctx ctx/current-page-id)
;; when `true` we've called the mount for the frame
rendered* (mf/use-var false)
;; when `true` we've called the mount for the frame
rendered? (mf/use-var false)
modifiers-ref (mf/with-memo [frame-id]
(refs/workspace-modifiers-by-frame-id frame-id))
modifiers (mf/deref modifiers-ref)
disable-thumbnail? (d/not-empty? (dm/get-in modifiers [frame-id :modifiers]))
[on-load-frame-dom render-frame? thumbnail-renderer]
(ftr/use-render-thumbnail page-id shape node-ref rendered? disable-thumbnail? @force-render)
fonts (mf/with-memo [shape objects]
(ff/shape->fonts shape objects))
fonts (hooks/use-equal-memo fonts)
on-frame-load
(fns/use-node-store thumbnail? node-ref rendered? render-frame?)]
disable-thumbnail? (d/not-empty? (dm/get-in modifiers [frame-id :modifiers]))
(mf/use-effect
(mf/deps fonts)
(fn []
(->> (rx/from fonts)
(rx/merge-map fonts/fetch-font-css)
(rx/ignore))))
[on-load-frame-dom render-frame? thumbnail-renderer]
(ftr/use-render-thumbnail page-id shape node* rendered* disable-thumbnail? force-render?)
(mf/use-effect
(fn []
;; When a change in the data is received a "force-render" event is emitted
;; that will force the component to be mounted in memory
(let [sub
(->> (dwt/force-render-stream (:id shape))
(rx/take-while #(not @rendered?))
(rx/subs #(reset! force-render true)))]
#(when sub
(rx/dispose! sub)))))
on-frame-load
(fns/use-node-store thumbnail? node* rendered* render-frame?)]
(mf/use-effect
(mf/deps shape fonts thumbnail? on-load-frame-dom @force-render render-frame?)
(fn []
(when (and (some? @node-ref) (or @rendered? (not thumbnail?) @force-render render-frame?))
(mf/mount
(mf/element frame-shape
#js {:ref on-load-frame-dom :shape shape :fonts fonts})
(fdm/use-dynamic-modifiers objects @node* modifiers)
@node-ref)
(when (not @rendered?) (reset! rendered? true)))))
(mf/with-effect []
;; When a change in the data is received a "force-render" event is emitted
;; that will force the component to be mounted in memory
(let [sub (->> (dwt/force-render-stream frame-id)
(rx/take-while #(not @rendered*))
(rx/subs #(reset! force-render* true)))]
#(some-> sub rx/dispose!)))
(mf/with-effect [shape fonts thumbnail? on-load-frame-dom force-render? render-frame?]
(when (and (some? @node*)
(or @rendered*
(not thumbnail?)
force-render?
render-frame?))
(let [elem (mf/element frame-shape #js {:ref on-load-frame-dom :shape shape :fonts fonts})]
(mf/mount elem @node*)
(when (not @rendered*)
(reset! rendered* true)))))
[:& shape-container {:shape shape}
[:g.frame-container {:id (dm/str "frame-container-" frame-id)
:key "frame-container"
:ref on-frame-load
:opacity (when (:hidden shape) 0)}
[:& ff/fontfaces-style {:fonts fonts}]
[:g.frame-thumbnail-wrapper
{:id (dm/str "thumbnail-container-" frame-id)
;; Hide the thumbnail when not displaying
:opacity (when-not thumbnail? 0)}
thumbnail-renderer]]
]))))
[:& shape-container {:shape shape}
[:g.frame-container {:id (dm/str "frame-container-" (:id shape))
:key "frame-container"
:ref on-frame-load
:opacity (when (:hidden shape) 0)}
[:& ff/fontfaces-style {:fonts fonts}]
[:g.frame-thumbnail-wrapper
{:id (dm/str "thumbnail-container-" (:id shape))
;; Hide the thumbnail when not displaying
:opacity (when-not thumbnail? 0)}
thumbnail-renderer]]])))))

View file

@ -22,27 +22,8 @@
[beicon.core :as rx]
[cuerdas.core :as str]
[debug :refer [debug?]]
[promesa.core :as p]
[rumext.v2 :as mf]))
(defn- draw-thumbnail-canvas!
[canvas-node img-node]
(try
(when (and (some? canvas-node) (some? img-node))
(let [canvas-context (.getContext canvas-node "2d")
canvas-width (.-width canvas-node)
canvas-height (.-height canvas-node)]
(.clearRect canvas-context 0 0 canvas-width canvas-height)
(.drawImage canvas-context img-node 0 0 canvas-width canvas-height)
;; Set a true on the next animation frame, we make sure the drawImage is completed
(ts/raf
#(dom/set-data! canvas-node "ready" "true"))
true))
(catch :default err
(.error js/console err)
false)))
(defn- remove-image-loading
"Remove the changes related to change a url for its embed value. This is necessary
so we don't have to recalculate the thumbnail when the image loads."
@ -57,19 +38,39 @@
(str/starts-with? (.-oldValue change) "http"))))))
[value]))
(defn- create-svg-blob-uri-from
[fixed-width fixed-height rect node style-node]
(let [{:keys [x y width height]} rect
viewbox (dm/str x " " y " " width " " height)
;; This is way faster than creating a node
;; through the DOM API
svg-data
(dm/fmt "<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" viewBox=\"%\" width=\"%\" height=\"%\" fill=\"none\">% %</svg>"
viewbox
fixed-width
fixed-height
(if (some? style-node) (dom/node->xml style-node) "")
(dom/node->xml node))
;; create SVG blob
blob (js/Blob. #js [svg-data] #js {:type "image/svg+xml;charset=utf-8"})
url (dm/str (.createObjectURL js/URL blob) "#svg")]
;; returns the url and the node
url))
(defn use-render-thumbnail
"Hook that will create the thumbnail thata"
"Hook that will create the thumbnail data"
[page-id {:keys [id] :as shape} node-ref rendered? disable? force-render]
(let [frame-canvas-ref (mf/use-ref nil)
frame-image-ref (mf/use-ref nil)
(let [frame-image-ref (mf/use-ref nil)
disable-ref? (mf/use-var disable?)
disable* (mf/use-var disable?)
regenerate* (mf/use-var false)
regenerate-thumbnail (mf/use-var false)
all-children-ref (mf/use-memo (mf/deps id) #(refs/all-children-objects id))
all-children (mf/deref all-children-ref)
all-children-ref (mf/with-memo [id]
(refs/all-children-objects id))
all-children (mf/deref all-children-ref)
{:keys [x y width height] :as shape-bb}
(if (:show-content shape)
@ -83,51 +84,46 @@
[(/ (* width (mth/clamp height 250 2000)) height)
(mth/clamp height 250 2000)])
image-url (mf/use-state nil)
observer-ref (mf/use-var nil)
svg-uri* (mf/use-state nil)
bitmap-uri* (mf/use-state nil)
observer* (mf/use-var nil)
shape-bb-ref (hooks/use-update-var shape-bb)
shape-bb* (hooks/use-update-var shape-bb)
updates-s (mf/use-memo rx/subject)
updates-str (mf/use-memo #(rx/subject))
thumbnail-data-ref (mf/use-memo (mf/deps page-id id) #(refs/thumbnail-frame-data page-id id))
thumbnail-data (mf/deref thumbnail-data-ref)
;; We only need the zoom level in Safari. For other browsers we don't want to activate this because
;; will render for every zoom change
zoom (when (cf/check-browser? :safari) (mf/deref refs/selected-zoom))
prev-thumbnail-data (hooks/use-previous thumbnail-data)
thumbnail-uri-ref (mf/with-memo [page-id id]
(refs/thumbnail-frame-data page-id id))
thumbnail-uri (mf/deref thumbnail-uri-ref)
;; State to indicate to the parent that should render the frame
render-frame? (mf/use-state (not thumbnail-data))
render-frame* (mf/use-state (not thumbnail-uri))
debug? (debug? :thumbnails)
;; State variable to select whether we show the image thumbnail or the canvas thumbnail
show-frame-thumbnail (mf/use-state (some? thumbnail-data))
disable-fills? (or @show-frame-thumbnail (some? @image-url))
on-image-load
(mf/use-callback
(mf/deps @show-frame-thumbnail)
on-bitmap-load
(mf/use-fn
(fn []
(let [canvas-node (mf/ref-val frame-canvas-ref)
img-node (mf/ref-val frame-image-ref)]
(when (draw-thumbnail-canvas! canvas-node img-node)
(when-not (cf/check-browser? :safari)
(reset! image-url nil))
;; We revoke the SVG Blob URI to free memory only when we
;; are sure that it is not used anymore.
(wapi/revoke-uri @svg-uri*)
(reset! svg-uri* nil)))
(when @show-frame-thumbnail
(reset! show-frame-thumbnail false))
;; If we don't have the thumbnail data saved (normally the first load) we update the data
;; when available
(when (not @thumbnail-data-ref)
(st/emit! (dwt/update-thumbnail page-id id) ))
on-svg-load
(mf/use-callback
(fn []
(let [image-node (mf/ref-val frame-image-ref)]
(dom/set-data! image-node "ready" "true")
(reset! render-frame? false)))))
;; If we don't have the thumbnail data saved (normally the first load) we update the data
;; when available
(when (not ^boolean @thumbnail-uri-ref)
(st/emit! (dwt/update-thumbnail page-id id)))
(reset! render-frame* false))))
generate-thumbnail
(mf/use-callback
(mf/use-fn
(mf/deps id)
(fn generate-thumbnail []
(try
;; When starting generating the canvas we mark it as not ready so its not send to back until
@ -135,158 +131,106 @@
(let [node @node-ref]
(if (dom/has-children? node)
;; The frame-content need to have children in order to generate the thumbnail
(let [style-node (dom/query (dm/str "#frame-container-" (:id shape) " style"))
{:keys [x y width height]} @shape-bb-ref
viewbox (dm/str x " " y " " width " " height)
;; This is way faster than creating a node through the DOM API
svg-data
(dm/fmt "<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" viewBox=\"%\" width=\"%\" height=\"%\" fill=\"none\">% %</svg>"
viewbox
width
height
(if (some? style-node) (dom/node->xml style-node) "")
(dom/node->xml node))
blob (js/Blob. #js [svg-data] #js {:type "image/svg+xml;charset=utf-8"})
img-src (.createObjectURL js/URL blob)]
(reset! image-url img-src))
(let [style-node (dom/query (dm/str "#frame-container-" id " style"))
url (create-svg-blob-uri-from fixed-width fixed-height @shape-bb* node style-node)]
(reset! svg-uri* url))
;; Node not yet ready, we schedule a new generation
(ts/schedule generate-thumbnail)))
(ts/raf generate-thumbnail)))
(catch :default e
(.error js/console e)))))
on-change-frame
(mf/use-callback
(mf/use-fn
(mf/deps id)
(fn []
(when (and (some? @node-ref) @rendered? @regenerate-thumbnail)
(when (and ^boolean @node-ref
^boolean @rendered?
^boolean @regenerate*)
(let [loading-images? (some? (dom/query @node-ref "[data-loading='true']"))
loading-fonts? (some? (dom/query (dm/str "#frame-container-" (:id shape) " > style[data-loading='true']")))]
(when (and (not loading-images?) (not loading-fonts?))
loading-fonts? (some? (dom/query (dm/str "#frame-container-" id " > style[data-loading='true']")))]
(when (and (not loading-images?)
(not loading-fonts?))
(generate-thumbnail)
(reset! regenerate-thumbnail false))))))
(reset! regenerate* false))))))
;; When the frame is updated, it is marked as not ready
;; so that it is not sent to the background until
;; it is regenerated.
on-update-frame
(mf/use-callback
(mf/use-fn
(fn []
(let [canvas-node (mf/ref-val frame-canvas-ref)]
(when (not= "false" (dom/get-data canvas-node "ready"))
(dom/set-data! canvas-node "ready" "false")))
(when (not @disable-ref?)
(reset! render-frame? true)
(reset! regenerate-thumbnail true))))
(let [image-node (mf/ref-val frame-image-ref)]
(when (not= "false" (dom/get-data image-node "ready"))
(dom/set-data! image-node "ready" "false")))
(when-not ^boolean @disable*
(reset! render-frame* true)
(reset! regenerate* true))))
on-load-frame-dom
(mf/use-callback
(mf/use-fn
(fn [node]
(when (and (some? node) (nil? @observer-ref))
(when-not (some? @thumbnail-data-ref)
(rx/push! updates-str :update))
(when (and (some? node)
(nil? @observer*))
(when-not (some? @thumbnail-uri-ref)
(rx/push! updates-s :update))
(let [observer (js/MutationObserver. (partial rx/push! updates-str))]
(let [observer (js/MutationObserver. (partial rx/push! updates-s))]
(.observe observer node #js {:childList true :attributes true :attributeOldValue true :characterData true :subtree true})
(reset! observer-ref observer)))))]
(reset! observer* observer)))))]
(mf/use-effect
(mf/deps thumbnail-data)
(fn []
(when (and (some? prev-thumbnail-data) (nil? thumbnail-data))
(rx/push! updates-str :update))))
(mf/with-effect [thumbnail-uri]
(when (some? thumbnail-uri)
(reset! bitmap-uri* thumbnail-uri)))
(mf/use-effect
(mf/deps force-render)
(fn []
(when force-render
(rx/push! updates-str :update))))
(mf/with-effect [force-render]
(when ^boolean force-render
(rx/push! updates-s :update)))
(mf/use-effect
(fn []
(let [subid (->> updates-str
(rx/map remove-image-loading)
(rx/filter d/not-empty?)
(rx/catch (fn [err] (.error js/console err)))
(rx/subs on-update-frame))]
#(rx/dispose! subid))))
(mf/with-effect []
(let [subid (->> updates-s
(rx/map remove-image-loading)
(rx/filter d/not-empty?)
(rx/catch (fn [err] (.error js/console err)))
(rx/subs on-update-frame))]
(partial rx/dispose! subid)))
;; on-change-frame will get every change in the frame
(mf/use-effect
(fn []
(let [subid (->> updates-str
(rx/debounce 400)
(rx/observe-on :af)
(rx/catch (fn [err] (.error js/console err)))
(rx/subs on-change-frame))]
#(rx/dispose! subid))))
(mf/with-effect []
(let [subid (->> updates-s
(rx/debounce 400)
(rx/observe-on :af)
(rx/catch (fn [err] (.error js/console err)))
(rx/subs on-change-frame))]
(partial rx/dispose! subid)))
(mf/use-effect
(mf/deps disable?)
(fn []
(when (and disable? (not @disable-ref?))
(rx/push! updates-str :update))
(reset! disable-ref? disable?)))
(mf/with-effect [disable?]
(when (and ^boolean disable?
(not @disable*))
(rx/push! updates-s :update))
(reset! disable* disable?)
nil)
(mf/use-effect
(fn []
#(when (and (some? @node-ref) @rendered?)
(mf/with-effect []
(fn []
(when (and (some? @node-ref)
^boolean @rendered?)
(mf/unmount @node-ref)
(reset! node-ref nil)
(reset! rendered? false)
(when (some? @observer-ref)
(.disconnect @observer-ref)
(reset! observer-ref nil)))))
;; When the thumbnail-data is empty we regenerate the thumbnail
(mf/use-effect
(mf/deps (:selrect shape) thumbnail-data)
(fn []
(let [{:keys [width height]} (:selrect shape)]
(p/then (wapi/empty-png-size width height)
(fn [data]
(when (<= (count thumbnail-data) (+ 100 (count data)))
(rx/push! updates-str :update)))))))
(when (some? @observer*)
(.disconnect @observer*)
(reset! observer* nil)))))
[on-load-frame-dom
@render-frame?
@render-frame*
(mf/html
[:& frame/frame-container {:bounds shape-bb
:shape (cond-> shape
(some? thumbnail-data)
(assoc :thumbnail thumbnail-data))}
(when @show-frame-thumbnail
[:> frame/frame-thumbnail-image
{:key (dm/str (:id shape))
:bounds shape-bb
:shape (cond-> shape
(some? thumbnail-data)
(assoc :thumbnail thumbnail-data))}])
[:foreignObject {:x x
:y y
:width width
:height height
:opacity (when disable-fills? 0)}
[:canvas.thumbnail-canvas
{:key (dm/str "thumbnail-canvas-" (:id shape))
:ref frame-canvas-ref
:data-object-id (dm/str page-id (:id shape))
:width width
:height height
:style {;; Safari has a problem with the positioning of the canvas. All this is to fix Safari behavior
;; https://bugs.webkit.org/show_bug.cgi?id=23113
:display (when (cf/check-browser? :safari) "none")
:position "fixed"
:transform-origin "top left"
:transform (when (cf/check-browser? :safari) (dm/fmt "scale(%)" zoom))
;; DEBUG
:filter (when (debug? :thumbnails) "invert(1)")}}]]
[:& frame/frame-container {:bounds shape-bb :shape shape}
;; Safari don't support filters so instead we add a rectangle around the thumbnail
(when (and (cf/check-browser? :safari) (debug? :thumbnails))
(when (and (cf/check-browser? :safari)
^boolean debug?)
[:rect {:x (+ x 2)
:y (+ y 2)
:width (- width 4)
@ -294,13 +238,32 @@
:stroke "blue"
:stroke-width 2}])
(when (some? @image-url)
[:foreignObject {:x x
:y y
:width fixed-width
:height fixed-height}
[:img {:ref frame-image-ref
:src @image-url
:width fixed-width
:height fixed-height
:on-load on-image-load}]])])]))
;; This is similar to how double-buffering works.
;; In svg-uri* we keep the SVG image that is used to
;; render the bitmap until the bitmap is ready
;; to be rendered on screen. Then we remove the
;; svg and keep the bitmap one.
;; This is the "buffer" that keeps the bitmap image.
(when ^boolean @bitmap-uri*
[:image.thumbnail-bitmap
{:x x
:y y
:width width
:height height
:href @bitmap-uri*
:style {:filter (when ^boolean debug? "sepia(1)")}
:on-load on-bitmap-load}])
;; This is the "buffer" that keeps the SVG image.
(when ^boolean @svg-uri*
[:image.thumbnail-canvas
{:x x
:y y
:key (dm/str "thumbnail-canvas-" id)
:data-object-id (dm/str page-id id)
:width width
:height height
:ref frame-image-ref
:href @svg-uri*
:style {:filter (when ^boolean debug? "sepia(0.5)")}
:on-load on-svg-load}])])]))

View file

@ -26,13 +26,19 @@
(reset! instance worker)))
(defn ask!
[message]
(when @instance (uw/ask! @instance message)))
([message]
(when @instance (uw/ask! @instance message)))
([message transfer]
(when @instance (uw/ask! @instance message transfer))))
(defn ask-buffered!
[message]
(when @instance (uw/ask-buffered! @instance message)))
([message]
(when @instance (uw/ask-buffered! @instance message)))
([message transfer]
(when @instance (uw/ask-buffered! @instance message transfer))))
(defn ask-many!
[message]
(when @instance (uw/ask-many! @instance message)))
([message]
(when @instance (uw/ask-many! @instance message)))
([message transfer]
(when @instance (uw/ask-many! @instance message transfer))))

View file

@ -57,7 +57,7 @@
(-> (fonts/ensure-loaded! font-id)
(p/then #(when (not (dom/check-font? font))
(load-font font)))
(p/catch #(.error js/console (dm/str "Cannot load font" font-id) %)))))
(p/catch #(.error js/console (dm/str "Cannot load font " font-id) %)))))
(defn- calc-text-node-positions
[shape-id]

View file

@ -51,8 +51,8 @@
(defn revoke-uri
[url]
(assert (string? url) "invalid arguments")
(js/URL.revokeObjectURL url))
(when ^boolean (str/starts-with? url "blob:")
(js/URL.revokeObjectURL url)))
(defn create-uri
"Create a url from blob."

View file

@ -8,6 +8,7 @@
"A lightweight layer on top of webworkers api."
(:require
[app.common.uuid :as uuid]
[app.util.object :as obj]
[app.worker.messages :as wm]
[beicon.core :as rx]))
@ -25,11 +26,14 @@
(rx/take-while #(not (:completed %)) ob)
(rx/take 1 ob)))
data (wm/encode message)
transfer (:transfer message)
data (cond-> (wm/encode (dissoc message :transfer))
(some? transfer)
(obj/set! "transfer" transfer))
instance (:instance worker)]
(if (some? instance)
(do (.postMessage instance data)
(do (.postMessage instance data transfer)
(->> (:stream worker)
(rx/filter #(= (:reply-to %) sender-id))
(take-messages)
@ -38,27 +42,36 @@
(rx/empty)))))
(defn ask!
[worker message]
(send-message!
worker
{:sender-id (uuid/next)
:payload message}))
([worker message]
(ask! worker message nil))
([worker message transfer]
(send-message!
worker
{:sender-id (uuid/next)
:payload message
:transfer transfer})))
(defn ask-many!
[worker message]
(send-message!
worker
{:sender-id (uuid/next)
:payload message}
{:many? true}))
([worker message]
(ask-many! worker message nil))
([worker message transfer]
(send-message!
worker
{:sender-id (uuid/next)
:payload message
:transfer transfer}
{:many? true})))
(defn ask-buffered!
[worker message]
(send-message!
worker
{:sender-id (uuid/next)
:payload message
:buffer? true}))
([worker message]
(ask-buffered! worker message nil))
([worker message transfer]
(send-message!
worker
{:sender-id (uuid/next)
:payload message
:buffer? true
:transfer transfer})))
(defn init
"Return a initialized webworker instance."

View file

@ -9,6 +9,7 @@
[app.common.data.macros :as dm]
[app.common.logging :as log]
[app.common.schema :as sm]
[app.util.object :as obj]
[app.worker.export]
[app.worker.impl :as impl]
[app.worker.import]
@ -38,7 +39,7 @@
(defn- handle-message
"Process the message and returns to the client"
[{:keys [sender-id payload] :as message}]
[{:keys [sender-id payload transfer] :as message}]
(dm/assert! (message? message))
(letfn [(post [msg]
(let [msg (-> msg (assoc :reply-to sender-id) (wm/encode))]
@ -63,7 +64,7 @@
:completed true})))]
(try
(let [result (impl/handler payload)
(let [result (impl/handler payload transfer)
promise? (p/promise? result)
stream? (or (rx/observable? result) (rx/subject? result))]
@ -145,7 +146,10 @@
[event]
(when (nil? (.-source event))
(let [message (.-data event)
message (wm/decode message)]
transfer (obj/get message "transfer")
message (cond-> (wm/decode message)
(some? transfer)
(assoc :transfer transfer))]
(if (:buffer? message)
(rx/push! buffer message)
(handle-message message)))))

View file

@ -474,9 +474,9 @@
(->> (rx/from files)
(rx/mapcat
(fn [file]
(->> (rp/command! :export-binfile {:file-id (:id file)
:include-libraries? (= export-type :all)
:embed-assets? (= export-type :merge)})
(->> (rp/cmd! :export-binfile {:file-id (:id file)
:include-libraries? (= export-type :all)
:embed-assets? (= export-type :merge)})
(rx/map #(hash-map :type :finish
:file-id (:id file)
:filename (:name file)

View file

@ -672,7 +672,7 @@
:response-type :blob
:method :get})
(rx/map :body)
(rx/mapcat #(rp/command! :import-binfile {:file %
(rx/mapcat #(rp/cmd! :import-binfile {:file %
:project-id project-id}))
(rx/map
(fn [_]

View file

@ -16,6 +16,7 @@
[app.worker.impl :as impl]
[beicon.core :as rx]
[debug :refer [debug?]]
[promesa.core :as p]
[rumext.v2 :as mf]))
(log/set-level! :trace)
@ -110,8 +111,8 @@
(rx/catch body-too-large? (constantly (rx/of nil)))
(rx/map (constantly params)))))
(defmethod impl/handler :thumbnails/generate
[{:keys [file-id revn features] :as message}]
(defmethod impl/handler :thumbnails/generate-for-file
[{:keys [file-id revn features] :as message} _]
(letfn [(on-result [{:keys [data props]}]
{:data data
:fonts (:fonts props)})
@ -130,3 +131,18 @@
(log/debug :hint "request-thumbnail" :file-id file-id :revn revn :cache "hit")))
(rx/catch not-found? on-cache-miss)
(rx/map on-result)))))
(defmethod impl/handler :thumbnails/render-offscreen-canvas
[_ ibpm]
(let [canvas (js/OffscreenCanvas. (.-width ^js ibpm) (.-height ^js ibpm))
ctx (.getContext ^js canvas "bitmaprenderer")]
(.transferFromImageBitmap ^js ctx ibpm)
(->> (.convertToBlob ^js canvas #js {:type "image/png"})
(p/fmap (fn [blob]
(js/console.log "[worker]: generated thumbnail")
{:result (.createObjectURL js/URL blob)}))
(p/fnly (fn [_]
(.close ^js ibpm))))))