0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-04-09 21:41:23 -05:00

Merge remote-tracking branch 'origin/staging' into develop

This commit is contained in:
Alejandro Alonso 2022-04-26 06:17:27 +02:00
commit b3847cafa8
33 changed files with 1128 additions and 936 deletions

View file

@ -52,6 +52,7 @@
- Round the size values on handoff to two decimals [Taiga #3227](https://tree.taiga.io/project/penpot/issue/3227)
- Fix internal error when hoverin over shape [Taiga #3237](https://tree.taiga.io/project/penpot/issue/3237)
- Fix mouse leave in handoff close overlay animation breaks [Taiga #3173](https://tree.taiga.io/project/penpot/issue/3173)
- Fix different behaviour during image drag [Taiga #2279](https://tree.taiga.io/project/penpot/issue/2279)
- Fix hidden file name on import [Taiga #3172](https://tree.taiga.io/project/penpot/issue/3172)
- Fix unneccessary scrollbars at the color list [Taiga #3211](https://tree.taiga.io/project/penpot/issue/3211)
@ -95,6 +96,7 @@
- Fix component name in sidebar widget [Taiga #3144](https://tree.taiga.io/project/penpot/issue/3144)
- Fix resize rotated shape with top&down constraints [Taiga #3167](https://tree.taiga.io/project/penpot/issue/3167)
- Fix multi user not working [Taiga #3195](https://tree.taiga.io/project/penpot/issue/3195)
- Fix guides are not duplicated with the artboard [Taiga #3072](https://tree.taiga.io/project/penpot/issue/3072)
### :arrow_up: Deps updates
### :heart: Community contributions by (Thank you!)

View file

@ -44,6 +44,21 @@
"image/svg+xml" :svg
nil))
(defn mtype->extension [mtype]
;; https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types
(case mtype
"image/apng" ".apng"
"image/avif" ".avif"
"image/gif" ".gif"
"image/jpeg" ".jpg"
"image/png" ".png"
"image/svg+xml" ".svg"
"image/webp" ".webp"
"application/zip" ".zip"
"application/penpot" ".penpot"
"application/pdf" ".pdf"
nil))
(def max-file-size (* 5 1024 1024))
(s/def ::id uuid?)

View file

@ -286,37 +286,38 @@
update-shape
(fn [changes id]
(let [old-obj (get objects id)
new-obj (update-fn old-obj)
new-obj (update-fn old-obj)]
(if (= old-obj new-obj)
changes
(let [attrs (or attrs (d/concat-set (keys old-obj) (keys new-obj)))
attrs (or attrs (d/concat-set (keys old-obj) (keys new-obj)))
{rops :rops uops :uops}
(reduce #(generate-operation %1 %2 old-obj new-obj ignore-geometry?)
{:rops [] :uops []}
attrs)
{rops :rops uops :uops}
(reduce #(generate-operation %1 %2 old-obj new-obj ignore-geometry?)
{:rops [] :uops []}
attrs)
uops (cond-> uops
(seq uops)
(d/preconj {:type :set-touched :touched (:touched old-obj)}))
uops (cond-> uops
(seq uops)
(d/preconj {:type :set-touched :touched (:touched old-obj)}))
change (cond-> {:type :mod-obj
:id id}
change (cond-> {:type :mod-obj
:id id}
(some? page-id)
(assoc :page-id page-id)
(some? page-id)
(assoc :page-id page-id)
(some? component-id)
(assoc :component-id component-id))]
(some? component-id)
(assoc :component-id component-id))]
(cond-> changes
(seq rops)
(update :redo-changes conj (assoc change :operations rops))
(cond-> changes
(seq rops)
(update :redo-changes conj (assoc change :operations rops))
(seq uops)
(update :undo-changes d/preconj (assoc change :operations uops)))))))]
(seq uops)
(update :undo-changes d/preconj (assoc change :operations uops)))))]
(-> (reduce update-shape changes ids)
(apply-changes-local)))))
(-> (reduce update-shape changes ids)
(apply-changes-local)))))
(defn remove-objects
[changes ids]

View file

@ -502,3 +502,20 @@
(reduce process-shape (transient {}))
(persistent!))
persistent!)))
(defn selected-subtree
"Given a set of shapes, returns an objects subtree with the parents
of the selected items up to the root. Useful to calculate a partial z-index"
[objects selected]
(let [selected+parents
(into selected
(mapcat #(get-parent-ids objects %))
selected)
remove-children
(fn [shape]
(update shape :shapes #(filterv selected+parents %)))]
(-> (select-keys objects selected+parents)
(d/update-vals remove-children))))

View file

@ -6,8 +6,10 @@
(ns app.common.spec.color
(:require
[app.common.spec :as us]
[clojure.spec.alpha :as s]))
[app.common.data :as d]
[app.common.spec :as us]
[app.common.text :as txt]
[clojure.spec.alpha :as s]))
;; TODO: waiting clojure 1.11 to rename this all :internal.stuff to a
;; more consistent name.
@ -46,7 +48,7 @@
:internal.gradient/width
:internal.gradient/stops]))
;;; --- COLORS
;; --- COLORS
(s/def :internal.color/name string?)
(s/def :internal.color/path (s/nilable string?))
@ -54,6 +56,15 @@
(s/def :internal.color/color (s/nilable string?))
(s/def :internal.color/opacity (s/nilable ::us/safe-number))
(s/def :internal.color/gradient (s/nilable ::gradient))
(s/def :internal.color/ref-id uuid?)
(s/def :internal.color/ref-file uuid?)
(s/def ::shape-color
(s/keys :req-un [:us/color
:internal.color/opacity]
:opt-un [:internal.color/gradient
:internal.color/ref-id
:internal.color/ref-file]))
(s/def ::color
(s/keys :opt-un [::id
@ -70,6 +81,197 @@
:internal.color/opacity
:internal.color/gradient]))
;; --- Helpers for color in different parts of a shape
;; fill
(defn fill->shape-color
[fill]
(d/without-nils {:color (:fill-color fill)
:opacity (:fill-opacity fill)
:gradient (:fill-color-gradient fill)
:ref-id (:fill-color-ref-id fill)
:ref-file (:fill-color-ref-file fill)}))
(defn set-fill-color
[shape position color opacity gradient]
(update-in shape [:fills position]
(fn [fill]
(d/without-nils (assoc fill
:fill-color color
:fill-opacity opacity
:fill-color-gradient gradient)))))
(defn detach-fill-color
[shape position]
(-> shape
(d/dissoc-in [:fills position :fill-color-ref-id])
(d/dissoc-in [:fills position :fill-color-ref-file])))
;; stroke
(defn stroke->shape-color
[stroke]
(d/without-nils {:color (:stroke-color stroke)
:opacity (:stroke-opacity stroke)
:gradient (:stroke-color-gradient stroke)
:ref-id (:stroke-color-ref-id stroke)
:ref-file (:stroke-color-ref-file stroke)}))
(defn set-stroke-color
[shape position color opacity gradient]
(update-in shape [:strokes position]
(fn [stroke]
(d/without-nils (assoc stroke
:stroke-color color
:stroke-opacity opacity
:stroke-color-gradient gradient)))))
(defn detach-stroke-color
[shape position]
(-> shape
(d/dissoc-in [:strokes position :stroke-color-ref-id])
(d/dissoc-in [:strokes position :stroke-color-ref-file])))
;; shadow
(defn shadow->shape-color
[shadow]
(d/without-nils {:color (-> shadow :color :color)
:opacity (-> shadow :color :opacity)
:gradient (-> shadow :color :gradient)
:ref-id (-> shadow :color :id)
:ref-file (-> shadow :color :file-id)}))
(defn set-shadow-color
[shape position color opacity gradient]
(update-in shape [:shadow position :color]
(fn [shadow-color]
(d/without-nils (assoc shadow-color
:color color
:opacity opacity
:gradient gradient)))))
(defn detach-shadow-color
[shape position]
(-> shape
(d/dissoc-in [:shadow position :color :id])
(d/dissoc-in [:shadow position :color :file-id])))
;; grid
(defn grid->shape-color
[grid]
(d/without-nils {:color (-> grid :params :color :color)
:opacity (-> grid :params :color :opacity)
:gradient (-> grid :params :color :gradient)
:ref-id (-> grid :params :color :id)
:ref-file (-> grid :params :color :file-id)}))
(defn set-grid-color
[shape position color opacity gradient]
(update-in shape [:grids position :params :color]
(fn [grid-color]
(d/without-nils (assoc grid-color
:color color
:opacity opacity
:gradient gradient)))))
(defn detach-grid-color
[shape position]
(-> shape
(d/dissoc-in [:grids position :params :color :id])
(d/dissoc-in [:grids position :params :color :file-id])))
;; --- Helpers for all colors in a shape
(defn get-text-node-colors
"Get all colors used by a node of a text shape"
[node]
(concat (map fill->shape-color (:fills node))
(map stroke->shape-color (:strokes node))))
(defn get-all-colors
"Get all colors used by a shape, in any section."
[shape]
(concat (map fill->shape-color (:fills shape))
(map stroke->shape-color (:strokes shape))
(map shadow->shape-color (:shadow shape))
(when (= (:type shape) :frame)
(map grid->shape-color (:grids shape)))
(when (= (:type shape) :text)
(reduce (fn [colors node]
(concat colors (get-text-node-colors node)))
()
(txt/node-seq (:content shape))))))
(defn uses-library-colors?
"Check if the shape uses any color in the given library."
[shape library-id]
(let [all-colors (get-all-colors shape)]
(some #(and (some? (:ref-id %))
(= (:ref-file %) library-id))
all-colors)))
(defn sync-shape-colors
"Look for usage of any color of the given library inside the shape,
and, in this case, copy the library color into the shape."
[shape library-id library-colors]
(let [sync-color (fn [shape position shape-color set-fn detach-fn]
(if (= (:ref-file shape-color) library-id)
(let [library-color (get library-colors (:ref-id shape-color))]
(if (some? library-color)
(set-fn shape
position
(:color library-color)
(:opacity library-color)
(:gradient library-color))
(detach-fn shape position)))
shape))
sync-fill (fn [shape [position fill]]
(sync-color shape
position
(fill->shape-color fill)
set-fill-color
detach-fill-color))
sync-stroke (fn [shape [position stroke]]
(sync-color shape
position
(stroke->shape-color stroke)
set-stroke-color
detach-stroke-color))
sync-shadow (fn [shape [position shadow]]
(sync-color shape
position
(shadow->shape-color shadow)
set-shadow-color
detach-shadow-color))
sync-grid (fn [shape [position grid]]
(sync-color shape
position
(grid->shape-color grid)
set-grid-color
detach-grid-color))
sync-text-node (fn [node]
(as-> node $
(reduce sync-fill $ (d/enumerate (:fills $)))
(reduce sync-stroke $ (d/enumerate (:strokes $)))))
sync-text (fn [shape]
(let [content (:content shape)
new-content (txt/transform-nodes sync-text-node content)]
(if (not= content new-content)
(assoc shape :content new-content)
shape)))]
(as-> shape $
(reduce sync-fill $ (d/enumerate (:fills $)))
(reduce sync-stroke $ (d/enumerate (:strokes $)))
(reduce sync-shadow $ (d/enumerate (:shadow $)))
(reduce sync-grid $ (d/enumerate (:grids $)))
(sync-text $))))

