diff --git a/frontend/src/app/main/exports.cljs b/frontend/src/app/main/exports.cljs index aa5e08c213..1f70e5e48d 100644 --- a/frontend/src/app/main/exports.cljs +++ b/frontend/src/app/main/exports.cljs @@ -26,7 +26,8 @@ [app.main.ui.shapes.text :as text] [app.main.ui.shapes.group :as group] [app.main.ui.shapes.svg-raw :as svg-raw] - [app.main.ui.shapes.shape :refer [shape-container]])) + [app.main.ui.shapes.shape :refer [shape-container]] + [app.main.ui.shapes.embed :as embed])) (def ^:private default-color "#E8E9EA") ;; $color-canvas @@ -43,8 +44,9 @@ [{:keys [objects] :as data} vport] (let [shapes (cp/select-toplevel-shapes objects {:include-frames? true}) to-finite (fn [val fallback] (if (not (mth/finite? val)) fallback val)) - rect (->> (gsh/selection-rect shapes) - (gal/adjust-to-viewport vport))] + rect (cond->> (gsh/selection-rect shapes) + (some? vport) + (gal/adjust-to-viewport vport))] (-> rect (update :x to-finite 0) (update :y to-finite 0) @@ -121,13 +123,14 @@ (mf/defc page-svg {::mf/wrap [mf/memo]} - [{:keys [data width height thumbnails?] :as props}] + [{:keys [data width height thumbnails? embed?] :as props}] (let [objects (:objects data) root (get objects uuid/zero) shapes (->> (:shapes root) (map #(get objects %))) - vport {:width width :height height} + vport (when (and (some? width) (some? height)) + {:width width :height height}) dim (calculate-dimensions data vport) vbox (get-viewbox dim) background-color (get-in data [:options :background] default-color) @@ -140,29 +143,31 @@ (mf/use-memo (mf/deps objects) #(shape-wrapper-factory objects))] - [:svg {:view-box vbox - :version "1.1" - :xmlnsXlink "http://www.w3.org/1999/xlink" - :xmlns "http://www.w3.org/2000/svg"} - [:& background {:vbox dim :color background-color}] - (for [item shapes] - (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/provider embed/context) {:value embed?} + [:svg {:view-box vbox + :version "1.1" + :xmlnsXlink "http://www.w3.org/1999/xlink" + :xmlns "http://www.w3.org/2000/svg" + :xmlns:penpot "https://penpot.app/xmlns"} + [:& background {:vbox dim :color background-color}] + (for [item shapes] + (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/fonts.cljs b/frontend/src/app/main/fonts.cljs index 8ca0ef906d..3c4799f5be 100644 --- a/frontend/src/app/main/fonts.cljs +++ b/frontend/src/app/main/fonts.cljs @@ -8,17 +8,19 @@ "Fonts management and loading logic." (:require-macros [app.main.fonts :refer [preload-gfonts]]) (:require - [app.config :as cf] [app.common.data :as d] + [app.common.text :as txt] + [app.config :as cf] [app.util.dom :as dom] + [app.util.http :as http] + [app.util.logging :as log] [app.util.object :as obj] [app.util.timers :as ts] - [app.util.logging :as log] - [lambdaisland.uri :as u] - [goog.events :as gev] [beicon.core :as rx] [clojure.set :as set] [cuerdas.core :as str] + [goog.events :as gev] + [lambdaisland.uri :as u] [okulary.core :as l] [promesa.core :as p])) @@ -216,3 +218,55 @@ (or (d/seek #(or (= (:id %) "regular") (= (:name %) "regular")) variants) (first variants))) + +;; Font embedding functions + +;; Template for a CSS font face + +(def font-face-template " +/* latin */ +@font-face { + font-family: '%(family)s'; + font-style: %(style)s; + font-weight: %(weight)s; + font-display: block; + src: url(/fonts/%(family)s-%(suffix)s.woff) format('woff'); +} +") + +(defn get-content-fonts + "Extracts the fonts used by the content of a text shape" + [{font-id :font-id children :children :as content}] + (let [current-font + (if (some? font-id) + #{(select-keys content [:font-id :font-variant-id])} + #{(select-keys txt/default-text-attrs [:font-id :font-variant-id])}) + children-font (->> children (mapv get-content-fonts))] + (reduce set/union (conj children-font current-font)))) + + +(defn fetch-font-css + "Given a font and the variant-id, retrieves the fontface CSS" + [{:keys [font-id font-variant-id] + :or {font-variant-id "regular"}}] + + (let [{:keys [backend family variants]} (get @fontsdb font-id)] + (if (= :google backend) + (-> (generate-gfonts-url + {:family family + :variants [{:id font-variant-id}]}) + (http/fetch-text)) + + (let [{:keys [weight style suffix] :as variant} + (d/seek #(= (:id %) font-variant-id) variants) + font-data {:family family + :style style + :suffix (or suffix font-variant-id) + :weight weight}] + (rx/of (str/fmt font-face-template font-data)))))) + +(defn extract-fontface-urls + "Parses the CSS and retrieves the font urls" + [^string css] + (->> (re-seq #"url\(([^)]+)\)" css) + (mapv second))) diff --git a/frontend/src/app/main/ui/context.cljs b/frontend/src/app/main/ui/context.cljs index fe18ce753d..631b4b6f58 100644 --- a/frontend/src/app/main/ui/context.cljs +++ b/frontend/src/app/main/ui/context.cljs @@ -8,7 +8,6 @@ (:require [rumext.alpha :as mf])) -(def embed-ctx (mf/create-context false)) (def render-ctx (mf/create-context nil)) (def def-ctx (mf/create-context false)) diff --git a/frontend/src/app/main/ui/hooks.cljs b/frontend/src/app/main/ui/hooks.cljs index 5b0b0645e8..789b5ab8f3 100644 --- a/frontend/src/app/main/ui/hooks.cljs +++ b/frontend/src/app/main/ui/hooks.cljs @@ -225,3 +225,28 @@ (fn [] (mf/set-ref-val! ref value))) (mf/ref-val ref))) + +(defn use-equal-memo + [val] + (let [ref (mf/use-ref nil)] + (when-not (= (mf/ref-val ref) val) + (mf/set-ref-val! ref val)) + (mf/ref-val ref))) + +(defn- ssr? + "Checks if the current environment is run under a SSR context" + [] + (try + (not js/window) + (catch :default e + ;; When exception accessing window we're in ssr + true))) + +(defn use-effect-ssr + "Use effect that handles SSR" + [deps effect-fn] + + (if (ssr?) + (let [ret (effect-fn)] + (when (fn? ret) (ret))) + (mf/use-effect deps effect-fn))) diff --git a/frontend/src/app/main/ui/render.cljs b/frontend/src/app/main/ui/render.cljs index 1060de4d39..f7de7b8628 100644 --- a/frontend/src/app/main/ui/render.cljs +++ b/frontend/src/app/main/ui/render.cljs @@ -14,7 +14,7 @@ [app.common.uuid :as uuid] [app.main.exports :as exports] [app.main.repo :as repo] - [app.main.ui.context :as muc] + [app.main.ui.shapes.embed :as embed] [app.main.ui.shapes.filters :as filters] [app.main.ui.shapes.shape :refer [shape-container]] [beicon.core :as rx] @@ -71,7 +71,7 @@ #(exports/shape-wrapper-factory objects)) ] - [:& (mf/provider muc/embed-ctx) {:value true} + [:& (mf/provider embed/context) {:value true} [:svg {:id "screenshot" :view-box vbox :width width diff --git a/frontend/src/app/main/ui/shapes/embed.cljs b/frontend/src/app/main/ui/shapes/embed.cljs new file mode 100644 index 0000000000..fd2de767a3 --- /dev/null +++ b/frontend/src/app/main/ui/shapes/embed.cljs @@ -0,0 +1,39 @@ +;; 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.shapes.embed + (:require + [app.main.ui.hooks :as hooks] + [app.util.http :as http] + [beicon.core :as rx] + [rumext.alpha :as mf])) + +(def context (mf/create-context false)) + +(defn use-data-uris [urls] + (let [embed? (mf/use-ctx context) + urls (hooks/use-equal-memo urls) + uri-data (mf/use-ref {}) + state (mf/use-state 0)] + + (hooks/use-effect-ssr + (mf/deps embed? urls) + (fn [] + (let [sub (when embed? + (->> (rx/from urls) + (rx/merge-map http/fetch-data-uri) + (rx/reduce conj {}) + (rx/subs (fn [data] + (when-not (= data (mf/ref-val uri-data)) + (mf/set-ref-val! uri-data data) + (reset! state inc))))))] + #(when sub + (rx/dispose! sub))))) + + ;; Use ref so if the urls are cached will return inmediately instead of the + ;; next render + (when embed? + (mf/ref-val uri-data)))) diff --git a/frontend/src/app/main/ui/shapes/fill_image.cljs b/frontend/src/app/main/ui/shapes/fill_image.cljs index 17d13291bb..8fb66a3ef9 100644 --- a/frontend/src/app/main/ui/shapes/fill_image.cljs +++ b/frontend/src/app/main/ui/shapes/fill_image.cljs @@ -8,10 +8,9 @@ (:require [app.common.geom.shapes :as gsh] [app.config :as cfg] + [app.main.ui.shapes.embed :as embed] [app.util.object :as obj] - [rumext.alpha :as mf] - [app.common.geom.point :as gpt] - [app.main.ui.shapes.image :as image])) + [rumext.alpha :as mf])) (mf/defc fill-image-pattern {::mf/wrap-props false} @@ -22,8 +21,8 @@ (when (contains? shape :fill-image) (let [{:keys [x y width height]} (:selrect shape) fill-image-id (str "fill-image-" render-id) - media (:fill-image shape) - uri (image/use-image-uri media) + uri (cfg/resolve-file-media (:fill-image shape)) + embed (embed/use-data-uris [uri]) transform (gsh/transform-matrix shape)] [:pattern {:id fill-image-id @@ -33,6 +32,6 @@ :height height :width width :patternTransform transform} - [:image {:xlinkHref uri + [:image {:xlinkHref (get embed uri uri) :width width :height height}]])))) diff --git a/frontend/src/app/main/ui/shapes/frame.cljs b/frontend/src/app/main/ui/shapes/frame.cljs index 966619c67c..4146200273 100644 --- a/frontend/src/app/main/ui/shapes/frame.cljs +++ b/frontend/src/app/main/ui/shapes/frame.cljs @@ -7,13 +7,16 @@ (ns app.main.ui.shapes.frame (:require [app.common.data :as d] - [app.common.geom.shapes :as geom] [app.main.ui.shapes.attrs :as attrs] + [app.main.ui.shapes.text.fontfaces :as ff] [app.util.object :as obj] [rumext.alpha :as mf])) (def frame-default-props {:fill-color "#ffffff"}) +(defn is-text? [{type :type}] + (= :text type)) + (defn frame-shape [shape-wrapper] (mf/fnc frame-shape @@ -23,6 +26,8 @@ shape (unchecked-get props "shape") {:keys [id width height]} shape + text-childs (->> childs (filterv is-text?)) + props (-> (merge frame-default-props shape) (attrs/extract-style-attrs) (obj/merge! @@ -32,6 +37,7 @@ :height height :className "frame-background"}))] [:* + [:& ff/fontfaces-style {:shapes text-childs}] [:> :rect props] (for [[i item] (d/enumerate childs)] [:& shape-wrapper {:frame shape diff --git a/frontend/src/app/main/ui/shapes/image.cljs b/frontend/src/app/main/ui/shapes/image.cljs index 45ceb303b7..a886049f1a 100644 --- a/frontend/src/app/main/ui/shapes/image.cljs +++ b/frontend/src/app/main/ui/shapes/image.cljs @@ -7,35 +7,13 @@ (ns app.main.ui.shapes.image (:require [app.common.geom.shapes :as geom] - [app.config :as cfg] - [app.main.ui.context :as muc] [app.main.ui.shapes.attrs :as attrs] + [app.main.ui.shapes.embed :as se] [app.util.dom :as dom] - [app.util.http :as http] [app.util.object :as obj] - [app.util.webapi :as wapi] - [beicon.core :as rx] - [rumext.alpha :as mf])) - -(defn use-image-uri - [media] - (let [uri (mf/use-memo (mf/deps (:id media)) - #(cfg/resolve-file-media media)) - embed-resources? (mf/use-ctx muc/embed-ctx) - data-uri (mf/use-state (when (not embed-resources?) uri))] - - (mf/use-effect - (mf/deps uri) - (fn [] - (if embed-resources? - (->> (http/send! {:method :get - :uri uri - :response-type :blob}) - (rx/map :body) - (rx/mapcat wapi/read-file-as-data-url) - (rx/subs #(reset! data-uri %)))))) - - (or @data-uri uri))) + [rumext.alpha :as mf] + [app.config :as cfg] + [app.main.ui.shapes.embed :as embed])) (mf/defc image-shape {::mf/wrap-props false} @@ -43,7 +21,8 @@ (let [shape (unchecked-get props "shape") {:keys [id x y width height rotation metadata]} shape - uri (use-image-uri metadata)] + uri (cfg/resolve-file-media metadata) + embed (embed/use-data-uris [uri])] (let [transform (geom/transform-matrix shape) props (-> (attrs/extract-style-attrs shape) @@ -60,5 +39,5 @@ [:> "image" (obj/merge! props - #js {:xlinkHref uri + #js {:xlinkHref (get embed uri uri) :onDragStart on-drag-start})]))) diff --git a/frontend/src/app/main/ui/shapes/text/embed.cljs b/frontend/src/app/main/ui/shapes/text/embed.cljs deleted file mode 100644 index 61ef3fd4ea..0000000000 --- a/frontend/src/app/main/ui/shapes/text/embed.cljs +++ /dev/null @@ -1,144 +0,0 @@ -;; 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.shapes.text.embed - (:refer-clojure :exclude [memoize]) - (:require - [app.common.data :as d] - [app.common.text :as txt] - [app.main.fonts :as fonts] - [app.util.http :as http] - [app.util.time :as dt] - [app.util.webapi :as wapi] - [app.util.object :as obj] - [clojure.set :as set] - [cuerdas.core :as str] - [promesa.core :as p] - [beicon.core :as rx] - [rumext.alpha :as mf])) - - -(defonce cache (atom {})) - -(defn with-cache - [{:keys [key max-age]} observable] - (let [entry (get @cache key) - age (when entry - (dt/diff (dt/now) - (:created-at entry)))] - (if (and (some? entry) - (< age max-age)) - (rx/of (:data entry)) - (->> observable - (rx/tap (fn [data] - (let [entry {:created-at (dt/now) :data data}] - (swap! cache assoc key entry)))))))) - -(def font-face-template " -/* latin */ -@font-face { - font-family: '%(family)s'; - font-style: %(style)s; - font-weight: %(weight)s; - font-display: block; - src: url(/fonts/%(family)s-%(suffix)s.woff) format('woff'); -} -") - -;; -- Embed fonts into styles - -(defn get-node-fonts - [node] - (let [current-font (if (not (nil? (:font-id node))) - #{(select-keys node [:font-id :font-variant-id])} - #{(select-keys txt/default-text-attrs [:font-id :font-variant-id])}) - children-font (map get-node-fonts (:children node))] - (reduce set/union (conj children-font current-font)))) - -(defn get-font-css - "Given a font and the variant-id, retrieves the style CSS for it." - [{:keys [id backend family variants] :as font} font-variant-id] - (if (= :google backend) - (let [uri (fonts/generate-gfonts-url {:family family :variants [{:id font-variant-id}]})] - (->> (http/send! {:method :get - :mode :cors - :omit-default-headers true - :uri uri - :response-type :text}) - (rx/map :body) - (http/as-promise))) - (let [{:keys [name weight style suffix] :as variant} (d/seek #(= (:id %) font-variant-id) variants) - result (str/fmt font-face-template {:family family - :style style - :suffix (or suffix font-variant-id) - :weight weight})] - (p/resolved result)))) - -(defn- to-promise - [observable] - (p/create (fn [resolve reject] - (->> (rx/take 1 observable) - (rx/subs resolve reject))))) - -(defn fetch-font-data - "Parses the CSS and retrieves the font data as DataURI." - [^string css] - (let [uris (->> (re-seq #"url\(([^)]+)\)" css) - (mapv second))] - (with-cache {:key uris :max-age (dt/duration {:hours 4})} - (->> (rx/from (seq uris)) - (rx/mapcat (fn [uri] - (->> (http/send! {:method :get :uri uri :response-type :blob :omit-default-headers true}) - (rx/map :body) - (rx/mapcat wapi/read-file-as-data-url) - (rx/map #(vector uri %))))) - (rx/reduce conj []))))) - -(defn get-font-data - "Parses the CSS and retrieves the font data as DataURI." - [^string css] - (->> (fetch-font-data css) - (http/as-promise))) - -(defn embed-font - "Given a font-id and font-variant-id, retrieves the CSS for it and - convert all external urls to embedded data URI's." - [{:keys [font-id font-variant-id] :or {font-variant-id "regular"}}] - (let [{:keys [backend family] :as font} (get @fonts/fontsdb font-id)] - (p/let [css (get-font-css font font-variant-id) - url-to-data (get-font-data css) - replace-text (fn [text [url data]] (str/replace text url data))] - (reduce replace-text css url-to-data)))) - -;; NOTE: we can't move this to generic hooks namespace because that -;; namespace imports some code incompatible with webworkers and this -;; font embbeding should be able run on browser and webworker -;; contexts. -(defn- memoize - [val] - (let [ref (mf/use-ref #js {})] - (when-not (= (mf/ref-val ref) val) - (mf/set-ref-val! ref val)) - (mf/ref-val ref))) - -(mf/defc embed-fontfaces-style - {::mf/wrap-props false - ::mf/wrap [#(mf/memo' % (mf/check-props ["shapes"]))]} - [props] - (let [shapes (obj/get props "shapes") - node {:children (->> shapes (map :content))} - fonts (-> node get-node-fonts memoize) - style (mf/use-state nil)] - - (mf/use-effect - (mf/deps fonts) - (fn [] - (-> (p/all (map embed-font fonts)) - (p/then (fn [result] - (reset! style (str/join "\n" result))))))) - - (when (some? @style) - [:style @style]))) diff --git a/frontend/src/app/main/ui/shapes/text/fontfaces.cljs b/frontend/src/app/main/ui/shapes/text/fontfaces.cljs new file mode 100644 index 0000000000..e00a6919c6 --- /dev/null +++ b/frontend/src/app/main/ui/shapes/text/fontfaces.cljs @@ -0,0 +1,79 @@ +;; 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.shapes.text.fontfaces + (:require + [app.main.fonts :as fonts] + [app.main.ui.hooks :as hooks] + [app.main.ui.shapes.embed :as embed] + [app.util.object :as obj] + [beicon.core :as rx] + [clojure.set :as set] + [cuerdas.core :as str] + [rumext.alpha :as mf])) + +(defn replace-embeds + "Replace into the font-faces of a CSS the URL's that are present in `embed-data` by its + data-uri" + [css urls embed-data] + (letfn [(replace-url [css url] + (str/replace css url (get embed-data url url)))] + (->> urls + (reduce replace-url css)))) + +(defn use-fonts-css + "Hook that retrieves the CSS of the fonts passed as parameter" + [fonts] + (let [fonts-css-ref (mf/use-ref "") + redraw (mf/use-state 0)] + + (hooks/use-effect-ssr + (mf/deps fonts) + (fn [] + (let [sub + (->> (rx/from fonts) + (rx/merge-map fonts/fetch-font-css) + (rx/reduce conj []) + (rx/subs + (fn [result] + (let [css (str/join "\n" result)] + (when-not (= (mf/ref-val fonts-css-ref) css) + (mf/set-ref-val! fonts-css-ref css) + (reset! redraw inc))))))] + #(rx/dispose! sub)))) + + (mf/ref-val fonts-css-ref))) + +(mf/defc fontfaces-style + {::mf/wrap-props false + ::mf/wrap [#(mf/memo' % (mf/check-props ["shapes"]))]} + [props] + (let [shapes (obj/get props "shapes") + + content (->> shapes (mapv :content)) + + ;; Retrieve the fonts ids used by the text shapes + fonts (->> content + (mapv fonts/get-content-fonts) + (reduce set/union #{}) + (hooks/use-equal-memo)) + + ;; Fetch its CSS fontfaces + fonts-css (use-fonts-css fonts) + + ;; Extract from the CSS the URL's to embed + fonts-urls (mf/use-memo + (mf/deps fonts-css) + #(fonts/extract-fontface-urls fonts-css)) + + ;; Calculate the data-uris for these fonts + fonts-embed (embed/use-data-uris fonts-urls) + + ;; Creates a style tag by replacing the urls with the data uri + style (replace-embeds fonts-css fonts-urls fonts-embed)] + + (when (and (some? style) (not (empty? style))) + [:style style]))) diff --git a/frontend/src/app/main/ui/workspace/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/shapes/frame.cljs index 8875ba6e5d..592bb5d451 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame.cljs @@ -6,45 +6,15 @@ (ns app.main.ui.workspace.shapes.frame (:require - [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] [app.main.ui.shapes.frame :as frame] [app.main.ui.shapes.shape :refer [shape-container]] - [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] - (and (= :move (:transform local)) - (contains? (:selected local) id)))] - (l/derived check-moving refs/workspace-local))) - -(defn check-props - ([props] (check-props props =)) - ([props eqfn?] - (fn [np op] - (every? #(eqfn? (unchecked-get np %) - (unchecked-get op %)) - props)))) - (defn check-frame-props "Checks for changes in the props of a frame" [new-props old-props] @@ -107,14 +77,9 @@ thumbnail? (unchecked-get props "thumbnail?") edition (mf/deref refs/selected-edition) - embed-fonts? (mf/use-ctx muc/embed-ctx) shape (gsh/transform-shape shape) children (mapv #(get objects %) (:shapes shape)) - text-childs (->> objects - vals - (filterv #(and (= :text (:type %)) - (= (:id shape) (:frame-id %))))) ds-modifier (get-in shape [:modifiers :displacement]) @@ -131,12 +96,7 @@ [:g.frame-wrapper {:display (when (:hidden shape) "none")} (when-not show-thumbnail? - [:> shape-container {:shape shape - :ref on-dom} - - (when embed-fonts? - [:& ste/embed-fontfaces-style {:shapes text-childs}]) - + [:> shape-container {:shape shape :ref on-dom} [:& 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 a210dad733..8ead981ca2 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -7,12 +7,13 @@ (ns app.main.ui.workspace.viewport (:require [app.common.data :as d] - [app.common.pages :as cp] [app.common.geom.shapes :as gsh] + [app.common.pages :as cp] [app.main.refs :as refs] [app.main.ui.context :as ctx] [app.main.ui.context :as muc] [app.main.ui.measurements :as msr] + [app.main.ui.shapes.embed :as embed] [app.main.ui.workspace.shapes :as shapes] [app.main.ui.workspace.shapes.text.editor :as editor] [app.main.ui.workspace.viewport.actions :as actions] @@ -187,7 +188,7 @@ :style {:background-color (get options :background "#E8E9EA") :pointer-events "none"}} - [:& (mf/provider muc/embed-ctx) {:value true} + [:& (mf/provider embed/context) {:value true} ;; Render root shape [:& shapes/root-shape {:key page-id :objects objects diff --git a/frontend/src/app/util/cache.cljs b/frontend/src/app/util/cache.cljs new file mode 100644 index 0000000000..53fa610ccb --- /dev/null +++ b/frontend/src/app/util/cache.cljs @@ -0,0 +1,26 @@ +;; 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.util.cache + (:require + [app.util.time :as dt] + [beicon.core :as rx])) + +(defonce cache (atom {})) + +(defn with-cache + [{:keys [key max-age]} observable] + (let [entry (get @cache key) + age (when entry + (dt/diff (dt/now) + (:created-at entry)))] + (if (and (some? entry) (< age max-age)) + (rx/of (:data entry)) + (->> observable + (rx/tap + (fn [data] + (let [entry {:created-at (dt/now) :data data}] + (swap! cache assoc key entry)))))))) diff --git a/frontend/src/app/util/http.cljs b/frontend/src/app/util/http.cljs index a5c4920b0c..cba4052375 100644 --- a/frontend/src/app/util/http.cljs +++ b/frontend/src/app/util/http.cljs @@ -10,9 +10,12 @@ [app.common.data :as d] [app.common.uri :as u] [app.config :as cfg] + [app.util.cache :as c] [app.util.globals :as globals] [app.util.object :as obj] + [app.util.time :as dt] [app.util.transit :as t] + [app.util.webapi :as wapi] [beicon.core :as rx] [cuerdas.core :as str] [promesa.core :as p])) @@ -152,6 +155,27 @@ (defn as-promise [observable] - (p/create (fn [resolve reject] - (->> (rx/take 1 observable) - (rx/subs resolve reject))))) + (p/create + (fn [resolve reject] + (->> (rx/take 1 observable) + (rx/subs resolve reject))))) + +(defn fetch-data-uri [uri] + (c/with-cache {:key uri :max-age (dt/duration {:hours 4})} + (->> (send! {:method :get + :uri uri + :response-type :blob + :omit-default-headers true}) + (rx/map :body) + (rx/mapcat wapi/read-file-as-data-url) + (rx/map #(hash-map uri %))))) + +(defn fetch-text [url] + (c/with-cache {:key url :max-age (dt/duration {:hours 4})} + (->> (send! + {:method :get + :mode :cors + :omit-default-headers true + :uri url + :response-type :text}) + (rx/map :body))))