From 369dc8ffb58d02174ceca10da8cf16153c525e23 Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Mon, 3 Oct 2022 11:39:58 +0200 Subject: [PATCH] :tada: Basic changes to use penpot as a library --- common/src/app/common/file_builder.cljc | 14 +++- frontend/deps.edn | 7 +- frontend/shadow-cljs.edn | 2 +- frontend/src/app/libs/file_builder.cljs | 66 ++++++++++++++++++- frontend/src/app/main/broadcast.cljs | 6 +- frontend/src/app/main/ui/hooks.cljs | 28 +------- frontend/src/app/main/ui/shapes/embed.cljs | 2 +- frontend/src/app/main/ui/shapes/fills.cljs | 2 +- frontend/src/app/main/ui/shapes/shape.cljs | 3 +- .../app/main/ui/shapes/text/fontfaces.cljs | 3 +- frontend/src/app/util/storage.cljs | 6 +- 11 files changed, 96 insertions(+), 43 deletions(-) diff --git a/common/src/app/common/file_builder.cljc b/common/src/app/common/file_builder.cljc index 7194fdf10..3c0bea732 100644 --- a/common/src/app/common/file_builder.cljc +++ b/common/src/app/common/file_builder.cljc @@ -21,6 +21,7 @@ [app.common.types.pages-list :as ctpl] [app.common.types.shape :as cts] [app.common.uuid :as uuid] + [clojure.spec.alpha :as spec] [cuerdas.core :as str])) (def root-frame uuid/zero) @@ -52,9 +53,18 @@ (when fail-on-spec? (us/verify ::pcs/change change)) - (let [valid? (us/valid? ::pcs/change change)] + (let [valid? (us/valid? ::pcs/change change) + explain (spec/explain-str ::pcs/change change)] #?(:cljs - (when-not valid? (.warn js/console "Invalid shape" (clj->js change)))) + (when-not valid? + (do + (.warn js/console "Invalid shape" (clj->js change)) + (.warn js/console explain))) + :clj + (when-not valid? + (do + (prn "Invalid shape" change) + (prn explain)))) (cond-> file valid? diff --git a/frontend/deps.edn b/frontend/deps.edn index 0464f4daa..b5944ea6e 100644 --- a/frontend/deps.edn +++ b/frontend/deps.edn @@ -13,9 +13,10 @@ funcool/tubax {:mvn/version "2021.05.20-0"} funcool/rumext - {:git/tag "v2.0" - :git/sha "fc617a8" - :git/url "https://github.com/funcool/rumext.git"} + {:git/tag "v2.1" + :git/sha "6343102" + :git/url "https://github.com/funcool/rumext.git" + } instaparse/instaparse {:mvn/version "1.4.12"} garden/garden {:git/url "https://github.com/noprompt/garden" diff --git a/frontend/shadow-cljs.edn b/frontend/shadow-cljs.edn index b88e95a35..b532672e9 100644 --- a/frontend/shadow-cljs.edn +++ b/frontend/shadow-cljs.edn @@ -53,7 +53,7 @@ :createFile app.libs.file-builder/create-file-export}}} :compiler-options - {:output-feature-set :es8 + {:output-feature-set :es2020 :output-wrapper false :warnings {:fn-deprecated false}} diff --git a/frontend/src/app/libs/file_builder.cljs b/frontend/src/app/libs/file_builder.cljs index caa760740..1a4af9fb5 100644 --- a/frontend/src/app/libs/file_builder.cljs +++ b/frontend/src/app/libs/file_builder.cljs @@ -8,6 +8,11 @@ (:require [app.common.data :as d] [app.common.file-builder :as fb] + [app.common.uuid :as uuid] + [app.util.dom :as dom] + [app.util.zip :as uz] + [app.worker.export :as e] + [beicon.core :as rx] [cuerdas.core :as str])) (defn parse-data [data] @@ -22,6 +27,50 @@ key (-> key d/name str/kebab keyword)] [key value])) $))) + +(defn export-file + [file] + (let [file (assoc file + :name (:name file) + :file-name (:name file) + :is-shared false) + + files-stream (->> (rx/of {(:id file) file}) + (rx/share)) + + manifest-stream + (->> files-stream + (rx/map #(e/create-manifest (uuid/next) (:id file) :all %)) + (rx/map (fn [a] + (vector "manifest.json" a)))) + + render-stream + (->> files-stream + (rx/flat-map vals) + (rx/flat-map e/process-pages) + (rx/observe-on :async) + (rx/flat-map e/get-page-data) + (rx/share)) + pages-stream + (->> render-stream + (rx/map e/collect-page))] + + (rx/merge + (->> render-stream + (rx/map #(hash-map + :type :progress + :file (:id file) + :data (str "Render " (:file-name %) " - " (:name %))))) + + (->> (rx/merge + manifest-stream + pages-stream) + (rx/reduce conj []) + (rx/with-latest-from files-stream) + (rx/flat-map (fn [[data _]] + (->> (uz/compress-files data) + (rx/map #(vector file %))))))))) + (deftype File [^:mutable file] Object @@ -50,6 +99,13 @@ (closeGroup [_] (set! file (fb/close-group file))) + (addBool [_ data] + (set! file (fb/add-bool file (parse-data data))) + (str (:last-id file))) + + (closeBool [_] + (set! file (fb/close-bool file))) + (createRect [_ data] (set! file (fb/create-rect file (parse-data data))) (str (:last-id file))) @@ -78,7 +134,15 @@ (set! file (fb/close-svg-raw file))) (asMap [_] - (clj->js file))) + (clj->js file)) + + (export [_] + (->> (export-file file) + (rx/subs + (fn [value] + (when (not (contains? value :type)) + (let [[file export-blob] value] + (dom/trigger-download (:name file) export-blob)))))))) (defn create-file-export [^string name] (File. (fb/create-file name))) diff --git a/frontend/src/app/main/broadcast.cljs b/frontend/src/app/main/broadcast.cljs index 15c5da74d..b83f2fa91 100644 --- a/frontend/src/app/main/broadcast.cljs +++ b/frontend/src/app/main/broadcast.cljs @@ -7,6 +7,7 @@ (ns app.main.broadcast "BroadcastChannel API." (:require + [app.common.exceptions :as ex] [app.common.transit :as t] [beicon.core :as rx] [potok.core :as ptk])) @@ -18,9 +19,12 @@ (def ^:const default-topic "penpot") ;; The main broadcast channel instance, used for emit data +;; If used as a library may be we can't access js/BroadcastChannel. +;; and even if it exists we can receive an exception like: +;; Failed to construct 'BroadcastChannel': Can't create BroadcastChannel in an opaque origin (defonce default-channel (when (exists? js/BroadcastChannel) - (js/BroadcastChannel. default-topic))) + (ex/ignoring (js/BroadcastChannel. default-topic)))) (defonce stream (if (exists? js/BroadcastChannel) diff --git a/frontend/src/app/main/ui/hooks.cljs b/frontend/src/app/main/ui/hooks.cljs index 9a49f806e..0d1238e40 100644 --- a/frontend/src/app/main/ui/hooks.cljs +++ b/frontend/src/app/main/ui/hooks.cljs @@ -7,9 +7,7 @@ (ns app.main.ui.hooks "A collection of general purpose react hooks." (:require - [app.common.data.macros :as dm] [app.common.pages :as cp] - [app.common.uuid :as uuid] [app.main.broadcast :as mbc] [app.main.data.shortcuts :as dsc] [app.main.refs :as refs] @@ -22,11 +20,6 @@ [goog.functions :as f] [rumext.v2 :as mf])) -(defn use-id - "Get a stable id value across rerenders." - [] - (mf/use-memo #(dm/str (uuid/next)))) - (defn use-rxsub [ob] (let [[state reset-state!] (mf/useState #(if (satisfies? IDeref ob) @ob nil))] @@ -253,24 +246,6 @@ (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))) - (defn with-focus-objects ([objects] (let [focus (mf/deref refs/workspace-focus-selected)] @@ -298,7 +273,7 @@ localStorage. And it will keep watching events with type equals to `key` for new values." [key default] - (let [id (use-id) + (let [id (mf/use-id) state (mf/use-state (get @storage key default)) stream (mf/with-memo [id] (->> mbc/stream @@ -311,7 +286,6 @@ (swap! storage assoc key @state)) (use-stream stream (partial reset! state)) - state)) (defonce ^:private intersection-subject (rx/subject)) diff --git a/frontend/src/app/main/ui/shapes/embed.cljs b/frontend/src/app/main/ui/shapes/embed.cljs index 2105d15d2..8d0f04b39 100644 --- a/frontend/src/app/main/ui/shapes/embed.cljs +++ b/frontend/src/app/main/ui/shapes/embed.cljs @@ -19,7 +19,7 @@ uri-data (mf/use-ref {}) state (mf/use-state 0)] - (hooks/use-effect-ssr + (mf/use-ssr-effect (mf/deps embed? urls) (fn [] (let [;; When not active the embedding we return the URI diff --git a/frontend/src/app/main/ui/shapes/fills.cljs b/frontend/src/app/main/ui/shapes/fills.cljs index 556a8020d..668ba8c39 100644 --- a/frontend/src/app/main/ui/shapes/fills.cljs +++ b/frontend/src/app/main/ui/shapes/fills.cljs @@ -80,7 +80,7 @@ (obj/set! "height" height))]) (when has-image? - [:image {:href (get embed uri uri) + [:image {:href (or (:data-uri shape) (get embed uri uri)) :preserveAspectRatio "none" :width width :height height}])]])]))))) diff --git a/frontend/src/app/main/ui/shapes/shape.cljs b/frontend/src/app/main/ui/shapes/shape.cljs index 8b4a8fc2a..c3b4c70d3 100644 --- a/frontend/src/app/main/ui/shapes/shape.cljs +++ b/frontend/src/app/main/ui/shapes/shape.cljs @@ -10,7 +10,6 @@ [app.common.data.macros :as dm] [app.common.pages.helpers :as cph] [app.main.ui.context :as muc] - [app.main.ui.hooks :as h] [app.main.ui.shapes.attrs :as attrs] [app.main.ui.shapes.export :as ed] [app.main.ui.shapes.fills :as fills] @@ -56,7 +55,7 @@ disable-shadows? (unchecked-get props "disable-shadows?") type (:type shape) - render-id (h/use-id) + render-id (mf/use-id) filter-id (dm/str "filter_" render-id) styles (-> (obj/create) (obj/set! "pointerEvents" pointer-events) diff --git a/frontend/src/app/main/ui/shapes/text/fontfaces.cljs b/frontend/src/app/main/ui/shapes/text/fontfaces.cljs index d3da39791..6bfcdf675 100644 --- a/frontend/src/app/main/ui/shapes/text/fontfaces.cljs +++ b/frontend/src/app/main/ui/shapes/text/fontfaces.cljs @@ -9,7 +9,6 @@ [app.common.data :as d] [app.common.pages.helpers :as cph] [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] @@ -32,7 +31,7 @@ (let [fonts-css-ref (mf/use-ref "") redraw (mf/use-state 0)] - (hooks/use-effect-ssr + (mf/use-ssr-effect (mf/deps fonts) (fn [] (let [sub diff --git a/frontend/src/app/util/storage.cljs b/frontend/src/app/util/storage.cljs index 2d920f6dc..a29049740 100644 --- a/frontend/src/app/util/storage.cljs +++ b/frontend/src/app/util/storage.cljs @@ -6,6 +6,7 @@ (ns app.util.storage (:require + [app.common.exceptions :as ex] [app.common.transit :as t] [app.util.globals :as g] [app.util.timers :as tm])) @@ -38,7 +39,8 @@ {} (range len))))) - -(defonce storage (atom (load (unchecked-get g/global "localStorage")))) +;; Using ex/ignoring because can receive a DOMException like this when importing the code as a library: +;; Failed to read the 'localStorage' property from 'Window': Storage is disabled inside 'data:' URLs. +(defonce storage (atom (load (ex/ignoring (unchecked-get g/global "localStorage"))))) (add-watch storage :persistence #(persist js/localStorage %3 %4))