View file

@ -166,11 +166,11 @@
::blocked
::collapsed
::fills
::fill-color
::fill-opacity
::fill-color-gradient
::fill-color-ref-file
::fill-color-ref-id
::fill-color ;; TODO: remove these attributes
::fill-opacity ;; when backward compatibility
::fill-color-gradient ;; is no longer needed
::fill-color-ref-file ;;
::fill-color-ref-id ;;
::hide-fill-on-export
::font-family
::font-size
@ -196,10 +196,10 @@
::exports
::shapes
::strokes
::stroke-color
::stroke-color-ref-file
::stroke-color-ref-id
::stroke-opacity
::stroke-color ;; TODO: same thing
::stroke-color-ref-file ;;
::stroke-color-ref-i ;;
::stroke-opacity ;;
::stroke-style
::stroke-width
::stroke-alignment

View file

@ -17,15 +17,14 @@
;; --- Predicates
(defn ^boolean file?
(defn file?
[o]
(instance? js/File o))
(defn ^boolean blob?
(defn blob?
[o]
(instance? js/Blob o))
;; --- Specs
(s/def ::blob blob?)
@ -36,8 +35,7 @@
;; --- Utility functions
(defn validate-file
;; Check that a file obtained with the file javascript API is valid.
(defn validate-file ;; Check that a file obtained with the file javascript API is valid.
[file]
(when (> (.-size file) cm/max-file-size)
(ex/raise :type :validation
@ -74,4 +72,3 @@
:else
(tr "errors.unexpected-error"))]
(rx/of (dm/error msg))))

View file

@ -36,20 +36,19 @@
[app.main.data.workspace.layers :as dwly]
[app.main.data.workspace.layout :as layout]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.media :as dwm]
[app.main.data.workspace.notifications :as dwn]
[app.main.data.workspace.path :as dwdp]
[app.main.data.workspace.path.shapes-to-path :as dwps]
[app.main.data.workspace.persistence :as dwp]
[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.data.workspace.thumbnails :as dwth]
[app.main.data.workspace.transforms :as dwt]
[app.main.data.workspace.undo :as dwu]
[app.main.data.workspace.zoom :as dwz]
[app.main.repo :as rp]
[app.main.streams :as ms]
[app.main.worker :as uw]
[app.util.dom :as dom]
[app.util.globals :as ug]
[app.util.http :as http]
@ -163,7 +162,7 @@
(defn finalize-file
[_project-id file-id]
(ptk/reify ::finalize
(ptk/reify ::finalize-file
ptk/UpdateEvent
(update [_ state]
(dissoc state
@ -1193,28 +1192,14 @@
(defn copy-selected
[]
(letfn [;; Sort objects so they have the same relative ordering
;; when pasted later.
(sort-selected-async [state data]
(let [selected (wsh/lookup-selected state)
objects (wsh/lookup-page-objects state)
page-id (:current-page-id state)]
(->> (uw/ask! {:cmd :selection/query-z-index
:page-id page-id
:objects objects
:ids selected})
(rx/map (fn [z-indexes]
(assoc data :selected
(->> (d/zip selected z-indexes)
(sort-by second)
(map first)
(into (d/ordered-set)))))))))
;; We cannot call to a remote procedure in Safari (for the copy) so we need
;; to calculate it here instead of on the worker
(sort-selected-sync [state data]
(letfn [(sort-selected [state data]
(let [selected (wsh/lookup-selected state)
objects (wsh/lookup-page-objects state)
;; Narrow the objects map so it contains only relevant data for
;; selected and its parents
objects (cph/selected-subtree objects selected)
z-index (cp/calculate-z-index objects)
z-values (->> selected
(map #(vector %
@ -1289,18 +1274,13 @@
:file-id (:current-file-id state)
:selected selected
:objects {}
:images #{}}
:images #{}}]
sort-results
(fn [obs]
;; Safari doesn't allow asynchronous sorting on the copy
(if (cfg/check-browser? :safari)
(rx/map (partial sort-selected-sync state) obs)
(rx/mapcat (partial sort-selected-async state) obs)))]
(->> (rx/from (seq (vals pdata)))
(rx/merge-map (partial prepare-object objects selected))
(rx/reduce collect-data initial)
(sort-results)
(rx/map (partial sort-selected state))
(rx/map t/encode-str)
(rx/map wapi/write-to-clipboard)
(rx/catch on-copy-error)
@ -1606,6 +1586,7 @@
(dwc/add-shape shape)
(dwu/commit-undo-transaction))))))
;; TODO: why not implement it in terms of upload-media-workspace?
(defn- paste-svg
[text]
(us/assert string? text)
@ -1614,8 +1595,8 @@
(watch [_ state _]
(let [position (deref ms/mouse-position)
file-id (:current-file-id state)]
(->> (dwp/parse-svg ["svg" text])
(rx/map #(svg/svg-uploaded % file-id position)))))))
(->> (dwm/svg->clj ["svg" text])
(rx/map #(dwm/svg-uploaded % file-id position)))))))
(defn- paste-image
[image]
@ -1626,7 +1607,7 @@
params {:file-id file-id
:blobs [image]
:position @ms/mouse-position}]
(rx/of (dwp/upload-media-workspace params))))))
(rx/of (dwm/upload-media-workspace params))))))
(defn toggle-distances-display [value]
(ptk/reify ::toggle-distances-display
@ -1708,17 +1689,6 @@
(dm/export dwt/flip-vertical-selected)
(dm/export dwly/set-opacity)
;; Persistence
(dm/export dwp/set-file-shared)
(dm/export dwp/fetch-shared-files)
(dm/export dwp/link-file-to-library)
(dm/export dwp/unlink-file-from-library)
(dm/export dwp/upload-media-asset)
(dm/export dwp/upload-media-workspace)
(dm/export dwp/clone-media-object)
(dm/export dwc/image-uploaded)
;; Common
(dm/export dwc/add-shape)
(dm/export dwc/clear-edition-mode)

View file

@ -6,11 +6,13 @@
(ns app.main.data.workspace.changes
(:require
[app.common.data :as d]
[app.common.logging :as log]
[app.common.pages :as cp]
[app.common.pages.changes-builder :as pcb]
[app.common.spec :as us]
[app.common.spec.change :as spec.change]
[app.common.uuid :as uuid]
[app.main.data.workspace.state-helpers :as wsh]
[app.main.data.workspace.undo :as dwu]
[app.main.store :as st]
@ -59,14 +61,52 @@
(let [changes (cond-> changes reg-objects? (pcb/resize-parents ids))]
(rx/of (commit-changes changes)))))))))
(defn send-update-indices
[]
(ptk/reify ::send-update-indices
ptk/WatchEvent
(watch [_ _ _]
(->> (rx/of
(fn [state]
(-> state
(dissoc ::update-indices-debounce)
(dissoc ::update-changes))))
(rx/observe-on :async)))
ptk/EffectEvent
(effect [_ state _]
(doseq [[page-id changes] (::update-changes state)]
(uw/ask! {:cmd :update-page-indices
:page-id page-id
:changes changes})))))
;; Update indices will debounce operations so we don't have to update
;; the index several times (which is an expensive operation)
(defn update-indices
[page-id changes]
(ptk/reify ::update-indices
ptk/EffectEvent
(effect [_ _ _]
(uw/ask! {:cmd :update-page-indices
:page-id page-id
:changes changes}))))
(let [start (uuid/next)]
(ptk/reify ::update-indices
ptk/UpdateEvent
(update [_ state]
(if (nil? (::update-indices-debounce state))
(assoc state ::update-indices-debounce start)
(update-in state [::update-changes page-id] (fnil d/concat-vec []) changes)))
ptk/WatchEvent
(watch [_ state stream]
(if (= (::update-indices-debounce state) start)
(let [stopper (->> stream (rx/filter (ptk/type? :app.main.data.workspace/finalize)))]
(rx/merge
(->> stream
(rx/filter (ptk/type? ::update-indices))
(rx/debounce 50)
(rx/take 1)
(rx/map #(send-update-indices))
(rx/take-until stopper))
(rx/of (update-indices page-id changes))))
(rx/empty))))))
(defn commit-changes
[{:keys [redo-changes undo-changes

View file

@ -393,6 +393,11 @@
interactions)))
(vals objects))
;; If any of the deleted shapes is a frame with guides
guides (into {} (map (juxt :id identity) (->> (get-in page [:options :guides])
(vals)
(filter #(not (contains? ids (:frame-id %)))))))
starting-flows
(filter (fn [flow]
;; If any of the deleted is a frame that starts a flow,
@ -432,6 +437,7 @@
changes (-> (pcb/empty-changes it page-id)
(pcb/with-page page)
(pcb/with-objects objects)
(pcb/set-page-option :guides guides)
(pcb/remove-objects all-children)
(pcb/remove-objects ids)
(pcb/remove-objects empty-parents)
@ -482,22 +488,3 @@
(assoc :frame-id frame-id)
(cp/setup-rect-selrect))]
(rx/of (add-shape shape))))))
(defn image-uploaded
[image {:keys [x y]}]
(ptk/reify ::image-uploaded
ptk/WatchEvent
(watch [_ _ _]
(let [{:keys [name width height id mtype]} image
shape {:name name
:width width
:height height
:x (- x (/ width 2))
:y (- y (/ height 2))
:metadata {:width width
:height height
:mtype mtype
:id id}}]
(rx/of (create-and-add-shape :image x y shape))))))

View file

@ -18,6 +18,7 @@
[app.common.spec.file :as spec.file]
[app.common.spec.typography :as spec.typography]
[app.common.uuid :as uuid]
[app.main.data.events :as ev]
[app.main.data.messages :as dm]
[app.main.data.workspace.changes :as dch]
[app.main.data.workspace.common :as dwc]
@ -31,6 +32,7 @@
[app.util.router :as rt]
[app.util.time :as dt]
[beicon.core :as rx]
[cljs.spec.alpha :as s]
[potok.core :as ptk]))
;; Change this to :info :debug or :trace to debug this module, or :warn to reset to default
@ -327,7 +329,7 @@
(-> component
(assoc :path path)
(assoc :name name)
(update :objects
(update :objects
;; Give the same name to the root shape
#(assoc-in % [id :name] name)))))
@ -710,3 +712,70 @@
:callback do-dismiss}]
:sync-dialog))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Backend interactions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn set-file-shared
[id is-shared]
{:pre [(uuid? id) (boolean? is-shared)]}
(ptk/reify ::set-file-shared
IDeref
(-deref [_]
{::ev/origin "workspace" :id id :shared is-shared})
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-file :is-shared] is-shared))
ptk/WatchEvent
(watch [_ _ _]
(let [params {:id id :is-shared is-shared}]
(->> (rp/mutation :set-file-shared params)
(rx/ignore))))))
(defn- shared-files-fetched
[files]
(us/verify (s/every ::file) files)
(ptk/reify ::shared-files-fetched
ptk/UpdateEvent
(update [_ state]
(let [state (dissoc state :files)]
(assoc state :workspace-shared-files files)))))
(defn fetch-shared-files
[{:keys [team-id] :as params}]
(us/assert ::us/uuid team-id)
(ptk/reify ::fetch-shared-files
ptk/WatchEvent
(watch [_ _ _]
(->> (rp/query :team-shared-files {:team-id team-id})
(rx/map shared-files-fetched)))))
;; --- Link and unlink Files
(defn link-file-to-library
[file-id library-id]
(ptk/reify ::attach-library
ptk/WatchEvent
(watch [_ _ _]
(let [fetched #(assoc-in %2 [:workspace-libraries (:id %1)] %1)
params {:file-id file-id
:library-id library-id}]
(->> (rp/mutation :link-file-to-library params)
(rx/mapcat #(rp/query :file {:id library-id}))
(rx/map #(partial fetched %)))))))
(defn unlink-file-from-library
[file-id library-id]
(ptk/reify ::detach-library
ptk/UpdateEvent
(update [_ state]
(d/dissoc-in state [:workspace-libraries library-id]))
ptk/WatchEvent
(watch [_ _ _]
(let [params {:file-id file-id
:library-id library-id}]
(->> (rp/mutation :unlink-file-from-library params)
(rx/ignore))))))

View file

@ -14,6 +14,7 @@
[app.common.pages.changes-builder :as pcb]
[app.common.pages.helpers :as cph]
[app.common.spec :as us]
[app.common.spec.color :as color]
[app.common.text :as txt]
[app.main.data.workspace.common :as dwc]
[app.main.data.workspace.groups :as dwg]
@ -24,18 +25,10 @@
;; Change this to :info :debug or :trace to debug this module, or :warn to reset to default
(log/set-level! :warn)
(defonce color-sync-attrs
[[:fill-color-ref-id :fill-color-ref-file :color :fill-color]
[:fill-color-ref-id :fill-color-ref-file :gradient :fill-color-gradient]
[:fill-color-ref-id :fill-color-ref-file :opacity :fill-opacity]
[:stroke-color-ref-id :stroke-color-ref-file :color :stroke-color]
[:stroke-color-ref-id :stroke-color-ref-file :gradient :stroke-color-gradient]
[:stroke-color-ref-id :stroke-color-ref-file :opacity :stroke-opacity]])
(declare generate-sync-container)
(declare generate-sync-shape)
(declare has-asset-reference-fn)
(declare generate-sync-text-shape)
(declare uses-assets?)
(declare get-assets)
(declare generate-sync-shape-direct)
@ -60,7 +53,7 @@
"<local>"
(str "<" (get-in state [:workspace-libraries file-id :name]) ">")))
;; ---- Create a new component ----
;; ---- Components and instances creation ----
(defn make-component-shape
"Clone the shape and all children. Generate new ids and detach
@ -278,9 +271,8 @@
(log/debug :msg "Sync page in local file" :page-id (:id container))
(log/debug :msg "Sync component in local library" :component-id (:id container)))
(let [has-asset-reference? (has-asset-reference-fn asset-type library-id (cph/page? container))
linked-shapes (->> (vals (:objects container))
(filter has-asset-reference?))]
(let [linked-shapes (->> (vals (:objects container))
(filter #(uses-assets? asset-type % library-id (cph/page? container))))]
(loop [shapes (seq linked-shapes)
changes (-> (pcb/empty-changes it)
(pcb/with-container container)
@ -295,49 +287,34 @@
shape))
changes))))
(defn- has-asset-reference-fn
"Gets a function that checks if a shape uses some asset of the given type
in the given library."
[asset-type library-id page?]
(case asset-type
:components
(fn [shape] (and (:component-id shape)
(or (:component-root? shape) (not page?))
(= (:component-file shape) library-id)))
(defmulti uses-assets?
"Checks if a shape uses some asset of the given type in the given library."
(fn [asset-type _ _ _] asset-type))
:colors
(fn [shape]
(if (= (:type shape) :text)
(->> shape
:content
;; Check if any node in the content has a reference for the library
(txt/node-seq
#(or (and (some? (:stroke-color-ref-id %))
(= library-id (:stroke-color-ref-file %)))
(and (some? (:fill-color-ref-id %))
(= library-id (:fill-color-ref-file %))))))
(some
#(let [attr (name %)
attr-ref-id (keyword (str attr "-ref-id"))
attr-ref-file (keyword (str attr "-ref-file"))]
(and (get shape attr-ref-id)
(= library-id (get shape attr-ref-file))))
(map #(nth % 3) color-sync-attrs))))
(defmethod uses-assets? :components
[_ shape library-id page?]
(and (some? (:component-id shape))
(= (:component-file shape) library-id)
(or (:component-root? shape) (not page?)))) ; avoid nested components inside pages
:typographies
(fn [shape]
(and (= (:type shape) :text)
(->> shape
:content
;; Check if any node in the content has a reference for the library
(txt/node-seq
#(and (some? (:typography-ref-id %))
(= library-id (:typography-ref-file %)))))))))
(defmethod uses-assets? :colors
[_ shape library-id _]
(color/uses-library-colors? shape library-id))
(defmethod uses-assets? :typographies
[_ shape library-id _]
(and (= (:type shape) :text)
(->> shape
:content
;; Check if any node in the content has a reference for the library
(txt/node-seq
#(and (some? (:typography-ref-id %))
(= (:typography-ref-file %) library-id))))))
(defmulti generate-sync-shape
"Generate changes to synchronize one shape with all assets of the given type
"Generate changes to synchronize one shape from all assets of the given type
that is using, in the given library."
(fn [type _changes _library-id _state _container _shape] type))
(fn [asset-type _changes _library-id _state _container _shape] asset-type))
(defmethod generate-sync-shape :components
[_ changes _library-id state container shape]
@ -345,6 +322,37 @@
libraries (wsh/get-libraries state)]
(generate-sync-shape-direct changes libraries container shape-id false)))
(defmethod generate-sync-shape :colors
[_ changes library-id state _ shape]
(log/debug :msg "Sync colors of shape" :shape (:name shape))
;; Synchronize a shape that uses some colors of the library. The value of the
;; color in the library is copied to the shape.
(let [library-colors (get-assets library-id :colors state)]
(pcb/update-shapes changes
[(:id shape)]
#(color/sync-shape-colors % library-id library-colors))))
(defmethod generate-sync-shape :typographies
[_ changes library-id state container shape]
(log/debug :msg "Sync typographies of shape" :shape (:name shape))
;; Synchronize a shape that uses some typographies of the library. The attributes
;; of the typography are copied to the shape."
(let [typographies (get-assets library-id :typographies state)
update-node (fn [node]
(if-let [typography (get typographies (:typography-ref-id node))]
(merge node (dissoc typography :name :id))
(dissoc node :typography-ref-id
:typography-ref-file)))]
(generate-sync-text-shape changes shape container update-node)))
(defn- get-assets
[library-id asset-type state]
(if (= library-id (:current-file-id state))
(get-in state [:workspace-data asset-type])
(get-in state [:workspace-libraries library-id :data asset-type])))
(defn- generate-sync-text-shape
[changes shape container update-node]
(let [old-content (:content shape)
@ -368,99 +376,6 @@
changes
changes')))
(defmethod generate-sync-shape :colors
[_ changes library-id state container shape]
(log/debug :msg "Sync colors of shape" :shape (:name shape))
;; Synchronize a shape that uses some colors of the library. The value of the
;; color in the library is copied to the shape.
(let [colors (get-assets library-id :colors state)]
(if (= :text (:type shape))
(let [update-node (fn [node]
(if-let [color (get colors (:fill-color-ref-id node))]
(assoc node
:fill-color (:color color)
:fill-opacity (:opacity color)
:fill-color-gradient (:gradient color))
(assoc node
:fill-color-ref-id nil
:fill-color-ref-file nil)))]
(generate-sync-text-shape changes shape container update-node))
(loop [attrs (seq color-sync-attrs)
roperations []
uoperations []]
(let [[attr-ref-id attr-ref-file color-attr attr] (first attrs)]
(if (nil? attr)
(if (empty? roperations)
changes
(-> changes
(update :redo-changes (make-change
container
{:type :mod-obj
:id (:id shape)
:operations roperations}))
(update :undo-changes (make-change
container
{:type :mod-obj
:id (:id shape)
:operations uoperations}))))
(if-not (contains? shape attr-ref-id)
(recur (next attrs)
roperations
uoperations)
(let [color (get colors (get shape attr-ref-id))
roperations' (if color
[{:type :set
:attr attr
:val (color-attr color)
:ignore-touched true}]
;; If the referenced color does no longer exist in the library,
;; we must unlink the color in the shape
[{:type :set
:attr attr-ref-id
:val nil
:ignore-touched true}
{:type :set
:attr attr-ref-file
:val nil
:ignore-touched true}])
uoperations' (if color
[{:type :set
:attr attr
:val (get shape attr)
:ignore-touched true}]
[{:type :set
:attr attr-ref-id
:val (get shape attr-ref-id)
:ignore-touched true}
{:type :set
:attr attr-ref-file
:val (get shape attr-ref-file)
:ignore-touched true}])]
(recur (next attrs)
(into roperations roperations')
(into uoperations uoperations'))))))))))
(defmethod generate-sync-shape :typographies
[_ changes library-id state container shape]
(log/debug :msg "Sync typographies of shape" :shape (:name shape))
;; Synchronize a shape that uses some typographies of the library. The attributes
;; of the typography are copied to the shape."
(let [typographies (get-assets library-id :typographies state)
update-node (fn [node]
(if-let [typography (get typographies (:typography-ref-id node))]
(merge node (dissoc typography :name :id))
(dissoc node :typography-ref-id
:typography-ref-file)))]
(generate-sync-text-shape changes shape container update-node)))
(defn- get-assets
[library-id asset-type state]
(if (= library-id (:current-file-id state))
(get-in state [:workspace-data asset-type])
(get-in state [:workspace-libraries library-id :data asset-type])))
;; ---- Component synchronization helpers ----

View file

@ -0,0 +1,286 @@
;; 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.data.workspace.media
(:require
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.main.data.media :as dmm]
[app.main.data.messages :as dm]
[app.main.data.workspace.common :as dwc]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.svg-upload :as svg]
[app.main.repo :as rp]
[app.main.store :as st]
[app.util.http :as http]
[app.util.i18n :refer [tr]]
[app.util.svg :as usvg]
[app.util.webapi :as wapi]
[beicon.core :as rx]
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[potok.core :as ptk]
[promesa.core :as p]
[tubax.core :as tubax]))
(defn svg->clj
[[name text]]
(try
(->> (rx/of (-> (tubax/xml->clj text)
(assoc :name name))))
(catch :default _err
(rx/throw {:type :svg-parser}))))
(defn extract-name [url]
(let [query-idx (str/last-index-of url "?")
url (if (> query-idx 0) (subs url 0 query-idx) url)
filename (->> (str/split url "/") (last))
ext-idx (str/last-index-of filename ".")]
(if (> ext-idx 0) (subs filename 0 ext-idx) filename)))
(defn data-uri->blob
[data-uri]
(let [[mtype b64-data] (str/split data-uri ";base64,")
mtype (subs mtype (inc (str/index-of mtype ":")))
decoded (.atob js/window b64-data)
size (.-length ^js decoded)
content (js/Uint8Array. size)]
(doseq [i (range 0 size)]
(aset content i (.charCodeAt decoded i)))
(wapi/create-blob content mtype)))
;; TODO: rename to bitmap-image-uploaded
(defn image-uploaded
[image {:keys [x y]}]
(ptk/reify ::image-uploaded
ptk/WatchEvent
(watch [_ _ _]
(let [{:keys [name width height id mtype]} image
shape {:name name
:width width
:height height
:x (- x (/ width 2))
:y (- y (/ height 2))
:metadata {:width width
:height height
:mtype mtype
:id id}}]
(rx/of (dwc/create-and-add-shape :image x y shape))))))
(defn svg-uploaded
[svg-data file-id position]
(ptk/reify ::svg-uploaded
ptk/WatchEvent
(watch [_ _ _]
;; Once the SVG is uploaded, we need to extract all the bitmap
;; images and upload them separately, then proceed to create
;; all shapes.
(->> (rx/from (usvg/collect-images svg-data))
(rx/map (fn [uri]
(merge
{:file-id file-id
:is-local true}
(if (str/starts-with? uri "data:")
{:name "image"
:content (data-uri->blob uri)}
{:name (extract-name uri)
:url uri}))))
(rx/mapcat (fn [uri-data]
(->> (rp/mutation! (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))
(rx/map #(vector (:url uri-data) %)))))
(rx/reduce (fn [acc [url image]] (assoc acc url image)) {})
(rx/map #(svg/create-svg-shapes (assoc svg-data :image-data %) position))))))
(defn- process-uris
[{:keys [file-id local? name uris mtype on-image on-svg]}]
(letfn [(svg-url? [url]
(or (and mtype (= mtype "image/svg+xml"))
(str/ends-with? url ".svg")))
(prepare [uri]
{:file-id file-id
:is-local local?
:name (or name (extract-name uri))
:url uri})
(fetch-svg [name uri]
(->> (http/send! {:method :get :uri uri :mode :no-cors})
(rx/map #(vector
(or name (extract-name uri))
(:body %)))))]
(rx/merge
(->> (rx/from uris)
(rx/filter (comp not svg-url?))
(rx/map prepare)
(rx/mapcat #(rp/mutation! :create-file-media-object-from-url %))
(rx/do on-image))
(->> (rx/from uris)
(rx/filter svg-url?)
(rx/merge-map (partial fetch-svg name))
(rx/merge-map svg->clj)
(rx/do on-svg)))))
(defn- process-blobs
[{:keys [file-id local? name blobs force-media on-image on-svg]}]
(letfn [(svg-blob? [blob]
(and (not force-media)
(= (.-type blob) "image/svg+xml")))
(prepare-blob [blob]
(let [name (or name (if (dmm/file? blob) (.-name blob) "blob"))]
{:file-id file-id
:name name
:is-local local?
:content blob}))
(extract-content [blob]
(let [name (or name (.-name blob))]
(-> (.text ^js blob)
(p/then #(vector name %)))))]
(rx/merge
(->> (rx/from blobs)
(rx/map dmm/validate-file)
(rx/filter (comp not svg-blob?))
(rx/map prepare-blob)
(rx/mapcat #(rp/mutation! :upload-file-media-object %))
(rx/do on-image))
(->> (rx/from blobs)
(rx/map dmm/validate-file)
(rx/filter svg-blob?)
(rx/merge-map extract-content)
(rx/merge-map svg->clj)
(rx/do on-svg)))))
(s/def ::local? ::us/boolean)
(s/def ::blobs ::dmm/blobs)
(s/def ::name ::us/string)
(s/def ::uris (s/coll-of ::us/string))
(s/def ::mtype ::us/string)
(s/def ::process-media-objects
(s/and
(s/keys :req-un [::file-id ::local?]
:opt-un [::name ::data ::uris ::mtype])
(fn [props]
(or (contains? props :blobs)
(contains? props :uris)))))
(defn- process-media-objects
[{:keys [uris on-error] :as params}]
(us/assert ::process-media-objects params)
(letfn [(handle-error [error]
(if (ex/ex-info? error)
(handle-error (ex-data error))
(cond
(= (:code error) :invalid-svg-file)
(rx/of (dm/error (tr "errors.media-type-not-allowed")))
(= (:code error) :media-type-not-allowed)
(rx/of (dm/error (tr "errors.media-type-not-allowed")))
(= (:code error) :unable-to-access-to-url)
(rx/of (dm/error (tr "errors.media-type-not-allowed")))
(= (:code error) :invalid-image)
(rx/of (dm/error (tr "errors.media-type-not-allowed")))
(= (:code error) :media-too-large)
(rx/of (dm/error (tr "errors.media-too-large")))
(= (:code error) :media-type-mismatch)
(rx/of (dm/error (tr "errors.media-type-mismatch")))
(= (:code error) :unable-to-optimize)
(rx/of (dm/error (:hint error)))
(fn? on-error)
(on-error error)
:else
(rx/throw error))))]
(ptk/reify ::process-media-objects
ptk/WatchEvent
(watch [_ _ _]
(rx/concat
(rx/of (dm/show {:content (tr "media.loading")
:type :info
:timeout nil
:tag :media-loading}))
(->> (if (seq uris)
;; Media objects is a list of URL's pointing to the path
(process-uris params)
;; Media objects are blob of data to be upload
(process-blobs params))
;; Every stream has its own sideeffect. We need to ignore the result
(rx/ignore)
(rx/catch handle-error)
(rx/finalize #(st/emit! (dm/hide-tag :media-loading)))))))))
(defn upload-media-asset
[params]
(let [params (assoc params
:force-media true
:local? false
:on-image #(st/emit! (dwl/add-media %)))]
(process-media-objects params)))
;; TODO: it is really need handle SVG here, looks like it already
;; handled separatelly
(defn upload-media-workspace
[{:keys [position file-id] :as params}]
(let [params (assoc params
:local? true
:on-image #(st/emit! (image-uploaded % position))
:on-svg #(st/emit! (svg-uploaded % file-id position)))]
(process-media-objects params)))
;; --- Upload File Media objects
(s/def ::object-id ::us/uuid)
(s/def ::clone-media-objects-params
(s/keys :req-un [::file-id ::object-id]))
(defn clone-media-object
[{:keys [file-id object-id] :as params}]
(us/assert ::clone-media-objects-params params)
(ptk/reify ::clone-media-objects
ptk/WatchEvent
(watch [_ _ _]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error identity}} (meta params)
params {:is-local true
:file-id file-id
:id object-id}]
(rx/concat
(rx/of (dm/show {:content (tr "media.loading")
:type :info
:timeout nil
:tag :media-loading}))
(->> (rp/mutation! :clone-file-media-object params)
(rx/do on-success)
(rx/catch on-error)
(rx/finalize #(st/emit! (dm/hide-tag :media-loading)))))))))

View file

@ -6,37 +6,28 @@
(ns app.main.data.workspace.persistence
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.logging :as log]
[app.common.pages :as cp]
[app.common.spec :as us]
[app.common.spec.change :as spec.change]
[app.common.spec.file :as spec.file]
[app.common.uuid :as uuid]
[app.config :as cfg]
[app.config :as cf]
[app.main.data.dashboard :as dd]
[app.main.data.events :as ev]
[app.main.data.fonts :as df]
[app.main.data.media :as di]
[app.main.data.messages :as dm]
[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.repo :as rp]
[app.main.store :as st]
[app.util.http :as http]
[app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt]
[app.util.time :as dt]
[app.util.uri :as uu]
[beicon.core :as rx]
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[potok.core :as ptk]
[promesa.core :as p]
[tubax.core :as tubax]))
[okulary.core :as l]
[potok.core :as ptk]))
(log/set-level! :info)
(declare persist-changes)
(declare persist-synchronous-changes)
@ -48,18 +39,17 @@
(defn initialize-file-persistence
[file-id]
(ptk/reify ::initialize-persistence
ptk/EffectEvent
(effect [_ _ stream]
ptk/WatchEvent
(watch [_ _ stream]
(log/debug :hint "initialize persistence")
(let [stoper (rx/filter #(= ::finalize %) stream)
forcer (rx/filter #(= ::force-persist %) stream)
notifier (->> stream
(rx/filter dch/commit-changes?)
(rx/debounce 2000)
(rx/merge stoper forcer))
commits (l/atom [])
local-file?
#(as-> (:file-id %) event-file-id
(or (nil? event-file-id)
(= event-file-id file-id)))
library-file?
#(as-> (:file-id %) event-file-id
(and (some? event-file-id)
@ -80,93 +70,89 @@
;; Disable reload stoper
(swap! st/ongoing-tasks disj :workspace-change)
(st/emit! (update-persistence-status {:status :saved})))]
(->> (rx/merge
(->> stream
(rx/filter dch/commit-changes?)
(rx/map deref)
(rx/filter local-file?)
(rx/tap on-dirty)
(rx/buffer-until notifier)
(rx/filter (complement empty?))
(rx/map (fn [buf]
(->> (into [] (comp (map #(assoc % :id (uuid/next)))
(map #(assoc % :file-id file-id)))
buf)
(persist-changes file-id))))
(rx/tap on-saving)
(rx/take-until (rx/delay 100 stoper)))
(->> stream
(rx/filter dch/commit-changes?)
(rx/map deref)
(rx/filter library-file?)
(rx/filter (complement #(empty? (:changes %))))
(rx/map persist-synchronous-changes)
(rx/take-until (rx/delay 100 stoper)))
(->> stream
(rx/filter (ptk/type? ::changes-persisted))
(rx/tap on-saved)
(rx/ignore)
(rx/take-until stoper)))
(rx/subs #(st/emit! %)
(constantly nil)
(fn []
(on-saved))))))))
(rx/merge
(->> stream
(rx/filter dch/commit-changes?)
(rx/map deref)
(rx/filter local-file?)
(rx/tap on-dirty)
(rx/filter (complement empty?))
(rx/map (fn [commit]
(-> commit
(assoc :id (uuid/next))
(assoc :file-id file-id))))
(rx/observe-on :async)
(rx/tap #(swap! commits conj %))
(rx/take-until (rx/delay 100 stoper))
(rx/finalize (fn []
(log/debug :hint "finalize persistence: changes watcher"))))
(->> (rx/from-atom commits)
(rx/filter (complement empty?))
(rx/sample-when (rx/merge
(rx/interval 5000)
(rx/filter #(= ::force-persist %) stream)
(->> (rx/from-atom commits)
(rx/filter (complement empty?))
(rx/debounce 2000))))
(rx/tap #(reset! commits []))
(rx/tap on-saving)
(rx/mapcat (fn [changes]
;; NOTE: this is needed for don't start the
;; next persistence before this one is
;; finished.
(rx/merge
(rx/of (persist-changes file-id changes))
(->> stream
(rx/filter (ptk/type? ::changes-persisted))
(rx/take 1)
(rx/tap on-saved)
(rx/ignore)))))
(rx/take-until (rx/delay 100 stoper))
(rx/finalize (fn []
(log/debug :hint "finalize persistence: save loop"))))
;; Synchronous changes
(->> stream
(rx/filter dch/commit-changes?)
(rx/map deref)
(rx/filter library-file?)
(rx/filter (complement #(empty? (:changes %))))
(rx/map persist-synchronous-changes)
(rx/take-until (rx/delay 100 stoper))
(rx/finalize (fn []
(log/debug :hint "finalize persistence: synchronous save loop"))))
)))))
(defn persist-changes
[file-id changes]
(log/debug :hint "persist changes" :changes (count changes))
(us/verify ::us/uuid file-id)
(ptk/reify ::persist-changes
ptk/UpdateEvent
(update [_ state]
(let [into* (fnil into [])]
(update-in state [:workspace-persistence :queue] into* changes)))
ptk/WatchEvent
(watch [_ state _]
(let [sid (:session-id state)
file (get state :workspace-file)
queue (get-in state [:workspace-persistence :queue] [])
params {:id (:id file)
:revn (:revn file)
:session-id sid
:changes-with-metadata (into [] queue)}
ids (into #{} (map :id) queue)
update-persistence-queue
(fn [state]
(update-in state [:workspace-persistence :queue]
(fn [items] (into [] (remove #(ids (:id %))) items))))
handle-response
(fn [lagged]
(let [lagged (cond->> lagged
(= #{sid} (into #{} (map :session-id) lagged))
(map #(assoc % :changes [])))]
(rx/concat
(rx/of update-persistence-queue)
(->> (rx/of lagged)
(rx/mapcat seq)
(rx/map #(shapes-changes-persisted file-id %))))))
on-error
(fn [{:keys [type] :as error}]
(if (or (= :bad-gateway type)
(= :service-unavailable type))
(rx/of (update-persistence-status {:status :error :reason type}))
(rx/concat
(rx/of update-persistence-queue)
(rx/of (update-persistence-status {:status :error :reason type}))
(rx/of (dws/deselect-all))
(->> (rx/of nil)
(rx/delay 200)
(rx/mapcat #(rx/throw error))))))]
:changes-with-metadata (into [] changes)}]
(when (= file-id (:id params))
(->> (rp/mutation :update-file params)
(rx/mapcat handle-response)
(rx/catch on-error)))))))
(rx/mapcat (fn [lagged]
(log/debug :hint "changes persisted" :lagged (count lagged))
(let [lagged (cond->> lagged
(= #{sid} (into #{} (map :session-id) lagged))
(map #(assoc % :changes [])))]
(->> (rx/of lagged)
(rx/mapcat seq)
(rx/map #(shapes-changes-persisted file-id %))))))
(rx/catch (fn [cause]
(rx/concat
(rx/of (rt/assign-exception cause))
(rx/throw cause))))))))))
(defn persist-synchronous-changes
[{:keys [file-id changes]}]
@ -274,271 +260,6 @@
(rx/of (ptk/data-event ::bundle-fetched bundle)
(df/load-team-fonts (:team-id project)))))))))
;; --- Set File shared
(defn set-file-shared
[id is-shared]
{:pre [(uuid? id) (boolean? is-shared)]}
(ptk/reify ::set-file-shared
IDeref
(-deref [_]
{::ev/origin "workspace" :id id :shared is-shared})
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-file :is-shared] is-shared))
ptk/WatchEvent
(watch [_ _ _]
(let [params {:id id :is-shared is-shared}]
(->> (rp/mutation :set-file-shared params)
(rx/ignore))))))
;; --- Fetch Shared Files
(declare shared-files-fetched)
(defn fetch-shared-files
[{:keys [team-id] :as params}]
(us/assert ::us/uuid team-id)
(ptk/reify ::fetch-shared-files
ptk/WatchEvent
(watch [_ _ _]
(->> (rp/query :team-shared-files {:team-id team-id})
(rx/map shared-files-fetched)))))
(defn shared-files-fetched
[files]
(us/verify (s/every ::file) files)
(ptk/reify ::shared-files-fetched
ptk/UpdateEvent
(update [_ state]
(let [state (dissoc state :files)]
(assoc state :workspace-shared-files files)))))
;; --- Link and unlink Files
(defn link-file-to-library
[file-id library-id]
(ptk/reify ::attach-library
ptk/WatchEvent
(watch [_ _ _]
(let [fetched #(assoc-in %2 [:workspace-libraries (:id %1)] %1)
params {:file-id file-id
:library-id library-id}]
(->> (rp/mutation :link-file-to-library params)
(rx/mapcat #(rp/query :file {:id library-id}))
(rx/map #(partial fetched %)))))))
(defn unlink-file-from-library
[file-id library-id]
(ptk/reify ::detach-library
ptk/UpdateEvent
(update [_ state]
(d/dissoc-in state [:workspace-libraries library-id]))
ptk/WatchEvent
(watch [_ _ _]
(let [params {:file-id file-id
:library-id library-id}]
(->> (rp/mutation :unlink-file-from-library params)
(rx/ignore))))))
;; --- Upload File Media objects
(defn parse-svg
[[name text]]
(try
(->> (rx/of (-> (tubax/xml->clj text)
(assoc :name name))))
(catch :default _err
(rx/throw {:type :svg-parser}))))
(defn fetch-svg [name uri]
(->> (http/send! {:method :get :uri uri :mode :no-cors})
(rx/map #(vector
(or name (uu/uri-name uri))
(:body %)))))
(defn- handle-upload-error
"Generic error handler for all upload methods."
[on-error stream]
(letfn [(on-error* [error]
(if (ex/ex-info? error)
(on-error* (ex-data error))
(cond
(= (:code error) :invalid-svg-file)
(rx/of (dm/error (tr "errors.media-type-not-allowed")))
(= (:code error) :media-type-not-allowed)
(rx/of (dm/error (tr "errors.media-type-not-allowed")))
(= (:code error) :unable-to-access-to-url)
(rx/of (dm/error (tr "errors.media-type-not-allowed")))
(= (:code error) :invalid-image)
(rx/of (dm/error (tr "errors.media-type-not-allowed")))
(= (:code error) :media-too-large)
(rx/of (dm/error (tr "errors.media-too-large")))
(= (:code error) :media-type-mismatch)
(rx/of (dm/error (tr "errors.media-type-mismatch")))
(= (:code error) :unable-to-optimize)
(rx/of (dm/error (:hint error)))
(fn? on-error)
(on-error error)
:else
(rx/throw error))))]
(rx/catch on-error* stream)))
(defn- process-uris
[{:keys [file-id local? name uris mtype on-image on-svg]}]
(letfn [(svg-url? [url]
(or (and mtype (= mtype "image/svg+xml"))
(str/ends-with? url ".svg")))
(prepare [uri]
{:file-id file-id
:is-local local?
:name (or name (uu/uri-name uri))
:url uri})]
(rx/merge
(->> (rx/from uris)
(rx/filter (comp not svg-url?))
(rx/map prepare)
(rx/mapcat #(rp/mutation! :create-file-media-object-from-url %))
(rx/do on-image))
(->> (rx/from uris)
(rx/filter svg-url?)
(rx/merge-map (partial fetch-svg name))
(rx/merge-map parse-svg)
(rx/do on-svg)))))
(defn- process-blobs
[{:keys [file-id local? name blobs force-media on-image on-svg]}]
(letfn [(svg-blob? [blob]
(and (not force-media)
(= (.-type blob) "image/svg+xml")))
(prepare-blob [blob]
(let [name (or name (if (di/file? blob) (.-name blob) "blob"))]
{:file-id file-id
:name name
:is-local local?
:content blob}))
(extract-content [blob]
(let [name (or name (.-name blob))]
(-> (.text ^js blob)
(p/then #(vector name %)))))]
(rx/merge
(->> (rx/from blobs)
(rx/map di/validate-file)
(rx/filter (comp not svg-blob?))
(rx/map prepare-blob)
(rx/mapcat #(rp/mutation! :upload-file-media-object %))
(rx/do on-image))
(->> (rx/from blobs)
(rx/map di/validate-file)
(rx/filter svg-blob?)
(rx/merge-map extract-content)
(rx/merge-map parse-svg)
(rx/do on-svg)))))
(s/def ::local? ::us/boolean)
(s/def ::blobs ::di/blobs)
(s/def ::name ::us/string)
(s/def ::uris (s/coll-of ::us/string))
(s/def ::mtype ::us/string)
(s/def ::process-media-objects
(s/and
(s/keys :req-un [::file-id ::local?]
:opt-un [::name ::data ::uris ::mtype])
(fn [props]
(or (contains? props :blobs)
(contains? props :uris)))))
(defn- process-media-objects
[{:keys [uris on-error] :as params}]
(us/assert ::process-media-objects params)
(ptk/reify ::process-media-objects
ptk/WatchEvent
(watch [_ _ _]
(rx/concat
(rx/of (dm/show {:content (tr "media.loading")
:type :info
:timeout nil
:tag :media-loading}))
(->> (if (seq uris)
;; Media objects is a list of URL's pointing to the path
(process-uris params)
;; Media objects are blob of data to be upload
(process-blobs params))
;; Every stream has its own sideeffect. We need to ignore the result
(rx/ignore)
(handle-upload-error on-error)
(rx/finalize (st/emitf (dm/hide-tag :media-loading))))))))
(defn upload-media-asset
[params]
(let [params (assoc params
:force-media true
:local? false
:on-image #(st/emit! (dwl/add-media %)))]
(process-media-objects params)))
(defn upload-media-workspace
[{:keys [position file-id] :as params}]
(let [params (assoc params
:local? true
:on-image #(st/emit! (dwc/image-uploaded % position))
:on-svg #(st/emit! (svg/svg-uploaded % file-id position)))]
(process-media-objects params)))
;; --- Upload File Media objects
(s/def ::object-id ::us/uuid)
(s/def ::clone-media-objects-params
(s/keys :req-un [::file-id ::object-id]))
(defn clone-media-object
[{:keys [file-id object-id] :as params}]
(us/assert ::clone-media-objects-params params)
(ptk/reify ::clone-media-objects
ptk/WatchEvent
(watch [_ _ _]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error identity}} (meta params)
params {:is-local true
:file-id file-id
:id object-id}]
(rx/concat
(rx/of (dm/show {:content (tr "media.loading")
:type :info
:timeout nil
:tag :media-loading}))
(->> (rp/mutation! :clone-file-media-object params)
(rx/do on-success)
(rx/catch on-error)
(rx/finalize #(st/emit! (dm/hide-tag :media-loading)))))))))
;; --- Helpers
@ -549,7 +270,6 @@
(update-in [:workspace-file :pages] #(filterv (partial not= id) %))
(update :workspace-pages dissoc id)))
(defn preload-data-uris
"Preloads the image data so it's ready when necesary"
[]
@ -560,10 +280,10 @@
(fn [{:keys [metadata fill-image]}]
(cond
(some? metadata)
[(cfg/resolve-file-media metadata)]
[(cf/resolve-file-media metadata)]
(some? fill-image)
[(cfg/resolve-file-media fill-image)]))
[(cf/resolve-file-media fill-image)]))
uris (into #{}
(comp (mapcat extract-urls)

View file

@ -274,6 +274,7 @@
(declare prepare-duplicate-frame-change)
(declare prepare-duplicate-shape-change)
(declare prepare-duplicate-flows)
(declare prepare-duplicate-guides)
(defn prepare-duplicate-changes
"Prepare objects to duplicate: generate new id, give them unique names,
@ -302,7 +303,9 @@
delta)
init-changes))]
(prepare-duplicate-flows changes shapes page ids-map)))
(-> changes
(prepare-duplicate-flows shapes page ids-map)
(prepare-duplicate-guides shapes page ids-map delta))))
(defn- prepare-duplicate-change
[changes objects page unames update-unames! ids-map shape delta]
@ -362,20 +365,20 @@
changes (-> (pcb/add-object changes new-obj {:ignore-touched true})
(pcb/amend-last-change #(assoc % :old-id (:id obj))))]
(reduce (fn [changes child]
(prepare-duplicate-shape-change changes
objects
page
unames
update-unames!
ids-map
child
delta
frame-id
new-id))
changes
(map (d/getf objects) (:shapes obj))))
changes))
(reduce (fn [changes child]
(prepare-duplicate-shape-change changes
objects
page
unames
update-unames!
ids-map
child
delta
frame-id
new-id))
changes
(map (d/getf objects) (:shapes obj))))
changes))
(defn- prepare-duplicate-flows
[changes shapes page ids-map]
@ -399,6 +402,32 @@
(pcb/update-page-option changes :flows update-flows))
changes)))
(defn- prepare-duplicate-guides
[changes shapes page ids-map delta]
(let [guides (get-in page [:options :guides])
frames (->> shapes
(filter #(= (:type %) :frame)))
new-guides (reduce
(fn [g frame]
(let [new-id (ids-map (:id frame))
new-frame (-> frame
(geom/move delta))
new-guides (->> guides
(vals)
(filter #(= (:frame-id %) (:id frame)))
(map #(-> %
(assoc :id (uuid/next))
(assoc :frame-id new-id)
(assoc :position (if (= (:axis %) :x)
(+ (:position %) (- (:x new-frame) (:x frame)))
(+ (:position %) (- (:y new-frame) (:y frame))))))))]
(conj g
(into {} (map (juxt :id identity) new-guides)))))
guides
frames)]
(-> (pcb/with-page changes page)
(pcb/set-page-option :guides new-guides))))
(defn duplicate-changes-update-indices
"Updates the changes to correctly set the indexes of the duplicated objects,
depending on the index of the original object respect their parent."

View file

@ -19,11 +19,9 @@
[app.main.data.workspace.changes :as dch]
[app.main.data.workspace.common :as dwc]
[app.main.data.workspace.state-helpers :as wsh]
[app.main.repo :as rp]
[app.util.color :as uc]
[app.util.path.parser :as upp]
[app.util.svg :as usvg]
[app.util.uri :as uu]
[beicon.core :as rx]
[cuerdas.core :as str]
[potok.core :as ptk]))
@ -429,37 +427,6 @@
[unames changes])))
(declare create-svg-shapes)
(defn svg-uploaded
[svg-data file-id position]
(ptk/reify ::svg-uploaded
ptk/WatchEvent
(watch [_ _ _]
;; Once the SVG is uploaded, we need to extract all the bitmap
;; images and upload them separately, then proceed to create
;; all shapes.
(->> (rx/from (usvg/collect-images svg-data))
(rx/map (fn [uri]
(merge
{:file-id file-id
:is-local true}
(if (str/starts-with? uri "data:")
{:name "image"
:content (uu/data-uri->blob uri)}
{:name (uu/uri-name uri)
:url uri}))))
(rx/mapcat (fn [uri-data]
(->> (rp/mutation! (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))
(rx/map #(vector (:url uri-data) %)))))
(rx/reduce (fn [acc [url image]] (assoc acc url image)) {})
(rx/map #(create-svg-shapes (assoc svg-data :image-data %) position))))))
(defn create-svg-shapes
[svg-data {:keys [x y] :as position}]
(ptk/reify ::create-svg-shapes

View file

@ -13,6 +13,7 @@
[app.common.math :as mth]
[app.common.pages.helpers :as cph]
[app.common.text :as txt]
[app.common.uuid :as uuid]
[app.main.data.workspace.changes :as dch]
[app.main.data.workspace.common :as dwc]
[app.main.data.workspace.selection :as dws]
@ -379,3 +380,44 @@
ptk/UpdateEvent
(update [_ state]
(d/dissoc-in state [:workspace-text-modifier id]))))
(defn commit-position-data
[]
(ptk/reify ::commit-position-data
ptk/WatchEvent
(watch [_ state _]
(let [position-data (::update-position-data state)]
(rx/concat
(rx/of (dch/update-shapes
(keys position-data)
(fn [shape]
(-> shape
(assoc :position-data (get position-data (:id shape)))))
{:save-undo? false :reg-objects? false}))
(rx/of (fn [state]
(dissoc state ::update-position-data-debounce ::update-position-data))))))))
(defn update-position-data
[id position-data]
(let [start (uuid/next)]
(ptk/reify ::update-position-data
ptk/UpdateEvent
(update [_ state]
(if (nil? (::update-position-data-debounce state))
(assoc state ::update-position-data-debounce start)
(assoc-in state [::update-position-data id] position-data)))
ptk/WatchEvent
(watch [_ state stream]
(if (= (::update-position-data-debounce state) start)
(let [stopper (->> stream (rx/filter (ptk/type? :app.main.data.workspace/finalize)))]
(rx/merge
(->> stream
(rx/filter (ptk/type? ::update-position-data))
(rx/debounce 50)
(rx/take 1)
(rx/map #(commit-position-data))
(rx/take-until stopper))
(rx/of (update-position-data id position-data))))
(rx/empty))))))

View file

@ -402,7 +402,7 @@
:style {:-webkit-print-color-adjust :exact}
:fill "none"}
(let [fonts (ff/frame->fonts object-id objects)]
(let [fonts (ff/frame->fonts object objects)]
[:& ff/fontfaces-style {:fonts fonts}])
(case (:type object)

View file

@ -17,6 +17,7 @@
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd]
[app.util.webapi :as wapi]
[beicon.core :as rx]
[potok.core :as ptk]
[rumext.alpha :as mf]))
@ -35,7 +36,7 @@
(mapv
(fn [file]
{:name (.-name file)
:uri (dom/create-uri file)})))]
:uri (wapi/create-uri file)})))]
(st/emit! (modal/show
{:type :import
:project-id project-id
@ -310,7 +311,7 @@
(fn []
;; dispose uris when the component is umount
#(doseq [file files]
(dom/revoke-uri (:uri file)))))
(wapi/revoke-uri (:uri file)))))
[:div.modal-overlay
[:div.modal-container.import-dialog

View file

@ -12,9 +12,9 @@
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[app.util.http :as http]
[app.util.i18n :as i18n :refer [tr]]
[app.util.webapi :as wapi]
[beicon.core :as rx]
[rumext.alpha :as mf]))
@ -39,7 +39,7 @@
(reset! downloading? true)
(->> (http/send! {:method :get :uri link :response-type :blob :mode :no-cors})
(rx/subs (fn [{:keys [body] :as response}]
(open-import-modal {:name name :uri (dom/create-uri body)}))
(open-import-modal {:name name :uri (wapi/create-uri body)}))
(fn [error]
(js/console.log "error" error))
(fn []

View file

@ -6,10 +6,11 @@
(ns app.main.ui.viewer.handoff.attributes.image
(:require
[app.config :as cfg]
[app.common.media :as cm]
[app.common.pages.helpers :as cph]
[app.config :as cf]
[app.main.ui.components.copy-button :refer [copy-button]]
[app.util.code-gen :as cg]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[cuerdas.core :as str]
[rumext.alpha :as mf]))
@ -17,28 +18,28 @@
(defn has-image? [shape]
(= (:type shape) :image))
(mf/defc image-panel [{:keys [shapes]}]
(let [shapes (->> shapes (filter has-image?))]
(for [shape shapes]
[:div.attributes-block {:key (str "image-" (:id shape))}
[:div.attributes-image-row
[:div.attributes-image
[:img {:src (cfg/resolve-file-media (-> shape :metadata))}]]]
(mf/defc image-panel
[{:keys [shapes]}]
(for [shape (filter cph/image-shape? shapes)]
[:div.attributes-block {:key (str "image-" (:id shape))}
[:div.attributes-image-row
[:div.attributes-image
[:img {:src (cf/resolve-file-media (-> shape :metadata))}]]]
[:div.attributes-unit-row
[:div.attributes-label (tr "handoff.attributes.image.width")]
[:div.attributes-value (-> shape :metadata :width) "px"]
[:& copy-button {:data (cg/generate-css-props shape :width)}]]
[:div.attributes-unit-row
[:div.attributes-label (tr "handoff.attributes.image.width")]
[:div.attributes-value (-> shape :metadata :width) "px"]
[:& copy-button {:data (cg/generate-css-props shape :width)}]]
[:div.attributes-unit-row
[:div.attributes-label (tr "handoff.attributes.image.height")]
[:div.attributes-value (-> shape :metadata :height) "px"]
[:& copy-button {:data (cg/generate-css-props shape :height)}]]
[:div.attributes-unit-row
[:div.attributes-label (tr "handoff.attributes.image.height")]
[:div.attributes-value (-> shape :metadata :height) "px"]
[:& copy-button {:data (cg/generate-css-props shape :height)}]]
(let [mtype (-> shape :metadata :mtype)
name (:name shape)
extension (dom/mtype->extension mtype)]
[:a.download-button {:target "_blank"
:download (cond-> name extension (str/concat extension))
:href (cfg/resolve-file-media (-> shape :metadata))}
(tr "handoff.attributes.image.download")])])))
(let [mtype (-> shape :metadata :mtype)
name (:name shape)
extension (cm/mtype->extension mtype)]
[:a.download-button {:target "_blank"
:download (cond-> name extension (str/concat extension))
:href (cf/resolve-file-media (-> shape :metadata))}
(tr "handoff.attributes.image.download")])]))

View file

@ -406,101 +406,103 @@
(defn animate-open-overlay
[animation overlay-viewport
wrapper-size overlay-size overlay-position]
(case (:animation-type animation)
(when (some? overlay-viewport)
(case (:animation-type animation)
:dissolve
(dom/animate! overlay-viewport
[#js {:opacity "0"}
#js {:opacity "100"}]
#js {:duration (:duration animation)
:easing (name (:easing animation))}
#(st/emit! (dv/complete-animation)))
:slide
(case (:direction animation) ;; way and offset-effect are ignored
:right
:dissolve
(dom/animate! overlay-viewport
[#js {:left (str "-" (:width overlay-size) "px")}
#js {:left (str (:x overlay-position) "px")}]
[#js {:opacity "0"}
#js {:opacity "100"}]
#js {:duration (:duration animation)
:easing (name (:easing animation))}
#(st/emit! (dv/complete-animation)))
:left
(dom/animate! overlay-viewport
[#js {:left (str (:width wrapper-size) "px")}
#js {:left (str (:x overlay-position) "px")}]
#js {:duration (:duration animation)
:easing (name (:easing animation))}
#(st/emit! (dv/complete-animation)))
:slide
(case (:direction animation) ;; way and offset-effect are ignored
:up
(dom/animate! overlay-viewport
[#js {:top (str (:height wrapper-size) "px")}
#js {:top (str (:y overlay-position) "px")}]
#js {:duration (:duration animation)
:easing (name (:easing animation))}
#(st/emit! (dv/complete-animation)))
:right
(dom/animate! overlay-viewport
[#js {:left (str "-" (:width overlay-size) "px")}
#js {:left (str (:x overlay-position) "px")}]
#js {:duration (:duration animation)
:easing (name (:easing animation))}
#(st/emit! (dv/complete-animation)))
:down
(dom/animate! overlay-viewport
[#js {:top (str "-" (:height overlay-size) "px")}
#js {:top (str (:y overlay-position) "px")}]
#js {:duration (:duration animation)
:easing (name (:easing animation))}
#(st/emit! (dv/complete-animation))))))
:left
(dom/animate! overlay-viewport
[#js {:left (str (:width wrapper-size) "px")}
#js {:left (str (:x overlay-position) "px")}]
#js {:duration (:duration animation)
:easing (name (:easing animation))}
#(st/emit! (dv/complete-animation)))
:up
(dom/animate! overlay-viewport
[#js {:top (str (:height wrapper-size) "px")}
#js {:top (str (:y overlay-position) "px")}]
#js {:duration (:duration animation)
:easing (name (:easing animation))}
#(st/emit! (dv/complete-animation)))
:down
(dom/animate! overlay-viewport
[#js {:top (str "-" (:height overlay-size) "px")}
#js {:top (str (:y overlay-position) "px")}]
#js {:duration (:duration animation)
:easing (name (:easing animation))}
#(st/emit! (dv/complete-animation)))))))
(defn animate-close-overlay
[animation overlay-viewport
wrapper-size overlay-size overlay-position overlay-id]
(case (:animation-type animation)
(when (some? overlay-viewport)
(case (:animation-type animation)
:dissolve
(dom/animate! overlay-viewport
[#js {:opacity "100"}
#js {:opacity "0"}]
#js {:duration (:duration animation)
:easing (name (:easing animation))}
#(st/emit! (dv/complete-animation)
(dv/close-overlay overlay-id)))
:slide
(case (:direction animation) ;; way and offset-effect are ignored
:right
:dissolve
(dom/animate! overlay-viewport
[#js {:left (str (:x overlay-position) "px")}
#js {:left (str (:width wrapper-size) "px")}]
[#js {:opacity "100"}
#js {:opacity "0"}]
#js {:duration (:duration animation)
:easing (name (:easing animation))}
#(st/emit! (dv/complete-animation)
(dv/close-overlay overlay-id)))
:left
(dom/animate! overlay-viewport
[#js {:left (str (:x overlay-position) "px")}
#js {:left (str "-" (:width overlay-size) "px")}]
#js {:duration (:duration animation)
:easing (name (:easing animation))}
#(st/emit! (dv/complete-animation)
(dv/close-overlay overlay-id)))
:slide
(case (:direction animation) ;; way and offset-effect are ignored
:up
(dom/animate! overlay-viewport
[#js {:top (str (:y overlay-position) "px")}
#js {:top (str "-" (:height overlay-size) "px")}]
#js {:duration (:duration animation)
:easing (name (:easing animation))}
#(st/emit! (dv/complete-animation)
(dv/close-overlay overlay-id)))
:right
(dom/animate! overlay-viewport
[#js {:left (str (:x overlay-position) "px")}
#js {:left (str (:width wrapper-size) "px")}]
#js {:duration (:duration animation)
:easing (name (:easing animation))}
#(st/emit! (dv/complete-animation)
(dv/close-overlay overlay-id)))
:down
(dom/animate! overlay-viewport
[#js {:top (str (:y overlay-position) "px")}
#js {:top (str (:height wrapper-size) "px")}]
#js {:duration (:duration animation)
:easing (name (:easing animation))}
#(st/emit! (dv/complete-animation)
(dv/close-overlay overlay-id))))))
:left
(dom/animate! overlay-viewport
[#js {:left (str (:x overlay-position) "px")}
#js {:left (str "-" (:width overlay-size) "px")}]
#js {:duration (:duration animation)
:easing (name (:easing animation))}
#(st/emit! (dv/complete-animation)
(dv/close-overlay overlay-id)))
:up
(dom/animate! overlay-viewport
[#js {:top (str (:y overlay-position) "px")}
#js {:top (str "-" (:height overlay-size) "px")}]
#js {:duration (:duration animation)
:easing (name (:easing animation))}
#(st/emit! (dv/complete-animation)
(dv/close-overlay overlay-id)))
:down
(dom/animate! overlay-viewport
[#js {:top (str (:y overlay-position) "px")}
#js {:top (str (:height wrapper-size) "px")}]
#js {:duration (:duration animation)
:easing (name (:easing animation))}
#(st/emit! (dv/complete-animation)
(dv/close-overlay overlay-id)))))))

View file

@ -12,6 +12,7 @@
[app.main.data.exports :as de]
[app.main.data.modal :as modal]
[app.main.data.workspace :as dw]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.shortcuts :as sc]
[app.main.refs :as refs]
[app.main.repo :as rp]
@ -111,10 +112,10 @@
frames (mf/deref refs/workspace-frames)
add-shared-fn
(st/emitf (dw/set-file-shared (:id file) true))
(st/emitf (dwl/set-file-shared (:id file) true))
del-shared-fn
(st/emitf (dw/set-file-shared (:id file) false))
(st/emitf (dwl/set-file-shared (:id file) false))
on-add-shared
(mf/use-fn

View file

@ -10,6 +10,7 @@
[app.common.media :as cm]
[app.main.data.events :as ev]
[app.main.data.workspace :as dw]
[app.main.data.workspace.media :as dwm]
[app.main.data.workspace.shortcuts :as sc]
[app.main.refs :as refs]
[app.main.store :as st]
@ -44,7 +45,7 @@
params {:file-id (:id file)
:blobs (seq blobs)
:position (gpt/point x y)}]
(st/emit! (dw/upload-media-workspace params)))))]
(st/emit! (dwm/upload-media-workspace params)))))]
[:li.tooltip.tooltip-right
{:alt (tr "workspace.toolbar.image" (sc/get-tooltip :insert-image))

View file

@ -8,7 +8,6 @@
(:require
[app.common.data :as d]
[app.main.data.modal :as modal]
[app.main.data.workspace :as dw]
[app.main.data.workspace.libraries :as dwl]
[app.main.refs :as refs]
[app.main.store :as st]
@ -72,13 +71,13 @@
(reset! search-term "")))
link-library
(mf/use-callback (mf/deps file) #(st/emit! (dw/link-file-to-library (:id file) %)))
(mf/use-callback (mf/deps file) #(st/emit! (dwl/link-file-to-library (:id file) %)))
unlink-library
(mf/use-callback
(mf/deps file)
(fn [library-id]
(st/emit! (dw/unlink-file-from-library (:id file) library-id)
(st/emit! (dwl/unlink-file-from-library (:id file) library-id)
(dwl/sync-file (:id file) library-id))))]
[:*
[:div.section
@ -164,7 +163,7 @@
(mf/deps project)
(fn []
(when (:team-id project)
(st/emit! (dw/fetch-shared-files {:team-id (:team-id project)})))))
(st/emit! (dwl/fetch-shared-files {:team-id (:team-id project)})))))
[:div.modal-overlay
[:div.modal.libraries-dialog

View file

@ -12,7 +12,6 @@
[app.common.math :as mth]
[app.common.pages.helpers :as cph]
[app.common.text :as txt]
[app.main.data.workspace.changes :as dch]
[app.main.data.workspace.texts :as dwt]
[app.main.fonts :as fonts]
[app.main.refs :as refs]
@ -54,12 +53,7 @@
;; Update the position-data of every text fragment
(let [position-data (utp/calc-position-data node)]
(st/emit! (dch/update-shapes
[id]
(fn [shape]
(-> shape
(assoc :position-data position-data)))
{:save-undo? false}))))
(st/emit! (dwt/update-position-data id position-data))))
(defn- update-text-modifier
[{:keys [grow-type id]} node]

View file

@ -18,6 +18,7 @@
[app.main.data.workspace :as dw]
[app.main.data.workspace.colors :as dc]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.media :as dwm]
[app.main.data.workspace.state-helpers :as wsh]
[app.main.data.workspace.texts :as dwt]
[app.main.data.workspace.undo :as dwu]
@ -912,7 +913,7 @@
(fn [blobs]
(let [params {:file-id file-id
:blobs (seq blobs)}]
(st/emit! (dw/upload-media-asset params)
(st/emit! (dwm/upload-media-asset params)
(ptk/event ::ev/event {::ev/name "add-asset-to-library"
:asset-type "graphics"})))))

View file

@ -13,6 +13,7 @@
[app.main.data.workspace :as dw]
[app.main.data.workspace.drawing :as dd]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.media :as dwm]
[app.main.data.workspace.path :as dwdp]
[app.main.store :as st]
[app.main.streams :as ms]
@ -423,82 +424,77 @@
(dnd/has-type? e "text/asset-id"))
(dom/prevent-default e)))))
(defn on-image-uploaded []
(mf/use-callback
(fn [image position]
(st/emit! (dw/image-uploaded image position)))))
(defn on-drop
[file viewport-ref zoom]
(mf/use-fn
(mf/deps zoom)
(fn [event]
(dom/prevent-default event)
(let [point (gpt/point (.-clientX event) (.-clientY event))
viewport (mf/ref-val viewport-ref)
viewport-coord (utils/translate-point-to-viewport viewport zoom point)
asset-id (-> (dnd/get-data event "text/asset-id") uuid/uuid)
asset-name (dnd/get-data event "text/asset-name")
asset-type (dnd/get-data event "text/asset-type")]
(cond
(dnd/has-type? event "penpot/shape")
(let [shape (dnd/get-data event "penpot/shape")
final-x (- (:x viewport-coord) (/ (:width shape) 2))
final-y (- (:y viewport-coord) (/ (:height shape) 2))]
(st/emit! (dw/add-shape (-> shape
(assoc :id (uuid/next))
(assoc :x final-x)
(assoc :y final-y)))))
(defn on-drop [file viewport-ref zoom]
(let [on-image-uploaded (on-image-uploaded)]
(mf/use-callback
(mf/deps zoom)
(fn [event]
(dom/prevent-default event)
(let [point (gpt/point (.-clientX event) (.-clientY event))
viewport (mf/ref-val viewport-ref)
viewport-coord (utils/translate-point-to-viewport viewport zoom point)
asset-id (-> (dnd/get-data event "text/asset-id") uuid/uuid)
asset-name (dnd/get-data event "text/asset-name")
asset-type (dnd/get-data event "text/asset-type")]
(cond
(dnd/has-type? event "penpot/shape")
(let [shape (dnd/get-data event "penpot/shape")
final-x (- (:x viewport-coord) (/ (:width shape) 2))
final-y (- (:y viewport-coord) (/ (:height shape) 2))]
(st/emit! (dw/add-shape (-> shape
(assoc :id (uuid/next))
(assoc :x final-x)
(assoc :y final-y)))))
(dnd/has-type? event "penpot/component")
(let [{:keys [component file-id]} (dnd/get-data event "penpot/component")
shape (get-in component [:objects (:id component)])
final-x (- (:x viewport-coord) (/ (:width shape) 2))
final-y (- (:y viewport-coord) (/ (:height shape) 2))]
(st/emit! (dwl/instantiate-component file-id
(:id component)
(gpt/point final-x final-y))))
(dnd/has-type? event "penpot/component")
(let [{:keys [component file-id]} (dnd/get-data event "penpot/component")
shape (get-in component [:objects (:id component)])
final-x (- (:x viewport-coord) (/ (:width shape) 2))
final-y (- (:y viewport-coord) (/ (:height shape) 2))]
(st/emit! (dwl/instantiate-component file-id
(:id component)
(gpt/point final-x final-y))))
;; Will trigger when the user drags an image from a browser to the viewport
(dnd/has-type? event "text/uri-list")
(let [data (dnd/get-data event "text/uri-list")
lines (str/lines data)
uris (filter #(and (not (str/blank? %))
(not (str/starts-with? % "#")))
lines)
params {:file-id (:id file)
:position viewport-coord
:uris uris}]
(st/emit! (dwm/upload-media-workspace params)))
;; Will trigger when the user drags an image from a browser to the viewport
(dnd/has-type? event "text/uri-list")
(let [data (dnd/get-data event "text/uri-list")
lines (str/lines data)
uris (filter #(and (not (str/blank? %))
(not (str/starts-with? % "#")))
lines)
params {:file-id (:id file)
:position viewport-coord
:uris uris}]
(st/emit! (dw/upload-media-workspace params)))
;; Will trigger when the user drags an SVG asset from the assets panel
(and (dnd/has-type? event "text/asset-id") (= asset-type "image/svg+xml"))
(let [path (cfg/resolve-file-media {:id asset-id})
params {:file-id (:id file)
:position viewport-coord
:uris [path]
:name asset-name
:mtype asset-type}]
(st/emit! (dwm/upload-media-workspace params)))
;; Will trigger when the user drags an SVG asset from the assets panel
(and (dnd/has-type? event "text/asset-id") (= asset-type "image/svg+xml"))
(let [path (cfg/resolve-file-media {:id asset-id})
params {:file-id (:id file)
:position viewport-coord
:uris [path]
:name asset-name
:mtype asset-type}]
(st/emit! (dw/upload-media-workspace params)))
;; Will trigger when the user drags an image from the assets SVG
(dnd/has-type? event "text/asset-id")
(let [params {:file-id (:id file)
:object-id asset-id
:name asset-name}]
(st/emit! (dwm/clone-media-object
(with-meta params
{:on-success #(st/emit! (dwm/image-uploaded % viewport-coord))}))))
;; Will trigger when the user drags an image from the assets SVG
(dnd/has-type? event "text/asset-id")
(let [params {:file-id (:id file)
:object-id asset-id
:name asset-name}]
(st/emit! (dw/clone-media-object
(with-meta params
{:on-success #(on-image-uploaded % viewport-coord)}))))
;; Will trigger when the user drags a file from their file explorer into the viewport
;; Or the user pastes an image
;; Or the user uploads an image using the image tool
:else
(let [files (dnd/get-files event)
params {:file-id (:id file)
:position viewport-coord
:blobs (seq files)}]
(st/emit! (dw/upload-media-workspace params)))))))))
;; Will trigger when the user drags a file from their file explorer into the viewport
;; Or the user pastes an image
;; Or the user uploads an image using the image tool
:else
(let [files (dnd/get-files event)
params {:file-id (:id file)
:position viewport-coord
:blobs (seq files)}]
(st/emit! (dwm/upload-media-workspace params))))))))
(defn on-paste [disable-paste in-viewport?]
(mf/use-callback

View file

@ -10,8 +10,10 @@
[app.common.data.macros :as dm]
[app.common.geom.point :as gpt]
[app.common.logging :as log]
[app.common.media :as cm]
[app.util.globals :as globals]
[app.util.object :as obj]
[app.util.webapi :as wapi]
[cuerdas.core :as str]
[goog.dom :as dom]
[promesa.core :as p]))
@ -329,28 +331,11 @@
(log/error :msg "Seems like the current browser does not support fullscreen api.")
false)))
(defn ^boolean blob?
(defn blob?
[^js v]
(when (some? v)
(instance? js/Blob v)))
(defn create-blob
"Create a blob from content."
([content]
(create-blob content "application/octet-stream"))
([content mimetype]
(js/Blob. #js [content] #js {:type mimetype})))
(defn revoke-uri
[url]
(js/URL.revokeObjectURL url))
(defn create-uri
"Create a url from blob."
[b]
{:pre [(blob? b)]}
(js/URL.createObjectURL b))
(defn make-node
([namespace name]
(.createElementNS globals/document namespace name))
@ -442,21 +427,6 @@
(when (some? node)
(.getAttribute node (str "data-" attr))))
(defn mtype->extension [mtype]
;; https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types
(case mtype
"image/apng" ".apng"
"image/avif" ".avif"
"image/gif" ".gif"
"image/jpeg" ".jpg"
"image/png" ".png"
"image/svg+xml" ".svg"
"image/webp" ".webp"
"application/zip" ".zip"
"application/penpot" ".penpot"
"application/pdf" ".pdf"
nil))
(defn set-attribute! [^js node ^string attr value]
(when (some? node)
(.setAttribute node attr value)))
@ -507,7 +477,7 @@
(defn trigger-download-uri
[filename mtype uri]
(let [link (create-element "a")
extension (mtype->extension mtype)
extension (cm/mtype->extension mtype)
filename (if (and extension (not (str/ends-with? filename extension)))
(str/concat filename extension)
filename)]
@ -520,14 +490,14 @@
(defn trigger-download
[filename blob]
(trigger-download-uri filename (.-type ^js blob) (create-uri blob)))
(trigger-download-uri filename (.-type ^js blob) (wapi/create-uri blob)))
(defn save-as
[uri filename mtype description]
;; Only chrome supports the save dialog
(if (obj/contains? globals/window "showSaveFilePicker")
(let [extension (mtype->extension mtype)
(let [extension (cm/mtype->extension mtype)
opts {:suggestedName (str filename "." extension)
:types [{:description description
:accept { mtype [(str "." extension)]}}]}]

View file

@ -1,34 +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.util.uri
(:require
[app.util.object :as obj]
[cuerdas.core :as str]))
(defn uri-name [url]
(let [query-idx (str/last-index-of url "?")
url (if (> query-idx 0) (subs url 0 query-idx) url)
filename (->> (str/split url "/") (last))
ext-idx (str/last-index-of filename ".")]
(if (> ext-idx 0) (subs filename 0 ext-idx) filename)))
(defn data-uri->blob
[data-uri]
(let [[mtype b64-data] (str/split data-uri ";base64,")
mtype (subs mtype (inc (str/index-of mtype ":")))
decoded (.atob js/window b64-data)
size (.-length decoded)
content (js/Uint8Array. size)]
(doseq [i (range 0 size)]
(obj/set! content i (.charCodeAt decoded i)))
(js/Blob. #js [content] #js {"type" mtype})))

View file

@ -37,7 +37,7 @@
[file]
(file-reader #(.readAsDataURL ^js %1 file)))
(defn ^boolean blob?
(defn blob?
[v]
(instance? js/Blob v))

View file

@ -7,13 +7,14 @@
(ns app.worker.export
(:require
[app.common.data :as d]
[app.common.media :as cm]
[app.common.text :as ct]
[app.config :as cfg]
[app.main.render :as r]
[app.main.repo :as rp]
[app.util.dom :as dom]
[app.util.http :as http]
[app.util.json :as json]
[app.util.webapi :as wapi]
[app.util.zip :as uz]
[app.worker.impl :as impl]
[beicon.core :as rx]
@ -135,7 +136,7 @@
(rx/map #(assoc % :file-id file-id))
(rx/flat-map
(fn [media]
(let [file-path (str/concat file-id "/media/" (:id media) (dom/mtype->extension (:mtype media)))]
(let [file-path (str/concat file-id "/media/" (:id media) (cm/mtype->extension (:mtype media)))]
(->> (http/send!
{:uri (cfg/resolve-file-media media)
:response-type :blob
@ -466,7 +467,7 @@
:filename (:name file)
:mtype "application/penpot"
:description "Penpot export (*.penpot)"
:uri (dom/create-uri export-blob)}))))
:uri (wapi/create-uri export-blob)}))))
(rx/catch
(fn [err]
(rx/of {:type :error

View file

@ -12,11 +12,11 @@
[app.common.geom.point :as gpt]
[app.common.geom.shapes.path :as gpa]
[app.common.logging :as log]
[app.common.media :as cm]
[app.common.pages :as cp]
[app.common.text :as ct]
[app.common.uuid :as uuid]
[app.main.repo :as rp]
[app.util.dom :as dom]
[app.util.http :as http]
[app.util.import.parser :as cip]
[app.util.json :as json]
@ -49,7 +49,7 @@
:colors (str file-id "/colors.json")
:typographies (str file-id "/typographies.json")
:media-list (str file-id "/media.json")
:media (let [ext (dom/mtype->extension (:mtype media))]
:media (let [ext (cm/mtype->extension (:mtype media))]
(str/concat file-id "/media/" id ext))
:components (str file-id "/components.svg"))