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:
parent
1d69da1ca5
commit
48834f96d3
29 changed files with 644 additions and 616 deletions
|
@ -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)))
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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))))))
|
||||
|
||||
|
|
|
@ -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))))
|
||||
|
|
|
@ -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))))))
|
||||
|
|
|
@ -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}))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)))))
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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}))
|
||||
|
|
|
@ -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))))))]
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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]])))
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]]])))))
|
||||
|
|
|
@ -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}])])]))
|
||||
|
|
|
@ -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))))
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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)))))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 [_]
|
||||
|
|
|
@ -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))))))
|
||||
|
||||
|
|
Loading…
Reference in a new issue