From e1dfd91e245afe6df6a4c380e1d1eab3d1984e66 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Mon, 10 May 2021 12:02:54 +0200 Subject: [PATCH] :zap: Frame thumbnails --- backend/src/app/rpc/queries/files.clj | 36 ++++- common/app/common/data.cljc | 42 ++++++ common/app/common/pages/helpers.cljc | 2 +- frontend/src/app/main/data/workspace.cljs | 2 + .../src/app/main/data/workspace/changes.cljs | 11 +- .../app/main/data/workspace/persistence.cljs | 127 +++++++++++++---- frontend/src/app/main/exports.cljs | 24 +++- frontend/src/app/main/ui/shapes/frame.cljs | 2 +- frontend/src/app/main/ui/workspace.cljs | 7 +- .../src/app/main/ui/workspace/shapes.cljs | 6 +- .../app/main/ui/workspace/shapes/frame.cljs | 45 +++++- .../src/app/main/ui/workspace/viewport.cljs | 8 +- .../app/main/ui/workspace/viewport/hooks.cljs | 31 +++++ .../viewport/thumbnail_renderer.cljs | 130 ++++++++++++++++++ frontend/src/app/worker/thumbnails.cljs | 4 +- 15 files changed, 427 insertions(+), 50 deletions(-) create mode 100644 frontend/src/app/main/ui/workspace/viewport/thumbnail_renderer.cljs diff --git a/backend/src/app/rpc/queries/files.clj b/backend/src/app/rpc/queries/files.clj index b781c783a..3fa857892 100644 --- a/backend/src/app/rpc/queries/files.clj +++ b/backend/src/app/rpc/queries/files.clj @@ -8,6 +8,7 @@ (:require [app.common.pages.migrations :as pmg] [app.common.spec :as us] + [app.common.uuid :as uuid] [app.db :as db] [app.rpc.permissions :as perms] [app.rpc.queries.projects :as projects] @@ -120,6 +121,7 @@ profile-id team-id search-term])) + ;; --- Query: Files ;; DEPRECATED: should be removed probably on 1.6.x @@ -185,13 +187,43 @@ (s/def ::page (s/keys :req-un [::profile-id ::file-id])) +(defn remove-thumbnails-frames + "Removes from data the children for frames that have a thumbnail set up" + [data] + (let [filter-shape? + (fn [objects [id shape]] + (let [frame-id (:frame-id shape)] + (or (= id uuid/zero) + (= frame-id uuid/zero) + (not (some? (get-in objects [frame-id :thumbnail])))))) + + ;; We need to remove from the attribute :shapes its childrens because + ;; they will not be sent in the data + remove-frame-children + (fn [[id shape]] + [id (cond-> shape + (some? (:thumbnail shape)) + (assoc :shapes []))]) + + update-objects + (fn [objects] + (into {} + (comp (map remove-frame-children) + (filter (partial filter-shape? objects))) + objects))] + + (update data :objects update-objects))) + (sv/defmethod ::page + [{:keys [pool] :as cfg} {:keys [profile-id file-id id strip-thumbnails]}] [{:keys [pool] :as cfg} {:keys [profile-id file-id]}] (db/with-atomic [conn pool] (check-edition-permissions! conn profile-id file-id) (let [file (retrieve-file conn file-id) page-id (get-in file [:data :pages 0])] - (get-in file [:data :pages-index page-id])))) + (cond-> (get-in file [:data :pages-index page-id]) + strip-thumbnails + (remove-thumbnails-frames))))) ;; --- Query: Shared Library Files @@ -244,6 +276,7 @@ (def ^:private sql:file-libraries "select fl.*, + flr.synced_at as synced_at from file as fl inner join file_library_rel as flr on (flr.library_file_id = fl.id) @@ -259,6 +292,7 @@ libraries (map :id libraries)))) + (s/def ::file-libraries (s/keys :req-un [::profile-id ::file-id])) diff --git a/common/app/common/data.cljc b/common/app/common/data.cljc index da478337a..f1b5f1091 100644 --- a/common/app/common/data.cljc +++ b/common/app/common/data.cljc @@ -12,6 +12,7 @@ (:require [linked.set :as lks] [app.common.math :as mth] + [clojure.set :as set] #?(:clj [cljs.analyzer.api :as aapi]) #?(:cljs [cljs.reader :as r] :clj [clojure.edn :as r]) @@ -467,3 +468,44 @@ [f coll] (f coll) coll) + +(defn map-diff + "Given two maps returns the diff of its attributes in a map where + the keys will be the attributes that change and the values the previous + and current value. For attributes which value is a map this will be recursive. + + For example: + (map-diff {:a 1 :b 2 :c { :foo 1 :var 2} + {:a 2 :c { :foo 10 } :d 10) + + => { :a [1 2] + :b [2 nil] + :c { :foo [1 10] + :var [2 nil]} + :d [nil 10] } + + If both maps are identical the result will be an empty map + " + [m1 m2] + + (let [m1ks (keys m1) + m2ks (keys m2) + keys (set/union m1ks m2ks) + + diff-attr + (fn [diff key] + + (let [v1 (get m1 key) + v2 (get m2 key)] + (cond + (= v1 v2) + diff + + (and (map? v1) (map? v2)) + (assoc diff key (map-diff v1 v2)) + + :else + (assoc diff key [(get m1 key) (get m2 key)]))))] + + (->> keys + (reduce diff-attr {})))) diff --git a/common/app/common/pages/helpers.cljc b/common/app/common/pages/helpers.cljc index f3a52cbc9..6bc748419 100644 --- a/common/app/common/pages/helpers.cljc +++ b/common/app/common/pages/helpers.cljc @@ -10,6 +10,7 @@ [app.common.geom.shapes :as gsh] [app.common.spec :as us] [app.common.uuid :as uuid] + [clojure.set :as set] [cuerdas.core :as str])) (defn walk-pages @@ -456,4 +457,3 @@ [path name] (let [path-split (split-path path)] (merge-path-item (first path-split) name))) - diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 18ad355a9..085d3c427 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -161,6 +161,7 @@ (->> stream (rx/filter (ptk/type? ::dwp/bundle-fetched)) (rx/take 1) + (rx/map deref) (rx/mapcat (fn [bundle] (rx/of (dwn/initialize file-id) @@ -1185,6 +1186,7 @@ (defn go-to-page ([] + (ptk/reify ::go-to-page ptk/WatchEvent (watch [it state stream] diff --git a/frontend/src/app/main/data/workspace/changes.cljs b/frontend/src/app/main/data/workspace/changes.cljs index 53f5cd421..56c918f38 100644 --- a/frontend/src/app/main/data/workspace/changes.cljs +++ b/frontend/src/app/main/data/workspace/changes.cljs @@ -62,7 +62,8 @@ (defn update-shapes ([ids f] (update-shapes ids f nil)) - ([ids f {:keys [reg-objects?] :or {reg-objects? false}}] + ([ids f {:keys [reg-objects? save-undo?] + :or {reg-objects? false save-undo? true}}] (us/assert ::coll-of-uuid ids) (us/assert fn? f) (ptk/reify ::update-shapes @@ -82,7 +83,8 @@ (when (and has-rch? has-uch?) (commit-changes {:redo-changes rch :undo-changes uch - :origin it})))) + :origin it + :save-undo? save-undo?})))) (let [id (first ids) obj1 (get objects id) @@ -164,6 +166,7 @@ [{:keys [redo-changes undo-changes origin save-undo? file-id] :or {save-undo? true}}] + (log/debug :msg "commit-changes" :js/redo-changes redo-changes :js/undo-changes undo-changes) @@ -182,12 +185,14 @@ (let [current-file-id (get state :current-file-id) file-id (or file-id current-file-id) path (if (= file-id current-file-id) + [:workspace-data] [:workspace-libraries file-id :data])] (try (us/assert ::spec/changes redo-changes) (us/assert ::spec/changes undo-changes) (update-in state path cp/process-changes redo-changes false) + (catch :default e (vreset! error e) state)))) @@ -217,4 +222,4 @@ (when (and save-undo? (seq undo-changes)) (let [entry {:undo-changes undo-changes :redo-changes redo-changes}] - (rx/of (dwu/append-undo entry))))))))))) + (rx/of (dwu/append-undo entry))))))))))) \ No newline at end of file diff --git a/frontend/src/app/main/data/workspace/persistence.cljs b/frontend/src/app/main/data/workspace/persistence.cljs index 665547944..2c8f0d7fc 100644 --- a/frontend/src/app/main/data/workspace/persistence.cljs +++ b/frontend/src/app/main/data/workspace/persistence.cljs @@ -16,11 +16,13 @@ [app.main.data.dashboard :as dd] [app.main.data.media :as di] [app.main.data.messages :as dm] - [app.main.data.workspace.common :as dwc] [app.main.data.workspace.changes :as dch] + [app.main.data.workspace.common :as dwc] [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.selection :as dws] + [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.svg-upload :as svg] + [app.main.refs :as refs] [app.main.repo :as rp] [app.main.store :as st] [app.util.avatars :as avatars] @@ -33,6 +35,7 @@ [app.util.uri :as uu] [beicon.core :as rx] [cljs.spec.alpha :as s] + [clojure.set :as set] [cuerdas.core :as str] [potok.core :as ptk] [promesa.core :as p])) @@ -349,30 +352,6 @@ (->> (rp/mutation :unlink-file-from-library params) (rx/map (constantly unlinked))))))) -;; --- Fetch Pages - -(declare page-fetched) - -(defn fetch-page - [page-id] - (us/verify ::us/uuid page-id) - (ptk/reify ::fetch-pages - ptk/WatchEvent - (watch [it state s] - (->> (rp/query :page {:id page-id}) - (rx/map page-fetched))))) - -(defn page-fetched - [{:keys [id] :as page}] - (us/verify ::page page) - (ptk/reify ::page-fetched - IDeref - (-deref [_] page) - - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:workspace-pages id] page)))) - ;; --- Upload File Media objects @@ -572,3 +551,101 @@ (update-in [:workspace-file :pages] #(filterv (partial not= id) %)) (update :workspace-pages dissoc id))) +(def update-frame-thumbnail? (ptk/type? ::update-frame-thumbnail)) + +(defn remove-thumbnails + [ids] + (ptk/reify ::remove-thumbnails + ptk/WatchEvent + (watch [_ state stream] + ;; Removes the thumbnail while it's regenerated + (rx/of (dch/update-shapes + ids + #(dissoc % :thumbnail) + {:save-undo? false}))))) + +(defn update-frame-thumbnail + [frame-id] + (ptk/event ::update-frame-thumbnail {:frame-id frame-id})) + +(defn- extract-frame-changes + "Process a changes set in a commit to extract the frames that are channging" + [[event objects]] + (let [changes (-> event deref :changes) + + extract-ids + (fn [{type :type :as change}] + (case type + :add-obj [(:id change)] + :mod-obj [(:id change)] + :del-obj [(:id change)] + :reg-objects (:shapes change) + :mov-objects (:shapes change) + [])) + + get-frame-id + (fn [id] + (or (and (= :frame (get-in objects [id :type])) id) + (get-in objects [id :frame-id]))) + + ;; Extracts the frames and then removes nils and the root frame + xform (comp (mapcat extract-ids) + (map get-frame-id) + (remove nil?) + (filter #(not= uuid/zero %)))] + + (into #{} xform changes))) + +(defn thumbnail-change? + "Checks if a event is only updating thumbnails to ignore in the thumbnail generation process" + [event] + (let [changes (-> event deref :changes) + + is-thumbnail-op? + (fn [{type :type attr :attr}] + (and (= type :set) + (= attr :thumbnail))) + + is-thumbnail-change? + (fn [change] + (and (= (:type change) :mod-obj) + (->> change :operations (every? is-thumbnail-op?))))] + + (->> changes (every? is-thumbnail-change?)))) + +(defn watch-state-changes [] + (ptk/reify ::watch-state-changes + ptk/WatchEvent + (watch [_ state stream] + (let [stopper (->> stream + (rx/filter #(or (= :app.main.data.workspace/finalize-page (ptk/type %)) + (= ::watch-state-changes (ptk/type %))))) + + objects-stream (rx/from-atom refs/workspace-page-objects {:emit-current-value? true}) + + frame-changes (->> stream + (rx/filter dch/commit-changes?) + (rx/filter (comp not thumbnail-change?)) + (rx/with-latest-from objects-stream) + (rx/map extract-frame-changes)) + + frames (-> state wsh/lookup-page-objects cp/select-frames) + no-thumb-frames (->> frames + (filter (comp nil? :thumbnail)) + (mapv :id))] + + (rx/concat + (->> (rx/from no-thumb-frames) + (rx/map #(update-frame-thumbnail %))) + + ;; We remove the thumbnails inmediately but defer their generation + (rx/merge + (->> frame-changes + (rx/take-until stopper) + (rx/map #(remove-thumbnails %))) + + (->> frame-changes + (rx/take-until stopper) + (rx/buffer-until (->> frame-changes (rx/debounce 1000))) + (rx/flat-map #(reduce set/union %)) + (rx/map #(update-frame-thumbnail %))))))))) diff --git a/frontend/src/app/main/exports.cljs b/frontend/src/app/main/exports.cljs index 1569144f3..aa5e08c21 100644 --- a/frontend/src/app/main/exports.cljs +++ b/frontend/src/app/main/exports.cljs @@ -121,7 +121,7 @@ (mf/defc page-svg {::mf/wrap [mf/memo]} - [{:keys [data width height] :as props}] + [{:keys [data width height thumbnails?] :as props}] (let [objects (:objects data) root (get objects uuid/zero) shapes (->> (:shapes root) @@ -146,11 +146,23 @@ :xmlns "http://www.w3.org/2000/svg"} [:& background {:vbox dim :color background-color}] (for [item shapes] - (if (= (:type item) :frame) - [:& frame-wrapper {:shape item - :key (:id item)}] - [:& shape-wrapper {:shape item - :key (:id item)}]))])) + (let [frame? (= (:type item) :frame)] + (cond + (and frame? thumbnails? (some? (:thumbnail item))) + [:image {:xlinkHref (:thumbnail item) + :x (:x item) + :y (:y item) + :width (:width item) + :height (:height item) + ;; DEBUG + ;; :style {:filter "sepia(1)"} + }] + frame? + [:& frame-wrapper {:shape item + :key (:id item)}] + :else + [:& shape-wrapper {:shape item + :key (:id item)}])))])) (mf/defc frame-svg {::mf/wrap [mf/memo]} diff --git a/frontend/src/app/main/ui/shapes/frame.cljs b/frontend/src/app/main/ui/shapes/frame.cljs index 22aa27c30..966619c67 100644 --- a/frontend/src/app/main/ui/shapes/frame.cljs +++ b/frontend/src/app/main/ui/shapes/frame.cljs @@ -21,7 +21,7 @@ [props] (let [childs (unchecked-get props "childs") shape (unchecked-get props "shape") - {:keys [id x y width height]} shape + {:keys [id width height]} shape props (-> (merge frame-default-props shape) (attrs/extract-style-attrs) diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs index c21de7bea..bb7f69c92 100644 --- a/frontend/src/app/main/ui/workspace.cljs +++ b/frontend/src/app/main/ui/workspace.cljs @@ -106,7 +106,7 @@ (fn [] (when page-id - (st/emitf (dw/finalize-page page-id)))))) + (st/emit! (dw/finalize-page page-id)))))) (when page [:& workspace-content {:key page-id @@ -158,6 +158,9 @@ (if (and (and file project) (:initialized file)) - [:& workspace-page {:page-id page-id :file file :layout layout}] + [:& workspace-page {:key (str "page-" page-id) + :page-id page-id + :file file + :layout layout}] [:& workspace-loader])]]]]])) diff --git a/frontend/src/app/main/ui/workspace/shapes.cljs b/frontend/src/app/main/ui/workspace/shapes.cljs index 9faf50f85..9b30723fd 100644 --- a/frontend/src/app/main/ui/workspace/shapes.cljs +++ b/frontend/src/app/main/ui/workspace/shapes.cljs @@ -51,7 +51,8 @@ "Draws the root shape of the viewport and recursively all the shapes" {::mf/wrap-props false} [props] - (let [objects (obj/get props "objects") + (let [objects (obj/get props "objects") + active-frames (obj/get props "active-frames") root-shapes (get-in objects [uuid/zero :shapes]) shapes (->> root-shapes (mapv #(get objects %)))] @@ -59,7 +60,8 @@ (if (= (:type item) :frame) [:& frame-wrapper {:shape item :key (:id item) - :objects objects}] + :objects objects + :thumbnail? (not (get active-frames (:id item) false))}] [:& shape-wrapper {:shape item :key (:id item)}])))) diff --git a/frontend/src/app/main/ui/workspace/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/shapes/frame.cljs index 91e31c6ed..8e8dd3186 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame.cljs @@ -9,6 +9,7 @@ [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.main.data.workspace :as dw] + [app.main.data.workspace.changes :as dch] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.context :as muc] @@ -17,11 +18,18 @@ [app.main.ui.shapes.text.embed :as ste] [app.util.dom :as dom] [app.util.keyboard :as kbd] + [app.util.object :as obj] [app.util.timers :as ts] [beicon.core :as rx] [okulary.core :as l] [rumext.alpha :as mf])) +(def obs-config + #js {:attributes true + :childList true + :subtree true + :characterData true}) + (defn make-is-moving-ref [id] (let [check-moving (fn [local] @@ -43,14 +51,32 @@ (let [new-shape (unchecked-get new-props "shape") old-shape (unchecked-get old-props "shape") + new-thumbnail? (unchecked-get new-props "thumbnail?") + old-thumbnail? (unchecked-get old-props "thumbnail?") + new-objects (unchecked-get new-props "objects") old-objects (unchecked-get old-props "objects") new-children (->> new-shape :shapes (mapv #(get new-objects %))) old-children (->> old-shape :shapes (mapv #(get old-objects %)))] (and (= new-shape old-shape) + (= new-thumbnail? old-thumbnail?) (= new-children old-children)))) +(mf/defc thumbnail + {::mf/wrap-props false} + [props] + (let [shape (obj/get props "shape")] + (when (:thumbnail shape) + [:image {:xlinkHref (:thumbnail shape) + :x (:x shape) + :y (:y shape) + :width (:width shape) + :height (:height shape) + ;; DEBUG + ;; :style {:filter "sepia(1)"} + }]))) + ;; This custom deffered don't deffer rendering when ghost rendering is ;; used. (defn custom-deferred @@ -76,6 +102,8 @@ [props] (let [shape (unchecked-get props "shape") objects (unchecked-get props "objects") + thumbnail? (unchecked-get props "thumbnail?") + edition (mf/deref refs/selected-edition) embed-fonts? (mf/use-ctx muc/embed-ctx) @@ -90,10 +118,15 @@ (when (and shape (not (:hidden shape))) [:g.frame-wrapper {:display (when (:hidden shape) "none")} - [:> shape-container {:shape shape} - (when embed-fonts? - [:& ste/embed-fontfaces-style {:shapes text-childs}]) - [:& frame-shape - {:shape shape - :childs children}]]]))))) + + (if (and thumbnail? (some? (:thumbnail shape))) + [:& thumbnail {:shape shape}] + + [:> shape-container {:shape shape } + + (when embed-fonts? + [:& ste/embed-fontfaces-style {:shapes text-childs}]) + + [:& frame-shape {:shape shape + :childs children}]])]))))) diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index b2acf0e1a..2adbe4189 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -27,6 +27,7 @@ [app.main.ui.workspace.viewport.selection :as selection] [app.main.ui.workspace.viewport.snap-distances :as snap-distances] [app.main.ui.workspace.viewport.snap-points :as snap-points] + [app.main.ui.workspace.viewport.thumbnail-renderer :as wtr] [app.main.ui.workspace.viewport.utils :as utils] [app.main.ui.workspace.viewport.widgets :as widgets] [beicon.core :as rx] @@ -69,6 +70,7 @@ hover-ids (mf/use-state nil) hover (mf/use-state nil) frame-hover (mf/use-state nil) + active-frames (mf/use-state {}) ;; REFS viewport-ref (mf/use-ref nil) @@ -145,9 +147,12 @@ (hooks/setup-hover-shapes page-id move-stream selected objects transform selected ctrl? hover hover-ids) (hooks/setup-viewport-modifiers modifiers selected objects render-ref) (hooks/setup-shortcuts path-editing? drawing-path?) + (hooks/setup-active-frames objects vbox hover active-frames) [:div.viewport [:div.viewport-overlays + [:& wtr/frame-renderer {:objects objects}] + (when show-comments? [:& comments/comments-layer {:vbox vbox :vport vport @@ -180,7 +185,8 @@ [:& (mf/provider muc/embed-ctx) {:value true} ;; Render root shape [:& shapes/root-shape {:key page-id - :objects objects}]]] + :objects objects + :active-frames @active-frames}]]] [:svg.viewport-controls {:xmlns "http://www.w3.org/2000/svg" diff --git a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs index 1fe1c1685..d5837c2e4 100644 --- a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs @@ -164,3 +164,34 @@ :else (dsc/bind-shortcuts wsc/shortcuts)) dsc/remove-shortcuts))) + +(defn inside-vbox [vbox objects frame-id] + (let [frame (get objects frame-id)] + + (and (some? frame) + (gsh/overlaps? frame vbox)))) + +(defn setup-active-frames + [objects vbox hover active-frames] + + (mf/use-effect + (mf/deps vbox) + + (fn [] + (swap! active-frames + (fn [active-frames] + (let [set-active-frames + (fn [active-frames id active?] + (cond-> active-frames + (and active? (inside-vbox vbox objects id)) + (assoc id true)))] + (reduce-kv set-active-frames {} active-frames)))))) + + (mf/use-effect + (mf/deps @hover @active-frames) + (fn [] + (let [frame-id (if (= :frame (:type @hover)) + (:id @hover) + (:frame-id @hover))] + (when (not (contains? @active-frames frame-id)) + (swap! active-frames assoc frame-id true)))))) diff --git a/frontend/src/app/main/ui/workspace/viewport/thumbnail_renderer.cljs b/frontend/src/app/main/ui/workspace/viewport/thumbnail_renderer.cljs new file mode 100644 index 000000000..92213c131 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/viewport/thumbnail_renderer.cljs @@ -0,0 +1,130 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.main.ui.workspace.viewport.thumbnail-renderer + (:require + [app.main.data.workspace.changes :as dwc] + [app.main.data.workspace.persistence :as dwp] + [app.main.store :as st] + [app.util.dom :as dom] + [app.util.object :as obj] + [app.util.timers :as timers] + [beicon.core :as rx] + [rumext.alpha :as mf])) + +(mf/defc frame-thumbnail + "Renders the canvas and image for a frame thumbnail and stores its value into the shape" + [{:keys [shape on-thumbnail-data on-frame-not-found]}] + + (let [thumbnail-img (mf/use-ref nil) + thumbnail-canvas (mf/use-ref nil) + + on-dom-rendered + (mf/use-callback + (mf/deps (:id shape)) + (fn [node] + (when node + (let [img-node (mf/ref-val thumbnail-img)] + (timers/schedule-on-idle + #(if-let [frame-node (dom/get-element (str "shape-" (:id shape)))] + (let [xml (-> (js/XMLSerializer.) + (.serializeToString frame-node) + js/encodeURIComponent + js/unescape + js/btoa) + img-src (str "data:image/svg+xml;base64," xml)] + (obj/set! img-node "src" img-src)) + + (on-frame-not-found (:id shape)))))))) + + on-image-load + (mf/use-callback + (mf/deps on-thumbnail-data) + (fn [] + (let [canvas-node (mf/ref-val thumbnail-canvas) + img-node (mf/ref-val thumbnail-img) + canvas-context (.getContext canvas-node "2d") + _ (.drawImage canvas-context img-node 0 0) + data (.toDataURL canvas-node "image/jpeg" 0.8)] + (on-thumbnail-data data))))] + + [:div.frame-renderer {:ref on-dom-rendered + :style {:display "none"}} + [:img.thumbnail-img + {:ref thumbnail-img + :width (:width shape) + :height (:height shape) + :on-load on-image-load}] + + [:canvas.thumbnail-canvas + {:ref thumbnail-canvas + :width (:width shape) + :height (:height shape)}]])) + +(mf/defc frame-renderer + "Component in charge of creating thumbnails and storing them" + {::mf/wrap-props false} + [props] + (let [objects (obj/get props "objects") + + ;; Id of the current frame being rendered + shape-id (mf/use-state nil) + + ;; This subject will emit a value every time there is a free "slot" to render + ;; a thumbnail + next (mf/use-memo #(rx/behavior-subject :next)) + + render-frame + (mf/use-callback + (fn [frame-id] + (reset! shape-id frame-id))) + + updates-stream + (mf/use-memo + (fn [] + (let [update-events + (->> st/stream + (rx/filter dwp/update-frame-thumbnail?))] + (->> (rx/zip update-events next) + (rx/map first))))) + + on-thumbnail-data + (mf/use-callback + (mf/deps @shape-id) + (fn [data] + (reset! shape-id nil) + (timers/schedule + (fn [] + (st/emit! (dwc/update-shapes [@shape-id] + #(assoc % :thumbnail data))) + (rx/push! next :next))))) + + on-frame-not-found + (mf/use-callback + (fn [frame-id] + ;; If we couldn't find the frame maybe is still rendering. We push the event again + ;; after a time + (timers/schedule-on-idle #(dwp/update-frame-thumbnail frame-id)) + (rx/push! next :next)))] + + (mf/use-effect + (mf/deps render-frame) + (fn [] + (let [sub (->> updates-stream + (rx/subs #(render-frame (-> (deref %) :frame-id))))] + + #(rx/dispose! sub)))) + + (mf/use-layout-effect + (fn [] + (timers/schedule-on-idle + #(st/emit! (dwp/watch-state-changes))))) + + (when (and (some? @shape-id) (contains? objects @shape-id)) + [:& frame-thumbnail {:key (str "thumbnail-" @shape-id) + :shape (get objects @shape-id) + :on-thumbnail-data on-thumbnail-data + :on-frame-not-found on-frame-not-found}]))) diff --git a/frontend/src/app/worker/thumbnails.cljs b/frontend/src/app/worker/thumbnails.cljs index f8db65343..ddf65c6b1 100644 --- a/frontend/src/app/worker/thumbnails.cljs +++ b/frontend/src/app/worker/thumbnails.cljs @@ -34,7 +34,7 @@ (p/create (fn [resolve reject] (->> (http/send! {:uri uri - :query {:file-id file-id :id page-id} + :query {:file-id file-id :id page-id :strip-thumbnails true} :method :get}) (rx/map http/conditional-decode-transit) (rx/mapcat handle-response) @@ -50,7 +50,7 @@ (let [prev (get @cache ckey)] (if (= (:data prev) data) (:result prev) - (let [elem (mf/element exports/page-svg #js {:data data :width "290" :height "150"}) + (let [elem (mf/element exports/page-svg #js {:data data :width "290" :height "150" :thumbnails? true}) result (rds/renderToStaticMarkup elem)] (swap! cache assoc ckey {:data data :result result}) result))))